diff --git a/.DS_Store b/.DS_Store old mode 100644 new mode 100755 index e12ce42..189f1c2 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/recap.xcodeproj/project.pbxproj b/recap.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 1c5b124..e8dc2cd --- a/recap.xcodeproj/project.pbxproj +++ b/recap.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 9D12A33A2D782FBE003A0C06 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9D12A3392D782FBE003A0C06 /* Lottie */; }; + 9D12A3492D784225003A0C06 /* DotLottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9D12A3482D784225003A0C06 /* DotLottie */; }; + 9D73CA212D87F42F00438D48 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA202D87F42F00438D48 /* FirebaseAnalytics */; }; + 9D73CA232D87F42F00438D48 /* FirebaseAnalyticsOnDeviceConversion in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA222D87F42F00438D48 /* FirebaseAnalyticsOnDeviceConversion */; }; + 9D73CA252D87F42F00438D48 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA242D87F42F00438D48 /* FirebaseAnalyticsWithoutAdIdSupport */; }; + 9D73CA272D87F42F00438D48 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA262D87F42F00438D48 /* FirebaseAppCheck */; }; + 9D73CA292D87F42F00438D48 /* FirebaseAppDistribution-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA282D87F42F00438D48 /* FirebaseAppDistribution-Beta */; }; + 9D73CA2B2D87F42F00438D48 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA2A2D87F42F00438D48 /* FirebaseCrashlytics */; }; + 9D73CA2D2D87F42F00438D48 /* FirebaseInstallations in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA2C2D87F42F00438D48 /* FirebaseInstallations */; }; + 9D73CA2F2D87F42F00438D48 /* FirebasePerformance in Frameworks */ = {isa = PBXBuildFile; productRef = 9D73CA2E2D87F42F00438D48 /* FirebasePerformance */; }; 9DBCC5392CEDE6D80004F3E2 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 9DBCC5382CEDE6D80004F3E2 /* SDWebImage */; }; 9DBCC53B2CEDE6D80004F3E2 /* SDWebImageMapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9DBCC53A2CEDE6D80004F3E2 /* SDWebImageMapKit */; }; 9DBEB2402D0CC00300813A4E /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 9DBEB23F2D0CC00300813A4E /* FirebaseAuth */; }; @@ -22,7 +32,21 @@ 9DE56A5D2D120D7500B73182 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9DE56A5C2D120D7500B73182 /* GoogleSignInSwift */; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 9D12A3462D78409B003A0C06 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 9D12A3412D783FC2003A0C06 /* SuccessAnimation.lottie */ = {isa = PBXFileReference; lastKnownFileType = file; path = SuccessAnimation.lottie; sourceTree = ""; }; 9DB56CA02CDA2F4300E2B3FF /* recap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = recap.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -52,28 +76,48 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9D73CA272D87F42F00438D48 /* FirebaseAppCheck in Frameworks */, 9DBEB2502D0CC00300813A4E /* FirebaseStorageCombine-Community in Frameworks */, + 9D73CA232D87F42F00438D48 /* FirebaseAnalyticsOnDeviceConversion in Frameworks */, + 9D73CA2B2D87F42F00438D48 /* FirebaseCrashlytics in Frameworks */, + 9D12A33A2D782FBE003A0C06 /* Lottie in Frameworks */, 9DBEB24C2D0CC00300813A4E /* FirebaseMessaging in Frameworks */, 9DBEB2462D0CC00300813A4E /* FirebaseDatabase in Frameworks */, 9DBCC53B2CEDE6D80004F3E2 /* SDWebImageMapKit in Frameworks */, + 9D73CA292D87F42F00438D48 /* FirebaseAppDistribution-Beta in Frameworks */, 9DBEB24E2D0CC00300813A4E /* FirebaseStorage in Frameworks */, + 9D73CA252D87F42F00438D48 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */, + 9D73CA2D2D87F42F00438D48 /* FirebaseInstallations in Frameworks */, 9DE56A5D2D120D7500B73182 /* GoogleSignInSwift in Frameworks */, 9DE56A5B2D120D7500B73182 /* GoogleSignIn in Frameworks */, 9DBEB2442D0CC00300813A4E /* FirebaseCore in Frameworks */, 9DBEB2402D0CC00300813A4E /* FirebaseAuth in Frameworks */, + 9D73CA212D87F42F00438D48 /* FirebaseAnalytics in Frameworks */, + 9D73CA2F2D87F42F00438D48 /* FirebasePerformance in Frameworks */, 9DBCC5392CEDE6D80004F3E2 /* SDWebImage in Frameworks */, 9DBEB24A2D0CC00300813A4E /* FirebaseFirestore in Frameworks */, 9DBEB2482D0CC00300813A4E /* FirebaseDynamicLinks in Frameworks */, 9DBEB2422D0CC00300813A4E /* FirebaseAuthCombine-Community in Frameworks */, + 9D12A3492D784225003A0C06 /* DotLottie in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9D12A3422D783FC2003A0C06 /* Animations */ = { + isa = PBXGroup; + children = ( + 9D12A3412D783FC2003A0C06 /* SuccessAnimation.lottie */, + ); + name = Animations; + path = recap/Resources/Animations; + sourceTree = ""; + }; 9DB56C972CDA2F4300E2B3FF = { isa = PBXGroup; children = ( + 9D12A3422D783FC2003A0C06 /* Animations */, 9DB56CA22CDA2F4300E2B3FF /* recap */, 9DB56CA12CDA2F4300E2B3FF /* Products */, ); @@ -97,6 +141,7 @@ 9DB56C9C2CDA2F4300E2B3FF /* Sources */, 9DB56C9D2CDA2F4300E2B3FF /* Frameworks */, 9DB56C9E2CDA2F4300E2B3FF /* Resources */, + 9D12A3462D78409B003A0C06 /* Embed Frameworks */, ); buildRules = ( ); @@ -120,6 +165,16 @@ 9DBEB24F2D0CC00300813A4E /* FirebaseStorageCombine-Community */, 9DE56A5A2D120D7500B73182 /* GoogleSignIn */, 9DE56A5C2D120D7500B73182 /* GoogleSignInSwift */, + 9D12A3392D782FBE003A0C06 /* Lottie */, + 9D12A3482D784225003A0C06 /* DotLottie */, + 9D73CA202D87F42F00438D48 /* FirebaseAnalytics */, + 9D73CA222D87F42F00438D48 /* FirebaseAnalyticsOnDeviceConversion */, + 9D73CA242D87F42F00438D48 /* FirebaseAnalyticsWithoutAdIdSupport */, + 9D73CA262D87F42F00438D48 /* FirebaseAppCheck */, + 9D73CA282D87F42F00438D48 /* FirebaseAppDistribution-Beta */, + 9D73CA2A2D87F42F00438D48 /* FirebaseCrashlytics */, + 9D73CA2C2D87F42F00438D48 /* FirebaseInstallations */, + 9D73CA2E2D87F42F00438D48 /* FirebasePerformance */, ); productName = recap; productReference = 9DB56CA02CDA2F4300E2B3FF /* recap.app */; @@ -133,7 +188,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1610; - LastUpgradeCheck = 1610; + LastUpgradeCheck = 1620; TargetAttributes = { 9DB56C9F2CDA2F4300E2B3FF = { CreatedOnToolsVersion = 16.1; @@ -146,6 +201,8 @@ knownRegions = ( en, Base, + hi, + "bn-IN", ); mainGroup = 9DB56C972CDA2F4300E2B3FF; minimizedProjectReferenceProxies = 1; @@ -153,6 +210,8 @@ 9DBCC5372CEDE6D80004F3E2 /* XCRemoteSwiftPackageReference "SDWebImage" */, 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 9DE56A592D120D7500B73182 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + 9D12A3382D782FBE003A0C06 /* XCRemoteSwiftPackageReference "lottie-ios" */, + 9D12A3472D784225003A0C06 /* XCRemoteSwiftPackageReference "dotlottie-ios" */, ); preferredProjectObjectVersion = 77; productRefGroup = 9DB56CA12CDA2F4300E2B3FF /* Products */; @@ -192,24 +251,31 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = G2TY2Q3KD8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = recap/Resources/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Recap; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = launchScreenViewController.swift; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = in.recap.recap; + MARKETING_VERSION = 1.0.2; + PRODUCT_BUNDLE_IDENTIFIER = com.srmist.recap; 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"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -220,24 +286,31 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = G2TY2Q3KD8; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = recap/Resources/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Recap; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = launchScreenViewController.swift; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = in.recap.recap; + MARKETING_VERSION = 1.0.2; + PRODUCT_BUNDLE_IDENTIFIER = com.srmist.recap; 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"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -246,6 +319,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -309,6 +383,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -384,6 +459,22 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 9D12A3382D782FBE003A0C06 /* XCRemoteSwiftPackageReference "lottie-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/airbnb/lottie-ios.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.5.1; + }; + }; + 9D12A3472D784225003A0C06 /* XCRemoteSwiftPackageReference "dotlottie-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LottieFiles/dotlottie-ios"; + requirement = { + branch = main; + kind = branch; + }; + }; 9DBCC5372CEDE6D80004F3E2 /* XCRemoteSwiftPackageReference "SDWebImage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SDWebImage/SDWebImage"; @@ -411,6 +502,56 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 9D12A3392D782FBE003A0C06 /* Lottie */ = { + isa = XCSwiftPackageProductDependency; + package = 9D12A3382D782FBE003A0C06 /* XCRemoteSwiftPackageReference "lottie-ios" */; + productName = Lottie; + }; + 9D12A3482D784225003A0C06 /* DotLottie */ = { + isa = XCSwiftPackageProductDependency; + package = 9D12A3472D784225003A0C06 /* XCRemoteSwiftPackageReference "dotlottie-ios" */; + productName = DotLottie; + }; + 9D73CA202D87F42F00438D48 /* FirebaseAnalytics */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalytics; + }; + 9D73CA222D87F42F00438D48 /* FirebaseAnalyticsOnDeviceConversion */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalyticsOnDeviceConversion; + }; + 9D73CA242D87F42F00438D48 /* FirebaseAnalyticsWithoutAdIdSupport */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAnalyticsWithoutAdIdSupport; + }; + 9D73CA262D87F42F00438D48 /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAppCheck; + }; + 9D73CA282D87F42F00438D48 /* FirebaseAppDistribution-Beta */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = "FirebaseAppDistribution-Beta"; + }; + 9D73CA2A2D87F42F00438D48 /* FirebaseCrashlytics */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseCrashlytics; + }; + 9D73CA2C2D87F42F00438D48 /* FirebaseInstallations */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseInstallations; + }; + 9D73CA2E2D87F42F00438D48 /* FirebasePerformance */ = { + isa = XCSwiftPackageProductDependency; + package = 9DBEB23E2D0CC00300813A4E /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebasePerformance; + }; 9DBCC5382CEDE6D80004F3E2 /* SDWebImage */ = { isa = XCSwiftPackageProductDependency; package = 9DBCC5372CEDE6D80004F3E2 /* XCRemoteSwiftPackageReference "SDWebImage" */; diff --git a/recap.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/recap.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 diff --git a/recap.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/recap.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved old mode 100644 new mode 100755 index be4ae28..3523ae5 --- a/recap.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/recap.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "839eaeec5820c6e61ec169e7afc3a96a18dedd938545013892c82c3e538a3b3b", + "originHash" : "afa63eeb38d3091740476f6260b60b01c8ea22d3a51133fafc70da28701a4a4a", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -28,6 +28,15 @@ "version" : "1.7.6" } }, + { + "identity" : "dotlottie-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/LottieFiles/dotlottie-ios", + "state" : { + "branch" : "main", + "revision" : "5882d81ccb677e2da0175fc9c97980bb61af3eb9" + } + }, { "identity" : "firebase-ios-sdk", "kind" : "remoteSourceControl", @@ -118,6 +127,15 @@ "version" : "1.22.5" } }, + { + "identity" : "lottie-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/airbnb/lottie-ios.git", + "state" : { + "revision" : "047aa81b77adcbf583a966dfef620d17650cc656", + "version" : "4.5.1" + } + }, { "identity" : "nanopb", "kind" : "remoteSourceControl", diff --git a/recap.xcodeproj/project.xcworkspace/xcuserdata/admin70.xcuserdatad/UserInterfaceState.xcuserstate b/recap.xcodeproj/project.xcworkspace/xcuserdata/admin70.xcuserdatad/UserInterfaceState.xcuserstate old mode 100644 new mode 100755 index ba559c6..c3b54e2 Binary files a/recap.xcodeproj/project.xcworkspace/xcuserdata/admin70.xcuserdatad/UserInterfaceState.xcuserstate and b/recap.xcodeproj/project.xcworkspace/xcuserdata/admin70.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/recap.xcodeproj/project.xcworkspace/xcuserdata/djdiptayan.xcuserdatad/IDEFindNavigatorScopes.plist b/recap.xcodeproj/project.xcworkspace/xcuserdata/djdiptayan.xcuserdatad/IDEFindNavigatorScopes.plist old mode 100644 new mode 100755 diff --git a/recap.xcodeproj/project.xcworkspace/xcuserdata/djdiptayan.xcuserdatad/UserInterfaceState.xcuserstate b/recap.xcodeproj/project.xcworkspace/xcuserdata/djdiptayan.xcuserdatad/UserInterfaceState.xcuserstate old mode 100644 new mode 100755 index cceb7ed..66701fb Binary files a/recap.xcodeproj/project.xcworkspace/xcuserdata/djdiptayan.xcuserdatad/UserInterfaceState.xcuserstate and b/recap.xcodeproj/project.xcworkspace/xcuserdata/djdiptayan.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/recap.xcodeproj/project.xcworkspace/xcuserdata/s1834.xcuserdatad/UserInterfaceState.xcuserstate b/recap.xcodeproj/project.xcworkspace/xcuserdata/s1834.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..eacbd4e Binary files /dev/null and b/recap.xcodeproj/project.xcworkspace/xcuserdata/s1834.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/recap.xcodeproj/project.xcworkspace/xcuserdata/user47.xcuserdatad/UserInterfaceState.xcuserstate b/recap.xcodeproj/project.xcworkspace/xcuserdata/user47.xcuserdatad/UserInterfaceState.xcuserstate old mode 100644 new mode 100755 index ab848b9..fd47d6e Binary files a/recap.xcodeproj/project.xcworkspace/xcuserdata/user47.xcuserdatad/UserInterfaceState.xcuserstate and b/recap.xcodeproj/project.xcworkspace/xcuserdata/user47.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/recap.xcodeproj/xcuserdata/admin70.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/recap.xcodeproj/xcuserdata/admin70.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..1f67df3 --- /dev/null +++ b/recap.xcodeproj/xcuserdata/admin70.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/recap.xcodeproj/xcuserdata/admin70.xcuserdatad/xcschemes/xcschememanagement.plist b/recap.xcodeproj/xcuserdata/admin70.xcuserdatad/xcschemes/xcschememanagement.plist old mode 100644 new mode 100755 diff --git a/recap.xcodeproj/xcuserdata/djdiptayan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/recap.xcodeproj/xcuserdata/djdiptayan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist old mode 100644 new mode 100755 index bde72c8..24a86aa --- a/recap.xcodeproj/xcuserdata/djdiptayan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/recap.xcodeproj/xcuserdata/djdiptayan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -7,16 +7,16 @@ diff --git a/recap.xcodeproj/xcuserdata/djdiptayan.xcuserdatad/xcschemes/xcschememanagement.plist b/recap.xcodeproj/xcuserdata/djdiptayan.xcuserdatad/xcschemes/xcschememanagement.plist old mode 100644 new mode 100755 diff --git a/recap.xcodeproj/xcuserdata/s1834.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/recap.xcodeproj/xcuserdata/s1834.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..6fe7a0c --- /dev/null +++ b/recap.xcodeproj/xcuserdata/s1834.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/recap.xcodeproj/xcuserdata/s1834.xcuserdatad/xcschemes/xcschememanagement.plist b/recap.xcodeproj/xcuserdata/s1834.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..21d8ec5 --- /dev/null +++ b/recap.xcodeproj/xcuserdata/s1834.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + recap.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/recap.xcodeproj/xcuserdata/user47.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/recap.xcodeproj/xcuserdata/user47.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist old mode 100644 new mode 100755 diff --git a/recap.xcodeproj/xcuserdata/user47.xcuserdatad/xcschemes/xcschememanagement.plist b/recap.xcodeproj/xcuserdata/user47.xcuserdatad/xcschemes/xcschememanagement.plist old mode 100644 new mode 100755 diff --git a/recap/.DS_Store b/recap/.DS_Store old mode 100644 new mode 100755 index a1b0fd9..9106496 Binary files a/recap/.DS_Store and b/recap/.DS_Store differ diff --git a/recap/App/AppDelegate.swift b/recap/App/AppDelegate.swift old mode 100644 new mode 100755 index 9e0783d..ad5166b --- a/recap/App/AppDelegate.swift +++ b/recap/App/AppDelegate.swift @@ -7,8 +7,10 @@ import UIKit import FirebaseAuth +import Firebase import GoogleSignIn import FirebaseCore +import FirebaseAnalytics import UserNotifications @main @@ -19,6 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Firebase Initialization FirebaseApp.configure() GIDSignIn.sharedInstance.restorePreviousSignIn() + Analytics.setAnalyticsCollectionEnabled(true) //Enable Analytics collection. // Set Notification Delegate let notificationCenter = UNUserNotificationCenter.current() diff --git a/recap/App/SceneDelegate.swift b/recap/App/SceneDelegate.swift old mode 100644 new mode 100755 index 665fc40..2a00ea2 --- a/recap/App/SceneDelegate.swift +++ b/recap/App/SceneDelegate.swift @@ -17,38 +17,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, PatientInfoDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) -// window.rootViewController = TabbarViewController() let launchVC = launchScreenViewController() - -// if UserDefaultsStorageProfile.shared.isLoggedIn() { -// // User is logged in, show the main tab bar controller -// let tabBarVC = TabbarViewController() -// window.rootViewController = tabBarVC -// } else { -// // User is not logged in, show the welcome screen -// let welcomeVC = WelcomeViewController() -// let navigationController = UINavigationController(rootViewController: welcomeVC) -// window.rootViewController = navigationController -// } window.rootViewController = launchVC self.window = window window.makeKeyAndVisible() - -// let questionItem = UIApplicationShortcutItem( -// type: "daily-questions", -// localizedTitle: "Daily Questions", -// localizedSubtitle: "Answer today's questions", -// icon: UIApplicationShortcutIcon(systemImageName: "questionmark.circle.fill") -// ) -// let familyItem = UIApplicationShortcutItem( -// type: "family-members", -// localizedTitle: "Family Members", -// localizedSubtitle: "View family members", -// icon: UIApplicationShortcutIcon(systemImageName: "person.3.fill") -// ) -// -// -// UIApplication.shared.shortcutItems = [questionItem, familyItem] } func didCompleteProfile() { diff --git a/recap/Controllers/.DS_Store b/recap/Controllers/.DS_Store old mode 100644 new mode 100755 index 5926672..52499c4 Binary files a/recap/Controllers/.DS_Store and b/recap/Controllers/.DS_Store differ diff --git a/recap/Controllers/Core/.DS_Store b/recap/Controllers/Core/.DS_Store old mode 100644 new mode 100755 index ffc1b74..4bed79a Binary files a/recap/Controllers/Core/.DS_Store and b/recap/Controllers/Core/.DS_Store differ diff --git a/recap/Controllers/Core/Family/.DS_Store b/recap/Controllers/Core/Family/.DS_Store new file mode 100755 index 0000000..af31a7f Binary files /dev/null and b/recap/Controllers/Core/Family/.DS_Store differ diff --git a/recap/Controllers/Core/Family/FamilyProfileViewController.swift b/recap/Controllers/Core/Family/FamilyProfileViewController.swift old mode 100644 new mode 100755 index 7586b83..73602d7 --- a/recap/Controllers/Core/Family/FamilyProfileViewController.swift +++ b/recap/Controllers/Core/Family/FamilyProfileViewController.swift @@ -2,7 +2,7 @@ // FamilyProfileViewController.swift // recap // -// Created by admin70 on 11/11/24. +// Created by khushi on 11/11/24. // import UIKit @@ -21,7 +21,7 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl private let nameLabel: UILabel = { let label = UILabel() - label.text = "Diptayan Jash" + label.text = "Unknown Family" label.font = .systemFont(ofSize: 24, weight: .semibold) label.textAlignment = .center return label @@ -30,8 +30,8 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl private let tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .grouped) tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.backgroundColor = .white // Set background color to white - tableView.isScrollEnabled = false // Disable scrolling to keep content fixed + tableView.backgroundColor = .white + tableView.isScrollEnabled = false return tableView }() @@ -39,28 +39,22 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl let button = UIButton(type: .system) button.setTitle("Logout", for: .normal) button.titleLabel?.font = .boldSystemFont(ofSize: 17) - button.backgroundColor = .systemRed + button.backgroundColor = AppColors.iconColor button.setTitleColor(.white, for: .normal) - button.layer.cornerRadius = 8 + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius button.clipsToBounds = true button.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside) return button }() + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground setupNavigationBar() setupUI() setupTableView() - - // Load stored family member details - if let familyMemberData = UserDefaults.standard.dictionary(forKey: "familyMemberDetails"), - let name = familyMemberData["name"] as? String { - nameLabel.text = name - } else { - nameLabel.text = "Unknown Family Member" - } + loadUserData(); } private func setupNavigationBar() { @@ -72,6 +66,7 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl action: #selector(doneButtonTapped) ) navigationItem.rightBarButtonItem = doneButton + doneButton.tintColor = AppColors.iconColor } @objc private func doneButtonTapped() { @@ -82,7 +77,7 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl view.addSubview(profileImageView) view.addSubview(nameLabel) view.addSubview(tableView) - view.addSubview(logoutButton) // Add logout button here + view.addSubview(logoutButton) profileImageView.translatesAutoresizingMaskIntoConstraints = false nameLabel.translatesAutoresizingMaskIntoConstraints = false @@ -110,7 +105,7 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl ]) profileImageView.layer.cornerRadius = 60 - tableView.layer.cornerRadius = 10 + tableView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius tableView.clipsToBounds = true } @@ -121,11 +116,11 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl } func numberOfSections(in tableView: UITableView) -> Int { - return 1 // Only one section for the list + return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 4 // Patients, About App, Language, Privacy + return 4 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -135,8 +130,6 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl let titles = ["Patients", "About App", "Language", "Privacy"] cell.textLabel?.text = titles[indexPath.row] - - // Ensure white background color cell.backgroundColor = .white return cell @@ -160,13 +153,32 @@ class FamilyProfileViewController: UIViewController, UITableViewDelegate, UITabl } navigationController?.pushViewController(viewController, animated: true) } + + private func loadUserData() { + if let familyData = UserDefaults.standard.dictionary(forKey: Constants.UserDefaultsKeys.familyMemberDetails), + let name = familyData["name"] as? String { + nameLabel.text = name + } else { + nameLabel.text = "Unknown Family" + } + + let placeholderImage = UIImage(systemName: "person.circle.fill")? + .withTintColor(AppColors.iconColor, renderingMode: .alwaysOriginal) + if let imageUrl = UserDefaults.standard.string(forKey: Constants.UserDefaultsKeys.familyMemberImageURL), + let url = URL(string: imageUrl) { + profileImageView.sd_setImage(with: url, placeholderImage: placeholderImage) + } else { + profileImageView.image = placeholderImage + } + } + @objc private func logoutTapped() { let familyLoginExtension = FamilyLoginViewController() familyLoginExtension.logoutTapped() } } -#Preview { +#Preview{ FamilyProfileViewController() } diff --git a/recap/Controllers/Core/Family/FamilyViewController.swift b/recap/Controllers/Core/Family/FamilyViewController.swift old mode 100644 new mode 100755 index 821ad52..08c3f89 --- a/recap/Controllers/Core/Family/FamilyViewController.swift +++ b/recap/Controllers/Core/Family/FamilyViewController.swift @@ -2,13 +2,16 @@ // FamilyViewController.swift // Recap // -// Created by admin70 on 04/11/24. +// Created by khushi on 04/11/24. // import UIKit import SwiftUI class FamilyViewController: UIViewController { + var analyticsService: CoreAnalyticsService? + var sessionStartTime: Date? + private var verifiedUserDocID: String? private let scrollView = UIScrollView() private let contentView = UIView() @@ -18,110 +21,112 @@ class FamilyViewController: UIViewController { let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .medium) let image = UIImage(systemName: "person.circle.fill", withConfiguration: config) button.setImage(image, for: .normal) - button.tintColor = .systemGreen + button.tintColor = AppColors.iconColor button.addTarget(self, action: #selector(profileButtonTapped), for: .touchUpInside) return button }() - let dailyQuestionsVC = DailyQuestionsViewController() override func viewDidLoad() { super.viewDidLoad() setupNavigationBar() setupUI() - applyGradientBackground() + view.applyGradientBackground() + analyticsService = CoreAnalyticsService() -// dailyQuestionsVC.addQuestionsToFirestore() + guard let verifiedUserDocID = UserDefaults.standard.string(forKey: Constants.UserDefaultsKeys.verifiedUserDocID) else { + print("❌ Error: verifiedUserDocID not found in UserDefaults.") + return + } + self.verifiedUserDocID = verifiedUserDocID } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + sessionStartTime = Date() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if let startTime = sessionStartTime { + let sessionDuration = Date().timeIntervalSince(startTime) / 60 + analyticsService?.trackTimeSpent(sessionDuration: sessionDuration, isFamily: true) + } + } + private func setupNavigationBar() { let profileBarButton = UIBarButtonItem(customView: profileButton) navigationItem.rightBarButtonItem = profileBarButton navigationController?.navigationBar.prefersLargeTitles = true title = "Family" + profileBarButton.tintColor = AppColors.iconColor } private func setupUI() { view.backgroundColor = UIColor.systemBackground - // ScrollView and ContentView Setup scrollView.translatesAutoresizingMaskIntoConstraints = false contentView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(contentView) view.addSubview(scrollView) - - let dailyQuestionCard = DailyQuestionCardView() - dailyQuestionCard.navigateToDetail = { - if let verifiedUserDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") { - let detailVC = DailyQuestionDetailViewController(verifiedUserDocID: verifiedUserDocID) - self.navigationController?.pushViewController(detailVC, animated: true) - } else { - print("Error: verifiedUserDocID not found in UserDefaults.") - } - } -// let dailyQuestionCard = DailyQuestionCardView() -// dailyQuestionCard.navigateToDetail = { -// if let verifiedUserDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") { -// // Create an instance of PatientQuestionsViewController -// let patientQuestionsVC = PatientQuestionsViewController(verifiedUserDocID: verifiedUserDocID) -// -// // Navigate to the PatientQuestionsViewController -// self.navigationController?.pushViewController(patientQuestionsVC, animated: true) -// } else { -// print("Error: verifiedUserDocID not found in UserDefaults.") -// } -// } - - - // Daily Question, Streak, and Trends Cards let streakCard = StreakCardView() - streakCard.onTap = { [weak self] in - if let verifiedUserDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") { - let streaksVC = StreaksViewController(verifiedUserDocID: verifiedUserDocID) + streakCard.onTap = { [weak self] in + let verifiedUserDocID = self?.verifiedUserDocID + let streaksVC = StreaksViewController(verifiedUserDocID: verifiedUserDocID!) self?.navigationController?.pushViewController(streaksVC, animated: true) - } else { - print("Error: verifiedUserDocID not found in UserDefaults.") } - } + let dailyQuestionCard = DailyQuestionCardView() + dailyQuestionCard.navigateToDetail = { [weak self] in + let verifiedUserDocID = self?.verifiedUserDocID + let detailVC = DailyQuestionDetailViewController(verifiedUserDocID: verifiedUserDocID!) + self?.navigationController?.pushViewController(detailVC, animated: true) + } + + + // Trends Card Setup with dynamic verifiedUserDocID + guard let verifiedUserDocID = UserDefaults.standard.string(forKey: Constants.UserDefaultsKeys.verifiedUserDocID) else { + print("Error: verifiedUserDocID not found in UserDefaults.") + return + } - let trendsCard = TrendsCardView() + let trendsCard = TrendsCardView(frame: CGRect(x: 0, y: 0, width: 200, height: 100), verifiedUserDocID: verifiedUserDocID) trendsCard.onInsightsTap = { [weak self] tag in var detailViewController: UIViewController? switch tag { - case 1: - let swiftUIView = ImmediateReportDetailViewController() - detailViewController = UIHostingController(rootView: swiftUIView) - case 2: - let recentReportDetailView = RecentReportDetailViewController(data: recentMemoryData) - detailViewController = UIHostingController(rootView: recentReportDetailView) - case 3: - let remoteReportDetailView = RemoteReportDetailViewController(monthlyData: novemberReports) - detailViewController = UIHostingController(rootView: remoteReportDetailView) - default: - return + case 1: + let swiftUIView = ImmediateReportDetailViewController(verifiedUserDocID: verifiedUserDocID) + detailViewController = UIHostingController(rootView: swiftUIView) + case 2: + let recentReportDetailView = RecentReportDetailViewController(verifiedUserDocID: verifiedUserDocID) + detailViewController = UIHostingController(rootView: recentReportDetailView) + case 3: + let remoteReportDetailView = RemoteReportDetailViewController(verifiedUserDocID: verifiedUserDocID) + detailViewController = UIHostingController(rootView: remoteReportDetailView) + default: + return } + if let detailVC = detailViewController { self?.navigationController?.pushViewController(detailVC, animated: true) } } - // Vertical StackView for Daily Question and Streak let dailyAndStreakStackView = UIStackView(arrangedSubviews: [dailyQuestionCard, streakCard]) - dailyAndStreakStackView.axis = .vertical - dailyAndStreakStackView.spacing = 16 - dailyAndStreakStackView.translatesAutoresizingMaskIntoConstraints = false + dailyAndStreakStackView.axis = .vertical + dailyAndStreakStackView.spacing = 16 + dailyAndStreakStackView.translatesAutoresizingMaskIntoConstraints = false let trendsCardStackView = UIStackView(arrangedSubviews: [trendsCard]) - trendsCardStackView.axis = .horizontal - trendsCardStackView.spacing = 16 - trendsCardStackView.distribution = .fillEqually - trendsCardStackView.translatesAutoresizingMaskIntoConstraints = false - + trendsCardStackView.axis = .horizontal + trendsCardStackView.spacing = 16 + trendsCardStackView.distribution = .fillEqually + trendsCardStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(dailyAndStreakStackView) contentView.addSubview(trendsCardStackView) - - // Constraints + NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), @@ -133,35 +138,25 @@ class FamilyViewController: UIViewController { contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - + dailyAndStreakStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), dailyAndStreakStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), dailyAndStreakStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), - + trendsCardStackView.topAnchor.constraint(equalTo: dailyAndStreakStackView.bottomAnchor, constant: 16), trendsCardStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), trendsCardStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), trendsCardStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16) - ]) - } - - private func applyGradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, - UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 1, y: 1) - gradientLayer.frame = view.bounds - view.layer.insertSublayer(gradientLayer, at: 0) - } - + ]) + } + @objc private func profileButtonTapped() { let profileVC = FamilyProfileViewController() let navController = UINavigationController(rootViewController: profileVC) present(navController, animated: true) } } -#Preview -{FamilyViewController()} + +#Preview { + FamilyViewController() +} diff --git a/recap/Controllers/Core/Family/PatientsViewController.swift b/recap/Controllers/Core/Family/PatientsViewController.swift old mode 100644 new mode 100755 index f667434..2dfdf8e --- a/recap/Controllers/Core/Family/PatientsViewController.swift +++ b/recap/Controllers/Core/Family/PatientsViewController.swift @@ -2,19 +2,18 @@ // PatientsViewController.swift // recap // -// Created by admin70 on 27/01/25. +// Created by khushi on 27/01/25. // import UIKit class PatientsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { - // Data passed from ProfileViewController var userDetails: [String: Any]? var prefetchedQuestions: [rapiMemory]? private let profileImageView: UIImageView = { let imageView = UIImageView() - imageView.image = UIImage(systemName: "person.circle.fill") // Replace with your image name + imageView.image = UIImage(systemName: "person.circle.fill") imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = 50 imageView.clipsToBounds = true @@ -34,8 +33,9 @@ class PatientsViewController: UIViewController, UITableViewDelegate, UITableView let tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false tableView.isScrollEnabled = false - tableView.layer.cornerRadius = 10 + tableView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius tableView.layer.masksToBounds = true + tableView.isUserInteractionEnabled = false return tableView }() @@ -43,25 +43,25 @@ class PatientsViewController: UIViewController, UITableViewDelegate, UITableView super.viewDidLoad() view.backgroundColor = .systemBackground title = "Patient" - setupNavigationBar() + navigationController?.navigationBar.prefersLargeTitles = false setupUI() setupTableView() - - // Fetch patient details from UserDefaults - if let savedUserData = UserDefaults.standard.object(forKey: "patientDetails") as? [String: Any] { - userDetails = savedUserData - } - + fetchPatientDetails() updateUIWithData() + animateContent() + } + + private func fetchPatientDetails() { + userDetails = UserDefaults.standard.object(forKey: Constants.UserDefaultsKeys.patientDetails) as? [String: Any] } + private func updateUIWithData() { if let userDetails = userDetails { nameLabel.text = "\(userDetails["firstName"] as? String ?? "") \(userDetails["lastName"] as? String ?? "")" if let profileImageURL = userDetails["profileImageURL"] as? String, !profileImageURL.isEmpty { if let url = URL(string: profileImageURL) { - // Load the image asynchronously DispatchQueue.global().async { if let data = try? Data(contentsOf: url) { DispatchQueue.main.async { @@ -71,23 +71,10 @@ class PatientsViewController: UIViewController, UITableViewDelegate, UITableView } } } - tableView.reloadData() } } - - - private func setupNavigationBar() { - let doneButton = UIBarButtonItem( - title: "Done", - style: .done, - target: self, - action: #selector(doneButtonTapped) - ) - navigationItem.rightBarButtonItem = doneButton - } - @objc private func doneButtonTapped() { navigationController?.popViewController(animated: true) } @@ -119,9 +106,7 @@ class PatientsViewController: UIViewController, UITableViewDelegate, UITableView tableView.dataSource = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } - - // MARK: - UITableViewDelegate & DataSource Methods - + func numberOfSections(in tableView: UITableView) -> Int { return 1 } @@ -132,7 +117,7 @@ class PatientsViewController: UIViewController, UITableViewDelegate, UITableView func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") - cell.accessoryType = .disclosureIndicator + cell.accessoryType = .none let titles = ["First Name", "Last Name", "Date of Birth", "Sex", "Blood Type"] let values = [ @@ -149,9 +134,31 @@ class PatientsViewController: UIViewController, UITableViewDelegate, UITableView if values[indexPath.row] == "Not Set" { cell.detailTextLabel?.textColor = .systemGray } - return cell } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + } + + private func animateContent() { + let views = [profileImageView, nameLabel, tableView] + + views.enumerated().forEach { index, view in + view.alpha = 0 + view.transform = CGAffineTransform(translationX: 0, y: 20) + + UIView.animate( + withDuration: 0.6, + delay: Double(index) * 0.2, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: .curveEaseOut + ) { + view.alpha = 1 + view.transform = .identity + } + } + } } #Preview { diff --git a/recap/Controllers/Core/Family/TabbarFamilyViewController.swift b/recap/Controllers/Core/Family/TabbarFamilyViewController.swift old mode 100644 new mode 100755 index 3fbcca6..e7d911b --- a/recap/Controllers/Core/Family/TabbarFamilyViewController.swift +++ b/recap/Controllers/Core/Family/TabbarFamilyViewController.swift @@ -2,56 +2,46 @@ // TabbarFamilyViewController.swift // Recap // -// Created by admin70 on 05/11/24. +// Created by khushi on 05/11/24. // import UIKit class TabbarFamilyViewController: UITabBarController, UITabBarControllerDelegate { - + + private let analyticsService = CoreAnalyticsService() + override func viewDidLoad() { super.viewDidLoad() self.delegate = self - - // Setup View Controllers - let tab1 = FamilyViewController() - let tab2 = ArticleTableViewController() - - tab1.title = "Home" - tab2.title = "Articles" - tab1.navigationItem.largeTitleDisplayMode = .always - tab2.navigationItem.largeTitleDisplayMode = .always + tabBar.backgroundColor = UIColor(white: 0.95, alpha: 0.85) + tabBar.isTranslucent = true + tabBar.tintColor = .label - let nav1 = UINavigationController(rootViewController: tab1) - let nav2 = UINavigationController(rootViewController: tab2) + let nav1 = UINavigationController(rootViewController: FamilyViewController()) + let nav2 = UINavigationController(rootViewController: ArticleTableViewController()) - nav1.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) + nav1.tabBarItem = UITabBarItem(title: "Recap", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) nav2.tabBarItem = UITabBarItem(title: "Articles", image: UIImage(systemName: "doc.text"), selectedImage: UIImage(systemName: "doc.text.fill")) nav1.navigationBar.prefersLargeTitles = true nav2.navigationBar.prefersLargeTitles = true - tabBar.tintColor = .label + tabBar.tintColor = AppColors.iconColor // Set selected tab color to systemBlue (or your desired color) + tabBar.unselectedItemTintColor = .gray + setViewControllers([nav1, nav2], animated: true) + + analyticsService!.initializeAnalytics() + analyticsService!.trackAppOpen(isFamily: true) } func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { - let generator = UIImpactFeedbackGenerator(style: .light) - generator.impactOccurred() - } - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. + UIImpactFeedbackGenerator(style: .light).impactOccurred() } - */ } -#Preview() { +#Preview { TabbarFamilyViewController() } diff --git a/recap/Controllers/Core/FamilyViewController_patient.swift b/recap/Controllers/Core/FamilyViewController_patient.swift old mode 100644 new mode 100755 index 4bea963..cc31145 --- a/recap/Controllers/Core/FamilyViewController_patient.swift +++ b/recap/Controllers/Core/FamilyViewController_patient.swift @@ -1,6 +1,6 @@ // -// FamilyViewController.swift +// FamilyViewController_Patient.swift // recap // // Created by Diptayan Jash on 05/11/24. @@ -29,29 +29,38 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, override func viewDidLoad() { super.viewDidLoad() - + applyGradientBackground() // Ensure user is logged in guard let currentUser = Auth.auth().currentUser else { print("No logged-in user found. Redirecting to login screen.") - // Redirect to login if needed // navigationController?.pushViewController(LoginViewController(), animated: true) return } setupUI() - loadFamilyMembers() + loadFamilyMembersFromCache() +// fetchFamilyMembersFromFirestore() + setupRealTimeFamilyMemberUpdates() setupNotifications() } + private func applyGradientBackground() { + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [ + UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, + UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 1, y: 1) + gradientLayer.frame = view.bounds + view.layer.insertSublayer(gradientLayer, at: 0) + } - private func loadFamilyMembers() { - // Load cached family members from local storage + private func loadFamilyMembersFromCache() { familyMembers = dataProtocol.getFamilyMembers() + collectionView.reloadData() + } - DispatchQueue.main.async { [weak self] in - self?.collectionView.reloadData() - } - - // Fetch updated family members from Firestore + private func fetchFamilyMembersFromFirestore() { guard let patientId = Auth.auth().currentUser?.uid else { print("Patient not logged in.") return @@ -64,19 +73,65 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, print("Error fetching family members: \(error.localizedDescription)") } else if let members = members { self.familyMembers = members - self.dataProtocol.saveFamilyMembers(members) // Bulk save family members + self.dataProtocol.saveFamilyMembers(members) DispatchQueue.main.async { self.collectionView.reloadData() } } } } + private func setupRealTimeFamilyMemberUpdates() { + guard let patientId = Auth.auth().currentUser?.uid else { + print("Patient not logged in.") + return + } + + let familyMemberCollection = FirebaseManager.shared.firestore + .collection(Constants.FirestoreKeys.usersCollection) + .document(patientId) + .collection(Constants.FirestoreKeys.familyMembersCollection) + + familyMemberCollection.addSnapshotListener { [weak self] snapshot, error in + guard let self = self else { return } + + if let error = error { + print("Error listening to changes: \(error.localizedDescription)") + return + } + + guard let documents = snapshot?.documents else { + print("No documents found.") + return + } + + self.familyMembers = documents.compactMap { doc -> FamilyMember? in + let data = doc.data() + return FamilyMember( + id: doc.documentID, + name: data["name"] as? String ?? "", + relationship: data["relationship"] as? String ?? "", + phone: data["phone"] as? String ?? "", + email: data["email"] as? String ?? "", + password: data["password"] as? String ?? "", + imageName: data["imageName"] as? String ?? "", + imageURL: data["imageURL"] as? String ?? "" + ) + } + + self.dataProtocol.saveFamilyMembers(self.familyMembers) + + DispatchQueue.main.async { + self.collectionView.reloadData() // Update UI immediately + } + } + } private func setupNotifications() { NotificationCenter.default.addObserver( self, selector: #selector(handleFamilyMemberAdded), - name: Notification.Name("FamilyMemberAdded"), + name: Notification + .Name(Constants.NotificationNames.FamilyMemberAdded), object: nil ) } @@ -105,32 +160,20 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) - GradientBackground() setupFloatingButton() NotificationCenter.default.addObserver( self, selector: #selector(handleFamilyMemberAdded), - name: Notification.Name("FamilyMemberAdded"), + name: Notification + .Name(Constants.NotificationNames.FamilyMemberAdded), object: nil ) } @objc private func handleFamilyMemberAdded() { print("Handling family member added...") - loadFamilyMembers() - } - - private func GradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor.systemOrange.withAlphaComponent(0.1).cgColor, - UIColor.systemBackground.cgColor, - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 0, y: 0.6) - gradientLayer.frame = view.bounds - - view.layer.insertSublayer(gradientLayer, at: 0) + loadFamilyMembersFromCache() // Load instantly after adding a family member + fetchFamilyMembersFromFirestore() // Sync in background } private func setupFloatingButton() { @@ -138,7 +181,7 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .medium) let image = UIImage(systemName: "plus.circle.fill", withConfiguration: config) button.setImage(image, for: .normal) - button.tintColor = .systemBlue + button.tintColor = AppColors.iconColor button.addTarget(self, action: #selector(didTapAdd), for: .touchUpInside) view.addSubview(button) @@ -164,7 +207,6 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, present(navController, animated: true, completion: nil) } - // MARK: - Collection View Data Source func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return familyMembers.count @@ -206,7 +248,6 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, let memberToDelete = familyMembers[indexPath.row] - // Show confirmation alert let alert = UIAlertController(title: "Delete Family Member", message: "Are you sure you want to delete \(memberToDelete.name)?", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in @@ -221,17 +262,21 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, return } - // Start by deleting the family member from Firebase FirebaseManager.shared.deleteFamilyMember(for: patientId, memberId: member.id) { [weak self] error in - if let error = error { - print("Failed to delete family member: \(error.localizedDescription)") - self?.showAlert(title: "Error", message: "Failed to delete family member.") - } else { - print("Family member deleted successfully") - self?.familyMembers.remove(at: indexPath.row) - self?.dataProtocol.saveFamilyMembers(self?.familyMembers ?? []) - DispatchQueue.main.async { - self?.collectionView.deleteItems(at: [indexPath]) + guard let self = self else { return } + + DispatchQueue.main.async { + if let error = error { + print("Failed to delete family member: \(error.localizedDescription)") + self.showAlert(title: "Error", message: "Failed to delete family member.") + } else { + print("Family member deleted successfully") + // Make sure the index is still valid + if indexPath.row < self.familyMembers.count { + self.familyMembers.remove(at: indexPath.row) + self.dataProtocol.saveFamilyMembers(self.familyMembers) + self.collectionView.deleteItems(at: [indexPath]) + } } } } @@ -239,10 +284,10 @@ class FamilyViewController_patient: UIViewController, UICollectionViewDelegate, override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - loadFamilyMembers() +// loadFamilyMembers() } } -#Preview{ +#Preview { FamilyViewController_patient() } diff --git a/recap/Controllers/Core/HomeViewController.swift b/recap/Controllers/Core/HomeViewController.swift old mode 100644 new mode 100755 index 01c734e..65a173f --- a/recap/Controllers/Core/HomeViewController.swift +++ b/recap/Controllers/Core/HomeViewController.swift @@ -4,248 +4,203 @@ // // Created by Diptayan Jash on 05/11/24. // +// import UIKit -import Foundation -import SDWebImage +import GoogleSignIn + class HomeViewController: UIViewController { + var analyticsService: CoreAnalyticsService? + var sessionStartTime: Date? + private var preloadedArticles: [Article] = [] - override func viewDidLoad() { - super.viewDidLoad() - applyGradientBackground() - setupNavigationBar() - setupDelegates() - setupLayout() - } - - private lazy var collectionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal - layout.minimumLineSpacing = 16 - layout.sectionInset = UIEdgeInsets(top: 8, left: 10, bottom: 8, right: 10) - layout.itemSize = CGSize(width: (view.frame.width - 60) / 2, height: 200) - - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.register(CardCell.self, forCellWithReuseIdentifier: "CardCell") - collectionView.backgroundColor = .clear - collectionView.isScrollEnabled = false - return collectionView - }() + private let scrollView = UIScrollView() + private let contentView = UIStackView() private let activitiesTitleLabel: UILabel = { let label = UILabel() label.text = "Activities" label.font = .systemFont(ofSize: 24, weight: .bold) - label.textColor = .label + label.textAlignment = .left + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) return label }() - - private let tableView: UITableView = { - let tableView = UITableView() - tableView.register(ActivityCell.self, forCellReuseIdentifier: "ActivityCell") - tableView.isScrollEnabled = false - tableView.separatorStyle = .none - tableView.backgroundColor = .clear - return tableView - }() + private lazy var profileButton: UIButton = { let button = UIButton(type: .system) let config = UIImage.SymbolConfiguration(pointSize: 28, weight: .medium) - let image = UIImage( - systemName: "person.circle.fill", - withConfiguration: config - ) + let image = UIImage(systemName: "person.circle.fill", withConfiguration: config) button.setImage(image, for: .normal) - button.tintColor = .systemGreen + button.tintColor = AppColors.iconColor button.addTarget(self, action: #selector(profileButtonTapped), for: .touchUpInside) return button + + if let verifiedUserDocID = UserDefaults.standard.string(forKey: Constants.UserDefaultsKeys.verifiedUserDocID) { + analyticsService = CoreAnalyticsService() + } }() - //MARK: - needs to be fixed and checked -// override func viewDidLayoutSubviews() { -// super.viewDidLayoutSubviews() -// let accessoryView = UIButton() -// let image = UIImage(named:"avatar") -// -// accessoryView.setImage(image, for: .normal) -// accessoryView.frame.size = CGSize(width: 34, height: 34) -// let largeTitleView = navigationController?.navigationBar.subviews.first { subview in -// return String(describing: type(of: subview)) == "_UINavigationBarLargeTitleView" -// } -// largeTitleView?.perform (Selector(("setAccessoryView:")), with: accessoryView) -// largeTitleView?.perform (Selector(("setAlignAccessoryViewToTitleBaseline:")), with: nil) -// largeTitleView?.perform(Selector (("updateContent") )) -// } - - // MARK: - This works -// override func viewDidAppear(_ animated: Bool) { -// let accessoryView = UIButton() -// let image = UIImage(named:"dj") -// -// accessoryView.setImage(image, for: .normal) -// accessoryView.frame.size = CGSize(width: 34, height: 34) -// accessoryView.clipsToBounds = true -// accessoryView.addTarget(self, action: #selector(profileButtonTapped), for: .touchUpInside) -// let largeTitleView = navigationController?.navigationBar.subviews.first { subview in -// return String(describing: type(of: subview)) == "_UINavigationBarLargeTitleView" -// } -// largeTitleView?.perform (Selector(("setAccessoryView:")), with: accessoryView) -// largeTitleView?.perform (Selector(("setAlignAccessoryViewToTitleBaseline:")), with: nil) -// largeTitleView?.perform(Selector (("updateContent") )) -// } + override func viewDidLoad() { + super.viewDidLoad() + applyGradientBackground() + setupNavigationBar() + setupScrollView() + setupContent() + prefetchArticles() // Prefetch articles + } + private func prefetchArticles() { + let dataFetch = DataFetch() + dataFetch.fetchArticles { [weak self] fetchedArticles, error in + guard let self = self else { return } + DispatchQueue.main.async { + if let fetchedArticles = fetchedArticles { + self.preloadedArticles = fetchedArticles + } + } + } + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + sessionStartTime = Date() + } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if let startTime = sessionStartTime { + let sessionDuration = Date().timeIntervalSince(startTime) / 60 + analyticsService?.trackTimeSpent(sessionDuration: sessionDuration, isFamily: false) + } + } + private func setupNavigationBar() { let profileBarButton = UIBarButtonItem(customView: profileButton) navigationItem.rightBarButtonItem = profileBarButton - navigationController?.navigationBar.prefersLargeTitles = true - title = "Home" + title = "Recap" } - + @objc private func profileButtonTapped() { let profileVC = ProfileViewController() let navController = UINavigationController(rootViewController: profileVC) present(navController, animated: true) - -// let userDetailsVC = patientInfo() -// let nav = UINavigationController(rootViewController: userDetailsVC) -// present(nav, animated: true) - -// let storyboard = UIStoryboard(name: "ProfileSection", bundle: nil) -// if let profileVC = storyboard.instantiateViewController(withIdentifier: "ProfileViewController2") as? ProfileViewController2 { -// self.present(profileVC, animated: true, completion: nil) -// } } - - private func setupLayout() { - view.addSubview(collectionView) - view.addSubview(activitiesTitleLabel) - view.addSubview(tableView) - GradientBackground() + + private func setupScrollView() { + view.addSubview(scrollView) + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) - collectionView.translatesAutoresizingMaskIntoConstraints = false - activitiesTitleLabel.translatesAutoresizingMaskIntoConstraints = false - tableView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - collectionView.heightAnchor.constraint(equalToConstant: 200), - - activitiesTitleLabel.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 24), - activitiesTitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - activitiesTitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - - tableView.topAnchor.constraint(equalTo: activitiesTitleLabel.bottomAnchor, constant: 20), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) ]) - } - private func GradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor.systemOrange.withAlphaComponent(0.1).cgColor, - UIColor.systemBackground.cgColor, + + contentView.axis = .vertical + contentView.spacing = 16 + contentView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate( +[ + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16), + contentView.leadingAnchor + .constraint( + equalTo: scrollView.leadingAnchor, + constant: Constants + .paddingKeys.DefaultPaddingLeft), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: Constants + .paddingKeys.DefaultPaddingRight), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: Constants + .paddingKeys.DefaultPaddingBottom), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -32) ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 0, y: 0.6) - gradientLayer.frame = view.bounds - - view.layer.insertSublayer(gradientLayer, at: 0) +) } - - private func setupDelegates() { - collectionView.delegate = self - collectionView.dataSource = self - tableView.delegate = self - tableView.dataSource = self - tableView.rowHeight = 164 - } -} - -// MARK: - UICollectionViewDelegate and DataSource - -extension HomeViewController: UICollectionViewDelegate, UICollectionViewDataSource { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return 2 - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCell", for: indexPath) as! CardCell + + private func setupContent() { + contentView.spacing = Constants.paddingKeys.DefaultPaddingLeft+8 + let questionsCard = QuestionsCardView() + let streaksCard = StreakCardView() + let letsReadCard = LetsReadCardView() - if indexPath.item == 0 { - cell.configure(with: "Questions", description: "Boost your memory by up to 20%", image: UIImage(named: "oldMan")) - } else { - cell.configure(with: "Streaks", description: "See how active you are", image: UIImage(named: "cosmonaut")) + [questionsCard, streaksCard].forEach { item in + item.translatesAutoresizingMaskIntoConstraints = false + contentView.addArrangedSubview(item) + NSLayoutConstraint.activate( +[ + item.heightAnchor + .constraint( + equalToConstant: Constants.CardSize.DefaultCardHeight + ) + ] +) } - return cell - } - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - DispatchQueue.main.asyncAfter(deadline: .now()) { - if let verifiedUserDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") { - if indexPath.item == 0 { - let questionsVC = PatientQuestionsViewController(verifiedUserDocID: verifiedUserDocID) - self.navigationController?.pushViewController(questionsVC, animated: true) - } else { - let streaksVC = StreaksViewController(verifiedUserDocID: verifiedUserDocID) - self.navigationController?.pushViewController(streaksVC, animated: true) - } - } else { - print("Error: verifiedUserDocID not found in UserDefaults.") - } +// contentView.addArrangedSubview(activitiesTitleLabel) + + [letsReadCard, + ] + .forEach { item in + item.translatesAutoresizingMaskIntoConstraints = false + contentView.addArrangedSubview(item) + NSLayoutConstraint.activate([ + item.heightAnchor.constraint(equalToConstant: Constants.CardSize.DefaultCardHeight) + ]) } + + addTapGesture(to: questionsCard, action: #selector(navigateToQuestions)) + addTapGesture(to: streaksCard, action: #selector(navigateToStreaks)) + addTapGesture(to: letsReadCard, action: #selector(navigateToLetsRead)) } -} - -// MARK: - UITableViewDelegate and DataSource - -extension HomeViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 2 + private func addTapGesture(to view: UIView, action: Selector) { + let tapGesture = UITapGestureRecognizer(target: self, action: action) + view.isUserInteractionEnabled = true + view.addGestureRecognizer(tapGesture) } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! ActivityCell - if indexPath.row == 0 { - cell.configure(with: "Let's Read", description: "Reduces memory decline by 30%.", icon: UIImage(named: "BigShoesTorso")) + + @objc private func navigateToQuestions() { + if let verifiedUserDocID = UserDefaults.standard.string( + forKey: Constants + .UserDefaultsKeys.verifiedUserDocID) { + let questionsVC = PatientQuestionViewController(verifiedUserDocID: verifiedUserDocID) + navigationController?.pushViewController(questionsVC, animated: true) } else { - cell.configure(with: "Play a Game", description: "Helps you to reduce memory decline by 32%.", icon: UIImage(named: "Game")) + print("Error: verifiedUserDocID not found in UserDefaults.") } - return cell } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - DispatchQueue.main.asyncAfter(deadline: .now()) { - if indexPath.row == 0 { - let letsReadVC = ArticleTableViewController() - self.navigationController?.pushViewController(letsReadVC, animated: true) - } else { - let playGameVC = PlayGameViewController() - self.navigationController?.pushViewController(playGameVC, animated: true) - } + + @objc private func navigateToStreaks() { + if let verifiedUserDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") { + let streaksVC = StreaksViewController(verifiedUserDocID: verifiedUserDocID) + navigationController?.pushViewController(streaksVC, animated: true) + } else { + print("Error: verifiedUserDocID not found in UserDefaults.") } } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 160 + + @objc private func navigateToLetsRead() { + let articlesVC = ArticleTableViewController(preloadedArticles: preloadedArticles) + navigationController?.pushViewController(articlesVC, animated: true) } private func applyGradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, - UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 1, y: 1) - gradientLayer.frame = view.bounds - view.layer.insertSublayer(gradientLayer, at: 0) - } + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [ + UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, + UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 1, y: 1) + gradientLayer.frame = view.bounds + view.layer.insertSublayer(gradientLayer, at: 0) + } } + + #Preview() { HomeViewController() diff --git a/recap/Controllers/Core/ProfileSection.storyboard b/recap/Controllers/Core/ProfileSection.storyboard deleted file mode 100644 index 675bfbb..0000000 --- a/recap/Controllers/Core/ProfileSection.storyboard +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/recap/Controllers/Core/ProfileViewController.swift b/recap/Controllers/Core/ProfileViewController.swift old mode 100644 new mode 100755 index 49551a1..992e915 --- a/recap/Controllers/Core/ProfileViewController.swift +++ b/recap/Controllers/Core/ProfileViewController.swift @@ -1,9 +1,352 @@ +//// +//// ProfileViewController.swift +//// recap +//// +//// Created by Diptayan Jash on 11/11/24. // -// ProfileViewController.swift -// recap // -// Created by Diptayan Jash on 11/11/24. +//import Foundation +//import SDWebImage +//import UIKit +//import FirebaseAuth +//import FirebaseFirestore // +//class ProfileViewController: UIViewController { +// private var userDetails: [String: Any]? +// private let dataFetchManager: DataFetchProtocol = DataFetch() +// +// private var dataProtocol: FamilyStorageProtocol +// +// init( +// storage: FamilyStorageProtocol = UserDefaultsStorageFamilyMember.shared +// ) { +// dataProtocol = storage +// super.init(nibName: nil, bundle: nil) +// } +// +// required init?(coder: NSCoder) { +// dataProtocol = UserDefaultsStorageFamilyMember.shared +// super.init(coder: coder) +// } +// +// private let profileImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.image = UIImage(systemName: "person.circle") +// imageView.contentMode = .scaleAspectFill +// imageView.layer.cornerRadius = 10 +// imageView.clipsToBounds = true +// imageView.translatesAutoresizingMaskIntoConstraints = false +// return imageView +// }() +// +// private let nameLabel: UILabel = { +// let label = UILabel() +// label.text = "Diptayan Jash" +// label.font = Constants.FontandColors.titleFont +// label.textAlignment = .center +// label.translatesAutoresizingMaskIntoConstraints = false +// return label +// }() +// +// private let uidCardView: UIView = { +// let view = UIView() +// view.backgroundColor = AppColors.primaryButtonColor +// view.layer.cornerRadius = 10 +// view.translatesAutoresizingMaskIntoConstraints = false +// return view +// }() +// +// private let patientIDLabel: UILabel = { +// let label = UILabel() +// label.text = "Patient ID:" +// label.font = .systemFont(ofSize: 16, weight: .medium) +// label.textColor = .label +// label.translatesAutoresizingMaskIntoConstraints = false +// return label +// }() +// +// private let uidLabel: UILabel = { +// let label = UILabel() +// label.text = "Not Set" +// label.font = .systemFont(ofSize: 16) +// label.textColor = .label +// label.translatesAutoresizingMaskIntoConstraints = false +// return label +// }() +// +// private let copyButton: UIButton = { +// let button = UIButton(type: .system) +// button.setImage(UIImage(systemName: "doc.on.doc"), for: .normal) +// button.tintColor = AppColors.iconColor +// button.translatesAutoresizingMaskIntoConstraints = false +// return button +// }() +// +// private let tableView: UITableView = { +// let table = UITableView(frame: .zero, style: .insetGrouped) +// table.translatesAutoresizingMaskIntoConstraints = false +// return table +// }() +// +// private var prefetchedQuestions: [rapiMemory]? +// +// override func viewDidLoad() { +// super.viewDidLoad() +// loadUserProfile() +// view.backgroundColor = .systemBackground +// setupNavigationBar() +// setupUI() +// setupTableView() +// prefetchQuestions() +// } +// +// private func loadUserProfile() { +// guard let userId = Auth.auth().currentUser?.uid else { +// print("User not logged in.") +// return +// } +// +// dataFetchManager.fetchUserProfile(userId: userId) { [weak self] userProfile, error in +// guard let self = self else { return } +// +// if let error = error { +// print("Error loading profile: \(error.localizedDescription)") +// self.showAlert(message: "Failed to load profile.") +// return +// } +// +// if let profile = userProfile { +// print("Profile fetched: \(profile)") +// UserDefaultsStorageProfile.shared.saveProfile(details: profile, image: nil) { success in +// if success { +// self.updateUI(with: profile) +// self.dataFetchManager.fetchLastMemoryCheck(userId: userId) { [weak self] date in +// DispatchQueue.main.async { +// self?.updateMemoryCheckDate(date) +// } +// } +// } else { +// print("Failed to save profile locally.") +// } +// } +// } +// } +// } +// +// private func updateUI(with details: [String: Any]) { +// nameLabel.text = "\(details["firstName"] as? String ?? "Not Set") \(details["lastName"] as? String ?? "Not Set")" +// uidLabel.text = details["patientUID"] as? String ?? "Not Set" +// +// if let profileImageURL = details["profileImageURL"] as? String, !profileImageURL.isEmpty, +// let url = URL(string: profileImageURL) { +// profileImageView.sd_setImage(with: url, placeholderImage: UIImage(named: "person.circle")) +// } else { +// profileImageView.image = UIImage(named: "person.circle") +// } +// +// tableView.reloadData() +// } +// +// private func prefetchQuestions() { +// dataFetchManager.fetchRapidQuestions { [weak self] questions, _ in +// if let questions = questions { +// self?.prefetchedQuestions = questions +// } +// } +// } +// +// private func setupNavigationBar() { +// navigationItem.largeTitleDisplayMode = .never +// let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneButtonTapped)) +// navigationItem.rightBarButtonItem = doneButton +// } +// +// @objc private func doneButtonTapped() { +// dismiss(animated: true) +// } +// +// private func setupUI() { +// [profileImageView, nameLabel, uidCardView, tableView].forEach { +// view.addSubview($0) +// } +// +// [patientIDLabel, uidLabel, copyButton].forEach { +// uidCardView.addSubview($0) +// } +// +// copyButton.addTarget(self, action: #selector(copyUIDTapped), for: .touchUpInside) +// +// profileImageView.layer.cornerRadius = 50 +// profileImageView.layer.masksToBounds = true +// +// NSLayoutConstraint.activate([ +// profileImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), +// profileImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), +// profileImageView.widthAnchor.constraint(equalToConstant: 95), +// profileImageView.heightAnchor.constraint(equalToConstant: 95), +// +// nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16), +// nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), +// nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), +// +// uidCardView.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 10), +// uidCardView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), +// uidCardView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), +// uidCardView.heightAnchor.constraint(equalToConstant: 44), +// +// patientIDLabel.centerYAnchor.constraint(equalTo: uidCardView.centerYAnchor), +// patientIDLabel.leadingAnchor.constraint(equalTo: uidCardView.leadingAnchor, constant: 16), +// +// uidLabel.centerYAnchor.constraint(equalTo: uidCardView.centerYAnchor), +// uidLabel.leadingAnchor.constraint(equalTo: patientIDLabel.trailingAnchor, constant: 8), +// uidLabel.trailingAnchor.constraint(lessThanOrEqualTo: copyButton.leadingAnchor, constant: -8), +// +// copyButton.centerYAnchor.constraint(equalTo: uidCardView.centerYAnchor), +// copyButton.trailingAnchor.constraint(equalTo: uidCardView.trailingAnchor, constant: -16), +// copyButton.widthAnchor.constraint(equalToConstant: 30), +// copyButton.heightAnchor.constraint(equalToConstant: 30), +// +// tableView.topAnchor.constraint(equalTo: uidCardView.bottomAnchor, constant: 20), +// tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// } +// +// private func setupTableView() { +// tableView.delegate = self +// tableView.dataSource = self +// tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") +// } +// +// @objc private func copyUIDTapped() { +// if let uid = UserDefaultsStorageProfile.shared.getProfile()?["patientUID"] as? String { +// UIPasteboard.general.string = uid +// showCopyConfirmation() +// } +// } +// +// private func showCopyConfirmation() { +// let alert = UIAlertController(title: "Copied!", message: "Patient ID copied to clipboard.", preferredStyle: .alert) +// alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) +// present(alert, animated: true) +// } +//} +// +//// MARK: - UITableViewDelegate & DataSource +// +//extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { +// func numberOfSections(in tableView: UITableView) -> Int { +// return 3 +// } +// +// func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { +// switch section { +// case 0: return 6 +// case 1: return 1 +// case 2: return 1 +// default: return 0 +// } +// } +// +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell") +// +// if indexPath.section == 1 { +// cell.accessoryType = .disclosureIndicator +// } else { +// cell.accessoryType = .none +// } +// +// switch indexPath.section { +// case 0: +// let titles = [ +// "First Name", +// "Last Name", +// "Date of Birth", +// "Sex", +// "Blood Type", +// "Stage", +// ] +// let values: [String] = { +// if let details = UserDefaultsStorageProfile.shared.getProfile() { +// return [ +// details["firstName"] as? String ?? "Not Set", +// details["lastName"] as? String ?? "Not Set", +// details["dateOfBirth"] as? String ?? "Not Set", +// details["sex"] as? String ?? "Not Set", +// details["bloodGroup"] as? String ?? "Not Set", +// details["stage"] as? String ?? "Not Set", +// ] +// } +// return Array(repeating: "Not Set", count: 6) +// }() +// +// cell.textLabel?.text = titles[indexPath.row] +// cell.detailTextLabel?.text = values[indexPath.row] +// cell.textLabel?.font = .systemFont(ofSize: 17) +// cell.selectionStyle = .none +// +// case 1: +// cell.textLabel?.text = "Memory Check" +// cell.detailTextLabel?.text = "Last check: Fetching" +// cell.imageView?.image = UIImage(systemName: "brain.head.profile") +// cell.imageView?.tintColor = AppColors.iconColor +// cell.selectionStyle = .default +// +// case 2: +// cell.textLabel?.text = "Logout" +// cell.textLabel?.textColor = .white +// cell.textLabel?.font = .boldSystemFont(ofSize: 17) +// cell.backgroundColor = .systemRed +// cell.textLabel?.textAlignment = .center +// cell.selectionStyle = .default +// +// default: +// break +// } +// +// return cell +// } +// +// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// tableView.deselectRow(at: indexPath, animated: true) +// +// if indexPath.section == 1 && indexPath.row == 0 { +// let memoryCheckVC = MemoryCheckViewController() +// memoryCheckVC.preloadedQuestions = prefetchedQuestions +// navigationController?.pushViewController(memoryCheckVC, animated: true) +// } else if indexPath.section == 2 { +// let loginVC = PatientLoginViewController() +// loginVC.logoutTapped() +// } +// } +// +// func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { +// switch section { +// case 0: return "Personal Information" +// case 1: return "Health" +// default: return nil +// } +// } +// +// func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { +// switch section { +// case 0: return "This information helps us personalize your experience." +// case 1: return "Regular memory checks help track your progress." +// default: return nil +// } +// } +// +// private func updateMemoryCheckDate(_ date: String) { +// let indexPath = IndexPath(row: 0, section: 1) +// if let cell = tableView.cellForRow(at: indexPath) { +// cell.detailTextLabel?.text = "Last check: \(date)" +// } +// } +//} +//#Preview {ProfileViewController()} + import Foundation import SDWebImage import UIKit @@ -30,39 +373,74 @@ class ProfileViewController: UIViewController { private let profileImageView: UIImageView = { let imageView = UIImageView() - imageView.sd_setImage( - with: URL(string: "https://portfoliodata.djdiptayan.in/profile_pics/dj.png"), - placeholderImage: UIImage(named: "person.circle"), - options: [.retryFailed, .highPriority], - completed: { _, error, _, _ in - if error != nil { - imageView.image = UIImage(named: "person.circle") - } - } - ) + imageView.image = UIImage(systemName: "person.circle") imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = 10 imageView.clipsToBounds = true - imageView.widthAnchor.constraint(equalToConstant: 95).isActive = true - imageView.heightAnchor.constraint(equalToConstant: 95).isActive = true imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView }() private let nameLabel: UILabel = { let label = UILabel() label.text = "Diptayan Jash" - label.font = .systemFont(ofSize: 20, weight: .semibold) + label.font = Constants.FontandColors.titleFont label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let uidCardView: UIView = { + let view = UIView() + view.backgroundColor = AppColors.primaryButtonColor + view.layer.cornerRadius = 10 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let patientIDLabel: UILabel = { + let label = UILabel() + label.text = "Patient ID:" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = AppColors.iconColor + label.translatesAutoresizingMaskIntoConstraints = false return label }() + private let uidLabel: UILabel = { + let label = UILabel() + label.text = "Not Set" + label.font = .systemFont(ofSize: 16) + label.textColor = AppColors.iconColor + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let copyButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(systemName: "doc.on.doc"), for: .normal) + button.tintColor = AppColors.iconColor + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + private let tableView: UITableView = { let table = UITableView(frame: .zero, style: .insetGrouped) + table.translatesAutoresizingMaskIntoConstraints = false return table }() + private let logoutButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Logout", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.backgroundColor = AppColors.iconColor.withAlphaComponent(0.8) + button.setTitleColor(.white, for: .normal) + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + return button + }() + private var prefetchedQuestions: [rapiMemory]? override func viewDidLoad() { @@ -92,8 +470,6 @@ class ProfileViewController: UIViewController { if let profile = userProfile { print("Profile fetched: \(profile)") - - // Save the profile locally UserDefaultsStorageProfile.shared.saveProfile(details: profile, image: nil) { success in if success { self.updateUI(with: profile) @@ -112,6 +488,7 @@ class ProfileViewController: UIViewController { private func updateUI(with details: [String: Any]) { nameLabel.text = "\(details["firstName"] as? String ?? "Not Set") \(details["lastName"] as? String ?? "Not Set")" + uidLabel.text = details["patientUID"] as? String ?? "Not Set" if let profileImageURL = details["profileImageURL"] as? String, !profileImageURL.isEmpty, let url = URL(string: profileImageURL) { @@ -135,6 +512,7 @@ class ProfileViewController: UIViewController { navigationItem.largeTitleDisplayMode = .never let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneButtonTapped)) navigationItem.rightBarButtonItem = doneButton + doneButton.tintColor = AppColors.iconColor } @objc private func doneButtonTapped() { @@ -142,11 +520,17 @@ class ProfileViewController: UIViewController { } private func setupUI() { - [profileImageView, nameLabel, tableView].forEach { + [profileImageView, nameLabel, uidCardView, tableView, logoutButton].forEach { view.addSubview($0) - $0.translatesAutoresizingMaskIntoConstraints = false } + [patientIDLabel, uidLabel, copyButton].forEach { + uidCardView.addSubview($0) + } + + copyButton.addTarget(self, action: #selector(copyUIDTapped), for: .touchUpInside) + logoutButton.addTarget(self, action: #selector(logoutTapped), for: .touchUpInside) + profileImageView.layer.cornerRadius = 50 profileImageView.layer.masksToBounds = true @@ -160,10 +544,32 @@ class ProfileViewController: UIViewController { nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - tableView.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20), + uidCardView.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 10), + uidCardView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + uidCardView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + uidCardView.heightAnchor.constraint(equalToConstant: 44), + + patientIDLabel.centerYAnchor.constraint(equalTo: uidCardView.centerYAnchor), + patientIDLabel.leadingAnchor.constraint(equalTo: uidCardView.leadingAnchor, constant: 16), + + uidLabel.centerYAnchor.constraint(equalTo: uidCardView.centerYAnchor), + uidLabel.leadingAnchor.constraint(equalTo: patientIDLabel.trailingAnchor, constant: 8), + uidLabel.trailingAnchor.constraint(lessThanOrEqualTo: copyButton.leadingAnchor, constant: -8), + + copyButton.centerYAnchor.constraint(equalTo: uidCardView.centerYAnchor), + copyButton.trailingAnchor.constraint(equalTo: uidCardView.trailingAnchor, constant: -16), + copyButton.widthAnchor.constraint(equalToConstant: 30), + copyButton.heightAnchor.constraint(equalToConstant: 30), + + tableView.topAnchor.constraint(equalTo: uidCardView.bottomAnchor, constant: 20), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.bottomAnchor.constraint(equalTo: logoutButton.topAnchor, constant: -20), + + logoutButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + logoutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + logoutButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + logoutButton.heightAnchor.constraint(equalToConstant: 50), ]) } @@ -173,28 +579,36 @@ class ProfileViewController: UIViewController { tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } -// private func prefetchQuestions() { -// let dataFetch = DataFetch() -// dataFetch.fetchRapidQuestions { [weak self] questions, _ in -// if let questions = questions { -// self?.prefetchedQuestions = questions -// } -// } -// } + @objc private func copyUIDTapped() { + if let uid = UserDefaultsStorageProfile.shared.getProfile()?["patientUID"] as? String { + UIPasteboard.general.string = uid + showCopyConfirmation() + } + } + + @objc private func logoutTapped() { + let loginVC = PatientLoginViewController() + loginVC.logoutTapped() + } + + private func showCopyConfirmation() { + let alert = UIAlertController(title: "Copied!", message: "Patient ID copied to clipboard.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + present(alert, animated: true) + } } // MARK: - UITableViewDelegate & DataSource extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - return 3 + return 2 // Personal Information and Health } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { - case 0: return 7 + case 0: return 6 case 1: return 1 - case 2: return 1 default: return 0 } } @@ -213,41 +627,23 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { let titles = [ "First Name", "Last Name", - "UID", "Date of Birth", "Sex", -// "Age", "Blood Type", "Stage", ] -// let values: [String] = { -// if let details = dataProtocol.getProfile() { -// return [ -// details["firstName"] as? String ?? "Not Set", -// details["lastName"] as? String ?? "Not Set", -// details["dateOfBirth"] as? String ?? "Not Set", -// details["sex"] as? String ?? "Not Set", -// details["bloodGroup"] as? String ?? "Not Set", -// details["stage"] as? String ?? "Not Set", -// ] -// } -// return Array(repeating: "Not Set", count: 6) -// }() - UserDefaultsStorageProfile.shared.getProfile() let values: [String] = { if let details = UserDefaultsStorageProfile.shared.getProfile() { return [ details["firstName"] as? String ?? "Not Set", details["lastName"] as? String ?? "Not Set", - details["patientUID"] as? String ?? "Not Set", details["dateOfBirth"] as? String ?? "Not Set", details["sex"] as? String ?? "Not Set", -// details["age"] as? String ?? "Not Set", details["bloodGroup"] as? String ?? "Not Set", details["stage"] as? String ?? "Not Set", ] } - return Array(repeating: "Not Set", count: 7) + return Array(repeating: "Not Set", count: 6) }() cell.textLabel?.text = titles[indexPath.row] @@ -259,15 +655,7 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { cell.textLabel?.text = "Memory Check" cell.detailTextLabel?.text = "Last check: Fetching" cell.imageView?.image = UIImage(systemName: "brain.head.profile") - cell.imageView?.tintColor = .systemGreen - cell.selectionStyle = .default - - case 2: - cell.textLabel?.text = "Logout" - cell.textLabel?.textColor = .white - cell.textLabel?.font = .boldSystemFont(ofSize: 17) - cell.backgroundColor = .systemRed - cell.textLabel?.textAlignment = .center + cell.imageView?.tintColor = AppColors.iconColor cell.selectionStyle = .default default: @@ -281,16 +669,9 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { tableView.deselectRow(at: indexPath, animated: true) if indexPath.section == 1 && indexPath.row == 0 { - // Memory Check cell tapped let memoryCheckVC = MemoryCheckViewController() - - // Pass the pre-fetched questions memoryCheckVC.preloadedQuestions = prefetchedQuestions - navigationController?.pushViewController(memoryCheckVC, animated: true) - } else if indexPath.section == 2 { - let loginVC = PatientLoginViewController() - loginVC.logoutTapped() } } diff --git a/recap/Controllers/Core/TabbarViewController.swift b/recap/Controllers/Core/TabbarViewController.swift old mode 100644 new mode 100755 index 4b39b62..83fb044 --- a/recap/Controllers/Core/TabbarViewController.swift +++ b/recap/Controllers/Core/TabbarViewController.swift @@ -8,51 +8,70 @@ import UIKit class TabbarViewController: UITabBarController, UITabBarControllerDelegate { + var analyticsService: CoreAnalyticsService? + var verifiedUserDocID: String? override func viewDidLoad() { super.viewDidLoad() self.delegate = self - - // Set the background color to white - view.backgroundColor = .white - + + let grayColor = UIColor(white: 0.95, alpha: 0.85) + + tabBar.backgroundColor = grayColor + tabBar.barTintColor = grayColor + tabBar.isTranslucent = true + // Setup View Controllers let tab1 = HomeViewController() let tab2 = FamilyViewController_patient() + let tab3 = PlayGameViewController() - tab1.title = "Home" + tab1.title = "Recap" tab2.title = "Family" + tab3.title = "Games" tab1.navigationItem.largeTitleDisplayMode = .always tab2.navigationItem.largeTitleDisplayMode = .always + tab3.navigationItem.largeTitleDisplayMode = .always let nav1 = UINavigationController(rootViewController: tab1) let nav2 = UINavigationController(rootViewController: tab2) + let nav3 = UINavigationController(rootViewController: tab3) - nav1.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) + nav1.tabBarItem = UITabBarItem(title: "Recap", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) nav2.tabBarItem = UITabBarItem(title: "Family", image: UIImage(systemName: "person.2"), selectedImage: UIImage(systemName: "person.2.fill")) + nav3.tabBarItem = UITabBarItem(title: "Games", image: UIImage(systemName: "gamecontroller"), selectedImage: UIImage(systemName: "gamecontroller.fill")) nav1.navigationBar.prefersLargeTitles = true nav2.navigationBar.prefersLargeTitles = true + nav3.navigationBar.prefersLargeTitles = true - tabBar.tintColor = .label - setViewControllers([nav1, nav2], animated: true) + tabBar.tintColor = AppColors.iconColor + setViewControllers([nav1, nav2, nav3], animated: true) + + fetchVerifiedUserID() // First, update `verifiedUserDocID` + if let userID = verifiedUserDocID { + print("📌 Initializing CoreAnalyticsService with User ID: \(userID)") + analyticsService = CoreAnalyticsService() + analyticsService?.initializeAnalytics() + } else { + print("❌ Error: No verifiedUserDocID found in UserDefaults") + } } + private func fetchVerifiedUserID() { + verifiedUserDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") + if let userID = verifiedUserDocID { + print("✅ Verified User ID: \(userID)") + } else { + print("❌ Error: No verifiedUserDocID found in UserDefaults") + } + } + func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { let generator = UIImpactFeedbackGenerator(style: .light) generator.impactOccurred() } - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ } #Preview() { diff --git a/recap/Controllers/Games/CardGameViewController.swift b/recap/Controllers/Games/CardGameViewController.swift old mode 100644 new mode 100755 index 8fd6e16..dc23dff --- a/recap/Controllers/Games/CardGameViewController.swift +++ b/recap/Controllers/Games/CardGameViewController.swift @@ -28,7 +28,7 @@ class CardGameViewController: UIViewController { super.viewDidLoad() setupUI() resetGame() - applyGradientBackground() +// applyGradientBackground() } private func setupUI() { @@ -51,7 +51,7 @@ class CardGameViewController: UIViewController { // Higher Button higherButton.setTitle("Higher", for: .normal) higherButton.backgroundColor = .systemGreen - higherButton.layer.cornerRadius = 10 + higherButton.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius higherButton.titleLabel?.font = .boldSystemFont(ofSize: 18) higherButton.setTitleColor(.white, for: .normal) higherButton.addTarget(self, action: #selector(higherButtonTapped), for: .touchUpInside) @@ -61,7 +61,7 @@ class CardGameViewController: UIViewController { // Lower Button lowerButton.setTitle("Lower", for: .normal) lowerButton.backgroundColor = .systemRed - lowerButton.layer.cornerRadius = 10 + lowerButton.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius lowerButton.titleLabel?.font = .boldSystemFont(ofSize: 18) lowerButton.setTitleColor(.white, for: .normal) lowerButton.addTarget(self, action: #selector(lowerButtonTapped), for: .touchUpInside) @@ -141,7 +141,7 @@ class CardGameViewController: UIViewController { cardPreview.image = UIImage(named: "card_\(deck[cardIndex])") cardPreview.contentMode = .scaleAspectFit cardPreview.clipsToBounds = true - cardPreview.layer.cornerRadius = 8 + cardPreview.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius rowStackView.addArrangedSubview(cardPreview) cardIndex += 1 } diff --git a/recap/Controllers/Games/GeoSorterViewController.swift b/recap/Controllers/Games/GeoSorterViewController.swift old mode 100644 new mode 100755 index 3627d6f..708d2bd --- a/recap/Controllers/Games/GeoSorterViewController.swift +++ b/recap/Controllers/Games/GeoSorterViewController.swift @@ -1,3 +1,5 @@ +import AudioToolbox +import Foundation import UIKit class GeoSorterViewController: UIViewController { @@ -5,374 +7,507 @@ class GeoSorterViewController: UIViewController { private var locations = [ "City": ["Paris", "Tokyo", "Mumbai", "New York", "Sydney"], "State": ["California", "Texas", "Florida", "Gujarat", "Victoria"], - "Country": ["France", "Japan", "India", "USA", "Australia"] + "Country": ["France", "Japan", "India", "USA", "Australia"], ] - + private var score = 0 - private var answeredWords: [String: String] = [:] + private var feedback: String = "" + private var incorrectAttempts = 0 + + private var timer: Timer? + private var secondsElapsed = 0 + private var moves = 0 private let subtitleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16) - label.textColor = .gray + label.textColor = .secondaryLabel label.numberOfLines = 0 - label.text = "Place the City, State and Country in correct group" + label.text = "Drag each location to its correct category" + return label + }() + + private let statsView: UIView = { + let view = UIView() + view.backgroundColor = AppColors.primaryButtonColor + view.layer.cornerRadius = 12 + return view + }() + + private let timeLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 18, weight: .medium) + label.textColor = AppColors.secondaryButtonTextColor + label.text = "Time: 0s" return label }() - - private let scoreLabel: UILabel = { + + private let movesLabel: UILabel = { let label = UILabel() - label.font = .boldSystemFont(ofSize: 28) - label.textColor = .systemIndigo - label.text = "Score: 0" + label.font = .systemFont(ofSize: 18, weight: .medium) + label.textColor = AppColors.secondaryButtonTextColor + label.text = "Moves: 0" return label }() - + private lazy var categoryStacks: [CategoryStackView] = categories.map { category in let stackView = CategoryStackView(title: category) + stackView.delegate = self return stackView } - + private let wordsContainer: UIView = { let view = UIView() - view.backgroundColor = .white - view.layer.cornerRadius = 16 + view.backgroundColor = AppColors.cardBackgroundColor + view.layer.cornerRadius = 12 view.layer.shadowColor = UIColor.black.cgColor view.layer.shadowOpacity = 0.1 view.layer.shadowOffset = CGSize(width: 0, height: 2) - view.layer.shadowRadius = 4 + view.layer.shadowRadius = 6 return view }() - - private let wordsStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 15 - stackView.alignment = .fill - stackView.distribution = .fillEqually - return stackView + + private let wordsCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.minimumInteritemSpacing = 10 + layout.minimumLineSpacing = 10 + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.backgroundColor = .clear + collectionView.register(WordCell.self, forCellWithReuseIdentifier: WordCell.identifier) + collectionView.contentInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + return collectionView }() - + override func viewDidLoad() { super.viewDidLoad() - title = "Word Game" - view.backgroundColor = .systemBackground - applyGradientBackground() + title = "GeoSorter" setupUI() + setupCollectionView() populateWords() + startTimer() // Start the timer when the view loads } - - - private func applyGradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, - UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 1, y: 1) - gradientLayer.frame = view.bounds - view.layer.insertSublayer(gradientLayer, at: 0) - } - + private func setupUI() { - [scoreLabel, wordsContainer].forEach { + view.backgroundColor = .systemBackground + + [subtitleLabel, statsView, wordsContainer].forEach { view.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false } - - wordsContainer.addSubview(wordsStackView) - wordsStackView.translatesAutoresizingMaskIntoConstraints = false - + + // Create a horizontal stack view for time and moves + let statsStack = UIStackView(arrangedSubviews: [timeLabel, movesLabel]) + statsStack.axis = .horizontal + statsStack.distribution = .equalSpacing + statsStack.spacing = 20 + statsStack.translatesAutoresizingMaskIntoConstraints = false + + statsView.addSubview(statsStack) + + wordsContainer.addSubview(wordsCollectionView) + wordsCollectionView.translatesAutoresizingMaskIntoConstraints = false + let categoriesStack = UIStackView(arrangedSubviews: categoryStacks) categoriesStack.axis = .horizontal categoriesStack.distribution = .fillEqually - categoriesStack.spacing = 20 + categoriesStack.spacing = 12 categoriesStack.translatesAutoresizingMaskIntoConstraints = false - + view.addSubview(categoriesStack) - view.addSubview(subtitleLabel) - - subtitleLabel.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ - subtitleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24), + subtitleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), - - scoreLabel.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 16), - scoreLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - categoriesStack.topAnchor.constraint(equalTo: scoreLabel.bottomAnchor, constant: 20), - categoriesStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - categoriesStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - categoriesStack.heightAnchor.constraint(equalToConstant: 200), - - wordsContainer.topAnchor.constraint(equalTo: categoriesStack.bottomAnchor, constant: 20), - wordsContainer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), - wordsContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), - wordsContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), - - wordsStackView.topAnchor.constraint(equalTo: wordsContainer.topAnchor, constant: 16), - wordsStackView.leadingAnchor.constraint(equalTo: wordsContainer.leadingAnchor, constant: 16), - wordsStackView.trailingAnchor.constraint(equalTo: wordsContainer.trailingAnchor, constant: -16), - wordsStackView.bottomAnchor.constraint(equalTo: wordsContainer.bottomAnchor, constant: -16) + + statsView.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 20), + statsView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + statsView.widthAnchor.constraint(equalToConstant: 240), + statsView.heightAnchor.constraint(equalToConstant: 60), + + statsStack.centerXAnchor.constraint(equalTo: statsView.centerXAnchor), + statsStack.centerYAnchor.constraint(equalTo: statsView.centerYAnchor), + statsStack.leadingAnchor.constraint(equalTo: statsView.leadingAnchor, constant: 20), + statsStack.trailingAnchor.constraint(equalTo: statsView.trailingAnchor, constant: -20), + + categoriesStack.topAnchor.constraint(equalTo: statsView.bottomAnchor, constant: 24), + categoriesStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + categoriesStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + + wordsContainer.topAnchor.constraint(equalTo: categoriesStack.bottomAnchor, constant: 24), + wordsContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + wordsContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + wordsContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + + wordsCollectionView.topAnchor.constraint(equalTo: wordsContainer.topAnchor), + wordsCollectionView.leadingAnchor.constraint(equalTo: wordsContainer.leadingAnchor), + wordsCollectionView.trailingAnchor.constraint(equalTo: wordsContainer.trailingAnchor), + wordsCollectionView.bottomAnchor.constraint(equalTo: wordsContainer.bottomAnchor), ]) } - + + private func setupCollectionView() { + wordsCollectionView.dataSource = self + wordsCollectionView.delegate = self + wordsCollectionView.dragDelegate = self + } + private func populateWords() { - wordsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - let rowStack1 = createHorizontalWordStack() - let rowStack2 = createHorizontalWordStack() - let rowStack3 = createHorizontalWordStack() - - wordsStackView.addArrangedSubview(rowStack1) - wordsStackView.addArrangedSubview(rowStack2) - wordsStackView.addArrangedSubview(rowStack3) - + // Choose all words for a single round var allWords: [(word: String, category: String)] = [] for (category, words) in locations { - words.forEach { allWords.append(($0, category)) } + for word in words { + allWords.append((word, category)) + } } + allWords.shuffle() - - let wordsPerRow = 4 - for (index, wordInfo) in allWords.enumerated() { - let row = index / wordsPerRow - let wordLabel = createWordLabel(for: wordInfo.word, category: wordInfo.category) - - wordLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true - - switch row { - case 0: rowStack1.addArrangedSubview(wordLabel) - case 1: rowStack2.addArrangedSubview(wordLabel) - case 2: rowStack3.addArrangedSubview(wordLabel) - default: break + + // Take 5 words from each category for a balanced gameplay + var selectedWords: [(word: String, category: String)] = [] + var countPerCategory = [String: Int]() + + for word in allWords where selectedWords.count < 10 { + let category = word.category + let count = countPerCategory[category] ?? 0 + + // Take maximum 5 words per category + if count < 5 { + selectedWords.append(word) + countPerCategory[category] = count + 1 } } + + currentWords = selectedWords + wordsCollectionView.reloadData() } - - private func createHorizontalWordStack() -> UIStackView { - let stack = UIStackView() - stack.axis = .horizontal - stack.spacing = 8 - stack.distribution = .fillProportionally - stack.alignment = .center - - stack.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - stack.isLayoutMarginsRelativeArrangement = true - - return stack + + private var currentWords: [(word: String, category: String)] = [] + + private func updateScore(increase: Bool, feedback: String) { + if increase { + // Increase score by 10 points for each correct match + score += 10 + + let generator = UIImpactFeedbackGenerator(style: .light) + generator.impactOccurred() + UIView.animate(withDuration: 0.2, animations: { + self.statsView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + }) { _ in + UIView.animate(withDuration: 0.1) { + self.statsView.transform = .identity + } + } + } else { + incorrectAttempts += 1 + if incorrectAttempts >= 4 { + showHintAlert() + incorrectAttempts = 0 + } + let generator = UIImpactFeedbackGenerator(style: .medium) + generator.impactOccurred() + } + + self.feedback = feedback } - - private func createWordLabel(for word: String, category: String) -> UILabel { - let label = UILabel() - label.text = word - label.font = .systemFont(ofSize: 16, weight: .semibold) - label.textAlignment = .center - label.backgroundColor = .systemIndigo.withAlphaComponent(0.1) - label.layer.cornerRadius = 12 - label.layer.borderWidth = 1.5 - label.layer.borderColor = UIColor.systemIndigo.cgColor - label.layer.masksToBounds = true - label.isUserInteractionEnabled = true - label.tag = category.hashValue - - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.7 - label.numberOfLines = 1 - - label.setContentHuggingPriority(.required, for: .horizontal) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - - label.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12) - - label.heightAnchor.constraint(equalToConstant: 40).isActive = true - - let tap = UITapGestureRecognizer(target: self, action: #selector(wordTapped(_:))) - label.addGestureRecognizer(tap) - return label + + private func checkGameStatus() { + if currentWords.isEmpty { + gameCompleted() + } } - - @objc private func wordTapped(_ sender: UITapGestureRecognizer) { - guard let label = sender.view as? UILabel, - let word = label.text else { return } - - let correctCategory = categories.first { $0.hashValue == label.tag } ?? "" - presentCategoryChoice(for: word, correctCategory: correctCategory, label: label) + + private func startTimer() { + timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } - - private func presentCategoryChoice(for word: String, correctCategory: String, label: UILabel) { - let alert = UIAlertController( - title: "Where does \(word) belong?", - message: "Select the correct category", - preferredStyle: .alert - ) - - for category in categories { - let action = UIAlertAction(title: category, style: .default) { [weak self] _ in - self?.checkAnswer(word: word, chosenCategory: category, correctCategory: correctCategory, label: label) + + @objc private func startNewGame() { + // Reset game state + secondsElapsed = 0 + moves = 0 + score = 0 + incorrectAttempts = 0 + + // Update UI + timeLabel.text = "Time: 0s" + movesLabel.text = "Moves: 0" + + // Clear category stacks + categoryStacks.forEach { stackView in + // Remove all arranged subviews from the wordsStack + for view in stackView.wordsStack.arrangedSubviews { + stackView.wordsStack.removeArrangedSubview(view) + view.removeFromSuperview() } - alert.addAction(action) + + // Reset empty state label + stackView.emptyStateLabel.isHidden = false } - - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) - - present(alert, animated: true) + + // Populate new words + populateWords() + + // Start timer + timer?.invalidate() + timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) + + // Provide feedback for a new game + feedback = "Match locations to their categories" + + // Announce new game started for accessibility + UIAccessibility.post(notification: .announcement, argument: "New game started. Sort locations into their correct categories.") } - - private func checkAnswer(word: String, chosenCategory: String, correctCategory: String, label: UILabel) { - answeredWords[word] = chosenCategory - let isCorrect = chosenCategory == correctCategory - - if isCorrect { - score += 10 - scoreLabel.text = "Score: \(score)" - - if let categoryStack = categoryStacks.first(where: { $0.category == correctCategory }) { - let finalFrame = categoryStack.containerView.convert(categoryStack.containerView.bounds, to: view) - - let animatingLabel = UILabel(frame: label.convert(label.bounds, to: view)) - animatingLabel.text = word - animatingLabel.font = label.font - animatingLabel.textAlignment = .center - animatingLabel.backgroundColor = label.backgroundColor - animatingLabel.layer.cornerRadius = label.layer.cornerRadius - animatingLabel.layer.masksToBounds = true - view.addSubview(animatingLabel) - - UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut) { - animatingLabel.frame = CGRect( - x: finalFrame.midX - 50, - y: finalFrame.minY + 10, - width: 100, - height: 40 - ) - } completion: { _ in - // Remove the animating label - animatingLabel.removeFromSuperview() - // Add word to category stack - categoryStack.addWord(word) - - // Replace the word in the original position - self.replaceWord(at: label) - } - - // Fade out the original label - UIView.animate(withDuration: 0.3) { - label.alpha = 0 - } completion: { _ in - label.removeFromSuperview() - } - + + private func gameCompleted() { + timer?.invalidate() + let notificationGenerator = UINotificationFeedbackGenerator() + notificationGenerator.notificationOccurred(.success) + + let impactGenerator = UIImpactFeedbackGenerator(style: .heavy) + impactGenerator.impactOccurred() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + let lightImpact = UIImpactFeedbackGenerator(style: .light) + lightImpact.impactOccurred() + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + let mediumImpact = UIImpactFeedbackGenerator(style: .medium) + mediumImpact.impactOccurred() + } + AudioServicesPlaySystemSound(1025) + + UIView.animate(withDuration: 0.3, animations: { + self.view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) + }) { _ in + UIView.animate(withDuration: 0.3, animations: { + self.view.transform = CGAffineTransform.identity + }) + } + + // Delay presenting the completion screen + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + let completionVC = GameCompletionViewController() + completionVC.secondsElapsed = self.secondsElapsed + completionVC.moves = self.moves + completionVC.score = self.score + completionVC.modalPresentationStyle = .overFullScreen + completionVC.modalTransitionStyle = .crossDissolve + + completionVC.onPlayAgainTapped = { [weak self] in + self?.startNewGame() } - } else { - label.backgroundColor = .systemRed.withAlphaComponent(0.3) - label.shake() - - // Reset background color after delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { - UIView.animate(withDuration: 0.3) { - label.backgroundColor = .systemIndigo.withAlphaComponent(0.1) - } + + completionVC.onExitTapped = { + self.navigationController?.popViewController(animated: true) } + + self.present(completionVC, animated: true, completion: nil) + + // Accessibility announcement + UIAccessibility.post(notification: .announcement, argument: "Congratulations! You completed the game!") } } - - private func replaceWord(at label: UILabel) { - // Get all unused words - var unusedWords: [(word: String, category: String)] = [] - for (category, words) in locations { - words.forEach { word in - if answeredWords[word] == nil { - unusedWords.append((word, category)) - } + + @objc private func updateTimer() { + secondsElapsed += 1 + timeLabel.text = "Time: \(secondsElapsed)s" + } + + // Increment moves when a move is made + private func incrementMoves() { + moves += 1 + movesLabel.text = "Moves: \(moves)" + } + + private func showHintAlert() { + let alertController = UIAlertController( + title: "Need a hint?", + message: "Remember: Cities are urban areas (like Tokyo), States are subdivisions of countries (like California), and Countries are sovereign nations (like France).", + preferredStyle: .alert + ) + + let okAction = UIAlertAction(title: "Got it", style: .default) + alertController.addAction(okAction) + present(alertController, animated: true) + } + + private func resetGame() { + score = 0 + moves = 0 + secondsElapsed = 0 + incorrectAttempts = 0 + timeLabel.text = "Time: 0s" + movesLabel.text = "Moves: 0" + updateScore(increase: false, feedback: "Match locations to their categories") + + // Clear category stacks + categoryStacks.forEach { stackView in + // Remove all arranged subviews from the wordsStack + for view in stackView.wordsStack.arrangedSubviews { + stackView.wordsStack.removeArrangedSubview(view) + view.removeFromSuperview() } + + // Reset empty state label + stackView.emptyStateLabel.isHidden = false } - - // If there are unused words, randomly select one - if let newWord = unusedWords.randomElement() { - let newLabel = createWordLabel(for: newWord.word, category: newWord.category) - - // Get the stack view that contained the old label - if let stackView = label.superview as? UIStackView { - // Add new label with fade in animation - newLabel.alpha = 0 - stackView.addArrangedSubview(newLabel) - - UIView.animate(withDuration: 0.3) { - newLabel.alpha = 1 - } - } + + populateWords() + } + + private func removeWord(_ word: String) { + if let index = currentWords.firstIndex(where: { $0.word == word }) { + currentWords.remove(at: index) + wordsCollectionView.reloadData() + checkGameStatus() + } + } + + // Method to verify if a word belongs to a category + private func isCorrectCategory(word: String, category: String) -> Bool { + guard let correctCategory = locations.first(where: { $0.value.contains(word) })?.key else { + return false + } + return correctCategory == category + } +} + +// MARK: - UICollectionViewDataSource + +extension GeoSorterViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return currentWords.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: WordCell.identifier, for: indexPath) as? WordCell else { + return UICollectionViewCell() } + + let word = currentWords[indexPath.item].word + cell.configure(with: word) + return cell } - - private func createCategoryContainer(title: String) -> UIView { - let container = UIView() - container.backgroundColor = .systemGray6 - container.layer.cornerRadius = 12 - container.layer.borderColor = UIColor.systemGray4.cgColor - container.layer.borderWidth = 1 - - let titleLabel = UILabel() - titleLabel.text = title - titleLabel.font = .boldSystemFont(ofSize: 18) - titleLabel.textColor = .darkGray - titleLabel.textAlignment = .center - - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 8 - stackView.distribution = .fillEqually - stackView.alignment = .fill - - // Add rows of empty labels to represent placeholders - for _ in 0..<2 { // Two rows - let rowStack = UIStackView() - rowStack.axis = .horizontal - rowStack.spacing = 8 - rowStack.distribution = .fillEqually - for _ in 0..<4 { // Four columns - let placeholderLabel = UILabel() - placeholderLabel.backgroundColor = .white - placeholderLabel.layer.cornerRadius = 8 - placeholderLabel.clipsToBounds = true - rowStack.addArrangedSubview(placeholderLabel) +} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension GeoSorterViewController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let width = (collectionView.bounds.width - 42) / 3 // 3 columns with spacing + return CGSize(width: width, height: 50) + } +} + +// MARK: - UICollectionViewDragDelegate + +extension GeoSorterViewController: UICollectionViewDragDelegate { + func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + let word = currentWords[indexPath.item].word + let itemProvider = NSItemProvider(object: word as NSString) + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = word + return [dragItem] + } +} + +// MARK: - CategoryStackViewDelegate + +extension GeoSorterViewController: CategoryStackViewDelegate { + func categoryStackView(_ stackView: CategoryStackView, didReceiveDropWith word: String, category: String) { + incrementMoves() // Increment moves on each attempt + let isCorrect = isCorrectCategory(word: word, category: category) + + if isCorrect { + stackView.addWord(word) + + removeWord(word) + updateScore(increase: true, feedback: "Correct! \(word) is a \(category).") + } else { + // Find correct category + if let correctCategory = locations.first(where: { $0.value.contains(word) })?.key { + updateScore(increase: false, feedback: "\(word) is a \(correctCategory), not a \(category).") } - stackView.addArrangedSubview(rowStack) + + UIView.animate(withDuration: 0.1, animations: { + stackView.transform = CGAffineTransform(translationX: 10, y: 0) + }, completion: { _ in + UIView.animate(withDuration: 0.1, animations: { + stackView.transform = CGAffineTransform(translationX: -10, y: 0) + }, completion: { _ in + UIView.animate(withDuration: 0.1) { + stackView.transform = .identity + } + }) + }) } + } +} + +// MARK: - WordCell - // Layout - container.addSubview(titleLabel) - container.addSubview(stackView) - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - stackView.translatesAutoresizingMaskIntoConstraints = false +class WordCell: UICollectionViewCell { + static let identifier = "WordCell" + + private let wordLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textAlignment = .center + label.textColor = .label + return label + }() + + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = AppColors.primaryButtonColor + view.layer.cornerRadius = 8 + view.layer.borderWidth = 1 + view.layer.borderColor = AppColors.iconColor.cgColor + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + contentView.addSubview(containerView) + containerView.addSubview(wordLabel) + + containerView.translatesAutoresizingMaskIntoConstraints = false + wordLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: container.topAnchor, constant: 8), - titleLabel.centerXAnchor.constraint(equalTo: container.centerXAnchor), - stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), - stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8), - stackView.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8), - stackView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8) + containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), + containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4), + containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), + + wordLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + wordLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + wordLabel.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 8), + wordLabel.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -8), ]) - - return container } -} -extension UIView { - func shake() { - let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") - animation.timingFunction = CAMediaTimingFunction(name: .linear) - animation.duration = 0.4 - animation.values = [-10.0, 10.0, -10.0, 10.0, -5.0, 5.0, -2.5, 2.5, 0.0] - layer.add(animation, forKey: "shake") + func configure(with word: String) { + wordLabel.text = word + } + + override func prepareForReuse() { + super.prepareForReuse() + wordLabel.text = nil } } -#Preview(){ +#Preview { GeoSorterViewController() } diff --git a/recap/Controllers/Games/MemoryGameViewController.swift b/recap/Controllers/Games/MemoryGameViewController.swift new file mode 100755 index 0000000..afbd01c --- /dev/null +++ b/recap/Controllers/Games/MemoryGameViewController.swift @@ -0,0 +1,555 @@ +// +// MemoryGameViewController.swift +// recap +// +// Created by Diptayan Jash on 06/03/25. +// +import AudioToolbox +import Foundation +import UIKit + +class MemoryGameViewController: UIViewController { + // MARK: - Properties + + private var cardButtons = [UIButton]() + private var cardImages = [UIImage]() + private var flippedCards = [UIButton]() + private var matchedCards = [UIButton]() + private var timer: Timer? + private var secondsElapsed = 0 + private var moves = 0 + + private let gridSize = 4 // 4x4 grid (16 cards, 8 pairs) + private let cardSpacing: CGFloat = 12.0 + private let animationDuration = 0.5 + + private lazy var containerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + + private lazy var scoreLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Matches: 0" + label.font = Constants.FontandColors.titleFont + label.adjustsFontForContentSizeCategory = true + label.textAlignment = .center + label.textColor = .darkGray + return label + }() + + private lazy var timeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Time: 0s" + label.font = Constants.FontandColors.titleFont + label.adjustsFontForContentSizeCategory = true + label.textAlignment = .center + label.textColor = .darkGray + return label + }() + + private lazy var movesLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Moves: 0" + label.font = Constants.FontandColors.titleFont + label.adjustsFontForContentSizeCategory = true + label.textAlignment = .center + label.textColor = .darkGray + return label + }() + + private lazy var newGameButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("New Game", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.backgroundColor = AppColors.primaryButtonColor + button + .setTitleColor( + AppColors.primaryButtonTextColor, + for: .normal + ) + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.addTarget(self, action: #selector(startNewGame), for: .touchUpInside) + return button + }() + + // MARK: - Instruction View Properties + + private var instructionOverlayView: UIView? + + private lazy var instructionView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .white + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + view.layer.shadowColor = Constants.FontandColors.defaultshadowColor + view.layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + view.layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + view.layer.shadowOpacity = Float( + Constants.FontandColors.defaultshadowRadius + ) + return view + }() + + private lazy var instructionTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "How to Play" + label.font = Constants.FontandColors.titleFont + label.textAlignment = .center + label.textColor = Constants.FontandColors.titleColor + return label + }() + + private lazy var instructionTextLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "1. Tap cards to flip them over\n2. Find matching pairs of cards\n3. Match all pairs to win\n4. Try to complete the game with fewer moves and in less time" + label.font = UIFont.preferredFont(forTextStyle: .body) + label.adjustsFontForContentSizeCategory = true + label.numberOfLines = 0 + label.textAlignment = .left + label.textColor = .darkGray + return label + }() + + private lazy var startGameButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Start Game", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.backgroundColor = AppColors.primaryButtonColor + button + .setTitleColor( + AppColors.primaryButtonTextColor, + for: .normal + ) + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.addTarget(self, action: #selector(dismissInstructions), for: .touchUpInside) + return button + }() + + // MARK: - Lifecycle Methods + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupAccessibility() + loadCardImages() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // Show instructions on first appearance + showInstructions() + } + + // MARK: - Setup Methods + + private func setupUI() { + view.backgroundColor = Constants.BGs.GreyBG + title = "Memory Match Game" + + // Add subviews + view.addSubview(scoreLabel) + view.addSubview(timeLabel) + view.addSubview(movesLabel) + view.addSubview(containerView) + view.addSubview(newGameButton) + + // Setup constraints + NSLayoutConstraint.activate([ + scoreLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + scoreLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), + + timeLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + timeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + movesLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + movesLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + + containerView.topAnchor.constraint(equalTo: scoreLabel.bottomAnchor, constant: 30), + containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), + containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + containerView.heightAnchor.constraint(equalTo: containerView.widthAnchor), + + newGameButton.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 30), + newGameButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + newGameButton.widthAnchor.constraint(equalToConstant: 200), + newGameButton.heightAnchor.constraint(equalToConstant: 60), + newGameButton.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + ]) + } + + private func setupAccessibility() { + // Make the game accessible + scoreLabel.isAccessibilityElement = true + timeLabel.isAccessibilityElement = true + movesLabel.isAccessibilityElement = true + newGameButton.isAccessibilityElement = true + + scoreLabel.accessibilityTraits = .updatesFrequently + timeLabel.accessibilityTraits = .updatesFrequently + movesLabel.accessibilityTraits = .updatesFrequently + + newGameButton.accessibilityHint = "Starts a new memory matching game" + } + + private func createGameBoard() { + // Remove existing cards + for subview in containerView.subviews { + subview.removeFromSuperview() + } + cardButtons.removeAll() + + // Calculate card size based on container size + let availableWidth = containerView.bounds.width + let cardSize = (availableWidth - (cardSpacing * CGFloat(gridSize - 1))) / CGFloat(gridSize) + + // Create and position cards + for row in 0 ..< gridSize { + for column in 0 ..< gridSize { + let cardButton = createCardButton() + containerView.addSubview(cardButton) + + // Position the card + let x = CGFloat(column) * (cardSize + cardSpacing) + let y = CGFloat(row) * (cardSize + cardSpacing) + + cardButton.frame = CGRect(x: x, y: y, width: cardSize, height: cardSize) + cardButtons.append(cardButton) + } + } + } + + private func createCardButton() -> UIButton { + let button = UIButton(type: .custom) + button.backgroundColor = .white + button.layer.cornerRadius = 10 + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOffset = CGSize(width: 0, height: 2) + button.layer.shadowRadius = 3 + button.layer.shadowOpacity = 0.2 + + // Set the card back image + let cardBackImage = UIImage(systemName: "questionmark.circle.fill") + button.setImage(cardBackImage, for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.contentVerticalAlignment = .fill + button.contentHorizontalAlignment = .fill + button.imageEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + button.tintColor = AppColors.iconColor.withAlphaComponent(0.7) + + button.addTarget(self, action: #selector(cardTapped(_:)), for: .touchUpInside) + + // Accessibility + button.isAccessibilityElement = true + button.accessibilityLabel = "Card" + button.accessibilityHint = "Double tap to flip this card" + + return button + } + + private func loadCardImages() { + // Simple images that are distinct and recognizable for seniors + let imageNames = [ + "house.fill", "star.fill", "heart.fill", "bell.fill", + "sun.max.fill", "moon.fill", "leaf.fill", "car.fill", + ] + + // Load images and create pairs + for name in imageNames { + if let image = UIImage(systemName: name) { + cardImages.append(image) + cardImages.append(image) // Duplicate for pairs + } + } + } + + // MARK: - Instruction Methods + + private func showInstructions() { + // Create blurred overlay + let blurEffect = UIBlurEffect(style: .systemMaterial) + let blurView = UIVisualEffectView(effect: blurEffect) + blurView.frame = view.bounds + blurView.alpha = 0 + blurView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(blurView) + + // Add instruction view and subviews + view.addSubview(instructionView) + instructionView.addSubview(instructionTitleLabel) + instructionView.addSubview(instructionTextLabel) + instructionView.addSubview(startGameButton) + + NSLayoutConstraint.activate( +[ + blurView.topAnchor.constraint(equalTo: view.topAnchor), + blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + blurView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + blurView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + instructionView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + instructionView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + instructionView.leadingAnchor + .constraint( + equalTo: view.leadingAnchor, + constant: Constants.paddingKeys + .DefaultPaddingLeft), + instructionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingRight), + instructionView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor, multiplier: 0.7), + + instructionTitleLabel.topAnchor.constraint(equalTo: instructionView.topAnchor, constant: 24), + instructionTitleLabel.leadingAnchor.constraint(equalTo: instructionView.leadingAnchor, constant: 24), + instructionTitleLabel.trailingAnchor.constraint(equalTo: instructionView.trailingAnchor, constant: -24), + + instructionTextLabel.topAnchor.constraint(equalTo: instructionTitleLabel.bottomAnchor, constant: 24), + instructionTextLabel.leadingAnchor.constraint(equalTo: instructionView.leadingAnchor, constant: 24), + instructionTextLabel.trailingAnchor.constraint(equalTo: instructionView.trailingAnchor, constant: -24), + + startGameButton.topAnchor.constraint(equalTo: instructionTextLabel.bottomAnchor, constant: 32), + startGameButton.centerXAnchor.constraint(equalTo: instructionView.centerXAnchor), + startGameButton.widthAnchor.constraint(equalToConstant: 200), + startGameButton.heightAnchor.constraint(equalToConstant: 60), + startGameButton.bottomAnchor.constraint(equalTo: instructionView.bottomAnchor, constant: -24), + ] +) + + // Save reference for dismissal + instructionOverlayView = blurView + + // Animation for blur and instruction card + instructionView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + instructionView.alpha = 0 + + UIView.animate(withDuration: 0.3) { + blurView.alpha = 1 + self.instructionView.alpha = 1 + self.instructionView.transform = .identity + } + + // Setup accessibility + instructionView.isAccessibilityElement = true + instructionView.accessibilityLabel = "Game Instructions" + instructionView.accessibilityHint = "Learn how to play the game" + UIAccessibility.post(notification: .screenChanged, argument: instructionView) + } + + @objc private func dismissInstructions() { + // Dismiss instructions with animation + UIView.animate(withDuration: 0.3, animations: { + self.instructionView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + self.instructionView.alpha = 0 + self.instructionOverlayView?.alpha = 0 + }) { _ in + self.instructionOverlayView?.removeFromSuperview() + self.instructionOverlayView = nil + // Start the game after dismissing instructions + self.startNewGame() + } + } + + // MARK: - Game Logic + + @objc private func startNewGame() { + // Reset game state + flippedCards.removeAll() + matchedCards.removeAll() + secondsElapsed = 0 + moves = 0 + + // Update UI + scoreLabel.text = "Matches: 0" + timeLabel.text = "Time: 0s" + movesLabel.text = "Moves: 0" + + // Shuffle and deal cards + cardImages.shuffle() + createGameBoard() + + // Start timer + timer?.invalidate() + timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) + + // Announce new game started for accessibility + UIAccessibility.post(notification: .announcement, argument: "New game started. Find matching pairs of cards.") + } + + @objc private func cardTapped(_ sender: UIButton) { + // Ignore taps on matched cards or if two cards are already flipped + guard !matchedCards.contains(sender) && flippedCards.count < 2 && !flippedCards.contains(sender) else { + return + } + + // Flip the card + flipCard(sender) + flippedCards.append(sender) + + // Check for a match if two cards are flipped + if flippedCards.count == 2 { + moves += 1 + movesLabel.text = "Moves: \(moves)" + + // Check for a match + let firstCardIndex = cardButtons.firstIndex(of: flippedCards[0])! + let secondCardIndex = cardButtons.firstIndex(of: flippedCards[1])! + + if cardImages[firstCardIndex] == cardImages[secondCardIndex] { + // Match found + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self else { return } + self.handleMatch() + } + } else { + // No match + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + guard let self = self else { return } + self.resetFlippedCards() + } + } + } + } + + private func flipCard(_ card: UIButton) { + let generator = UIImpactFeedbackGenerator(style: .light) + generator.impactOccurred() + // If card is already flipped, show its image + if let cardIndex = cardButtons.firstIndex(of: card) { + UIView.transition(with: card, duration: animationDuration, options: .transitionFlipFromLeft, animations: { + // Show the card image + card.setImage(self.cardImages[cardIndex], for: .normal) + card.tintColor = .systemIndigo + card.backgroundColor = UIColor(white: 0.95, alpha: 1.0) + }, completion: nil) + + // Update accessibility + card.accessibilityLabel = "Flipped card showing \(cardIndex)" + } + } + + private func flipCardBack(_ card: UIButton) { + UIView.transition(with: card, duration: animationDuration, options: .transitionFlipFromRight, animations: { + // Show the card back + card.setImage(UIImage(systemName: "questionmark.circle.fill"), for: .normal) + card.tintColor = AppColors.iconColor.withAlphaComponent(0.7) + card.backgroundColor = .white + }, completion: nil) + + // Update accessibility + card.accessibilityLabel = "Card" + } + + private func handleMatch() { + // Handle matched cards + for card in flippedCards { + matchedCards.append(card) + + // Animate a success indicator + let generator = UIImpactFeedbackGenerator(style: .heavy) + generator.impactOccurred() + UIView.animate(withDuration: 0.3) { + card.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) + } completion: { _ in + UIView.animate(withDuration: 0.3) { + card.transform = .identity + card.layer.borderColor = UIColor.green.cgColor + card.layer.borderWidth = 3.0 + } + } + } + + // Clear flipped cards array + flippedCards.removeAll() + + // Update score + let matchCount = matchedCards.count / 2 + scoreLabel.text = "Matches: \(matchCount)" + + // Announce match for accessibility + UIAccessibility.post(notification: .announcement, argument: "Match found! \(matchCount) pairs matched.") + + // Check if game is completed + if matchedCards.count == cardButtons.count { + gameCompleted() + } + } + + private func resetFlippedCards() { + // Flip cards back if they don't match + for card in flippedCards { + flipCardBack(card) + } + flippedCards.removeAll() + } + private func gameCompleted() { + // Stop timer + timer?.invalidate() + + // Play multiple haptics in sequence + let notificationGenerator = UINotificationFeedbackGenerator() + notificationGenerator.notificationOccurred(.success) + + let impactGenerator = UIImpactFeedbackGenerator(style: .heavy) + impactGenerator.impactOccurred() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + let lightImpact = UIImpactFeedbackGenerator(style: .light) + lightImpact.impactOccurred() + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + let mediumImpact = UIImpactFeedbackGenerator(style: .medium) + mediumImpact.impactOccurred() + } + + // Play a success sound + AudioServicesPlaySystemSound(1025) // System sound for success (You can change it) + + // Animate the view slightly before presenting the completion screen + UIView.animate(withDuration: 0.3, animations: { + self.view.transform = CGAffineTransform(scaleX: 1.1, y: 1.1) + }) { _ in + UIView.animate(withDuration: 0.3, animations: { + self.view.transform = CGAffineTransform.identity + }) + } + + // Delay presenting the completion screen + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + let completionVC = GameCompletionViewController() + completionVC.secondsElapsed = self.secondsElapsed + completionVC.moves = self.moves + completionVC.modalPresentationStyle = .overFullScreen + completionVC.modalTransitionStyle = .crossDissolve + + completionVC.onPlayAgainTapped = { [weak self] in + self?.startNewGame() + } + + completionVC.onExitTapped = { + self.navigationController?.popViewController(animated: true) + } + + self.present(completionVC, animated: true, completion: nil) + + // Accessibility announcement + UIAccessibility.post(notification: .announcement, argument: "Congratulations! You completed the game!") + } + } + + + @objc private func updateTimer() { + secondsElapsed += 1 + timeLabel.text = "Time: \(secondsElapsed)s" + } +} +#Preview { + MemoryGameViewController() +} diff --git a/recap/Controllers/Games/PuzzleGameViewController.swift b/recap/Controllers/Games/PuzzleGameViewController.swift deleted file mode 100644 index 0611dd3..0000000 --- a/recap/Controllers/Games/PuzzleGameViewController.swift +++ /dev/null @@ -1,317 +0,0 @@ -// -// PuzzleGameViewController.swift -// recap -// -// Created by Diptayan Jash on 14/12/24. -// -import UIKit - -class PuzzleGameViewController: UIViewController { - - // MARK: - Properties - private let gridSize = 3 // 3x3 grid - private var pieces: [UIImageView] = [] - private var emptyPosition: Int = 8 // Last position is initially empty - private var moves = 0 - private var timer: Timer? - private var seconds = 0 - private var selectedPiece: UIImageView? - private var originalPosition: CGPoint? - - // MARK: - UI Components - private let containerView: UIView = { - let view = UIView() - view.backgroundColor = .systemGray6 - view.layer.cornerRadius = 12 - return view - }() - - private let gridView: UIView = { - let view = UIView() - view.backgroundColor = .clear - return view - }() - - private let statsView: UIView = { - let view = UIView() - view.backgroundColor = .systemBackground - view.layer.cornerRadius = 8 - return view - }() - - private let movesLabel: UILabel = { - let label = UILabel() - label.text = "Moves: 0" - label.font = .systemFont(ofSize: 16, weight: .medium) - return label - }() - - private let timerLabel: UILabel = { - let label = UILabel() - label.text = "Time: 00:00" - label.font = .systemFont(ofSize: 16, weight: .medium) - return label - }() - - private let newGameButton: UIButton = { - let button = UIButton() - button.setTitle("New Game", for: .normal) - button.backgroundColor = .systemBlue - button.layer.cornerRadius = 8 - return button - }() - - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupGame() - } - - // MARK: - Setup - private func setupUI() { - title = "Memory Puzzle" - view.backgroundColor = .systemBackground - - [containerView, statsView, newGameButton].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - view.addSubview($0) - } - - containerView.addSubview(gridView) - gridView.translatesAutoresizingMaskIntoConstraints = false - - [movesLabel, timerLabel].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - statsView.addSubview($0) - } - - NSLayoutConstraint.activate([ - containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - containerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9), - containerView.heightAnchor.constraint(equalTo: containerView.widthAnchor), - - gridView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), - gridView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), - gridView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.9), - gridView.heightAnchor.constraint(equalTo: gridView.widthAnchor), - - statsView.bottomAnchor.constraint(equalTo: containerView.topAnchor, constant: -20), - statsView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - statsView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - statsView.heightAnchor.constraint(equalToConstant: 50), - - movesLabel.leadingAnchor.constraint(equalTo: statsView.leadingAnchor, constant: 16), - movesLabel.centerYAnchor.constraint(equalTo: statsView.centerYAnchor), - - timerLabel.trailingAnchor.constraint(equalTo: statsView.trailingAnchor, constant: -16), - timerLabel.centerYAnchor.constraint(equalTo: statsView.centerYAnchor), - - newGameButton.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 20), - newGameButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - newGameButton.widthAnchor.constraint(equalToConstant: 200), - newGameButton.heightAnchor.constraint(equalToConstant: 44) - ]) - - newGameButton.addTarget(self, action: #selector(newGameTapped), for: .touchUpInside) - } - - private func setupGame() { - pieces.forEach { $0.removeFromSuperview() } - pieces.removeAll() - - // Initialize pieces - let pieceSize = gridView.bounds.width / CGFloat(gridSize) - let image = UIImage(named: "puzzle_image") ?? UIImage(systemName: "photo")! - - for row in 0.. UIImageView { - let pieceView = UIImageView() - pieceView.contentMode = .scaleAspectFill - pieceView.clipsToBounds = true - pieceView.isUserInteractionEnabled = true - - let row = CGFloat(index / gridSize) - let col = CGFloat(index % gridSize) - pieceView.frame = CGRect(x: col * size, y: row * size, width: size, height: size) - - let pieceSize = CGSize(width: image.size.width / CGFloat(gridSize), - height: image.size.height / CGFloat(gridSize)) - let pieceRect = CGRect(x: col * pieceSize.width, - y: row * pieceSize.height, - width: pieceSize.width, - height: pieceSize.height) - - if let cgImage = image.cgImage?.cropping(to: pieceRect) { - pieceView.image = UIImage(cgImage: cgImage) - } - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(piecePanned(_:))) - pieceView.addGestureRecognizer(panGesture) - - pieceView.tag = index - return pieceView - } - - private func shufflePieces() { - var positions = Array(0.. Int { - let size = gridView.bounds.width / CGFloat(gridSize) - let row = Int(piece.frame.minY / size) - let col = Int(piece.frame.minX / size) - return row * gridSize + col - } - - private func canMovePiece(at position: Int) -> Bool { - let emptyRow = emptyPosition / gridSize - let emptyCol = emptyPosition % gridSize - let pieceRow = position / gridSize - let pieceCol = position % gridSize - - return (abs(emptyRow - pieceRow) == 1 && emptyCol == pieceCol) || - (abs(emptyCol - pieceCol) == 1 && emptyRow == pieceRow) - } - - private func movePiece(_ piece: UIImageView, to position: Int) { - let size = gridView.bounds.width / CGFloat(gridSize) - let row = CGFloat(position / gridSize) - let col = CGFloat(position % gridSize) - - UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) { - piece.frame = CGRect(x: col * size, y: row * size, width: size, height: size) - } - - emptyPosition = position // Update empty position after move - } - - private func checkWin() -> Bool { - for piece in pieces { - if getPiecePosition(piece) != piece.tag { - return false - } - } - return true - } - - private func gameWon() { - timer?.invalidate() - - let alert = UIAlertController( - title: "Congratulations!", - message: "You solved the puzzle in \(moves) moves and \(formatTime(seconds))!", - preferredStyle: .alert - ) - - alert.addAction(UIAlertAction(title: "New Game", style: .default) { [weak self] _ in - self?.newGameTapped() - }) - - present(alert, animated: true) - } - - // MARK: - Timer - private func startTimer() { - seconds = 0 - timer?.invalidate() - timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in - self?.seconds += 1 - self?.updateTimerLabel() - } - } - - private func updateTimerLabel() { - timerLabel.text = "Time: \(formatTime(seconds))" - } - - private func formatTime(_ totalSeconds: Int) -> String { - let minutes = totalSeconds / 60 - let seconds = totalSeconds % 60 - return String(format: "%02d:%02d", minutes, seconds) - } - - @objc private func newGameTapped() { - moves = 0 - movesLabel.text = "Moves: 0" - setupGame() - } - - deinit { - timer?.invalidate() - } -} diff --git a/recap/Controllers/Others/.DS_Store b/recap/Controllers/Others/.DS_Store old mode 100644 new mode 100755 index 1948b0d..ca47d52 Binary files a/recap/Controllers/Others/.DS_Store and b/recap/Controllers/Others/.DS_Store differ diff --git a/recap/Controllers/Others/AnswerViewController.swift b/recap/Controllers/Others/AnswerViewController.swift deleted file mode 100644 index be253b5..0000000 --- a/recap/Controllers/Others/AnswerViewController.swift +++ /dev/null @@ -1,205 +0,0 @@ -// -// QuestionCell.swift -// recap -// -// Created by Diptayan Jash on 18/11/24. -// - -import Foundation -import UIKit -class AnswerViewController: UIViewController { - - var onAnswerSubmitted: (() -> Void)? - - private var question: Question - - private let gradientLayer = CAGradientLayer() - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - configureConstraints() - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(dismissView)) - } - @objc private func dismissView() { - dismiss(animated: true, completion: nil) - } - - private let questionLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.font = .systemFont(ofSize: 24, weight: .medium) - label.textColor = .label - label.textAlignment = .center - return label - }() - - private lazy var optionsStackView: UIStackView = { - let stack = UIStackView() - stack.translatesAutoresizingMaskIntoConstraints = false - stack.axis = .vertical - stack.spacing = 16 - return stack - }() - - private let submitButton: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle("Submit", for: .normal) - button.backgroundColor = .systemGray5 - button.setTitleColor(.systemGray, for: .normal) - button.layer.cornerRadius = 12 - button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) - return button - }() - - private let quoteLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = "Keep going — each one sharpens your mind and warms hearts!" - label.textColor = .secondaryLabel - label.textAlignment = .center - label.font = .systemFont(ofSize: 16) - label.numberOfLines = 0 - return label - }() - - private var selectedButton: UIButton? - - private let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.translatesAutoresizingMaskIntoConstraints = false - return scrollView - }() - - private let contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - init(question: Question) { - self.question = question - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - gradientLayer.frame = view.bounds - } - - private func setupUI() { - view.backgroundColor = .systemBackground - title = "Answer" - view.addSubview(scrollView) - scrollView.addSubview(contentView) - - contentView.addSubview(questionLabel) - contentView.addSubview(optionsStackView) - contentView.addSubview(submitButton) - contentView.addSubview(quoteLabel) - - questionLabel.text = question.text - - let options = question.answerOptions - options.forEach { option in - let button = createOptionButton(with: option) - optionsStackView.addArrangedSubview(button) - } - - submitButton.addTarget(self, action: #selector(didTapSubmit), for: .touchUpInside) - } - - private func createOptionButton(with title: String) -> UIButton { - let button = UIButton() - button.setTitle(title, for: .normal) - button.setTitleColor(.label, for: .normal) - button.backgroundColor = .white - button.layer.cornerRadius = 12 - button.layer.borderWidth = 1 - button.layer.borderColor = UIColor.systemGray5.cgColor - button.titleLabel?.font = .systemFont(ofSize: 18) - button.heightAnchor.constraint(equalToConstant: 50).isActive = true - button.addTarget(self, action: #selector(optionButtonTapped(_:)), for: .touchUpInside) - return button - } - - @objc private func optionButtonTapped(_ sender: UIButton) { - selectedButton?.backgroundColor = .white - selectedButton?.layer.borderColor = UIColor.systemGray5.cgColor - - sender.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1) - sender.layer.borderColor = UIColor.systemBlue.cgColor - selectedButton = sender - - submitButton.backgroundColor = .systemBlue - submitButton.setTitleColor(.white, for: .normal) - } - - @objc private func didTapSubmit() { - print("Selected Ans:" , selectedButton?.titleLabel?.text ?? "") - print(question.isAnswered) - guard selectedButton != nil else { - let alert = UIAlertController( - title: "No Selection", - message: "Please select an answer before submitting", - preferredStyle: .alert - ) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - present(alert, animated: true) - return - } - - question.isAnswered = true - print(question.isAnswered) - let alert = UIAlertController( - title: "Great Work, Keep Going!!", - message: "Studies show that consistent mental exercises can improve cognitive function by up to 20%", - preferredStyle: .alert - ) - - alert.addAction(UIAlertAction(title: "Continue", style: .default, handler: { [weak self] _ in - self?.onAnswerSubmitted?() - self?.dismiss(animated: true, completion: nil) - })) - - present(alert, animated: true) - } - private func configureConstraints() { - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - - contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), - contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - - questionLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), - questionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), - questionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - - optionsStackView.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 30), - optionsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40), - optionsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40), - - submitButton.topAnchor.constraint(equalTo: optionsStackView.bottomAnchor, constant: 30), - submitButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40), - submitButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40), - submitButton.heightAnchor.constraint(equalToConstant: 50), - - quoteLabel.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 20), - quoteLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), - quoteLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - quoteLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20) - ]) - } -} diff --git a/recap/Controllers/Others/ArticleTableViewController.swift b/recap/Controllers/Others/ArticleTableViewController.swift new file mode 100755 index 0000000..6c7567f --- /dev/null +++ b/recap/Controllers/Others/ArticleTableViewController.swift @@ -0,0 +1,150 @@ +import FirebaseFirestore +import UIKit + +class ArticleTableViewController: UITableViewController { + private let dataFetch: DataFetchProtocol + private var articles = [Article]() + + init(dataFetch: DataFetchProtocol = DataFetch()) { + self.dataFetch = dataFetch + super.init(nibName: nil, bundle: nil) + } + + init(preloadedArticles: [Article] = [], dataFetch: DataFetchProtocol = DataFetch()) { + articles = preloadedArticles + self.dataFetch = dataFetch + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + dataFetch = DataFetch() + super.init(coder: coder) + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + fetchArticles() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // Apply background color when view appears + applyBackgroundColor() + } + + private func setupUI() { + title = "Articles" + + tableView.register(ArticleTableViewCell.self, forCellReuseIdentifier: ArticleTableViewCell.identifier) + tableView.separatorStyle = .none + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 317 + + // Apply background color immediately + applyBackgroundColor() + } + + private func applyBackgroundColor() { + if traitCollection.userInterfaceStyle == .light { + // Subtle grey background for light mode (similar to Apple Health) + let healthAppGrey = Constants.BGs.GreyBG + + // Apply to table view directly + tableView.backgroundColor = healthAppGrey + + // Also apply to parent view to ensure complete coverage + view.backgroundColor = healthAppGrey + } else { + // Default dark mode background + tableView.backgroundColor = .systemBackground + view.backgroundColor = .systemBackground + } + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + applyBackgroundColor() + } + } + + private func fetchArticles() { + if !articles.isEmpty { + tableView.reloadData() // Use preloaded data + return + } + + dataFetch.fetchArticles { [weak self] fetchedArticles, error in + guard let self = self else { return } + + DispatchQueue.main.async { + if let error = error { + self.showErrorAlert(message: "Failed to load articles: \(error.localizedDescription)") + return + } + + if let fetchedArticles = fetchedArticles, !fetchedArticles.isEmpty { + self.articles = fetchedArticles + self.tableView.reloadData() + } else { + self.showErrorAlert(message: "No articles available at the moment.") + } + } + } + } + + private func showErrorAlert(message: String) { + let alert = UIAlertController( + title: "Error", + message: message, + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction( + title: "Retry", + style: .default, + handler: { [weak self] _ in + self?.fetchArticles() + } + )) + + alert.addAction(UIAlertAction( + title: "Cancel", + style: .cancel + )) + + present(alert, animated: true) + } + + // MARK: - Table view data source + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return articles.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: ArticleTableViewCell.identifier, + for: indexPath + ) as? ArticleTableViewCell else { + return UITableViewCell() + } + + cell.configure(with: articles[indexPath.row]) + cell.selectionStyle = .none + + // Ensure cell background is transparent to show table view background + cell.backgroundColor = .clear + cell.contentView.backgroundColor = .clear + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let article = articles[indexPath.row] + let detailVC = ArticleDetailViewController(article: article) + navigationController?.pushViewController(detailVC, animated: true) + } +} diff --git a/recap/Controllers/Others/Family/ArticleTableViewController.swift b/recap/Controllers/Others/Family/ArticleTableViewController.swift deleted file mode 100644 index 04d6b84..0000000 --- a/recap/Controllers/Others/Family/ArticleTableViewController.swift +++ /dev/null @@ -1,59 +0,0 @@ -import UIKit -import FirebaseFirestore - -class ArticleTableViewController: UITableViewController { - var db: Firestore! - var articles = [Article]() // This is the array to store fetched articles - - override func viewDidLoad() { - super.viewDidLoad() - db = Firestore.firestore() - fetchArticles() - - title = "Articles" - - tableView.register(ArticleTableViewCell.self, forCellReuseIdentifier: ArticleTableViewCell.identifier) - tableView.separatorStyle = .none - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 317 - } - - // Fetch articles from Firestore - func fetchArticles() { - let articleFetcher = ArticleFetcher() - articleFetcher.fetchArticles { [weak self] fetchedArticles, error in - if let error = error { - print("Failed to fetch articles: \(error.localizedDescription)") - return - } - - if let fetchedArticles = fetchedArticles { - self?.articles = fetchedArticles - DispatchQueue.main.async { - self?.tableView.reloadData() // Reload table view after fetching - } - } - } - } - - // MARK: - Table view data source - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return articles.count // Return the count of articles - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: ArticleTableViewCell.identifier, for: indexPath) as? ArticleTableViewCell else { - return UITableViewCell() - } - cell.configure(with: articles[indexPath.row]) // Pass the article data to the cell - cell.selectionStyle = .none - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let article = articles[indexPath.row] - let detailVC = ArticleDetailViewController(article: article) - navigationController?.pushViewController(detailVC, animated: true) - tableView.backgroundColor = .clear - } -} diff --git a/recap/Controllers/Others/Family/CalenderCell.swift b/recap/Controllers/Others/Family/CalenderCell.swift old mode 100644 new mode 100755 index ba39c39..cc60810 --- a/recap/Controllers/Others/Family/CalenderCell.swift +++ b/recap/Controllers/Others/Family/CalenderCell.swift @@ -2,7 +2,7 @@ // CalenderCell.swift // recap // -// Created by user@47 on 16/01/25. +// Created by s1834 on 16/01/25. // import UIKit @@ -29,7 +29,6 @@ class CalendarCell: UICollectionViewCell { ]) } - // Customize the appearance of the cell func configure(withDay day: String?, isSelected: Bool, isCurrentDay: Bool) { if let day = day { dayLabel.text = day @@ -39,8 +38,6 @@ class CalendarCell: UICollectionViewCell { dayLabel.text = "" contentView.backgroundColor = .clear } - - // Highlight the selected day if isSelected { contentView.backgroundColor = UIColor.systemBlue dayLabel.textColor = .white @@ -49,7 +46,6 @@ class CalendarCell: UICollectionViewCell { dayLabel.textColor = .black } - // Add border to current day if isCurrentDay { contentView.layer.borderColor = UIColor.systemGreen.cgColor } else { diff --git a/recap/Controllers/Others/Family/StreaksViewController.swift b/recap/Controllers/Others/Family/StreaksViewController.swift old mode 100644 new mode 100755 index ac8b046..63e3ead --- a/recap/Controllers/Others/Family/StreaksViewController.swift +++ b/recap/Controllers/Others/Family/StreaksViewController.swift @@ -2,7 +2,7 @@ // StreaksViewController.swift // recap_home // -// Created by user@47 on 19/11/24. +// Created by s1834 on 19/11/24. // import UIKit @@ -18,12 +18,20 @@ class StreaksViewController: UIViewController { var currentMonth: Int = 11 var currentYear: Int = 2024 - + var maxStreakLabel: UILabel! + var currentStreakLabel: UILabel! + var activeDaysLabel: UILabel! + var streakDates: [String: Bool] = [:] private var verifiedUserDocID: String var streakService: StreakService! + private var infoCard: UIView? + private var blurredView: UIVisualEffectView? + private var isInfoCardVisible = false + + init(verifiedUserDocID: String) { self.verifiedUserDocID = verifiedUserDocID @@ -35,11 +43,7 @@ class StreaksViewController: UIViewController { self.calendarCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) super.init(nibName: nil, bundle: nil) - - // Initialize StreakService after `super.init` self.streakService = StreakService(verifiedUserDocID: self.verifiedUserDocID) - - print("✅ StreaksViewController initialized with User Doc ID: \(self.verifiedUserDocID)") } required init?(coder: NSCoder) { @@ -48,7 +52,7 @@ class StreaksViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - + view.backgroundColor = Constants.BGs.GreyBG let currentDate = Date() let calendar = Calendar.current let currentComponents = calendar.dateComponents([.year, .month], from: currentDate) @@ -56,86 +60,40 @@ class StreaksViewController: UIViewController { currentYear = currentComponents.year! currentMonth = currentComponents.month! - view.backgroundColor = .white - setupGradientBackground() setupNavBar() setupProfileView() - setupStreakStatsView() setupHeaderView() setupCalendarView() - // Initialize the StreakService streakService = StreakService(verifiedUserDocID: verifiedUserDocID) -// addNovember2024StreakData() - // Fetch and update streak data from Firestore when view loads + streakService.streakDataFetched = { [weak self] maxStreak, currentStreak, activeDays in + self?.updateStreakStats(maxStreak: maxStreak, currentStreak: currentStreak, activeDays: activeDays) + } + fetchAndUpdateStreakData() + setupStreakStatsView() + streakService.fetchAndUpdateStreakStats() } func fetchAndUpdateStreakData() { let yearMonth = formattedYearMonth() - + streakService.getStreaksForUser(yearMonth: yearMonth) { [weak self] streak in guard let self = self else { return } - + if let streak = streak { - self.updateStreakDatesWithStreaks(streak.streakDates) + self.streakDates = streak.streakDates } else { - print("⚠️ No streak data found for \(yearMonth), but not uploading default data.") + print("⚠️⚠️ No streak data found for \(yearMonth), but not uploading default data.") } - // Ensure the calendar reloads DispatchQueue.main.async { self.calendarCollectionView.reloadData() } } } - - - func uploadDefaultStreakData(for yearMonth: String) { - guard !yearMonth.isEmpty else { - print("Error: Year-Month string is empty.") - return - } - - let defaultMonth = "02" // Always set default to February - let currentYear = Calendar.current.component(.year, from: Date()) - let febYearMonth = "\(currentYear)-\(defaultMonth)" // Set to "YYYY-02" - - var defaultStreaks: [String: Bool] = [:] - let numberOfDaysInFebruary = daysInMonth(year: currentYear, month: 2) - - // Create streaks for February, setting odd days as streak days - for day in 1...numberOfDaysInFebruary { - let date = String(format: "%02d", day) - defaultStreaks["\(febYearMonth)-\(date)"] = day % 2 == 1 - } - - // Upload the default streaks to Firestore - streakService.updateStreaksForUser(streaks: defaultStreaks) { success in - if success { - print("✅ Default streak data for February uploaded successfully.") - // self.updateStreakDatesWithStreaks(defaultStreaks) // This line is now removed - } else { - print("❌ Failed to upload default streak data.") - } - } - } - - func updateStreakDatesWithStreaks(_ streaks: [String: Bool]) { - // Clear any existing streak data - streakDates.removeAll() - - // Update the streakDates with the fetched streak data - for (dateStr, isCompleted) in streaks { - streakDates[dateStr] = isCompleted - } - - // Reload the collection view to reflect updated streak data - calendarCollectionView.reloadData() - } - @objc func handlePreviousMonth() { if currentMonth > 1 { currentMonth -= 1 @@ -162,28 +120,4 @@ class StreaksViewController: UIViewController { monthYearLabel.text = formattedMonthYear() } } -// func addNovember2024StreakData() { -// let targetYear = 2025 -// let targetMonth = "02" // November -// let yearMonth = "\(targetYear)-\(targetMonth)" -// -// var defaultStreaks: [String: Bool] = [:] -// let numberOfDaysInNovember = daysInMonth(year: targetYear, month: 1) -// -// // Create streaks for November, setting alternate days as streak days (example logic) -// for day in 1...numberOfDaysInNovember { -// let date = String(format: "%02d", day) -// defaultStreaks["\(yearMonth)-\(date)"] = day % 2 == 0 // Even days are streaks -// } -// -//// Upload the default streaks to Firestore -// streakService.updateStreaksForUser(streaks: defaultStreaks) { success in -// if success { -// print("✅ Default streak data for November 2024 uploaded successfully.") -// } else { -// print("❌ Failed to upload default streak data.") -// } -// } -// } - } diff --git a/recap/Controllers/Others/Family/familyInfo.swift b/recap/Controllers/Others/Family/familyInfo.swift old mode 100644 new mode 100755 index 609c5ad..c440416 --- a/recap/Controllers/Others/Family/familyInfo.swift +++ b/recap/Controllers/Others/Family/familyInfo.swift @@ -8,7 +8,6 @@ import FirebaseAuth import UIKit -// MARK: - FamilyMemberDetails struct FamilyMemberDetails { let firstName: String let lastName: String @@ -29,7 +28,6 @@ struct FamilyMemberDetails { } } -// MARK: - RelationshipOptions enum RelationshipOptions: String, CaseIterable { case parent = "Parent" case sibling = "Sibling" @@ -38,12 +36,10 @@ enum RelationshipOptions: String, CaseIterable { case other = "Other" } -// MARK: - FamilyInfoDelegate protocol FamilyInfoDelegate: AnyObject { func didSaveFamilyMember(_ member: FamilyMemberDetails) } -// MARK: - familyInfo class class familyInfo: UIViewController { weak var delegate: FamilyInfoDelegate? @@ -59,7 +55,6 @@ class familyInfo: UIViewController { super.init(coder: coder) } - // MARK: - UI Components private let profileImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill @@ -90,12 +85,11 @@ class familyInfo: UIViewController { let button = UIButton() button.setTitle("Save", for: .normal) button.backgroundColor = .systemBlue - button.layer.cornerRadius = 8 + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) return button }() - // MARK: - Properties private let imagePicker = UIImagePickerController() private let datePicker = UIDatePicker() private let relationshipPicker = UIPickerView() @@ -104,17 +98,12 @@ class familyInfo: UIViewController { let relationshipOptions = RelationshipOptions.allCases.map { $0.rawValue } let bloodGroupOptions = BloodGroupOptions.allCases.map { $0.rawValue } - // MARK: - Lifecycle override func viewDidLoad() { title = "Add Family Member" super.viewDidLoad() setupUI() -// setupImagePicker() -// setupPickers() -// setupTextFields() } - // MARK: - Setup private func setupUI() { view.backgroundColor = .systemBackground @@ -143,15 +132,10 @@ class familyInfo: UIViewController { saveButton.heightAnchor.constraint(equalToConstant: 50), ]) -// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped)) -// profileImageView.addGestureRecognizer(tapGesture) - saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside) } - // MARK: - Actions @objc private func saveButtonTapped() { - // Validate inputs guard let firstName = firstNameField.text, !firstName.isEmpty, let lastName = lastNameField.text, !lastName.isEmpty, let dob = dobField.text, !dob.isEmpty, @@ -161,18 +145,12 @@ class familyInfo: UIViewController { return } - // Create family member details object guard let userId = Auth.auth().currentUser?.uid else { showAlert(message: "User not logged in.") return } - let familyMemberDetails = FamilyMemberDetails(firstName: firstName, - lastName: lastName, - dateOfBirth: dob, - relationship: relationship, - bloodGroup: bloodGroup, - id: userId) + let familyMemberDetails = FamilyMemberDetails(firstName: firstName, lastName: lastName, dateOfBirth: dob, relationship: relationship, bloodGroup: bloodGroup, id: userId) // Save to UserDefaults UserDefaultsStorageProfile.shared.saveProfile( @@ -180,7 +158,6 @@ class familyInfo: UIViewController { image: profileImageView.image ) { [weak self] success in if success { - // Notify delegate and pop the view self?.delegate?.didSaveFamilyMember(familyMemberDetails) self?.navigationController?.popViewController(animated: true) } else { @@ -189,7 +166,6 @@ class familyInfo: UIViewController { } } - // MARK: - Helper Methods private func showAlert(message: String) { let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) diff --git a/recap/Controllers/Others/Family/settings/AboutAppViewController.swift b/recap/Controllers/Others/Family/settings/AboutAppViewController.swift new file mode 100755 index 0000000..276bce5 --- /dev/null +++ b/recap/Controllers/Others/Family/settings/AboutAppViewController.swift @@ -0,0 +1,302 @@ +// +// AboutAppViewController.swift +// recap +// +// Created by khushi on 27/01/25. +// + +import UIKit + +class AboutAppViewController: UIViewController { + + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.showsVerticalScrollIndicator = false + return scrollView + }() + + private let contentView: UIView = { + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + return contentView + }() + + private let headerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = AppColors.secondaryButtonColor + view.layer.cornerRadius = 25 + view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner] + return view + }() + + private let appIconImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage(named: "recapLogo") + imageView.tintColor = AppColors.secondaryButtonColor + imageView.layer.cornerRadius = 30 + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = LocalizationManager.shared.localizedString(for: "Recap") + label.font = .systemFont(ofSize: 32, weight: .bold) + label.textAlignment = .center + label.textColor = AppColors.primaryButtonTextColor + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let versionLabel: UILabel = { + let label = UILabel() + if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + label.text = "Version \(version)" + } else { + label.text = "Version Not Available" + } + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = AppColors.secondaryButtonTextColor.withAlphaComponent(0.7) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.text = """ + Pre-clinical Alzheimer patients face memory recall challenges, leading to further degradation of memory over time. + + Addressing Alzheimer's memory recall is vital for reducing cognitive decline. It matters because better monitoring can enhance quality of life for millions worldwide. + """ + label.font = .systemFont(ofSize: 16) + label.textColor = .label + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let featuresSectionView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .secondarySystemBackground + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + return view + }() + + private let featuresTitleLabel: UILabel = { + let label = UILabel() + label.text = "Key Features" + label.font = .systemFont(ofSize: 24, weight: .bold) + label.textColor = .label + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let featuresStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 16 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private let features: [(icon: String, title: String, description: String)] = [ + ("calendar.badge.clock", "Routine Questions", "Daily questions verified by family members"), + ("book.fill", "Supporting Resources", "Personalized resources for lifestyle improvement"), + ("brain", "Rapid Memory Check", "Monthly assessment with detailed scoring"), + ("chart.bar.fill", "Progress Reports", "Comprehensive progress tracking with graphs"), + ("gamecontroller.fill", "Gamification", "Age-appropriate games and achievements"), + ("person.2.fill", "Family View", "Manage family details and contacts") + ] + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + NotificationCenter.default.addObserver( + self, + selector: #selector(languageChanged), + name: Notification.Name("LanguageChanged"), + object: nil + ) + + setupUI() + setupNavigationBar() + animateContent() + } + @objc private func languageChanged() { + // Update all text in the view controller + titleLabel.text = LocalizationManager.shared.localizedString(for: "about_title") + descriptionLabel.text = LocalizationManager.shared.localizedString(for: "about_description") + featuresTitleLabel.text = LocalizationManager.shared.localizedString(for: "features_title") + // Update any other text elements... + } + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Setup + private func setupNavigationBar() { + navigationItem.title = "About" + navigationController?.navigationBar.prefersLargeTitles = false + + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance + } + + private func setupUI() { + view.backgroundColor = .systemBackground + + view.addSubview(scrollView) + scrollView.addSubview(contentView) + + contentView.addSubview(headerView) + headerView.addSubview(appIconImageView) + headerView.addSubview(titleLabel) + headerView.addSubview(versionLabel) + + contentView.addSubview(descriptionLabel) + contentView.addSubview(featuresSectionView) + featuresSectionView.addSubview(featuresTitleLabel) + featuresSectionView.addSubview(featuresStackView) + + setupFeatures() + setupConstraints() + } + + private func setupFeatures() { + features.forEach { feature in + let featureView = createFeatureView( + icon: feature.icon, + title: feature.title, + description: feature.description + ) + featuresStackView.addArrangedSubview(featureView) + } + } + + private func createFeatureView(icon: String, title: String, description: String) -> UIView { + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + + let iconImage = UIImageView() + iconImage.translatesAutoresizingMaskIntoConstraints = false + iconImage.image = UIImage(systemName: icon) + iconImage.tintColor = AppColors.iconColor + iconImage.contentMode = .scaleAspectFit + + let titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.text = title + titleLabel.font = .systemFont(ofSize: 16, weight: .semibold) + titleLabel.textColor = .label + + let descriptionLabel = UILabel() + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.text = description + descriptionLabel.font = .systemFont(ofSize: 14) + descriptionLabel.textColor = .secondaryLabel + descriptionLabel.numberOfLines = 0 + + container.addSubview(iconImage) + container.addSubview(titleLabel) + container.addSubview(descriptionLabel) + + NSLayoutConstraint.activate([ + iconImage.leadingAnchor.constraint(equalTo: container.leadingAnchor), + iconImage.topAnchor.constraint(equalTo: container.topAnchor), + iconImage.widthAnchor.constraint(equalToConstant: 24), + iconImage.heightAnchor.constraint(equalToConstant: 24), + + titleLabel.leadingAnchor.constraint(equalTo: iconImage.trailingAnchor, constant: 12), + titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor), + titleLabel.centerYAnchor.constraint(equalTo: iconImage.centerYAnchor), + + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), + descriptionLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor), + descriptionLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor) + ]) + + return container + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + headerView.topAnchor.constraint(equalTo: contentView.topAnchor), + headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + headerView.heightAnchor.constraint(equalToConstant: 260), + + appIconImageView.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 24), + appIconImageView.centerXAnchor.constraint(equalTo: headerView.centerXAnchor), + appIconImageView.widthAnchor.constraint(equalToConstant: 80), + appIconImageView.heightAnchor.constraint(equalToConstant: 80), + + titleLabel.topAnchor.constraint(equalTo: appIconImageView.bottomAnchor, constant: 16), + titleLabel.centerXAnchor.constraint(equalTo: headerView.centerXAnchor), + + versionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + versionLabel.centerXAnchor.constraint(equalTo: headerView.centerXAnchor), + versionLabel.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -24), + + descriptionLabel.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 24), + descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + + featuresSectionView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 24), + featuresSectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + featuresSectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + featuresSectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16), + + featuresTitleLabel.topAnchor.constraint(equalTo: featuresSectionView.topAnchor, constant: 16), + featuresTitleLabel.leadingAnchor.constraint(equalTo: featuresSectionView.leadingAnchor, constant: 16), + featuresTitleLabel.trailingAnchor.constraint(equalTo: featuresSectionView.trailingAnchor, constant: -16), + + featuresStackView.topAnchor.constraint(equalTo: featuresTitleLabel.bottomAnchor, constant: 16), + featuresStackView.leadingAnchor.constraint(equalTo: featuresSectionView.leadingAnchor, constant: 16), + featuresStackView.trailingAnchor.constraint(equalTo: featuresSectionView.trailingAnchor, constant: -16), + featuresStackView.bottomAnchor.constraint(equalTo: featuresSectionView.bottomAnchor, constant: -16) + ]) + } + + private func animateContent() { + let views = [headerView, descriptionLabel, featuresSectionView] + + views.enumerated().forEach { index, view in + view.alpha = 0 + view.transform = CGAffineTransform(translationX: 0, y: 20) + + UIView.animate( + withDuration: 0.6, + delay: Double(index) * 0.2, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: .curveEaseOut + ) { + view.alpha = 1 + view.transform = .identity + } + } + } +} + +#Preview { + AboutAppViewController() +} diff --git a/recap/Controllers/Others/Family/settings/LanguageViewController.swift b/recap/Controllers/Others/Family/settings/LanguageViewController.swift new file mode 100755 index 0000000..2fdd68c --- /dev/null +++ b/recap/Controllers/Others/Family/settings/LanguageViewController.swift @@ -0,0 +1,211 @@ +// +// LanguageViewController.swift +// recap +// +// Created by admin70 on 27/01/25. +// + +import UIKit + +// Language model to represent available languages +struct Language { + let name: String + let nativeName: String + let code: String + let flag: String +} + +class LanguageViewController: UIViewController { + + // MARK: - Properties + private let languages: [Language] = [ + Language(name: "English", nativeName: "English", code: "en", flag: "🇬🇧"), + Language(name: "Hindi", nativeName: "हिंदी", code: "hi", flag: "🇮🇳"), + Language(name: "Bengali", nativeName: "বাংলা", code: "bn", flag: "🇧🇩") + ] + + private var selectedLanguageCode: String { + get { + return UserDefaults.standard.string(forKey: "AppLanguage") ?? "en" + } + set { + UserDefaults.standard.set(newValue, forKey: "AppLanguage") + UserDefaults.standard.synchronize() + } + } + + // MARK: - UI Components + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Choose Your Language" + label.font = .systemFont(ofSize: 24, weight: .bold) + label.textAlignment = .center + return label + }() + + private let subtitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Select the language you prefer for the app interface" + label.font = .systemFont(ofSize: 16) + label.textColor = .secondaryLabel + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + private lazy var languageStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 16 + return stackView + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupNavigationBar() + } + + // MARK: - Setup + private func setupNavigationBar() { + title = "Language" + navigationController?.navigationBar.prefersLargeTitles = false + + // Add Done button if presented modally + if presentingViewController != nil { + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "Done", + style: .done, + target: self, + action: #selector(dismissVC) + ) + } + } + + private func setupUI() { + view.backgroundColor = .systemBackground + + view.addSubview(titleLabel) + view.addSubview(subtitleLabel) + view.addSubview(languageStackView) + + // Add language options + languages.forEach { language in + let languageButton = createLanguageButton(for: language) + languageStackView.addArrangedSubview(languageButton) + } + + setupConstraints() + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 32), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + subtitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + subtitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + + languageStackView.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 32), + languageStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + languageStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20) + ]) + } + + private func createLanguageButton(for language: Language) -> UIButton { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + + // Configure button appearance + var configuration = UIButton.Configuration.filled() + configuration.cornerStyle = .medium + configuration.baseBackgroundColor = selectedLanguageCode == language.code ? AppColors.iconColor : .secondarySystemBackground + configuration.baseForegroundColor = selectedLanguageCode == language.code ? .white : .label + + // Create attributed string for button title + let title = "\(language.flag) \(language.name)" + let subtitle = language.nativeName != language.name ? language.nativeName : nil + + configuration.title = title + configuration.subtitle = subtitle + configuration.titleAlignment = .leading + + // Set content padding + configuration.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) + + button.configuration = configuration + button.tag = languages.firstIndex(where: { $0.code == language.code }) ?? 0 + button.addTarget(self, action: #selector(languageButtonTapped(_:)), for: .touchUpInside) + + // Set height constraint + button.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true + + return button + } + + // MARK: - Actions + @objc private func languageButtonTapped(_ sender: UIButton) { + guard let language = languages[safe: sender.tag] else { return } + + // Update selected language + selectedLanguageCode = language.code + + // Update UI + languageStackView.arrangedSubviews.enumerated().forEach { index, view in + guard let button = view as? UIButton else { return } + + var configuration = button.configuration + let isSelected = index == sender.tag + configuration?.baseBackgroundColor = isSelected ? AppColors.iconColor : .secondarySystemBackground + configuration?.baseForegroundColor = isSelected ? .white : .label + button.configuration = configuration + } + + // Post notification for language change + NotificationCenter.default.post(name: Notification.Name("LanguageChanged"), object: nil) + + // Show success feedback + showLanguageChangeSuccess(language: language) + } + + @objc private func dismissVC() { + dismiss(animated: true) + } + + private func showLanguageChangeSuccess(language: Language) { + // Create and configure alert + let alertController = UIAlertController( + title: "Language Changed", + message: "The app language has been changed to \(language.name)", + preferredStyle: .alert + ) + + let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in + // If presented modally, dismiss after language change + if self?.presentingViewController != nil { + self?.dismiss(animated: true) + } + } + + alertController.addAction(okAction) + present(alertController, animated: true) + } +} + +// MARK: - Array Extension +extension Array { + subscript(safe index: Int) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} + +// MARK: - Preview Provider +#Preview { + UINavigationController(rootViewController: LanguageViewController()) +} diff --git a/recap/Controllers/Others/Family/settings/PrivacyViewController.swift b/recap/Controllers/Others/Family/settings/PrivacyViewController.swift new file mode 100755 index 0000000..1e538d8 --- /dev/null +++ b/recap/Controllers/Others/Family/settings/PrivacyViewController.swift @@ -0,0 +1,197 @@ +// +// PrivacyViewController.swift +// recap +// +// Created by admin70 on 27/01/25. +// + +import UIKit + +class PrivacyViewController: UIViewController { + + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.showsVerticalScrollIndicator = true + return scrollView + }() + + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let headerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = AppColors.secondaryButtonColor + view.layer.cornerRadius = 25 + view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner] + return view + }() + + private let iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.tintColor = AppColors.iconColor + let config = UIImage.SymbolConfiguration(pointSize: 50, weight: .medium) + imageView.image = UIImage(systemName: "shield.checkerboard", withConfiguration: config) + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "Privacy Policy" + label.textColor = AppColors.primaryButtonTextColor + label.font = .systemFont(ofSize: 32, weight: .bold) + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let subtitleLabel: UILabel = { + let label = UILabel() + label.text = "Your privacy is our priority" + label.textColor = AppColors.secondaryButtonTextColor.withAlphaComponent(0.7) + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let sectionsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 16 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private let lastUpdatedLabel: UILabel = { + let label = UILabel() + label.text = "Last updated: February 19, 2025" + label.textColor = .secondaryLabel + label.font = .systemFont(ofSize: 14) + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupSections() + } + + private func setupUI() { + view.backgroundColor = .systemBackground + navigationItem.title = "Privacy Policy" + + view.addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(headerView) + headerView.addSubview(iconImageView) + headerView.addSubview(titleLabel) + headerView.addSubview(subtitleLabel) + contentView.addSubview(sectionsStackView) + contentView.addSubview(lastUpdatedLabel) + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + headerView.topAnchor.constraint(equalTo: contentView.topAnchor), + headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + headerView.heightAnchor.constraint(equalToConstant: 260), + + iconImageView.centerXAnchor.constraint(equalTo: headerView.centerXAnchor), + iconImageView.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 60), + iconImageView.heightAnchor.constraint(equalToConstant: 80), + iconImageView.widthAnchor.constraint(equalToConstant: 80), + + titleLabel.topAnchor.constraint(equalTo: iconImageView.bottomAnchor, constant: 16), + titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20), + titleLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -20), + + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + subtitleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20), + subtitleLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -20), + + sectionsStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 24), + sectionsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + sectionsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + lastUpdatedLabel.topAnchor.constraint(equalTo: sectionsStackView.bottomAnchor, constant: 24), + lastUpdatedLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + lastUpdatedLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + lastUpdatedLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) + ]) + } + + private func createSectionView(title: String, content: String) -> UIView { + let containerView = UIView() + containerView.backgroundColor = .secondarySystemBackground + containerView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.font = .systemFont(ofSize: 18, weight: .bold) + titleLabel.textColor = .label + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + let contentLabel = UILabel() + contentLabel.text = content + contentLabel.font = .systemFont(ofSize: 15) + contentLabel.textColor = .secondaryLabel + contentLabel.numberOfLines = 0 + contentLabel.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(titleLabel) + containerView.addSubview(contentLabel) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16), + titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + + contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + contentLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + contentLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + contentLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16) + ]) + + return containerView + } + + private func setupSections() { + let sections = [ + ("Personal Information", "We collect your email address, name, and profile information to provide you with a personalized experience."), + ("Usage Data", "We collect app interaction data and device information to improve our services."), + ("Analytics", "We use Google Analytics for Firebase to enhance app performance and user experience."), + ("Data Protection", "Your data is securely stored using industry-standard encryption and security measures."), + ("User Rights", "You have full control over your data. Access, modify, or delete your information at any time."), + ("Contact Us", "Questions about your privacy? Reach out to us at privacy@ourapp.com") + ] + + sections.forEach { title, content in + let sectionView = createSectionView(title: title, content: content) + sectionsStackView.addArrangedSubview(sectionView) + } + } +} + +#Preview("Privacy Policy") { + let vc = PrivacyViewController() + return UINavigationController(rootViewController: vc) +} diff --git a/recap/Controllers/Others/MemoryCheckViewController.swift b/recap/Controllers/Others/MemoryCheckViewController.swift old mode 100644 new mode 100755 index 7eedcd5..f9cbcf9 --- a/recap/Controllers/Others/MemoryCheckViewController.swift +++ b/recap/Controllers/Others/MemoryCheckViewController.swift @@ -24,9 +24,10 @@ class MemoryCheckViewController: UIViewController { private let submitButton: UIButton = { let button = UIButton() button.setTitle("Submit", for: .normal) - button.backgroundColor = .systemBlue - button.layer.cornerRadius = 8 - button.titleLabel?.font = .systemFont(ofSize: 20, weight: .heavy) + button.setTitleColor(.systemBlue, for: .normal) + button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.2) + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + button.titleLabel?.font = Constants.FontandColors.titleFont button.isEnabled = false return button }() @@ -34,7 +35,7 @@ class MemoryCheckViewController: UIViewController { private let progressLabel: UILabel = { let label = UILabel() label.textAlignment = .center - label.font = .systemFont(ofSize: 16) + label.font = Constants.FontandColors.descriptionFont return label }() @@ -228,6 +229,7 @@ class MemoryCheckViewController: UIViewController { formatter.dateFormat = "yyyy-MM-dd" return formatter.string(from: Date()) } + } #Preview{ diff --git a/recap/Controllers/Others/PlayGameViewController.swift b/recap/Controllers/Others/PlayGameViewController.swift old mode 100644 new mode 100755 index fafa9db..9aacdf4 --- a/recap/Controllers/Others/PlayGameViewController.swift +++ b/recap/Controllers/Others/PlayGameViewController.swift @@ -5,63 +5,59 @@ // Created by Diptayan Jash on 06/11/24. // + import UIKit -class PlayGameViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - private var collectionView: UICollectionView! +class PlayGameViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + private var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() applyGradientBackground() title = "Games" - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.minimumInteritemSpacing = 10 - layout.minimumLineSpacing = 10 - layout.sectionInset = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15) - layout.itemSize = CGSize(width: 170, height: 226) - - collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collectionView.register(GamesCell.self, forCellWithReuseIdentifier: GamesCell.identifier) - collectionView.delegate = self - collectionView.dataSource = self - collectionView.backgroundColor = .clear - collectionView.alwaysBounceVertical = true - - view.addSubview(collectionView) - - collectionView.translatesAutoresizingMaskIntoConstraints = false + // Initialize table view + tableView = UITableView() + tableView.delegate = self + tableView.dataSource = self + tableView.register(GamesTableViewCell.self, forCellReuseIdentifier: GamesTableViewCell.identifier) + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.rowHeight = Constants.CardSize.DefaultCardWidth + // Add table view to view hierarchy + view.addSubview(tableView) + + // Set up constraints + tableView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - GradientBackground() - } - - private func GradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor.systemPink.withAlphaComponent(0.1).cgColor, - UIColor.systemBackground.cgColor, - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 0, y: 0.6) - gradientLayer.frame = view.bounds - - view.layer.insertSublayer(gradientLayer, at: 0) } - - // MARK: - Collection View Data Source - - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + // MARK: - Table View Data Source + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return gamesDemo.count } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: GamesTableViewCell.identifier, for: indexPath) as? GamesTableViewCell else { + fatalError("Unable to dequeue GamesTableViewCell") + } + + let game = gamesDemo[indexPath.row] + cell.configure(with: game) + return cell + } + + // MARK: - Table View Delegate + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let selectedGame = gamesDemo[indexPath.row] if let viewControllerType = NSClassFromString("recap.\(selectedGame.screenName)") as? UIViewController.Type { @@ -71,15 +67,6 @@ class PlayGameViewController: UIViewController, UICollectionViewDelegate, UIColl print("Error: ViewController \(selectedGame.screenName) not found.") } } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GamesCell.identifier, for: indexPath) as? GamesCell else { - fatalError("Unable to dequeue FamilyMemberCell") - } - let games = gamesDemo[indexPath.row] - cell.configure(with: games) - return cell - } private func applyGradientBackground() { let gradientLayer = CAGradientLayer() @@ -93,6 +80,7 @@ class PlayGameViewController: UIViewController, UICollectionViewDelegate, UIColl view.layer.insertSublayer(gradientLayer, at: 0) } } + #Preview(){ PlayGameViewController() } diff --git a/recap/Controllers/Others/QuestionsViewController.swift b/recap/Controllers/Others/QuestionsViewController.swift deleted file mode 100644 index 1974f15..0000000 --- a/recap/Controllers/Others/QuestionsViewController.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// QuestionsViewController.swift -// recap -// -// Created by Diptayan Jash on 09/11/24. -// - -import UIKit - -class QuestionsViewController: UITableViewController { - -// private var randomQuestions: [Question] = [] -// -// override func viewDidLoad() { -// super.viewDidLoad() -// -// tableView() -// -// title = "Questions" -// navigationController?.navigationBar.prefersLargeTitles = true -// randomQuestions = Array(questionsDemo.shuffled().prefix(5)) -// } -// -// private let headerView: UIView = { -// let view = UIView() -// view.backgroundColor = .systemBackground -// return view -// }() -// -// override func viewDidLayoutSubviews() { -// super.viewDidLayoutSubviews() -// view.applyGradientBackground() -// } -// -// private func tableView() { -// tableView.backgroundColor = .systemGroupedBackground -// tableView.separatorStyle = .none -// tableView.tableHeaderView = headerView -// tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) -// -// tableView.register(QuestionCell.self, forCellReuseIdentifier: QuestionCell.identifier) -// tableView.rowHeight = UITableView.automaticDimension -// tableView.estimatedRowHeight = 88 -// } -// -// // MARK: - Table view data source & delegate -// -// override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { -// return randomQuestions.count -// } -// -// override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { -// guard let cell = tableView.dequeueReusableCell( -// withIdentifier: QuestionCell.identifier, -// for: indexPath -// ) as? QuestionCell else { -// return UITableViewCell() -// } -// -// let question = randomQuestions[indexPath.row] -// cell.configure(with: question) -// -// cell.contentView.preservesSuperviewLayoutMargins = false -// cell.preservesSuperviewLayoutMargins = false -// -// return cell -// } -// -// override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// tableView.deselectRow(at: indexPath, animated: true) -// -// let selectedQuestion = randomQuestions[indexPath.row] -// let answerVC = AnswerViewController(question: selectedQuestion) -// -// answerVC.onAnswerSubmitted = { [weak self] in -// guard let self = self else { return } -// self.randomQuestions[indexPath.row].isAnswered = true -// self.tableView.reloadRows(at: [indexPath], with: .none) -// } -// -// let navController = UINavigationController(rootViewController: answerVC) -// navController.modalPresentationStyle = .pageSheet -// if let sheet = navController.sheetPresentationController { -// sheet.detents = [.large()] -// sheet.prefersGrabberVisible = true -// sheet.prefersEdgeAttachedInCompactHeight = true -// } -// present(navController, animated: true, completion: nil) -// } -//} -// -//extension UIView { -// func applyGradientBackground() { -// let gradientLayer = CAGradientLayer() -// gradientLayer.colors = [ -// UIColor.systemOrange.withAlphaComponent(0.1).cgColor, -// UIColor.systemBackground.cgColor, -// ] -// gradientLayer.startPoint = CGPoint(x: 0, y: 0) -// gradientLayer.endPoint = CGPoint(x: 0, y: 0.6) -// gradientLayer.frame = bounds -// -// layer.sublayers?.removeAll(where: { $0 is CAGradientLayer }) -// -// layer.insertSublayer(gradientLayer, at: 0) -// } -} - -#Preview() { - QuestionsViewController() -} diff --git a/recap/Controllers/Others/QuestionsViewController_Patient.swift b/recap/Controllers/Others/QuestionsViewController_Patient.swift deleted file mode 100644 index fbbe9e8..0000000 --- a/recap/Controllers/Others/QuestionsViewController_Patient.swift +++ /dev/null @@ -1,114 +0,0 @@ - -// -// QuestionsViewController.swift -// recap -// -// Created by Diptayan Jash on 09/11/24. -// - -import UIKit - -class QuestionsViewController_Patient: UITableViewController { - - private var randomQuestions: [Question] = [] - - override func viewDidLoad() { - super.viewDidLoad() - - tableView() - - title = "Questions" - navigationController?.navigationBar.prefersLargeTitles = true - - //TODO :-YOU NEED TO ADD QUESTIONS IN THE ARRAY -// randomQuestions = Array(questionsDemo.shuffled().prefix(5)) - } - - private let headerView: UIView = { - let view = UIView() - view.backgroundColor = .systemBackground - return view - }() - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - view.applyGradientBackground() - } - - private func tableView() { - tableView.backgroundColor = .systemGroupedBackground - tableView.separatorStyle = .none - tableView.tableHeaderView = headerView - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) - - tableView.register(QuestionCell.self, forCellReuseIdentifier: QuestionCell.identifier) - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = 88 - } - - // MARK: - Table view data source & delegate - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return randomQuestions.count - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: QuestionCell.identifier, - for: indexPath - ) as? QuestionCell else { - return UITableViewCell() - } - - let question = randomQuestions[indexPath.row] - cell.configure(with: question) - - cell.contentView.preservesSuperviewLayoutMargins = false - cell.preservesSuperviewLayoutMargins = false - - return cell - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let selectedQuestion = randomQuestions[indexPath.row] - let answerVC = AnswerViewController(question: selectedQuestion) - - answerVC.onAnswerSubmitted = { [weak self] in - guard let self = self else { return } - self.randomQuestions[indexPath.row].isAnswered = true - self.tableView.reloadRows(at: [indexPath], with: .none) - } - - let navController = UINavigationController(rootViewController: answerVC) - navController.modalPresentationStyle = .pageSheet - if let sheet = navController.sheetPresentationController { - sheet.detents = [.large()] - sheet.prefersGrabberVisible = true - sheet.prefersEdgeAttachedInCompactHeight = true - } - present(navController, animated: true, completion: nil) - } -} - -extension UIView { - func applyGradientBackground() { - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor.systemOrange.withAlphaComponent(0.1).cgColor, - UIColor.systemBackground.cgColor, - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 0, y: 0.6) - gradientLayer.frame = bounds - - layer.sublayers?.removeAll(where: { $0 is CAGradientLayer }) - - layer.insertSublayer(gradientLayer, at: 0) - } -} - -#Preview() { - QuestionsViewController() -} diff --git a/recap/Controllers/Others/patientInfo.swift b/recap/Controllers/Others/patientInfo.swift old mode 100644 new mode 100755 index ec2c977..5aa2db6 --- a/recap/Controllers/Others/patientInfo.swift +++ b/recap/Controllers/Others/patientInfo.swift @@ -1,7 +1,7 @@ import FirebaseAuth -import UIKit import FirebaseFirestore import GoogleSignIn +import UIKit class patientInfo: UIViewController { weak var delegate: PatientInfoDelegate? @@ -20,6 +20,19 @@ class patientInfo: UIViewController { // MARK: - UI Components + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.showsVerticalScrollIndicator = true + scrollView.alwaysBounceVertical = true + scrollView.keyboardDismissMode = .interactive + return scrollView + }() + + private let contentView: UIView = { + let view = UIView() + return view + }() + private let profileImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill @@ -50,9 +63,14 @@ class patientInfo: UIViewController { private let saveButton: UIButton = { let button = UIButton() button.setTitle("Save", for: .normal) - button.backgroundColor = .systemBlue - button.layer.cornerRadius = 8 - button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) + button.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button + .setTitleColor( + Constants.ButtonStyle.DefaultButtonTextColor, + for: .normal + ) return button }() @@ -79,14 +97,24 @@ class patientInfo: UIViewController { setupPickers() setupTextFields() + setupKeyboardNotifications() + + // Prevent dismissal by swipe down gesture + if #available(iOS 13.0, *) { + isModalInPresentation = true + } if let user = GIDSignIn.sharedInstance.currentUser, - let imageURL = user.profile?.imageURL(withDimension: 200) { - downloadImage(from: imageURL) - } + let imageURL = user.profile?.imageURL(withDimension: 200) { + downloadImage(from: imageURL) + } } - + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + removeKeyboardNotifications() + } + private func downloadImage(from url: URL) { - URLSession.shared.dataTask(with: url) { [weak self] data, response, error in + URLSession.shared.dataTask(with: url) { [weak self] data, _, error in guard let self = self, error == nil, let data = data, let image = UIImage(data: data) else { print("Failed to download image: \(error?.localizedDescription ?? "Unknown error")") return @@ -102,9 +130,29 @@ class patientInfo: UIViewController { private func setupUI() { view.backgroundColor = .systemBackground + // Add scrollView to view + scrollView.translatesAutoresizingMaskIntoConstraints = false + contentView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + scrollView.addSubview(contentView) + + // Setup scrollView constraints + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + ]) + [profileImageView, stackView, saveButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false - view.addSubview($0) + contentView.addSubview($0) } [firstNameField, lastNameField, dobField, sexField, bloodGroupField, stageField].forEach { @@ -112,24 +160,29 @@ class patientInfo: UIViewController { } NSLayoutConstraint.activate([ - profileImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), - profileImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + profileImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), profileImageView.widthAnchor.constraint(equalToConstant: 100), profileImageView.heightAnchor.constraint(equalToConstant: 100), stackView.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 32), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - saveButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), - saveButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - saveButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + saveButton.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 32), + saveButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + saveButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), saveButton.heightAnchor.constraint(equalToConstant: 50), + saveButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20), ]) let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped)) profileImageView.addGestureRecognizer(tapGesture) + // Add tap gesture to dismiss keyboard when tapping outside text fields + let tapToDismiss = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tapToDismiss) + saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside) } @@ -203,6 +256,80 @@ class patientInfo: UIViewController { // MARK: - Actions + private func setupKeyboardNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + } + + private func removeKeyboardNotifications() { + NotificationCenter.default.removeObserver( + self, + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + } + + @objc private func keyboardWillShow(notification: Notification) { + guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + + let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardFrame.height, right: 0) + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = contentInsets + + // Determine if the active text field is obscured by the keyboard + var visibleRect = view.frame + visibleRect.size.height -= keyboardFrame.height + + if let activeField = findFirstResponder() as? UITextField { + let fieldRect = activeField.convert(activeField.bounds, to: scrollView) + if !visibleRect.contains(fieldRect.origin) { + scrollView.scrollRectToVisible(fieldRect, animated: true) + } + } + } + + @objc private func keyboardWillHide(notification: Notification) { + scrollView.contentInset = .zero + scrollView.scrollIndicatorInsets = .zero + } + + private func findFirstResponder() -> UIView? { + return findFirstResponder(in: view) + } + + private func findFirstResponder(in view: UIView) -> UIView? { + if view.isFirstResponder { + return view + } + + for subview in view.subviews { + if let firstResponder = findFirstResponder(in: subview) { + return firstResponder + } + } + + return nil + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + } + @objc private func datePickerChanged() { let formatter = DateFormatter() formatter.dateFormat = "dd/MM/yyyy" @@ -224,7 +351,7 @@ class patientInfo: UIViewController { let sex = sexField.text, !sex.isEmpty, let bloodGroup = bloodGroupField.text, !bloodGroup.isEmpty, let stage = stageField.text, !stage.isEmpty else { - showAlert(message: "Please fill in all fields") + showLocalAlert(message: "Please fill in all fields") return } @@ -236,10 +363,10 @@ class patientInfo: UIViewController { saveButton.isEnabled = false guard let userId = Auth.auth().currentUser?.uid else { - showAlert(message: "User not logged in.") + showLocalAlert(message: "User not logged in.") return } - + let profileImageURL = GIDSignIn.sharedInstance.currentUser?.profile?.imageURL(withDimension: 200)?.absoluteString ?? "" let updatedData: [String: Any] = [ @@ -249,7 +376,7 @@ class patientInfo: UIViewController { "sex": sex, "bloodGroup": bloodGroup, "stage": stage, - "profileImageURL": profileImageURL + "profileImageURL": profileImageURL, ] let db = Firestore.firestore() @@ -262,17 +389,39 @@ class patientInfo: UIViewController { if let error = error { print("Error updating profile: \(error.localizedDescription)") - self?.showAlert(message: "Failed to save profile. Please try again.") + self?.showLocalAlert(message: "Failed to save profile. Please try again.") } else { print("Profile updated successfully") - let tabBarVC = TabbarViewController() - guard let window = UIApplication.shared.windows.first else { return } - window.rootViewController = tabBarVC - window.makeKeyAndVisible() + + UserDefaultsStorageProfile.shared.saveProfile(details: updatedData, image: nil) { success in + if success { + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.hasPatientCompletedProfile) + UserDefaults.standard.synchronize() + + if let delegate = self?.delegate { + delegate.didCompleteProfile() + } else { + // Fallback if delegate is not set + let tabBarVC = TabbarViewController() + guard let window = UIApplication.shared.windows.first else { return } + window.rootViewController = tabBarVC + window.makeKeyAndVisible() + } + } else { + self?.showLocalAlert(message: "Failed to save profile locally. Please try again.") + } + } } } } } + + // Local alert method that doesn't dismiss the view controller + private func showLocalAlert(message: String) { + let alertController = UIAlertController(title: "Notice", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default)) + present(alertController, animated: true) + } } // MARK: - UIImagePickerControllerDelegate @@ -368,7 +517,7 @@ class CustomTextField: UITextField { private func setup() { backgroundColor = .systemGray6 - layer.cornerRadius = 8 + layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1)) leftViewMode = .always heightAnchor.constraint(equalToConstant: 44).isActive = true diff --git a/recap/Controllers/Others/settings/AboutAppViewController.swift b/recap/Controllers/Others/settings/AboutAppViewController.swift deleted file mode 100644 index 1a0b188..0000000 --- a/recap/Controllers/Others/settings/AboutAppViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AboutAppViewController.swift -// recap -// -// Created by admin70 on 27/01/25. -// - -import UIKit - -class AboutAppViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - title = "About App" - } -} -#Preview { - AboutAppViewController() -} diff --git a/recap/Controllers/Others/settings/LanguageViewController.swift b/recap/Controllers/Others/settings/LanguageViewController.swift deleted file mode 100644 index 784802a..0000000 --- a/recap/Controllers/Others/settings/LanguageViewController.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// LanguageViewController.swift -// recap -// -// Created by admin70 on 27/01/25. -// - -import UIKit - -class LanguageViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - title = "Language" - } -} diff --git a/recap/Controllers/Others/settings/PrivacyViewController.swift b/recap/Controllers/Others/settings/PrivacyViewController.swift deleted file mode 100644 index 80f65ee..0000000 --- a/recap/Controllers/Others/settings/PrivacyViewController.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// PrivacyViewController.swift -// recap -// -// Created by admin70 on 27/01/25. -// - -import UIKit - -class PrivacyViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - title = "Privacy" - } -} diff --git a/recap/Controllers/initial/.DS_Store b/recap/Controllers/initial/.DS_Store new file mode 100755 index 0000000..0094dc2 Binary files /dev/null and b/recap/Controllers/initial/.DS_Store differ diff --git a/recap/Controllers/initial/Family/FamilyLoginViewController.swift b/recap/Controllers/initial/Family/FamilyLoginViewController.swift old mode 100644 new mode 100755 index b532136..c3d4038 --- a/recap/Controllers/initial/Family/FamilyLoginViewController.swift +++ b/recap/Controllers/initial/Family/FamilyLoginViewController.swift @@ -6,26 +6,28 @@ // import UIKit +import FirebaseFirestore class FamilyLoginViewController: UIViewController { var verifiedUserDocID: String? var isRemembered = true - + // MARK: - UI Components + private let logoImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.image = UIImage(named: "recapLogo") return imageView }() - + private let titleLabel: UILabel = { let label = UILabel() - label.text = "Login 👋" + label.text = "Login" label.font = .systemFont(ofSize: 32, weight: .bold) return label }() - + let patientUIDField: UITextField = { let field = UITextField() field.placeholder = "Patient UID" @@ -36,67 +38,107 @@ class FamilyLoginViewController: UIViewController { field.autocapitalizationType = .none return field }() - + let verifyButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Verify", for: .normal) - button.backgroundColor = .systemBlue - button.setTitleColor(.white, for: .normal) + button.backgroundColor = AppColors.primaryButtonColor + button.setTitleColor(AppColors.iconColor, for: .normal) button.layer.cornerRadius = 12 - button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold) + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) return button }() - - let emailField: UITextField = { - let field = UITextField() - field.placeholder = "Email address" - field.keyboardType = .emailAddress - field.backgroundColor = .systemGray6 - field.layer.cornerRadius = 12 - field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) - field.leftViewMode = .always - field.autocapitalizationType = .none - field.isEnabled = false // Initially disabled - return field - }() - - let passwordField: UITextField = { - let field = UITextField() - field.placeholder = "Password" - field.isSecureTextEntry = true - field.backgroundColor = .systemGray6 - field.layer.cornerRadius = 12 - field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) - field.leftViewMode = .always - field.isEnabled = false // Initially disabled - - let button = UIButton(type: .custom) - button.setImage(UIImage(systemName: "eye.slash"), for: .normal) - button.tintColor = .systemGray - button.frame = CGRect(x: 0, y: 0, width: 30, height: 30) - button.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside) - field.rightView = button - field.rightViewMode = .always - - return field + + let googleSignInButton: UIButton = { + let button = UIButton(type: .system) + button.backgroundColor = .white + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + button.layer.borderWidth = 1 + button.layer.borderColor = UIColor.systemGray4.cgColor + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + + // Google Logo + let imageView = UIImageView(image: UIImage(named: "googleLogo")) + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.widthAnchor.constraint(equalToConstant: 24).isActive = true + imageView.heightAnchor.constraint(equalToConstant: 24).isActive = true + + // Label + let label = UILabel() + label.text = "Sign in with Google" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = .black + + // StackView for image + text + let stackView = UIStackView(arrangedSubviews: [imageView, label]) + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + + button.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: button.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor) + ]) + + return button }() - - let rememberMeButton: UIButton = { + + let appleSignInButton: UIButton = { let button = UIButton(type: .system) - button.setTitle(" Remember me", for: .normal) // Space for better alignment - button.tintColor = .black - button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal) // Default checked - button.tintColor = .systemBlue + button.backgroundColor = .black + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + + // Apple Logo + let imageView = UIImageView(image: UIImage(systemName: "applelogo")) + imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.widthAnchor.constraint(equalToConstant: 24).isActive = true + imageView.heightAnchor.constraint(equalToConstant: 24).isActive = true + + // Label + let label = UILabel() + label.text = "Sign in with Apple" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = .white + + // StackView for image + text + let stackView = UIStackView(arrangedSubviews: [imageView, label]) + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + + button.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: button.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor) + ]) + return button }() - + + private let signInStackView: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 16 + stack.distribution = .fillEqually + return stack + }() + fileprivate let forgotPasswordButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Forgot password?", for: .normal) button.tintColor = .black return button }() - + fileprivate let loginButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Log in", for: .normal) @@ -106,8 +148,9 @@ class FamilyLoginViewController: UIViewController { button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) return button }() - + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() title = "Family Login" @@ -115,87 +158,80 @@ class FamilyLoginViewController: UIViewController { if isUserLoggedIn { if let savedEmail = UserDefaults.standard.string(forKey: "savedEmail"), let savedPassword = UserDefaults.standard.string(forKey: "savedPassword") { - emailField.text = savedEmail - passwordField.text = savedPassword - +// emailField.text = savedEmail +// passwordField.text = savedPassword + // Auto-login - loginTapped() +// loginTapped() } } - setupUI() + let Dismisskeyboard = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(Dismisskeyboard) + } + + @objc func dismissKeyboard() { + view.endEditing(true) } - + // MARK: - Setup UI + private func setupUI() { view.backgroundColor = .systemBackground - + // Add subviews - [logoImageView, titleLabel, patientUIDField, verifyButton, emailField, passwordField, - rememberMeButton, forgotPasswordButton, loginButton].forEach { + [logoImageView, titleLabel, patientUIDField, verifyButton, signInStackView].forEach { view.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false } - + + // Add buttons to stack + signInStackView.addArrangedSubview(googleSignInButton) + signInStackView.addArrangedSubview(appleSignInButton) + // Setup constraints NSLayoutConstraint.activate([ logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), logoImageView.widthAnchor.constraint(equalToConstant: 100), logoImageView.heightAnchor.constraint(equalToConstant: 100), - + titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20), titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - + patientUIDField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30), patientUIDField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), patientUIDField.trailingAnchor.constraint(equalTo: verifyButton.leadingAnchor, constant: -10), patientUIDField.heightAnchor.constraint(equalToConstant: 50), - + verifyButton.centerYAnchor.constraint(equalTo: patientUIDField.centerYAnchor), verifyButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), verifyButton.widthAnchor.constraint(equalToConstant: 80), verifyButton.heightAnchor.constraint(equalToConstant: 50), - - emailField.topAnchor.constraint(equalTo: patientUIDField.bottomAnchor, constant: 16), - emailField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - emailField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - emailField.heightAnchor.constraint(equalToConstant: 50), - - passwordField.topAnchor.constraint(equalTo: emailField.bottomAnchor, constant: 16), - passwordField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - passwordField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - passwordField.heightAnchor.constraint(equalToConstant: 50), - - rememberMeButton.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: 16), - rememberMeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - - forgotPasswordButton.centerYAnchor.constraint(equalTo: rememberMeButton.centerYAnchor), - forgotPasswordButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - - loginButton.topAnchor.constraint(equalTo: rememberMeButton.bottomAnchor, constant: 30), - loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - loginButton.heightAnchor.constraint(equalToConstant: 50) + + signInStackView.topAnchor.constraint(equalTo: patientUIDField.bottomAnchor, constant: 30), + signInStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + signInStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + signInStackView.heightAnchor.constraint(equalToConstant: 116) // 50 * 2 + 16 spacing ]) - - // Add targets (Fixed function name) - rememberMeButton.addTarget(self, action: #selector(toggleRememberMe), for: .touchUpInside) - loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside) + + // Add targets verifyButton.addTarget(self, action: #selector(verifyPatientUID), for: .touchUpInside) + googleSignInButton.addTarget(self, action: #selector(googleSignInTapped), for: .touchUpInside) + appleSignInButton.addTarget(self, action: #selector(appleSignInTapped), for: .touchUpInside) } - + @objc private func togglePasswordVisibility(_ sender: UIButton) { - passwordField.isSecureTextEntry.toggle() - let imageName = passwordField.isSecureTextEntry ? "eye.slash" : "eye" - sender.setImage(UIImage(systemName: imageName), for: .normal) - } - - @objc private func toggleRememberMe() { - isRemembered.toggle() - let imageName = isRemembered ? "checkmark.circle.fill" : "circle" - rememberMeButton.setImage(UIImage(systemName: imageName), for: .normal) +// passwordField.isSecureTextEntry.toggle() +// let imageName = passwordField.isSecureTextEntry ? "eye.slash" : "eye" +// sender.setImage(UIImage(systemName: imageName), for: .normal) } + +// @objc private func toggleRememberMe() { +// isRemembered.toggle() +// let imageName = isRemembered ? "checkmark.circle.fill" : "circle" +// rememberMeButton.setImage(UIImage(systemName: imageName), for: .normal) +// } } #Preview { FamilyLoginViewController() } diff --git a/recap/Controllers/initial/Family/FamilyRegistrationViewController.swift b/recap/Controllers/initial/Family/FamilyRegistrationViewController.swift new file mode 100644 index 0000000..86097f6 --- /dev/null +++ b/recap/Controllers/initial/Family/FamilyRegistrationViewController.swift @@ -0,0 +1,747 @@ +// +// FamilyRegistrationViewController.swift +// recap +// +// Created by Diptayan Jash on 23/03/25. +// + +import Foundation +import UIKit +import FirebaseStorage +import SwiftUICore +import FirebaseFirestore +import Lottie +import PhotosUI + +class FamilyRegistrationViewController: UIViewController { + var email: String = "" + var userDocID: String = "" + var profileImageURL: String = "" + var selectedImage: UIImage? + var currentStep = 0 + + private let cardView: UIView = { + let view = UIView() + view.backgroundColor = .systemBackground + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + view.layer.shadowColor = Constants.FontandColors.defaultshadowColor + view.layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) + view.layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + view.layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + return view + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "Join Family Circle" + label.font = .systemFont(ofSize: 28, weight: .bold) + label.textColor = .label + return label + }() + + private let subtitleLabel: UILabel = { + let label = UILabel() + label.text = "Complete your profile to connect" + label.font = .systemFont(ofSize: 16, weight: .regular) + label.textColor = .secondaryLabel + return label + }() + + private let stepsLabel: UILabel = { + let label = UILabel() + label.text = "STEP 1 OF 4" + label.font = .systemFont(ofSize: 12, weight: .bold) + label.textColor = .systemBlue + return label + }() + + private let stepTitleLabel: UILabel = { + let label = UILabel() + label.text = "Personal Information" + label.font = .systemFont(ofSize: 20, weight: .semibold) + label.textColor = .label + return label + }() + + private let fieldContainer: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + private static func createTextField(placeholder: String, iconName: String, keyboardType: UIKeyboardType = .default) -> UITextField { + let field = UITextField() + field.placeholder = placeholder + field.keyboardType = keyboardType + field.backgroundColor = .systemGray6 + field.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + field.autocapitalizationType = .none + field.autocorrectionType = .no + field.borderStyle = .none + field.translatesAutoresizingMaskIntoConstraints = false + field.clearButtonMode = .whileEditing + + // Create container view for icon + let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 50)) + + // Fix icon positioning and size to be consistent + let iconView = UIImageView(frame: CGRect(x: 15, y: 13, width: 24, height: 24)) + iconView.image = UIImage(systemName: iconName) + iconView.tintColor = .systemGray + iconView.contentMode = .scaleAspectFit + + containerView.addSubview(iconView) + + field.leftView = containerView + field.leftViewMode = .always + + return field + } + + // Now let's define our fields using the helper function + let nameField: UITextField = { + return createTextField(placeholder: "Full name", iconName: "person") + }() + + let phoneField: UITextField = { + return createTextField(placeholder: "Phone number", iconName: "phone", keyboardType: .numberPad) + }() + let relationField: UITextField = { + let field = UITextField() + field.placeholder = "Your Relationship" + field.backgroundColor = .systemGray6 + field.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + field.autocapitalizationType = .none + field.autocorrectionType = .no + field.borderStyle = .none + field.translatesAutoresizingMaskIntoConstraints = false + field.clearButtonMode = .whileEditing + + // Create container view for icon + let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 50)) + + // Fix icon positioning to match other fields + let iconView = UIImageView(frame: CGRect(x: 15, y: 13, width: 24, height: 24)) + iconView.image = UIImage(systemName: "heart.fill") + iconView.tintColor = .systemGray + iconView.contentMode = .scaleAspectFit + + containerView.addSubview(iconView) + + field.leftView = containerView + field.leftViewMode = .always + + return field + }() + + private let profileImageContainer: UIView = { + let view = UIView() + view.backgroundColor = .clear + return view + }() + + private let profileImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.layer.cornerRadius = 60 + imageView.clipsToBounds = true + imageView.backgroundColor = .systemGray6 + imageView.isUserInteractionEnabled = true + + // Add camera icon overlay + let cameraIconView = UIImageView(image: UIImage(systemName: "camera.fill")) + cameraIconView.tintColor = .white + cameraIconView.contentMode = .scaleAspectFit + cameraIconView.tag = 100 + cameraIconView.frame = CGRect(x: 45, y: 45, width: 30, height: 30) + imageView.addSubview(cameraIconView) + + // Add gradient overlay + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [UIColor.clear.cgColor, UIColor.black.withAlphaComponent(0.4).cgColor] + gradientLayer.locations = [0.6, 1.0] + gradientLayer.frame = CGRect(x: 0, y: 0, width: 120, height: 120) + imageView.layer.addSublayer(gradientLayer) + + return imageView + }() + + private let addPhotoLabel: UILabel = { + let label = UILabel() + label.text = "Add your photo" + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = .secondaryLabel + label.textAlignment = .center + return label + }() + + private let progressView: UIProgressView = { + let progress = UIProgressView(progressViewStyle: .bar) + progress.progressTintColor = .systemBlue + progress.trackTintColor = .systemGray6 + progress.layer.cornerRadius = 4 + progress.clipsToBounds = true + progress.layer.sublayers?[1].cornerRadius = 4 + progress.layer.sublayers?[1].masksToBounds = true + progress.progress = 0.25 + return progress + }() + + private let nextButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Continue", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) + button.backgroundColor = .systemBlue + button.setTitleColor(.white, for: .normal) + button.layer.cornerRadius = 24 + + // Create shadow + button.layer.shadowColor = UIColor.systemBlue.withAlphaComponent(0.5).cgColor + button.layer.shadowOffset = CGSize(width: 0, height: 4) + button.layer.shadowRadius = 8 + button.layer.shadowOpacity = 0.5 + + return button + }() + + private let relations = ["Father", "Mother", "Sister", "Brother", "Spouse", "Other"] + private var relationPicker = UIPickerView() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupNavigationBar() + setupDelegates() + setupRelationPickerView() + updateUIForCurrentStep() + + if #available(iOS 13.0, *) { + isModalInPresentation = true + } + } + + // MARK: - Setup + private func setupUI() { + view.backgroundColor = .systemGroupedBackground + + // Add views to hierarchy + view.addSubview(cardView) + view.addSubview(nextButton) + + cardView.addSubview(titleLabel) + cardView.addSubview(subtitleLabel) + cardView.addSubview(stepsLabel) + cardView.addSubview(stepTitleLabel) + cardView.addSubview(progressView) + cardView.addSubview(fieldContainer) + + fieldContainer.addSubview(nameField) + fieldContainer.addSubview(phoneField) + fieldContainer.addSubview(relationField) + fieldContainer.addSubview(profileImageContainer) + + profileImageContainer.addSubview(profileImageView) + profileImageContainer.addSubview(addPhotoLabel) + + // Configure auto layout + cardView.translatesAutoresizingMaskIntoConstraints = false + nextButton.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + stepsLabel.translatesAutoresizingMaskIntoConstraints = false + stepTitleLabel.translatesAutoresizingMaskIntoConstraints = false + progressView.translatesAutoresizingMaskIntoConstraints = false + fieldContainer.translatesAutoresizingMaskIntoConstraints = false + nameField.translatesAutoresizingMaskIntoConstraints = false + phoneField.translatesAutoresizingMaskIntoConstraints = false + relationField.translatesAutoresizingMaskIntoConstraints = false + profileImageContainer.translatesAutoresizingMaskIntoConstraints = false + profileImageView.translatesAutoresizingMaskIntoConstraints = false + addPhotoLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + // Card View + cardView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + cardView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + cardView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + cardView.bottomAnchor.constraint(equalTo: nextButton.topAnchor, constant: -30), + + // Title and Subtitle + titleLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 24), + titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24), + titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24), + + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + subtitleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24), + subtitleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24), + + // Steps information + stepsLabel.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 24), + stepsLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24), + stepsLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24), + + stepTitleLabel.topAnchor.constraint(equalTo: stepsLabel.bottomAnchor, constant: 8), + stepTitleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24), + stepTitleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24), + + // Progress + progressView.topAnchor.constraint(equalTo: stepTitleLabel.bottomAnchor, constant: 16), + progressView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24), + progressView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24), + progressView.heightAnchor.constraint(equalToConstant: 8), + + // Field Container + fieldContainer.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 32), + fieldContainer.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24), + fieldContainer.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24), + fieldContainer.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -24), + + // Input Fields + nameField.topAnchor.constraint(equalTo: fieldContainer.topAnchor), + nameField.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor), + nameField.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor), + nameField.heightAnchor.constraint(equalToConstant: 60), + + phoneField.topAnchor.constraint(equalTo: nameField.bottomAnchor, constant: 16), + phoneField.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor), + phoneField.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor), + phoneField.heightAnchor.constraint(equalToConstant: 60), + + relationField.topAnchor.constraint(equalTo: phoneField.bottomAnchor, constant: 16), + relationField.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor), + relationField.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor), + relationField.heightAnchor.constraint(equalToConstant: 60), + + // Profile Image Container + profileImageContainer.topAnchor.constraint(equalTo: fieldContainer.topAnchor, constant: 20), + profileImageContainer.centerXAnchor.constraint(equalTo: fieldContainer.centerXAnchor), + profileImageContainer.widthAnchor.constraint(equalToConstant: 200), + profileImageContainer.heightAnchor.constraint(equalToConstant: 150), + + // Profile Image + profileImageView.topAnchor.constraint(equalTo: profileImageContainer.topAnchor), + profileImageView.centerXAnchor.constraint(equalTo: profileImageContainer.centerXAnchor), + profileImageView.widthAnchor.constraint(equalToConstant: 120), + profileImageView.heightAnchor.constraint(equalToConstant: 120), + + // Add Photo Label + addPhotoLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 12), + addPhotoLabel.centerXAnchor.constraint(equalTo: profileImageContainer.centerXAnchor), + addPhotoLabel.leadingAnchor.constraint(equalTo: profileImageContainer.leadingAnchor), + addPhotoLabel.trailingAnchor.constraint(equalTo: profileImageContainer.trailingAnchor), + + // Next Button + nextButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + nextButton.heightAnchor.constraint(equalToConstant: 56) + ]) + + // Add action targets + nextButton.addTarget(self, action: #selector(nextTapped), for: .touchUpInside) + + // Add tap gesture to profile image + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(addPhotoTapped)) + profileImageView.addGestureRecognizer(tapGesture) + + // Add tap gesture to relation field + let relationTapGesture = UITapGestureRecognizer(target: self, action: #selector(showRelationPicker)) + relationField.addGestureRecognizer(relationTapGesture) + } + + private func setupNavigationBar() { + navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationController?.navigationBar.shadowImage = UIImage() + navigationController?.navigationBar.isTranslucent = true + navigationController?.navigationBar.tintColor = .systemBlue + + let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelTapped)) + cancelButton.tintColor = .secondaryLabel + navigationItem.rightBarButtonItem = cancelButton + + // Hide the default back button + navigationItem.hidesBackButton = true + } + + private func setupDelegates() { + nameField.delegate = self + phoneField.delegate = self + relationPicker.delegate = self + relationPicker.dataSource = self + } + + private func setupRelationPickerView() { + let toolBar = UIToolbar() + toolBar.barStyle = .default + toolBar.isTranslucent = true + toolBar.tintColor = .systemBlue + toolBar.sizeToFit() + + let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneRelationPickerTapped)) + let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelRelationPickerTapped)) + + toolBar.setItems([cancelButton, spaceButton, doneButton], animated: false) + toolBar.isUserInteractionEnabled = true + + relationField.inputView = relationPicker + relationField.inputAccessoryView = toolBar + } + + // MARK: - UI Update Methods + private func updateUIForCurrentStep() { + // Update step labels + stepsLabel.text = "STEP \(currentStep + 1) OF 4" + + // Update progress + progressView.progress = Float(currentStep + 1) / 4.0 + + // Hide all fields + nameField.isHidden = true + phoneField.isHidden = true + relationField.isHidden = true + profileImageContainer.isHidden = true + + // Show fields based on current step + switch currentStep { + case 0: + stepTitleLabel.text = "Personal Information" + nameField.isHidden = false + nextButton.setTitle("Continue", for: .normal) + case 1: + stepTitleLabel.text = "Contact Information" + phoneField.isHidden = false + nextButton.setTitle("Continue", for: .normal) + case 2: + stepTitleLabel.text = "Your Relationship" + relationField.isHidden = false + nextButton.setTitle("Continue", for: .normal) + case 3: + stepTitleLabel.text = "Profile Photo" + profileImageContainer.isHidden = false + nextButton.setTitle("Complete Registration", for: .normal) + default: + break + } + + // Add animation + UIView.animate(withDuration: 0.3) { + self.view.layoutIfNeeded() + } + } + + // MARK: - Actions + @objc private func cancelTapped() { + // Show confirmation alert + let alert = UIAlertController(title: "Cancel Registration", message: "Are you sure you want to cancel? Your progress will be lost.", preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Continue Registration", style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: "Yes, Cancel", style: .destructive, handler: { _ in + self.dismiss(animated: true) + })) + + present(alert, animated: true) + } + + @objc private func nextTapped() { + switch currentStep { + case 0: + guard let name = nameField.text, !name.isEmpty else { + showErrorAnimation(for: nameField) + return + } + animateToNextStep() + case 1: + guard let phone = phoneField.text, !phone.isEmpty else { + showErrorAnimation(for: phoneField) + return + } + animateToNextStep() + case 2: + guard let relation = relationField.text, !relation.isEmpty else { + showErrorAnimation(for: relationField) + return + } + animateToNextStep() + case 3: + guard selectedImage != nil else { + showErrorAnimation(for: profileImageView) + return + } + submitRegistration() + default: + break + } + } + + @objc private func backTapped() { + if currentStep > 0 { + currentStep -= 1 + updateUIForCurrentStep() + } + } + + @objc private func addPhotoTapped() { + var config = PHPickerConfiguration() + config.filter = .images + config.selectionLimit = 1 + let picker = PHPickerViewController(configuration: config) + picker.delegate = self + present(picker, animated: true) + } + + @objc private func showRelationPicker() { + relationField.becomeFirstResponder() + } + + @objc private func doneRelationPickerTapped() { + let selectedRow = relationPicker.selectedRow(inComponent: 0) + relationField.text = relations[selectedRow] + view.endEditing(true) + } + + @objc private func cancelRelationPickerTapped() { + view.endEditing(true) + } + + private func animateToNextStep() { + // First animate out current fields + UIView.animate(withDuration: 0.2, animations: { + self.fieldContainer.alpha = 0 + }) { _ in + // Update to next step + self.currentStep += 1 + self.updateUIForCurrentStep() + + // Then animate in new fields + self.fieldContainer.alpha = 0 + UIView.animate(withDuration: 0.3) { + self.fieldContainer.alpha = 1 + } + } + } + + private func showErrorAnimation(for view: UIView) { + // Vibrate + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.error) + + // Shake animation + let animation = CAKeyframeAnimation(keyPath: "transform.translation.x") + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.duration = 0.6 + animation.values = [-10.0, 10.0, -8.0, 8.0, -5.0, 5.0, 0.0] + view.layer.add(animation, forKey: "shake") + + // Highlight border in red + let originalBorderColor = view.layer.borderColor + let originalBorderWidth = view.layer.borderWidth + + UIView.animate(withDuration: 0.1, animations: { + view.layer.borderColor = UIColor.systemRed.cgColor + view.layer.borderWidth = 2.0 + }) { _ in + UIView.animate(withDuration: 0.5) { + view.layer.borderColor = originalBorderColor + view.layer.borderWidth = originalBorderWidth + } + } + } + + private func submitRegistration() { + let loadingAnimation = showLoadingAnimation() + guard let name = nameField.text, !name.isEmpty, + let phone = phoneField.text, !phone.isEmpty, + let relation = relationField.text, !relation.isEmpty, + let image = selectedImage else { + showAlert(message: "Please fill in all fields") + return + } + + // Upload image to Firebase Storage + uploadImage(image) { [weak self] imageURL in + guard let self = self else { return } + + let db = Firestore.firestore() + let firestoreData: [String: Any] = [ + "name": name, + "email": self.email, + "phone": phone, + "relation": relation, + "imageURL": imageURL, + "createdAt": FieldValue.serverTimestamp() + ] + + // Create data for UserDefaults without timestamp + let userDefaultsData: [String: Any] = [ + "name": name, + "email": self.email, + "phone": phone, + "relation": relation, + "imageURL": imageURL + ] + + db.collection("users").document(self.userDocID).collection("family_members").addDocument(data: firestoreData) { [weak self] error in + guard let self = self else { return } + + removeLoadingAnimation(loadingAnimation) + + if let error = error { + self.showAlert(message: "Error registering family member: \(error.localizedDescription)") + return + } + + // Save family member details to UserDefaults + UserDefaults.standard.set(userDefaultsData, forKey: "familyMemberDetails") + UserDefaults.standard.set(imageURL, forKey: Constants.UserDefaultsKeys.familyMemberImageURL) + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.isFamilyMemberLoggedIn) + UserDefaults.standard.synchronize() + + self.fetchPatientDetails(userDocID: self.userDocID) + + } + } + } + + private func uploadImage(_ image: UIImage, completion: @escaping (String) -> Void) { + guard let imageData = image.jpegData(compressionQuality: 0.5) else { + completion("") + return + } + + let storage = Storage.storage() + let storageRef = storage.reference() + let imageName = "\(UUID().uuidString).jpg" + let imageRef = storageRef.child("family_members/\(imageName)") + + let metadata = StorageMetadata() + metadata.contentType = "image/jpeg" + + // Show upload progress indicator + let loadingView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)) + loadingView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + loadingView.tag = 999 + + let progressIndicator = UIProgressView(progressViewStyle: .bar) + progressIndicator.progressTintColor = .systemBlue + progressIndicator.trackTintColor = .white + progressIndicator.progress = 0.0 + progressIndicator.frame = CGRect(x: 50, y: view.center.y, width: view.frame.width - 100, height: 10) + progressIndicator.layer.cornerRadius = 5 + progressIndicator.clipsToBounds = true + + loadingView.addSubview(progressIndicator) + view.addSubview(loadingView) + + // Upload with progress tracking + let uploadTask = imageRef.putData(imageData, metadata: metadata) { metadata, error in + // Remove loading view + loadingView.removeFromSuperview() + + if let error = error { + print("Error uploading image: \(error.localizedDescription)") + completion("") + return + } + + imageRef.downloadURL { url, error in + if let error = error { + print("Error getting download URL: \(error.localizedDescription)") + completion("") + return + } + + completion(url?.absoluteString ?? "") + } + } + } + private func fetchPatientDetails(userDocID: String) { + let loadingAnimation = showLoadingAnimation() + let db = Firestore.firestore() + db.collection("users").document(userDocID).getDocument { [weak self] document, error in + guard let self = self else { return } + + removeLoadingAnimation(loadingAnimation) + + if let error = error { + print("Error fetching patient details: \(error.localizedDescription)") + return + } + + guard let document = document, document.exists else { + print("Patient document not found.") + return + } + + let userData = document.data() ?? [:] + UserDefaults.standard.set(userData, forKey: "patientDetails") + + DispatchQueue.main.async { + self.animateSlideToMainScreen() + } + } + } + private func animateSlideToMainScreen() { + let mainVC = TabbarFamilyViewController() + let navigationController = UINavigationController(rootViewController: mainVC) + + guard let window = UIApplication.shared.windows.first else { return } + window.addSubview(navigationController.view) + navigationController.view.frame = CGRect(x: window.frame.width, y: 0, width: window.frame.width, height: window.frame.height) + + UIView.animate(withDuration: 0.5, animations: { + self.view.frame.origin.x = -self.view.frame.width + navigationController.view.frame = window.bounds + }) { _ in + window.rootViewController = navigationController + window.makeKeyAndVisible() + } + } +} +// MARK: - UIPickerViewDelegate & UIPickerViewDataSource +extension FamilyRegistrationViewController: UIPickerViewDelegate, UIPickerViewDataSource { + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return relations.count + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return relations[row] + } +} + +// MARK: - UITextFieldDelegate +extension FamilyRegistrationViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } +} + +// MARK: - PHPickerViewControllerDelegate +extension FamilyRegistrationViewController: PHPickerViewControllerDelegate { + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true) + + guard let provider = results.first?.itemProvider else { return } + + if provider.canLoadObject(ofClass: UIImage.self) { + provider.loadObject(ofClass: UIImage.self) { [weak self] image, error in + guard let self = self, + let image = image as? UIImage else { return } + + DispatchQueue.main.async { + self.selectedImage = image + self.profileImageView.image = image + } + } + } + } +} + +#Preview { FamilyRegistrationViewController() } diff --git a/recap/Controllers/initial/OnboardingViewController.swift b/recap/Controllers/initial/OnboardingViewController.swift new file mode 100755 index 0000000..8315347 --- /dev/null +++ b/recap/Controllers/initial/OnboardingViewController.swift @@ -0,0 +1,181 @@ +// +// OnboardingViewController.swift +// recap +// +// Created by Diptayan Jash on 11/02/25. +// +import Foundation +import UIKit + +// MARK: - OnboardingPage Model +struct OnboardingFeature { + let image: String // SF Symbol name + let title: String + let description: String +} + +// MARK: - OnboardingViewController +class OnboardingViewController: UIViewController { + + // MARK: - Properties + private let features: [OnboardingFeature] = [ + OnboardingFeature( + image: "brain.head.profile", + title: "Memory Companion", + description: "Your personal assistant for maintaining and improving memory health" + ), + OnboardingFeature( + image: "list.clipboard", + title: "Daily Questions", + description: "Answer routine questions and get them verified by family members" + ), + OnboardingFeature( + image: "chart.line.uptrend.xyaxis", + title: "Track Progress", + description: "Monitor memory improvements with detailed reports and analytics" + ), + OnboardingFeature( + image: "gamecontroller", + title: "Memory Games", + description: "Engage with fun memory exercises and cognitive games" + ) + ] + + // MARK: - UI Components + private let mainStack: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .vertical + stack.spacing = 28 + stack.alignment = .center + return stack + }() + + private let welcomeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Welcome to Recap" + label.font = .systemFont(ofSize: 30, weight: .bold) + label.textAlignment = .center + return label + }() + + private let getStartedButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + + var configuration = UIButton.Configuration.filled() + configuration.cornerStyle = .capsule + configuration.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 36, bottom: 14, trailing: 36) + configuration.title = "Get Started" + configuration.baseBackgroundColor = AppColors.primaryButtonColor // Change to your desired color + configuration.baseForegroundColor = AppColors.primaryButtonTextColor // Change text color if needed + + button.configuration = configuration + return button + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + isModalInPresentation = true + } + + // MARK: - Setup + private func setupUI() { + view.backgroundColor = .systemBackground + + view.addSubview(mainStack) + + mainStack.addArrangedSubview(welcomeLabel) + + // Add feature views to stack + features.forEach { feature in + let featureView = createFeatureView(feature) + mainStack.addArrangedSubview(featureView) + + NSLayoutConstraint.activate([ + featureView.leadingAnchor.constraint(equalTo: mainStack.leadingAnchor, constant: 24), + featureView.trailingAnchor.constraint(equalTo: mainStack.trailingAnchor, constant: -24) + ]) + } + + mainStack.addArrangedSubview(getStartedButton) + + getStartedButton.addTarget(self, action: #selector(getStartedTapped), for: .touchUpInside) + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40), + mainStack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + mainStack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainStack.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40) + ]) + } + + private func createFeatureView(_ feature: OnboardingFeature) -> UIView { + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .horizontal + stack.spacing = 16 + stack.alignment = .center + + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.tintColor = AppColors.iconColor + imageView.image = UIImage(systemName: feature.image)?.applyingSymbolConfiguration(.init(pointSize: 32)) + + let textStack = UIStackView() + textStack.translatesAutoresizingMaskIntoConstraints = false + textStack.axis = .vertical + textStack.spacing = 4 + + let titleLabel = UILabel() + titleLabel.text = feature.title + titleLabel.font = .systemFont(ofSize: 18, weight: .semibold) + + let descriptionLabel = UILabel() + descriptionLabel.text = feature.description + descriptionLabel.font = .systemFont(ofSize: 14) + descriptionLabel.textColor = .secondaryLabel + descriptionLabel.numberOfLines = 2 + + textStack.addArrangedSubview(titleLabel) + textStack.addArrangedSubview(descriptionLabel) + + stack.addArrangedSubview(imageView) + stack.addArrangedSubview(textStack) + + containerView.addSubview(stack) + + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 40), + imageView.heightAnchor.constraint(equalToConstant: 40), + + stack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12), + stack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + stack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + stack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -12) + ]) + + return containerView + } + + // MARK: - Actions + @objc private func getStartedTapped() { + UserDefaults.standard.set(true, forKey: "HasCompletedOnboarding") + dismiss(animated: true) { + if let rootVC = UIApplication.shared.windows.first?.rootViewController as? launchScreenViewController { + rootVC.transitionToMainScreen() + } + } + } +} + +#Preview { + OnboardingViewController() +} diff --git a/recap/Controllers/initial/PatientLoginViewController.swift b/recap/Controllers/initial/PatientLoginViewController.swift old mode 100644 new mode 100755 index 13f79e2..6c8c490 --- a/recap/Controllers/initial/PatientLoginViewController.swift +++ b/recap/Controllers/initial/PatientLoginViewController.swift @@ -5,229 +5,328 @@ // Created by Diptayan Jash on 15/12/24. // +import AuthenticationServices +import GoogleSignIn import UIKit class PatientLoginViewController: UIViewController { - + var isRemembered = true + // MARK: - UI Components + private let logoImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.image = UIImage(named: "recapLogo") return imageView }() - + private let titleLabel: UILabel = { let label = UILabel() label.text = "Login" label.font = .systemFont(ofSize: 32, weight: .bold) return label }() - - let emailField: UITextField = { + + let emailField: UITextField = { let field = UITextField() field.placeholder = "Email address" field.keyboardType = .emailAddress field.backgroundColor = .systemGray6 - field.layer.cornerRadius = 12 - field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) - field.leftViewMode = .always + field.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius field.autocapitalizationType = .none + field.autocorrectionType = .no + field.borderStyle = .none + field.translatesAutoresizingMaskIntoConstraints = false + field.clearButtonMode = .whileEditing + + let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 50)) + + let iconView = UIImageView(frame: CGRect(x: 13, y: 13, width: 24, height: 24)) + iconView.image = UIImage(systemName: "envelope") + iconView.tintColor = .systemGray + iconView.contentMode = .scaleAspectFit + + containerView.addSubview(iconView) + + field.leftView = containerView + field.leftViewMode = .always + return field }() - - let passwordField: UITextField = { + + let passwordField: UITextField = { let field = UITextField() field.placeholder = "Password" field.isSecureTextEntry = true field.backgroundColor = .systemGray6 - field.layer.cornerRadius = 12 - field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) + field.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + field.autocapitalizationType = .none + field.autocorrectionType = .no + field.borderStyle = .none + field.translatesAutoresizingMaskIntoConstraints = false + + let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 50)) + + let iconView = UIImageView(frame: CGRect(x: 13, y: 13, width: 24, height: 24)) + iconView.image = UIImage(systemName: "lock") + iconView.tintColor = .systemGray + iconView.contentMode = .scaleAspectFit + + containerView.addSubview(iconView) + + field.leftView = containerView field.leftViewMode = .always - - // Add show/hide password button + + let buttonContainer = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) let button = UIButton(type: .custom) + button.frame = CGRect(x: 5, y: 13, width: 24, height: 24) button.setImage(UIImage(systemName: "eye.slash"), for: .normal) button.tintColor = .systemGray - button.frame = CGRect(x: 0, y: 0, width: 30, height: 30) button.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside) - field.rightView = button + + // Add the button to its container + buttonContainer.addSubview(button) + + // Set as rightView + field.rightView = buttonContainer field.rightViewMode = .always - + return field }() - - let rememberMeButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Remember me", for: .normal) - button.tintColor = .black - button.setImage(UIImage(systemName: "circle"), for: .normal) - button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected) - button.tintColor = .systemBlue - return button - }() - + +// let rememberMeButton: UIButton = { +// let button = UIButton(type: .system) +// button.setTitle(" Remember me", for: .normal) +// button.tintColor = .black +// button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal) +// button.tintColor = .systemBlue +// return button +// }() + fileprivate let forgotPasswordButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Forgot password?", for: .normal) button.tintColor = .black return button }() - + fileprivate let loginButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Log in", for: .normal) - button.backgroundColor = .systemBlue.withAlphaComponent(0.2) - button.setTitleColor(.systemBlue, for: .normal) - button.layer.cornerRadius = 12 + button.backgroundColor = AppColors.primaryButtonColor + button.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) return button }() - + private let dividerLabel: UILabel = { let label = UILabel() - label.text = "Or Login with" - label.textColor = .systemGray + label.text = "or" + label.textColor = AppColors.primaryTextColor label.textAlignment = .center label.font = .systemFont(ofSize: 14) return label }() - + private let socialButtonsStack: UIStackView = { let stack = UIStackView() - stack.axis = .horizontal + stack.axis = .vertical + stack.spacing = 16 stack.distribution = .fillEqually - stack.spacing = 20 return stack }() - + fileprivate let googleButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "google"), for: .normal) - button.imageView?.contentMode = .scaleAspectFit - button.backgroundColor = .systemBackground - button.layer.cornerRadius = 12 + let button = UIButton(type: .system) + button.backgroundColor = .white + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius button.layer.borderWidth = 1 button.layer.borderColor = UIColor.systemGray4.cgColor + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + + // Google Logo + let imageView = UIImageView(image: UIImage(named: "googleLogo")) + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.widthAnchor.constraint(equalToConstant: 24).isActive = true + imageView.heightAnchor.constraint(equalToConstant: 24).isActive = true + + // Label + let label = UILabel() + label.text = "Sign in with Google" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = .black + + // StackView for image + text + let stackView = UIStackView(arrangedSubviews: [imageView, label]) + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + + button.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: button.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor) + ]) + return button }() - + + // Apple Sign In Button - Custom implementation with border fileprivate let appleButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(named: "apple"), for: .normal) - button.imageView?.contentMode = .scaleAspectFit - button.backgroundColor = .systemBackground - button.tintColor = .label - button.layer.cornerRadius = 12 - button.layer.borderWidth = 1 - button.layer.borderColor = UIColor.systemGray4.cgColor + let button = UIButton(type: .system) + button.backgroundColor = .black + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + + // Apple Logo + let imageView = UIImageView(image: UIImage(systemName: "applelogo")) + imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.widthAnchor.constraint(equalToConstant: 24).isActive = true + imageView.heightAnchor.constraint(equalToConstant: 24).isActive = true + + // Label + let label = UILabel() + label.text = "Sign in with Apple" + label.font = .systemFont(ofSize: 16, weight: .medium) + label.textColor = .white + + // StackView for image + text + let stackView = UIStackView(arrangedSubviews: [imageView, label]) + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + + button.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: button.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor) + ]) + + return button + }() + + + private let signupButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Sign up", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont return button }() - + private let signupPromptLabel: UILabel = { let label = UILabel() - label.text = "Don't have an account?" + label.text = "with email" label.textColor = .systemGray label.font = .systemFont(ofSize: 14) return label }() - - private let signupButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Sign up", for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) - return button - }() - + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() title = "Patient Login" setupUI() + let Dismisskeyboard = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(Dismisskeyboard) } - + + @objc func dismissKeyboard() { + view.endEditing(true) + } + // MARK: - Setup UI + private func setupUI() { view.backgroundColor = .systemBackground - + // Add subviews - [logoImageView, titleLabel, emailField, passwordField, rememberMeButton, - forgotPasswordButton, loginButton, dividerLabel, socialButtonsStack, - signupPromptLabel, signupButton].forEach { + [logoImageView, titleLabel, emailField, passwordField, +// rememberMeButton, + forgotPasswordButton, loginButton, dividerLabel, socialButtonsStack, + signupButton, signupPromptLabel].forEach { view.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false } - + // Setup social buttons stack socialButtonsStack.addArrangedSubview(googleButton) socialButtonsStack.addArrangedSubview(appleButton) - - // Set specific sizes for social buttons - NSLayoutConstraint.activate([ - googleButton.widthAnchor.constraint(equalToConstant: 105), - googleButton.heightAnchor.constraint(equalToConstant: 50), - appleButton.widthAnchor.constraint(equalToConstant: 105), - appleButton.heightAnchor.constraint(equalToConstant: 50) - ]) - + // Setup constraints - NSLayoutConstraint.activate([ - logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), - logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - logoImageView.widthAnchor.constraint(equalToConstant: 100), - logoImageView.heightAnchor.constraint(equalToConstant: 100), - - titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - - emailField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30), - emailField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - emailField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - emailField.heightAnchor.constraint(equalToConstant: 50), - - passwordField.topAnchor.constraint(equalTo: emailField.bottomAnchor, constant: 16), - passwordField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - passwordField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - passwordField.heightAnchor.constraint(equalToConstant: 50), - - rememberMeButton.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: 16), - rememberMeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - - forgotPasswordButton.centerYAnchor.constraint(equalTo: rememberMeButton.centerYAnchor), - forgotPasswordButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - - loginButton.topAnchor.constraint(equalTo: rememberMeButton.bottomAnchor, constant: 30), - loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - loginButton.heightAnchor.constraint(equalToConstant: 50), - - dividerLabel.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 30), - dividerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - socialButtonsStack.topAnchor.constraint(equalTo: dividerLabel.bottomAnchor, constant: 20), - socialButtonsStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - signupPromptLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), - signupPromptLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: -30), - - signupButton.centerYAnchor.constraint(equalTo: signupPromptLabel.centerYAnchor), - signupButton.leadingAnchor.constraint(equalTo: signupPromptLabel.trailingAnchor, constant: 4) - ]) - + NSLayoutConstraint.activate( + [ + logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + logoImageView.widthAnchor.constraint(equalToConstant: 100), + logoImageView.heightAnchor.constraint(equalToConstant: 100), + + titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + + emailField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30), + emailField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + emailField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + emailField.heightAnchor.constraint(equalToConstant: Constants.ButtonStyle.DefaultButtonHeight), + + passwordField.topAnchor.constraint(equalTo: emailField.bottomAnchor, constant: 16), + passwordField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + passwordField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + passwordField.heightAnchor.constraint(equalToConstant: Constants.ButtonStyle.DefaultButtonHeight), + + forgotPasswordButton.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: 16), + forgotPasswordButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + + loginButton.topAnchor.constraint(equalTo: forgotPasswordButton.bottomAnchor, constant: 15), + loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + loginButton.heightAnchor.constraint(equalToConstant: Constants.ButtonStyle.DefaultButtonHeight - 6), + + dividerLabel.topAnchor.constraint(equalTo: loginButton.bottomAnchor, constant: 30), + dividerLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + // For the social buttons stack (already correctly centered) + socialButtonsStack.topAnchor.constraint(equalTo: dividerLabel.bottomAnchor, constant: 10), + socialButtonsStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + socialButtonsStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + socialButtonsStack.heightAnchor.constraint(equalToConstant: 116), // 50 * 2 + 16 spacing + + // For the signup section at the bottom + signupButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + signupPromptLabel.centerYAnchor.constraint(equalTo: signupButton.centerYAnchor), + signupPromptLabel.leadingAnchor.constraint(equalTo: signupButton.trailingAnchor, constant: 4), + + // Add this to create a horizontal stack effect that's centered + signupButton.trailingAnchor.constraint(equalTo: view.centerXAnchor, constant: -2), + ] + ) + // Add targets - rememberMeButton.addTarget(self, action: #selector(rememberMeTapped), for: .touchUpInside) +// rememberMeButton.addTarget(self, action: #selector(toggleRememberMe), for: .touchUpInside) loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside) signupButton.addTarget(self, action: #selector(signupTapped), for: .touchUpInside) googleButton.addTarget(self, action: #selector(googleLoginTapped), for: .touchUpInside) appleButton.addTarget(self, action: #selector(appleLoginTapped), for: .touchUpInside) } - + // MARK: - Actions - + @objc private func togglePasswordVisibility(_ sender: UIButton) { passwordField.isSecureTextEntry.toggle() let imageName = passwordField.isSecureTextEntry ? "eye.slash" : "eye" sender.setImage(UIImage(systemName: imageName), for: .normal) } +} +#Preview{ + PatientLoginViewController() } diff --git a/recap/Controllers/initial/PatientSignupViewController.swift b/recap/Controllers/initial/PatientSignupViewController.swift old mode 100644 new mode 100755 index b8bcb37..35dfed6 --- a/recap/Controllers/initial/PatientSignupViewController.swift +++ b/recap/Controllers/initial/PatientSignupViewController.swift @@ -1,29 +1,606 @@ -// -// PatientSignupViewController.swift -// recap -// -// Created by Diptayan Jash on 15/12/24. -// - +import Firebase +import FirebaseAuth +import FirebaseFirestore +import Lottie import UIKit -class PatientSignupViewController: UIViewController { +class PatientSignupViewController: UIViewController, UITextFieldDelegate { + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.showsVerticalScrollIndicator = false + return scrollView + }() + + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let logoImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "recapLogo") + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "Create Account" + label.font = UIFont.systemFont(ofSize: 28, weight: .bold) + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let emailContainerView: UIView = { + let view = UIView() + view.backgroundColor = .systemGray6 + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let emailIconView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "envelope") + imageView.contentMode = .scaleAspectFit + imageView.tintColor = .systemGray + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let emailField: UITextField = { + let textField = UITextField() + textField.placeholder = "Email" + textField.keyboardType = .emailAddress + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.borderStyle = .none + textField.translatesAutoresizingMaskIntoConstraints = false + textField.clearButtonMode = .whileEditing + return textField + }() + + private let passwordContainerView: UIView = { + let view = UIView() + view.backgroundColor = .systemGray6 + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let passwordIconView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "lock") + imageView.contentMode = .scaleAspectFit + imageView.tintColor = .systemGray + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let passwordField: UITextField = { + let textField = UITextField() + textField.placeholder = "Password (min. 6 characters)" + textField.isSecureTextEntry = true + textField.borderStyle = .none + textField.translatesAutoresizingMaskIntoConstraints = false + return textField + }() + + private let passwordToggleButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(systemName: "eye.slash"), for: .normal) + button.tintColor = .systemGray + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private let emailErrorLabel: UILabel = { + let label = UILabel() + label.textColor = .systemRed + label.font = UIFont.systemFont(ofSize: 12) + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + private let passwordErrorLabel: UILabel = { + let label = UILabel() + label.textColor = .systemRed + label.font = UIFont.systemFont(ofSize: 12) + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + private let confirmPasswordContainerView: UIView = { + let view = UIView() + view.backgroundColor = .systemGray6 + view.layer.cornerRadius = 12 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let confirmPasswordIconView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "lock.shield") + imageView.contentMode = .scaleAspectFit + imageView.tintColor = .systemGray + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let confirmPasswordField: UITextField = { + let textField = UITextField() + textField.placeholder = "Confirm Password" + textField.isSecureTextEntry = true + textField.borderStyle = .none + textField.translatesAutoresizingMaskIntoConstraints = false + return textField + }() + + private let confirmPasswordToggleButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(systemName: "eye.slash"), for: .normal) + button.tintColor = .systemGray + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private let confirmPasswordErrorLabel: UILabel = { + let label = UILabel() + label.textColor = .systemRed + label.font = UIFont.systemFont(ofSize: 12) + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + private let continueButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Create Account", for: .normal) + button.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor + button + .setTitleColor( + Constants.ButtonStyle.DefaultButtonTextColor, + for: .normal + ) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private let backButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Already have an account? Log in", for: .normal) + button.setTitleColor(.systemBlue, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() override func viewDidLoad() { super.viewDidLoad() + setupUI() + setupActions() + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + + let Dismisskeyboard = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(Dismisskeyboard) + + emailField.delegate = self + passwordField.delegate = self + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc func keyboardWillShow(notification: NSNotification) { + guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return } + + let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0) + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = contentInsets + + // Scroll to active text field if needed + var visibleRect = view.frame + visibleRect.size.height -= keyboardSize.height - // Do any additional setup after loading the view. + if let activeField = [emailField, passwordField].first(where: { $0.isFirstResponder }) { + let activeRect = activeField.convert(activeField.bounds, to: scrollView) + if !visibleRect.contains(activeRect.origin) { + scrollView.scrollRectToVisible(activeRect, animated: true) + } + } } - - /* - // MARK: - Navigation + @objc func keyboardWillHide(notification: NSNotification) { + scrollView.contentInset = .zero + scrollView.scrollIndicatorInsets = .zero + } - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. + @objc func dismissKeyboard() { + view.endEditing(true) } - */ + private func setupUI() { + view.backgroundColor = .white + + view.addSubview(scrollView) + scrollView.addSubview(contentView) + + contentView.addSubview(logoImageView) + contentView.addSubview(titleLabel) + + contentView.addSubview(emailContainerView) + emailContainerView.addSubview(emailIconView) + emailContainerView.addSubview(emailField) + contentView.addSubview(emailErrorLabel) + + contentView.addSubview(passwordContainerView) + passwordContainerView.addSubview(passwordIconView) + passwordContainerView.addSubview(passwordField) + passwordContainerView.addSubview(passwordToggleButton) + contentView.addSubview(passwordErrorLabel) + + contentView.addSubview(confirmPasswordContainerView) + confirmPasswordContainerView.addSubview(confirmPasswordIconView) + confirmPasswordContainerView.addSubview(confirmPasswordField) + confirmPasswordContainerView.addSubview(confirmPasswordToggleButton) + contentView.addSubview(confirmPasswordErrorLabel) + + contentView.addSubview(continueButton) + contentView.addSubview(backButton) + + NSLayoutConstraint.activate([ + // ScrollView + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + // ContentView + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + // Make contentView at least as tall as the view to allow scrolling when keyboard appears + contentView.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor, multiplier: 0.9), + + // Logo + logoImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 40), + logoImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + logoImageView.widthAnchor.constraint(equalToConstant: 120), + logoImageView.heightAnchor.constraint(equalToConstant: 120), + + // Title + titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + // Email Container + emailContainerView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40), + emailContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + emailContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + emailContainerView.heightAnchor.constraint(equalToConstant: 56), + + // Email Icon + emailIconView.leadingAnchor.constraint(equalTo: emailContainerView.leadingAnchor, constant: 12), + emailIconView.centerYAnchor.constraint(equalTo: emailContainerView.centerYAnchor), + emailIconView.widthAnchor.constraint(equalToConstant: 24), + emailIconView.heightAnchor.constraint(equalToConstant: 24), + + // Email Field + emailField.leadingAnchor.constraint(equalTo: emailIconView.trailingAnchor, constant: 12), + emailField.trailingAnchor.constraint(equalTo: emailContainerView.trailingAnchor, constant: -12), + emailField.centerYAnchor.constraint(equalTo: emailContainerView.centerYAnchor), + emailField.heightAnchor.constraint(equalToConstant: 40), + + // Email Error Label + emailErrorLabel.topAnchor.constraint(equalTo: emailContainerView.bottomAnchor, constant: 4), + emailErrorLabel.leadingAnchor.constraint(equalTo: emailContainerView.leadingAnchor, constant: 12), + emailErrorLabel.trailingAnchor.constraint(equalTo: emailContainerView.trailingAnchor, constant: -12), + + // Password Container + passwordContainerView.topAnchor.constraint(equalTo: emailContainerView.bottomAnchor, constant: 24), + passwordContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + passwordContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + passwordContainerView.heightAnchor.constraint(equalToConstant: 56), + + // Password Icon + passwordIconView.leadingAnchor.constraint(equalTo: passwordContainerView.leadingAnchor, constant: 12), + passwordIconView.centerYAnchor.constraint(equalTo: passwordContainerView.centerYAnchor), + passwordIconView.widthAnchor.constraint(equalToConstant: 24), + passwordIconView.heightAnchor.constraint(equalToConstant: 24), + + // Password Field + passwordField.leadingAnchor.constraint(equalTo: passwordIconView.trailingAnchor, constant: 12), + passwordField.trailingAnchor.constraint(equalTo: passwordToggleButton.leadingAnchor, constant: -8), + passwordField.centerYAnchor.constraint(equalTo: passwordContainerView.centerYAnchor), + passwordField.heightAnchor.constraint(equalToConstant: 40), + + // Password Toggle Button + passwordToggleButton.trailingAnchor.constraint(equalTo: passwordContainerView.trailingAnchor, constant: -12), + passwordToggleButton.centerYAnchor.constraint(equalTo: passwordContainerView.centerYAnchor), + passwordToggleButton.widthAnchor.constraint(equalToConstant: 24), + passwordToggleButton.heightAnchor.constraint(equalToConstant: 24), + + // Password Error Label + passwordErrorLabel.topAnchor.constraint(equalTo: passwordContainerView.bottomAnchor, constant: 4), + passwordErrorLabel.leadingAnchor.constraint(equalTo: passwordContainerView.leadingAnchor, constant: 12), + passwordErrorLabel.trailingAnchor.constraint(equalTo: passwordContainerView.trailingAnchor, constant: -12), + + // Confirm Password Container + confirmPasswordContainerView.topAnchor.constraint(equalTo: passwordContainerView.bottomAnchor, constant: 24), + confirmPasswordContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + confirmPasswordContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + confirmPasswordContainerView.heightAnchor.constraint(equalToConstant: 56), + + // Confirm Password Icon + confirmPasswordIconView.leadingAnchor.constraint(equalTo: confirmPasswordContainerView.leadingAnchor, constant: 12), + confirmPasswordIconView.centerYAnchor.constraint(equalTo: confirmPasswordContainerView.centerYAnchor), + confirmPasswordIconView.widthAnchor.constraint(equalToConstant: 24), + confirmPasswordIconView.heightAnchor.constraint(equalToConstant: 24), + + // Confirm Password Field + confirmPasswordField.leadingAnchor.constraint(equalTo: confirmPasswordIconView.trailingAnchor, constant: 12), + confirmPasswordField.trailingAnchor.constraint(equalTo: confirmPasswordToggleButton.leadingAnchor, constant: -8), + confirmPasswordField.centerYAnchor.constraint(equalTo: confirmPasswordContainerView.centerYAnchor), + confirmPasswordField.heightAnchor.constraint(equalToConstant: 40), + + // Confirm Password Toggle Button + confirmPasswordToggleButton.trailingAnchor.constraint(equalTo: confirmPasswordContainerView.trailingAnchor, constant: -12), + confirmPasswordToggleButton.centerYAnchor.constraint(equalTo: confirmPasswordContainerView.centerYAnchor), + confirmPasswordToggleButton.widthAnchor.constraint(equalToConstant: 24), + confirmPasswordToggleButton.heightAnchor.constraint(equalToConstant: 24), + + // Confirm Password Error Label + confirmPasswordErrorLabel.topAnchor.constraint(equalTo: confirmPasswordContainerView.bottomAnchor, constant: 4), + confirmPasswordErrorLabel.leadingAnchor.constraint(equalTo: confirmPasswordContainerView.leadingAnchor, constant: 12), + confirmPasswordErrorLabel.trailingAnchor.constraint(equalTo: confirmPasswordContainerView.trailingAnchor, constant: -12), + + // Continue Button + continueButton.topAnchor.constraint(equalTo: confirmPasswordContainerView.bottomAnchor, constant: 40), + continueButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + continueButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + continueButton.heightAnchor.constraint(equalToConstant: 56), + + // Back Button + backButton.topAnchor.constraint(equalTo: continueButton.bottomAnchor, constant: 20), + backButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + backButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -40), + ]) + } + + // MARK: - Actions + + private func setupActions() { + continueButton.addTarget(self, action: #selector(continueButtonTapped), for: .touchUpInside) + backButton.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) + passwordToggleButton.addTarget(self, action: #selector(togglePasswordVisibility), for: .touchUpInside) + confirmPasswordToggleButton.addTarget(self, action: #selector(toggleConfirmPasswordVisibility), for: .touchUpInside) + + // Add real-time validation + emailField.addTarget(self, action: #selector(validateEmailField), for: .editingChanged) + passwordField.addTarget(self, action: #selector(validatePasswordField), for: .editingChanged) + confirmPasswordField.addTarget(self, action: #selector(validateConfirmPasswordField), for: .editingChanged) + } + + // MARK: - Validation + + @objc private func validateEmailField() { + if let email = emailField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !email.isEmpty { + if isValidEmail(email) { + emailErrorLabel.isHidden = true + emailContainerView.layer.borderWidth = 0 + } else { + emailErrorLabel.text = "Please enter a valid email address" + emailErrorLabel.isHidden = false + emailContainerView.layer.borderWidth = 1 + emailContainerView.layer.borderColor = UIColor.systemRed.cgColor + } + } else { + emailErrorLabel.isHidden = true + emailContainerView.layer.borderWidth = 0 + } + } + + @objc private func validatePasswordField() { + if let password = passwordField.text, !password.isEmpty { + if password.count < 6 { + passwordErrorLabel.text = "Password must be at least 6 characters" + passwordErrorLabel.isHidden = false + passwordContainerView.layer.borderWidth = 1 + passwordContainerView.layer.borderColor = UIColor.systemRed.cgColor + } else { + passwordErrorLabel.isHidden = true + passwordContainerView.layer.borderWidth = 0 + } + + // Also validate confirm password when password changes + validateConfirmPasswordField() + } else { + passwordErrorLabel.isHidden = true + passwordContainerView.layer.borderWidth = 0 + } + } + + @objc private func validateConfirmPasswordField() { + guard let password = passwordField.text, !password.isEmpty, + let confirmPassword = confirmPasswordField.text, !confirmPassword.isEmpty else { + confirmPasswordErrorLabel.isHidden = true + confirmPasswordContainerView.layer.borderWidth = 0 + return + } + + if password != confirmPassword { + confirmPasswordErrorLabel.text = "Passwords do not match" + confirmPasswordErrorLabel.isHidden = false + confirmPasswordContainerView.layer.borderWidth = 1 + confirmPasswordContainerView.layer.borderColor = UIColor.systemRed.cgColor + } else { + confirmPasswordErrorLabel.isHidden = true + confirmPasswordContainerView.layer.borderWidth = 0 + } + } + + @objc private func togglePasswordVisibility() { + passwordField.isSecureTextEntry = !passwordField.isSecureTextEntry + + if passwordField.isSecureTextEntry { + passwordToggleButton.setImage(UIImage(systemName: "eye.slash"), for: .normal) + } else { + passwordToggleButton.setImage(UIImage(systemName: "eye"), for: .normal) + } + } + + @objc private func toggleConfirmPasswordVisibility() { + confirmPasswordField.isSecureTextEntry = !confirmPasswordField.isSecureTextEntry + + // Update the button image based on password visibility + if confirmPasswordField.isSecureTextEntry { + confirmPasswordToggleButton.setImage(UIImage(systemName: "eye.slash"), for: .normal) + } else { + confirmPasswordToggleButton.setImage(UIImage(systemName: "eye"), for: .normal) + } + } + + @objc private func continueButtonTapped() { + let email = emailField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let password = passwordField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let confirmPassword = confirmPasswordField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + guard !email.isEmpty, !password.isEmpty else { + showAlert(message: "Please enter both email and password.") + return + } + + if !isValidEmail(email) { + emailErrorLabel.text = "Please enter a valid email address" + emailErrorLabel.isHidden = false + emailContainerView.layer.borderWidth = 1 + emailContainerView.layer.borderColor = UIColor.systemRed.cgColor + return + } + + // Validate password strength + if password.count < 6 { + passwordErrorLabel.text = "Password must be at least 6 characters" + passwordErrorLabel.isHidden = false + passwordContainerView.layer.borderWidth = 1 + passwordContainerView.layer.borderColor = UIColor.systemRed.cgColor + return + } + + // Validate passwords match + if password != confirmPassword { + confirmPasswordErrorLabel.text = "Passwords do not match" + confirmPasswordErrorLabel.isHidden = false + confirmPasswordContainerView.layer.borderWidth = 1 + confirmPasswordContainerView.layer.borderColor = UIColor.systemRed.cgColor + return + } + + // Show loading animation + let loadingAnimation = showLoadingAnimation() + + // Create the user with Firebase Auth + Auth.auth().createUser(withEmail: email, password: password) { [weak self] authResult, error in + guard let self = self else { return } + + if let error = error { + // Remove loading animation + self.removeLoadingAnimation(loadingAnimation) + print("Error creating user: \(error.localizedDescription)") + self.showAlert(message: "Failed to create account: \(error.localizedDescription)") + return + } + + guard let user = authResult?.user else { + // Remove loading animation + self.removeLoadingAnimation(loadingAnimation) + self.showAlert(message: "Failed to create user account.") + return + } + + // Store user ID in UserDefaults + let userId = user.uid + UserDefaults.standard.set(userId, forKey: Constants.UserDefaultsKeys.verifiedUserDocID) + UserDefaults.standard.set(email, forKey: "userEmail") + + // Use the existing generateUniquePatientID function + generateUniquePatientID { patientUID in + guard let patientUID = patientUID else { + self.removeLoadingAnimation(loadingAnimation) + print("Failed to generate unique Patient ID.") + self.showAlert(message: "Unable to create profile. Please try again.") + return + } + + // Initial data structure - same as in Google sign-in flow + let initialData: [String: Any] = [ + "email": email, + "patientUID": patientUID, + "firstName": "", + "lastName": "", + "dateOfBirth": "", + "sex": "", + "bloodGroup": "", + "stage": "", + "profileImageURL": "", + "familyMembers": [], + "type": "patient", + ] + + // Save the initial user profile to Firestore + let db = Firestore.firestore() + db.collection("users").document(userId).setData(initialData) { error in + // Remove loading animation + self.removeLoadingAnimation(loadingAnimation) + + if let error = error { + print("Error saving initial user profile: \(error.localizedDescription)") + self.showAlert(message: "Failed to create profile. Please try again.") + } else { + print("New user profile created successfully") + + // Navigate to patient info screen to complete profile + let patientInfoVC = patientInfo() + // Set the delegate to SceneDelegate to handle navigation after profile completion + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + patientInfoVC.delegate = sceneDelegate + } + let nav = UINavigationController(rootViewController: patientInfoVC) + nav.modalPresentationStyle = .pageSheet + self.present(nav, animated: true) + } + } + } + } + } + + @objc private func backButtonTapped() { + dismiss(animated: true) + } + + // MARK: - Helpers + + private func isValidEmail(_ email: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: email) + } + + private func showAlert(message: String) { + let alertController = UIAlertController(title: "Notice", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default)) + present(alertController, animated: true) + } } diff --git a/recap/Controllers/initial/WelcomeViewController.swift b/recap/Controllers/initial/WelcomeViewController.swift old mode 100644 new mode 100755 index d40fc61..113e309 --- a/recap/Controllers/initial/WelcomeViewController.swift +++ b/recap/Controllers/initial/WelcomeViewController.swift @@ -1,9 +1,9 @@ -//// -//// WelcomeViewController.swift -//// recap -//// -//// Created by Diptayan Jash on 05/11/24. -//// +// +// WelcomeViewController.swift +// recap +// +// Created by Diptayan Jash on 05/11/24. +// // //import UIKit // @@ -20,7 +20,8 @@ // private let titleLabel: UILabel = { // let label = UILabel() // label.text = "Recap" -// label.font = .systemFont(ofSize: 32, weight: .bold) +//// label.font = .systemFont(ofSize: 32, weight: .bold) +// label.font = UIFont(name: "Pacifico-Regular", size: 45) // label.textAlignment = .center // return label // }() @@ -30,7 +31,7 @@ // button.setTitle("Patient", for: .normal) // button.setTitleColor(.systemBlue, for: .normal) // button.backgroundColor = .systemBlue.withAlphaComponent(0.1) -// button.layer.cornerRadius = 12 +// button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius // button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) // return button // }() @@ -40,7 +41,7 @@ // button.setTitle("Family", for: .normal) // button.setTitleColor(.systemBlue, for: .normal) // button.backgroundColor = .systemBlue.withAlphaComponent(0.1) -// button.layer.cornerRadius = 12 +// button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius // button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) // return button // }() @@ -48,7 +49,7 @@ // private let containerView: UIView = { // let view = UIView() // view.backgroundColor = .systemBackground -// view.layer.cornerRadius = 20 +// view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius // // Add shadow // view.layer.shadowColor = UIColor.black.cgColor // view.layer.shadowOffset = CGSize(width: 0, height: 2) @@ -120,30 +121,35 @@ // containerView.translatesAutoresizingMaskIntoConstraints = false // // // Setup constraints -// NSLayoutConstraint.activate([ +// NSLayoutConstraint.activate( +//[ // containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), // containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), // containerView.widthAnchor.constraint(equalToConstant: 350), // containerView.heightAnchor.constraint(equalToConstant: 514), // -// titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 40), +// titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 25), // titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), // -// logoImageView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40), +// logoImageView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30), // logoImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), // logoImageView.widthAnchor.constraint(equalToConstant: 150), // logoImageView.heightAnchor.constraint(equalToConstant: 180), // -// patientButton.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 40), +// patientButton.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 35), // patientButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), // patientButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), -// patientButton.heightAnchor.constraint(equalToConstant: 50), +// patientButton.heightAnchor +// .constraint( +// equalToConstant: Constants.ButtonStyle.DefaultButtonHeight +// ), // // familyButton.topAnchor.constraint(equalTo: patientButton.bottomAnchor, constant: 16), // familyButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), // familyButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), -// familyButton.heightAnchor.constraint(equalToConstant: 50) -// ]) +// familyButton.heightAnchor.constraint(equalToConstant: Constants.ButtonStyle.DefaultButtonHeight) +// ] +//) // // // Add button targets // patientButton.addTarget(self, action: #selector(patientButtonTapped), for: .touchUpInside) @@ -158,178 +164,501 @@ // // @objc private func familyButtonTapped() { // print("Family button tapped") -// let familyLoginVC = FamilyViewController() -// navigationController?.pushViewController(familyLoginVC, animated: true) +// let tabBarFamilyVC = FamilyLoginViewController() +// navigationController?.pushViewController(tabBarFamilyVC, animated: true) // } +// //} +//#Preview{WelcomeViewController()} +import UIKit -// -// WelcomeViewController.swift -// recap -// -// Created by Diptayan Jash on 05/11/24. -// - -import UIKit class WelcomeViewController: UIViewController { + + // MARK: - UI Components + private let logoImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage(named: "recapLogo") + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "Recap" - label.font = .systemFont(ofSize: 32, weight: .bold) + + label.font = UIFont(name: "Pacifico-Regular", size: 36) // Use Pacifico font + label.textAlignment = .center + + label.textColor = AppColors.primaryTextColor + return label + }() + - private let patientButton: UIButton = { - let button = UIButton() - button.setTitle("Patient", for: .normal) - button.setTitleColor(.systemBlue, for: .normal) - button.backgroundColor = .systemBlue.withAlphaComponent(0.1) - button.layer.cornerRadius = 12 - button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) - return button + + private let subtitleLabel: UILabel = { + + let label = UILabel() + + label.text = "Select your role" + + label.font = .systemFont(ofSize: 18, weight: .medium) + + label.textAlignment = .center + + label.textColor = AppColors.primaryTextColor + + return label + }() + - private let familyButton: UIButton = { - let button = UIButton() - button.setTitle("Family", for: .normal) - button.setTitleColor(.systemBlue, for: .normal) - button.backgroundColor = .systemBlue.withAlphaComponent(0.1) - button.layer.cornerRadius = 12 - button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) - return button + + private let patientCard: UIView = { + + let view = UIView() + + view.backgroundColor = AppColors.cardBackgroundColor + + view.layer.cornerRadius = 16 + +// view.layer.shadowColor = UIColor.black.cgColor +// +// view.layer.shadowOffset = CGSize(width: 0, height: 4) +// +// view.layer.shadowRadius = 8 +// +// view.layer.shadowOpacity = 0.1 + + view.isUserInteractionEnabled = true + + return view + }() + - private let containerView: UIView = { + + private let familyCard: UIView = { + let view = UIView() - view.backgroundColor = .systemBackground - view.layer.cornerRadius = 20 - // Add shadow - view.layer.shadowColor = UIColor.black.cgColor - view.layer.shadowOffset = CGSize(width: 0, height: 2) - view.layer.shadowRadius = 8 - view.layer.shadowOpacity = 0.1 + + view.backgroundColor = AppColors.cardBackgroundColor + + view.layer.cornerRadius = 16 + +// view.layer.shadowColor = UIColor.black.cgColor +// +// view.layer.shadowOffset = CGSize(width: 0, height: 4) +// +// view.layer.shadowRadius = 8 +// +// view.layer.shadowOpacity = 0.1 + + view.isUserInteractionEnabled = true + return view + }() + - private let gradientLayer: CAGradientLayer = { - let gradient = CAGradientLayer() - gradient.colors = [ -// UIColor.systemBlue.cgColor, -// UIColor.systemPurple.cgColor - UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, - UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor - - - ] -// gradient.locations = [0.0, 1.0] - gradient.startPoint = CGPoint(x: 0.0, y: 0.0) - gradient.endPoint = CGPoint(x: 1.0, y: 1.0) - return gradient + + private let patientIconImageView: UIImageView = { + + let imageView = UIImageView() + + imageView.contentMode = .scaleAspectFit + + imageView.image = UIImage(systemName: "heart.fill") + + imageView.tintColor = AppColors.iconColor // Royal Purple + + return imageView + }() + - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupNavigationBar() - } + + private let familyIconImageView: UIImageView = { + + let imageView = UIImageView() + + imageView.contentMode = .scaleAspectFit + + imageView.image = UIImage(systemName: "person.3.fill") + + imageView.tintColor = AppColors.iconColor // Royal Purple + + return imageView + + }() + - private func setupNavigationBar() { + + private let patientLabel: UILabel = { + + let label = UILabel() + + label.text = "Patient" + + label.font = .systemFont(ofSize: 20, weight: .semibold) + + label.textColor = AppColors.primaryTextColor // Royal Purple + + return label + + }() + + + + private let familyLabel: UILabel = { + + let label = UILabel() + + label.text = "Family" + + label.font = .systemFont(ofSize: 20, weight: .semibold) + + label.textColor = AppColors.primaryTextColor + + return label + + }() + + + + private let patientDescriptionLabel: UILabel = { + + let label = UILabel() + label.text = "Your memories are precious—let’s keep them close, together" + + label.font = .systemFont(ofSize: 14, weight: .regular) + + label.textColor = AppColors.secondaryTextColor // Deep Purple (Light Mode Text) + + label.numberOfLines = 2 + + return label + + }() + + + + private let familyDescriptionLabel: UILabel = { + + let label = UILabel() + + label.text = "Monitor and support your loved ones. You can help keep memories alive" + + label.font = .systemFont(ofSize: 14, weight: .regular) + + label.textColor = AppColors.secondaryTextColor // Deep Purple (Light Mode Text) + + label.numberOfLines = 2 + + return label + + }() + + + + // MARK: - Lifecycle + + override func viewDidLoad() { + + super.viewDidLoad() + + setupUI() + + setupNavigationBar() + + setupGestures() + + let gradientLayer = AppColors.createAppBackgroundGradientLayer() + + gradientLayer.frame = view.bounds + + view.layer.insertSublayer(gradientLayer, at: 0) + + } + + + + private func setupNavigationBar() { + navigationController?.setNavigationBarHidden(true, animated: false) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(true, animated: animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.setNavigationBarHidden(false, animated: animated) + } - - // MARK: - Setup UI + + + + // MARK: - Setup UI + private func setupUI() { - view.backgroundColor = .systemBackground - - // Add gradient background - let gradientLayer = CAGradientLayer() - gradientLayer.colors = [ - UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, - UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor - - ] - gradientLayer.startPoint = CGPoint(x: 0, y: 0) - gradientLayer.endPoint = CGPoint(x: 1, y: 1) - - gradientLayer.frame = view.bounds - view.layer.insertSublayer(gradientLayer, at: 0) - - // Add subviews - view.addSubview(containerView) - [logoImageView, titleLabel, patientButton, familyButton].forEach { - containerView.addSubview($0) - $0.translatesAutoresizingMaskIntoConstraints = false - } - containerView.translatesAutoresizingMaskIntoConstraints = false - - // Setup constraints - NSLayoutConstraint.activate([ - containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - containerView.widthAnchor.constraint(equalToConstant: 350), - containerView.heightAnchor.constraint(equalToConstant: 514), - - titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 40), - titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), - - logoImageView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40), - logoImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), - logoImageView.widthAnchor.constraint(equalToConstant: 150), - logoImageView.heightAnchor.constraint(equalToConstant: 180), - - patientButton.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 40), - patientButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), - patientButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), - patientButton.heightAnchor.constraint(equalToConstant: 50), - - familyButton.topAnchor.constraint(equalTo: patientButton.bottomAnchor, constant: 16), - familyButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), - familyButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), - familyButton.heightAnchor.constraint(equalToConstant: 50) - ]) - // Add button targets - patientButton.addTarget(self, action: #selector(patientButtonTapped), for: .touchUpInside) - familyButton.addTarget(self, action: #selector(familyButtonTapped), for: .touchUpInside) - } - - // MARK: - Actions - @objc private func patientButtonTapped() { - let patientLoginVC = PatientLoginViewController() - navigationController?.pushViewController(patientLoginVC, animated: true) + // view.backgroundColor = UIColor(hex: "#F3E5F5") // Soft Lilac (Background) + [logoImageView, titleLabel, subtitleLabel, patientCard, familyCard].forEach { + + view.addSubview($0) + + $0.translatesAutoresizingMaskIntoConstraints = false + + } + + // Add card content + + [patientIconImageView, patientLabel, patientDescriptionLabel].forEach { + + patientCard.addSubview($0) + + $0.translatesAutoresizingMaskIntoConstraints = false + + } + + + + [familyIconImageView, familyLabel, familyDescriptionLabel].forEach { + + familyCard.addSubview($0) + + $0.translatesAutoresizingMaskIntoConstraints = false + + } + + // Setup constraints for centering + + NSLayoutConstraint.activate([ + + // Logo ImageView + + logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -250), + + logoImageView.widthAnchor.constraint(equalToConstant: 120), + + logoImageView.heightAnchor.constraint(equalToConstant: 120), + + + + // Title Label + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + titleLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 10), // Independent top anchor + titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -50), // Bottom constraint with limit, not tied to top + + + + + + // Subtitle Label + + subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 30), // Top padding of 15 from titleLabel + subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -25), // Separate bottom constraint with -25 padding + + + // Patient Card + + patientCard.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + patientCard.topAnchor.constraint(equalTo: subtitleLabel.bottomAnchor, constant: 20), + + patientCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), + + patientCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), + + patientCard.heightAnchor.constraint(equalToConstant: 120), + + + + // Family Card + + familyCard.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + familyCard.topAnchor.constraint(equalTo: patientCard.bottomAnchor, constant: 30), + + familyCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), + + familyCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), + + familyCard.heightAnchor.constraint(equalToConstant: 120), + + + + // Patient Card Content + + patientIconImageView.leadingAnchor.constraint(equalTo: patientCard.leadingAnchor, constant: 24), + + patientIconImageView.centerYAnchor.constraint(equalTo: patientCard.centerYAnchor), + + patientIconImageView.widthAnchor.constraint(equalToConstant: 36), + + patientIconImageView.heightAnchor.constraint(equalToConstant: 36), + + + + patientLabel.leadingAnchor.constraint(equalTo: patientIconImageView.trailingAnchor, constant: 16), + + patientLabel.topAnchor.constraint(equalTo: patientCard.topAnchor, constant: 32), + + + + patientDescriptionLabel.leadingAnchor.constraint(equalTo: patientIconImageView.trailingAnchor, constant: 16), + + patientDescriptionLabel.topAnchor.constraint(equalTo: patientLabel.bottomAnchor, constant: 8), + + patientDescriptionLabel.trailingAnchor.constraint(equalTo: patientCard.trailingAnchor, constant: -16), + + + + // Family Card Content + + familyIconImageView.leadingAnchor.constraint(equalTo: familyCard.leadingAnchor, constant: 24), + + familyIconImageView.centerYAnchor.constraint(equalTo: familyCard.centerYAnchor), + + familyIconImageView.widthAnchor.constraint(equalToConstant: 36), + + familyIconImageView.heightAnchor.constraint(equalToConstant: 36), + + + + familyLabel.leadingAnchor.constraint(equalTo: familyIconImageView.trailingAnchor, constant: 16), + + familyLabel.topAnchor.constraint(equalTo: familyCard.topAnchor, constant: 32), + + + + familyDescriptionLabel.leadingAnchor.constraint(equalTo: familyIconImageView.trailingAnchor, constant: 16), + + familyDescriptionLabel.topAnchor.constraint(equalTo: familyLabel.bottomAnchor, constant: 8), + + familyDescriptionLabel.trailingAnchor.constraint(equalTo: familyCard.trailingAnchor, constant: -16) + + ]) + + } + + + + private func setupGestures() { + + let patientTapGesture = UITapGestureRecognizer(target: self, action: #selector(patientCardTapped)) + + patientCard.addGestureRecognizer(patientTapGesture) + + + + let familyTapGesture = UITapGestureRecognizer(target: self, action: #selector(familyCardTapped)) + + familyCard.addGestureRecognizer(familyTapGesture) + + } + + + + // MARK: - Actions + + @objc private func patientCardTapped() { + + let generator = UIImpactFeedbackGenerator(style: .medium) + + generator.impactOccurred() + + + + UIView.animate(withDuration: 0.1, animations: { + + self.patientCard.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + + }) { _ in + + UIView.animate(withDuration: 0.1) { + + self.patientCard.transform = CGAffineTransform.identity + + } completion: { _ in + + let patientLoginVC = PatientLoginViewController() + + self.navigationController?.pushViewController(patientLoginVC, animated: true) + + } + + } + + } + + + + @objc private func familyCardTapped() { + + let generator = UIImpactFeedbackGenerator(style: .medium) + + generator.impactOccurred() + + + + UIView.animate(withDuration: 0.1, animations: { + + self.familyCard.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + + }) { _ in + + UIView.animate(withDuration: 0.1) { + + self.familyCard.transform = CGAffineTransform.identity + + } completion: { _ in + + let familyLoginVC = FamilyLoginViewController() + + self.navigationController?.pushViewController(familyLoginVC, animated: true) + + } + + } + + } + } - - @objc private func familyButtonTapped() { - print("Family button tapped") - let tabBarFamilyVC = FamilyLoginViewController() - // Use rootViewController or present as modal depending on your flow - if let window = view.window { - window.rootViewController = tabBarFamilyVC - } - } - -} #Preview{WelcomeViewController()} + diff --git a/recap/Controllers/initial/launchScreenViewController.swift b/recap/Controllers/initial/launchScreenViewController.swift old mode 100644 new mode 100755 index f14cd20..59f18f6 --- a/recap/Controllers/initial/launchScreenViewController.swift +++ b/recap/Controllers/initial/launchScreenViewController.swift @@ -6,9 +6,10 @@ // import UIKit +import FirebaseFirestore +import FirebaseAuth class launchScreenViewController: UIViewController { - private let logoImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit @@ -16,51 +17,51 @@ class launchScreenViewController: UIViewController { imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - + private let gradientLayer: CAGradientLayer = { let gradient = CAGradientLayer() gradient.colors = [ // UIColor.systemBlue.cgColor, // UIColor.systemPurple.cgColor UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, - UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor - - + UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor, ] // gradient.locations = [0.0, 1.0] gradient.startPoint = CGPoint(x: 0.0, y: 0.0) gradient.endPoint = CGPoint(x: 1.0, y: 1.0) return gradient }() - + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() setupUI() animateLogo() } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() gradientLayer.frame = view.bounds } - + // MARK: - Setup + private func setupUI() { view.layer.insertSublayer(gradientLayer, at: 0) - + view.addSubview(logoImageView) - + NSLayoutConstraint.activate([ logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), logoImageView.widthAnchor.constraint(equalToConstant: 200), - logoImageView.heightAnchor.constraint(equalToConstant: 200) + logoImageView.heightAnchor.constraint(equalToConstant: 200), ]) - + logoImageView.alpha = 0 } - + private func animateLogo() { // Fade in animation UIView.animate(withDuration: 1.0, animations: { [weak self] in @@ -71,37 +72,104 @@ class launchScreenViewController: UIViewController { } } } - - private func transitionToMainScreen() { - if UserDefaults.standard.bool(forKey: "hasCompletedProfile") { - let tabBar = TabbarViewController() - UIView.transition(with: view.window!, - duration: 0.3, - options: .transitionCrossDissolve, - animations: { - self.view.window?.rootViewController = tabBar - }) + + func transitionToMainScreen() { + // If onboarding is not completed, present it + if !UserDefaults.standard + .bool(forKey: Constants.UserDefaultsKeys.HasCompletedOnboarding) { + let onboardingVC = OnboardingViewController() + let navController = UINavigationController(rootViewController: onboardingVC) + if let sheet = navController.sheetPresentationController { + sheet.detents = [.custom( + identifier: .init( + "customHeight" + ), + resolver: { _ in + return UIScreen.main.bounds.height * 0.83 + })] + sheet.prefersGrabberVisible = true + sheet.prefersEdgeAttachedInCompactHeight = true + } + present(navController, animated: true) } else { -// let patientInfoVC = patientInfo() -// patientInfoVC.delegate = UIApplication.shared.connectedScenes -// .first?.delegate as? PatientInfoDelegate -// let nav = UINavigationController(rootViewController: patientInfoVC) -// -// if let sheet = nav.sheetPresentationController { -// sheet.detents = [.large()] -// sheet.preferredCornerRadius = 30 -// } -// present(nav, animated: true) - let welcomeVC = WelcomeViewController() - let nav = UINavigationController(rootViewController: welcomeVC) - nav.modalPresentationStyle = .fullScreen + // If onboarding is completed, handle user login state + if UserDefaults.standard.bool(forKey: Constants.UserDefaultsKeys.isFamilyMemberLoggedIn) { + // Navigate to the family tab bar if family member is logged in + let familyTabBarVC = TabbarFamilyViewController() + transitionToRootViewController(familyTabBarVC) + } else if UserDefaults.standard.bool(forKey: Constants.UserDefaultsKeys.hasPatientCompletedProfile) { + // Check if the user is actually logged in with Firebase + if let userId = Auth.auth().currentUser?.uid { + // Verify profile completeness in Firebase + checkProfileCompleteness(userId: userId) + } else { + // No Firebase user, go to welcome screen + navigateToWelcomeScreen() + } + } else { + // Navigate to the welcome screen if neither condition is met + navigateToWelcomeScreen() + } + } + } + + private func checkProfileCompleteness(userId: String) { + let db = Firestore.firestore() + + db.collection("users").document(userId).getDocument { [weak self] document, error in + guard let self = self else { return } + + if let error = error { + print("Error fetching user profile: \(error.localizedDescription)") + self.navigateToWelcomeScreen() + return + } - UIView.transition(with: self.view.window!, duration: 0.5, options: .transitionCrossDissolve, animations: { - self.view.window?.rootViewController = nav - }, completion: nil) + if let document = document, document.exists, let userData = document.data() { + // Check if profile is complete by verifying required fields + let requiredFields = ["firstName", "lastName", "dateOfBirth", "sex", "bloodGroup", "stage"] + let isProfileComplete = requiredFields.allSatisfy { field in + guard let value = userData[field] as? String else { return false } + return !value.isEmpty + } + + if isProfileComplete { + // Profile is complete, navigate to main view + let patientTabBarVC = TabbarViewController() + self.transitionToRootViewController(patientTabBarVC) + } else { + // Profile exists but is incomplete, navigate to profile completion + let patientInfoVC = patientInfo() + // Set the delegate to SceneDelegate to handle navigation after profile completion + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + patientInfoVC.delegate = sceneDelegate + } + let nav = UINavigationController(rootViewController: patientInfoVC) + nav.modalPresentationStyle = .fullScreen + self.transitionToRootViewController(nav) + } + } else { + // No user document, go to welcome screen + self.navigateToWelcomeScreen() + } } } + + private func navigateToWelcomeScreen() { + let welcomeVC = WelcomeViewController() + let navController = UINavigationController(rootViewController: welcomeVC) + navController.modalPresentationStyle = .fullScreen + transitionToRootViewController(navController) + } + + private func transitionToRootViewController(_ viewController: UIViewController) { + guard let window = view.window else { return } + UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: { + window.rootViewController = viewController + }) + } } -#Preview{ + +#Preview { launchScreenViewController() } diff --git a/recap/Controllers/initial/onboard.storyboard b/recap/Controllers/initial/onboard.storyboard deleted file mode 100644 index cd98974..0000000 --- a/recap/Controllers/initial/onboard.storyboard +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - - - - - - - Pacifico-Regular - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/recap/Managers/.DS_Store b/recap/Managers/.DS_Store new file mode 100755 index 0000000..6673828 Binary files /dev/null and b/recap/Managers/.DS_Store differ diff --git a/recap/Managers/CoreAnalyticsService.swift b/recap/Managers/CoreAnalyticsService.swift new file mode 100755 index 0000000..5073ff3 --- /dev/null +++ b/recap/Managers/CoreAnalyticsService.swift @@ -0,0 +1,113 @@ +// +// CoreAnalyticsService.swift +// recap +// +// Created by s1834 on 12/02/25. +// + +import Foundation +import FirebaseFirestore + +class CoreAnalyticsService { + private let db = Firestore.firestore() + private var verifiedUserDocID: String + + init?() { + guard let docID = UserDefaults.standard.string(forKey: Constants.UserDefaultsKeys.verifiedUserDocID) else { + print("❌ Error: verifiedUserDocID not found in UserDefaults.") + return nil + } + self.verifiedUserDocID = docID + } + + private lazy var analyticsRef: DocumentReference? = { + guard !verifiedUserDocID.isEmpty else { return nil } + return db.collection("users").document(verifiedUserDocID).collection("core").document("analytics") + }() + + // MARK: - Initialize Analytics + func initializeAnalytics() { + let initialData: [String: Any] = [ + "lastFetched": Timestamp(), + "lastAnswered": Timestamp(date: Date(timeIntervalSince1970: 0)), + "appInstalled": true, + "appOpenedFamily": 0, + "appOpenedPatient": 0, + "totalSessionsFamily": 0, + "totalSessionsPatient": 0, + "totalTimeSpentFamily": 0.0, + "totalTimeSpentPatient": 0.0, + "averageUsageTimeFamily": 0.0, + "averageUsageTimePatient": 0.0, + "createdAt": Timestamp(), + "updatedAt": Timestamp() + ] + + updateAnalyticsData(initialData, logFailure: "❌ Firestore Analytics Initialization Failed") + } + + // MARK: - Helper Method for Firestore Updates + private func updateAnalyticsData(_ data: [String: Any], logFailure: String) { + analyticsRef?.setData(data, merge: true) { error in + if let error = error { + print("\(logFailure): \(error.localizedDescription)") + } + } + } + + // MARK: - Track App Open + func trackAppOpen(isFamily: Bool) { + let fieldKey = isFamily ? "appOpenedFamily" : "appOpenedPatient" + + updateAnalyticsData([ + fieldKey: FieldValue.increment(Int64(1)), + "updatedAt": Timestamp() + ], logFailure: "❌ Firestore Update Failed (App Open)") + } + + // MARK: - Track Time Spent + func trackTimeSpent(sessionDuration: Double, isFamily: Bool) { + analyticsRef?.getDocument { document, error in + if let error = error { + print("❌ Firestore Read Failed: \(error.localizedDescription)") + return + } + + guard let data = document?.data() else { + print("❌ Firestore Document is Empty or Nil") + return + } + + let totalSessionsKey = isFamily ? "totalSessionsFamily" : "totalSessionsPatient" + let totalTimeSpentKey = isFamily ? "totalTimeSpentFamily" : "totalTimeSpentPatient" + let averageUsageTimeKey = isFamily ? "averageUsageTimeFamily" : "averageUsageTimePatient" + + let totalSessions = (data[totalSessionsKey] as? Int ?? 0) + 1 + let totalTimeSpent = (data[totalTimeSpentKey] as? Double ?? 0.0) + sessionDuration + let averageUsageTime = totalTimeSpent / Double(totalSessions) + + self.updateAnalyticsData([ + totalSessionsKey: totalSessions, + totalTimeSpentKey: totalTimeSpent, + averageUsageTimeKey: averageUsageTime, + "updatedAt": Timestamp() + ], logFailure: "❌ Error updating time spent") + } + } + + // MARK: - Track Last Answered + func trackLastAnswered() { + updateAnalyticsData([ + "lastAnswered": Timestamp(), + "updatedAt": Timestamp() + ], logFailure: "❌ Error updating last answered timestamp") + } + + // MARK: - Track Data Fetched + func trackDataFetched() { + updateAnalyticsData([ + "lastFetched": Timestamp(), + "updatedAt": Timestamp() + ], logFailure: "❌ Error updating last fetched timestamp") + } +} diff --git a/recap/Managers/DataFetchManager.swift b/recap/Managers/DataFetchManager.swift old mode 100644 new mode 100755 index e4b51b2..49b251c --- a/recap/Managers/DataFetchManager.swift +++ b/recap/Managers/DataFetchManager.swift @@ -5,14 +5,15 @@ // Created by Diptayan Jash on 14/12/24. // -import Foundation import FirebaseFirestore +import Foundation protocol DataFetchProtocol { func fetchRapidQuestions(completion: @escaping ([rapiMemory]?, Error?) -> Void) func fetchUserProfile(userId: String, completion: @escaping ([String: Any]?, Error?) -> Void) func fetchFamilyMembers(userId: String, completion: @escaping ([FamilyMember]?, Error?) -> Void) func fetchLastMemoryCheck(userId: String, completion: @escaping (String) -> Void) + func fetchArticles(completion: @escaping ([Article]?, Error?) -> Void) } class DataFetch: DataFetchProtocol { @@ -43,28 +44,32 @@ class DataFetch: DataFetchProtocol { // Fetch family members func fetchFamilyMembers(userId: String, completion: @escaping ([FamilyMember]?, Error?) -> Void) { - firestore.collection("users").document(userId).collection("family_members").getDocuments { snapshot, error in - if let error = error { - print("Error fetching family members: \(error.localizedDescription)") - completion(nil, error) - return - } - let familyMembers = snapshot?.documents.compactMap { doc -> FamilyMember? in - let data = doc.data() - return FamilyMember( - id: doc.documentID, - name: data["name"] as? String ?? "", - relationship: data["relationship"] as? String ?? "", - phone: data["phone"] as? String ?? "", - email: data["email"] as? String ?? "", - password: data["password"] as? String ?? "", - imageName: data["imageName"] as? String ?? "", - imageURL: data["imageURL"] as? String ?? "" - ) + firestore.collection(Constants.FirestoreKeys.usersCollection) + .document(userId) + .collection(Constants.FirestoreKeys.familyMembersCollection) + .getDocuments { snapshot, error in + if let error = error { + print("Error fetching family members: \(error.localizedDescription)") + completion(nil, error) + return + } + let familyMembers = snapshot?.documents.compactMap { doc -> FamilyMember? in + let data = doc.data() + return FamilyMember( + id: doc.documentID, + name: data["name"] as? String ?? "", + relationship: data["relationship"] as? String ?? "", + phone: data["phone"] as? String ?? "", + email: data["email"] as? String ?? "", + password: data["password"] as? String ?? "", + imageName: data["imageName"] as? String ?? "", + imageURL: data["imageURL"] as? String ?? "" + ) + } + completion(familyMembers, nil) } - completion(familyMembers, nil) - } } + func fetchLastMemoryCheck(userId: String, completion: @escaping (String) -> Void) { FirebaseManager.shared.firestore .collection("users").document(userId) @@ -88,4 +93,60 @@ class DataFetch: DataFetchProtocol { } } } + func fetchArticles(completion: @escaping ([Article]?, Error?) -> Void) { + firestore.collection("Articles").getDocuments { (snapshot, error) in + if let error = error { + completion(nil, error) + return + } + + var articles = [Article]() + let group = DispatchGroup() + + for document in snapshot!.documents { + let data = document.data() + if let title = data["title"] as? String, + let author = data["author"] as? String, + let content = data["content"] as? String, + let imageUrl = data["image"] as? String, + let link = data["link"] as? String { + + group.enter() + self.fetchImage(from: imageUrl) { image in + let article = Article( + title: title, + author: author, + content: content, + image: image ?? UIImage(), + link: link + ) + articles.append(article) + group.leave() + } + } + } + + group.notify(queue: .main) { + completion(articles, nil) + } + } + } + + private func fetchImage(from urlString: String, completion: @escaping (UIImage?) -> Void) { + guard let url = URL(string: urlString) else { + completion(nil) + return + } + + let task = URLSession.shared.dataTask(with: url) { data, _, _ in + guard let data = data, let image = UIImage(data: data) else { + completion(nil) + return + } + DispatchQueue.main.async { + completion(image) + } + } + task.resume() + } } diff --git a/recap/Managers/DataUploadManager.swift b/recap/Managers/DataUploadManager.swift old mode 100644 new mode 100755 diff --git a/recap/Managers/Family/ArticleFetcher.swift b/recap/Managers/Family/ArticleFetcher.swift deleted file mode 100644 index 7e9c42f..0000000 --- a/recap/Managers/Family/ArticleFetcher.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ArticleFetcher.swift -// Recap -// -// Created by user@47 on 15/01/25. -// - -import FirebaseFirestore -import UIKit - -class ArticleFetcher { - - private let db = Firestore.firestore() - - // Fetch articles from Firestore - func fetchArticles(completion: @escaping ([Article]?, Error?) -> Void) { - db.collection("Articles").getDocuments { (snapshot, error) in - if let error = error { - completion(nil, error) - return - } - - var articles = [Article]() - for document in snapshot!.documents { - let data = document.data() - if let title = data["title"] as? String, - let author = data["author"] as? String, - let content = data["content"] as? String, - let imageUrl = data["image"] as? String, - let link = data["link"] as? String { - - // Fetch image asynchronously - self.fetchImage(from: imageUrl) { image in - let article = Article( - title: title, - author: author, - content: content, - image: image ?? UIImage(), // Default to an empty image if fetching fails - link: link - ) - articles.append(article) - - // Call completion handler after all articles are fetched - if articles.count == snapshot!.documents.count { - completion(articles, nil) - } - } - } - } - } - } - - // Helper method to fetch the image - private func fetchImage(from urlString: String, completion: @escaping (UIImage?) -> Void) { - guard let url = URL(string: urlString) else { - completion(nil) - return - } - - let task = URLSession.shared.dataTask(with: url) { data, _, _ in - guard let data = data, let image = UIImage(data: data) else { - completion(nil) - return - } - completion(image) - } - task.resume() - } -} diff --git a/recap/Managers/Family/FamilyLoginFunctions.swift b/recap/Managers/Family/FamilyLoginFunctions.swift old mode 100644 new mode 100755 index f6f8833..4b17a4d --- a/recap/Managers/Family/FamilyLoginFunctions.swift +++ b/recap/Managers/Family/FamilyLoginFunctions.swift @@ -2,7 +2,7 @@ // FamilyLoginFunctions.swift // recap // -// Created by user@47 on 29/01/25. +// Created by s1834 on 29/01/25. // import FirebaseAuth @@ -10,174 +10,224 @@ import FirebaseCore import FirebaseFirestore import GoogleSignIn import UIKit +import Lottie extension FamilyLoginViewController { - @objc func verifyPatientUID() { - print("Verify Button tapped") - - guard let verifyUID = self as? FamilyLoginViewController else { return } - - let patientUID = verifyUID.patientUIDField.text ?? "" - print("Patient UID entered: \(patientUID)") // Log the entered patient UID - + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + spinner.color = .white + verifyButton.setTitle("", for: .normal) + verifyButton.addSubview(spinner) + spinner.center = CGPoint(x: verifyButton.bounds.midX, y: verifyButton.bounds.midY) + + let patientUID = patientUIDField.text ?? "" let db = Firestore.firestore() - db.collection("users").getDocuments { (usersSnapshot, error) in - if let error = error { - print("Error fetching users: \(error.localizedDescription)") - verifyUID.showAlert(message: "Unable to retrieve user details.") + db.collection("users").getDocuments { usersSnapshot, error in + spinner.stopAnimating() + self.verifyButton.setTitle("Verify", for: .normal) + guard error == nil else { + self.showAlert(message: "Unable to retrieve user details.") return } guard let userDocs = usersSnapshot?.documents, !userDocs.isEmpty else { - verifyUID.showAlert(message: "No users found.") + self.animateShake(for: self.patientUIDField) + self.showAlert(message: "No users found.") return } - var patientUIDFound = false for userDoc in userDocs { - let userData = userDoc.data() - // Log the fetched user data for debugging purposes - print("Fetched user data: \(userData)") - - // Check if the patient UID exists and matches - if let storedUID = userData["patientUID"] as? String, storedUID == patientUID { - print("Patient UID verified successfully") - - // Enable the email and password fields - verifyUID.emailField.isEnabled = true - verifyUID.passwordField.isEnabled = true - - // Store the document ID in UserDefaults or similar + if let storedUID = userDoc.data()["patientUID"] as? String, storedUID == patientUID { UserDefaults.standard.set(userDoc.documentID, forKey: "verifiedUserDocID") - print("Verified user document ID: \(userDoc.documentID)") - - // Change the verifyButton's appearance to indicate success - verifyUID.verifyButton.setTitle("Verified", for: .normal) - verifyUID.verifyButton.backgroundColor = .systemGreen - - patientUIDFound = true - break + self.googleSignInButton.isEnabled = true + + UIView.animate(withDuration: 0.3) { + self.verifyButton.setTitle("Verified", for: .normal) + self.verifyButton.backgroundColor = AppColors.iconColor + self.verifyButton.setTitleColor(.white, for: .normal) + } + return } } - if !patientUIDFound { - verifyUID.showAlert(message: "Patient UID does not match. Please try again.") - } + self.showAlert(message: "Patient UID does not match. Please try again.") } } - @objc func loginTapped() { - print("Login tapped") - - guard let loginVC = self as? FamilyLoginViewController else { return } - + @objc func googleSignInTapped() { guard let userDocID = UserDefaults.standard.string(forKey: "verifiedUserDocID") else { - print("No user document found. Please verify UID first.") - loginVC.showAlert(message: "Please verify patient UID first.") + showAlert(message: "Please verify patient UID first.") return } - let enteredEmail = loginVC.emailField.text ?? "" - let enteredPassword = loginVC.passwordField.text ?? "" + let loadingAnimation = self.showLoadingAnimation() - print("Email entered: \(enteredEmail)") - print("Password entered: \(enteredPassword)") - - let db = Firestore.firestore() - - db.collection("users").document(userDocID).collection("family_members").getDocuments { (familySnapshot, error) in + GIDSignIn.sharedInstance.signIn(withPresenting: self) { [weak self] result, error in + guard let self = self else { return } + if let error = error { - print("Error fetching family members: \(error.localizedDescription)") - loginVC.showAlert(message: "Unable to retrieve family details.") + self.stopLoadingAnimation(loadingAnimation) + self.showAlert(message: "Google Sign-In failed: \(error.localizedDescription)") return } - guard let familyDocs = familySnapshot?.documents, !familyDocs.isEmpty else { - loginVC.showAlert(message: "No family members found.") + guard let user = result?.user else { + self.stopLoadingAnimation(loadingAnimation) + self.showAlert(message: "Failed to get user information") return } - var matchedFamilyMember: [String: Any]? = nil - - for familyDoc in familyDocs { - let familyData = familyDoc.data() - print("Fetched family member data: \(familyData)") - - if let storedPassword = familyData["password"] as? String, storedPassword == enteredPassword, - let email = familyData["email"] as? String, email == enteredEmail { - matchedFamilyMember = familyData - break - } - } - - if let matchedMember = matchedFamilyMember { - print("Family member authenticated: \(matchedMember)") + // Get user's email and profile picture + let email = user.profile?.email ?? "" + let profileImageURL = user.profile?.imageURL(withDimension: 200)?.absoluteString ?? "" - // Store family member details - UserDefaults.standard.set(matchedMember, forKey: "familyMemberDetails") - - db.collection("users").document(userDocID).getDocument { (document, error) in + // Check if this email is already registered as a family member + let db = Firestore.firestore() + db.collection("users").document(userDocID).collection("family_members") + .whereField("email", isEqualTo: email) + .getDocuments { [weak self] snapshot, error in + guard let self = self else { return } + if let error = error { - print("Error fetching patient details: \(error.localizedDescription)") + self.stopLoadingAnimation(loadingAnimation) + self.showAlert(message: "Error checking family member status: \(error.localizedDescription)") return } - guard let document = document, document.exists else { - print("Patient document not found.") - return + if let documents = snapshot?.documents, !documents.isEmpty { + // Family member exists, proceed with login + let familyData = documents[0].data() + + // Create UserDefaults data without any Firestore-specific fields + let userDefaultsData: [String: Any] = [ + "name": familyData["name"] as? String ?? "", + "email": familyData["email"] as? String ?? "", + "phone": familyData["phone"] as? String ?? "", + "relation": familyData["relation"] as? String ?? "", + "imageURL": profileImageURL + ] + + UserDefaults.standard.set(userDefaultsData, forKey: "familyMemberDetails") + UserDefaults.standard.set(profileImageURL, forKey: Constants.UserDefaultsKeys.familyMemberImageURL) + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.isFamilyMemberLoggedIn) + UserDefaults.standard.synchronize() + + // Fetch patient details + self.fetchPatientDetails(userDocID: userDocID, loadingAnimation: loadingAnimation) + } else { + // New family member, show registration screen + self.stopLoadingAnimation(loadingAnimation) + self.showFamilyRegistration(email: email, profileImageURL: profileImageURL, userDocID: userDocID) } + } + } + } + + @objc func appleSignInTapped() { + print("Apple Sign-In tapped") + } - let userData = document.data() ?? [:] - print("Fetched user data for patient: \(userData)") + private func showFamilyRegistration(email: String, profileImageURL: String, userDocID: String) { + let registrationVC = FamilyRegistrationViewController() + registrationVC.email = email + registrationVC.profileImageURL = profileImageURL + registrationVC.userDocID = userDocID + let navController = UINavigationController(rootViewController: registrationVC) + present(navController, animated: true) + } - // Store patient details - UserDefaults.standard.set(userData, forKey: "patientDetails") + private func fetchPatientDetails(userDocID: String, loadingAnimation: LottieAnimationView) { + let db = Firestore.firestore() + db.collection("users").document(userDocID).getDocument { [weak self] document, error in + guard let self = self else { return } + + self.stopLoadingAnimation(loadingAnimation) - // Navigate to FamilyViewController - DispatchQueue.main.async { - let familyVC = TabbarFamilyViewController() - if let window = UIApplication.shared.windows.first { - let navigationController = UINavigationController(rootViewController: familyVC) - window.rootViewController = navigationController - window.makeKeyAndVisible() - } - } - } - } else { - loginVC.showAlert(message: "Incorrect email or password. Please try again.") + if let error = error { + print("Error fetching patient details: \(error.localizedDescription)") + return + } + + guard let document = document, document.exists else { + print("Patient document not found.") + return + } + + let userData = document.data() ?? [:] + UserDefaults.standard.set(userData, forKey: "patientDetails") + + DispatchQueue.main.async { + self.animateSlideToMainScreen() } } } - - @objc func rememberMeTapped() { - rememberMeButton.isSelected.toggle() - print("Remember me tapped. Current state: \(rememberMeButton.isSelected ? "Selected" : "Deselected")") + private func animateSlideToMainScreen() { + let mainVC = TabbarFamilyViewController() + let navigationController = UINavigationController(rootViewController: mainVC) + + guard let window = UIApplication.shared.windows.first else { return } + window.addSubview(navigationController.view) + navigationController.view.frame = CGRect(x: window.frame.width, y: 0, width: window.frame.width, height: window.frame.height) + + UIView.animate(withDuration: 0.5, animations: { + self.view.frame.origin.x = -self.view.frame.width + navigationController.view.frame = window.bounds + }) { _ in + window.rootViewController = navigationController + window.makeKeyAndVisible() + } + } + + private func animateShake(for view: UIView) { + let shake = CABasicAnimation(keyPath: "position") + shake.duration = 0.05 + shake.repeatCount = 3 + shake.autoreverses = true + shake.fromValue = NSValue(cgPoint: CGPoint(x: view.center.x - 8, y: view.center.y)) + shake.toValue = NSValue(cgPoint: CGPoint(x: view.center.x + 8, y: view.center.y)) + view.layer.add(shake, forKey: "position") + } + private func showLoadingAnimation() -> LottieAnimationView { + let animationView = LottieAnimationView(name: "loading") + animationView.frame = CGRect(x: 0, y: 0, width: 100, height: 100) + animationView.center = view.center + animationView.loopMode = .loop + animationView.animationSpeed = 1.5 + view.addSubview(animationView) + animationView.play() + return animationView } + private func stopLoadingAnimation(_ animationView: LottieAnimationView) { + DispatchQueue.main.async { + animationView.stop() + animationView.removeFromSuperview() + } + } @objc func logoutTapped() { do { try Auth.auth().signOut() GIDSignIn.sharedInstance.signOut() - UserDefaults.standard.removeObject(forKey: "hasCompletedProfile") + + UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.isFamilyMemberLoggedIn) UserDefaultsStorageProfile.shared.clearProfile() + guard let window = UIApplication.shared.windows.first else { return } let welcomeVC = WelcomeViewController() let navigationController = UINavigationController(rootViewController: welcomeVC) - navigationController.view.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: window.frame.height) + window.rootViewController = navigationController window.makeKeyAndVisible() - UIView.animate(withDuration: 0.5, animations: { - self.view.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: window.frame.height) + + UIView.animate(withDuration: 0.5) { + self.view.frame.origin.y = window.frame.height navigationController.view.frame = window.bounds - }) { _ in - window.rootViewController = navigationController } } catch { - print("Error signing out: \(error.localizedDescription)") showAlert(message: "Failed to log out. Please try again.") } } diff --git a/recap/Managers/Family/LocalizationManager.swift b/recap/Managers/Family/LocalizationManager.swift new file mode 100755 index 0000000..3d6b7af --- /dev/null +++ b/recap/Managers/Family/LocalizationManager.swift @@ -0,0 +1,20 @@ +// +// LocalizationManager.swift +// recap +// +// Created by Diptayan Jash on 11/02/25. +// + +import Foundation +class LocalizationManager { + static let shared = LocalizationManager() + + func localizedString(for key: String) -> String { + let language = UserDefaults.standard.string(forKey: "AppLanguage") ?? "en" + guard let path = Bundle.main.path(forResource: language, ofType: "lproj"), + let bundle = Bundle(path: path) else { + return NSLocalizedString(key, comment: "") + } + return bundle.localizedString(forKey: key, value: nil, table: nil) + } +} diff --git a/recap/Managers/Family/NotificationManager.swift b/recap/Managers/Family/NotificationManager.swift old mode 100644 new mode 100755 index 4dc1bc3..1dd2e24 --- a/recap/Managers/Family/NotificationManager.swift +++ b/recap/Managers/Family/NotificationManager.swift @@ -2,7 +2,7 @@ // NotificationManager.swift // recap // -// Created by user@47 on 04/02/25. +// Created by s1834 on 04/02/25. // import UserNotifications @@ -93,8 +93,6 @@ class NotificationManager { UNUserNotificationCenter.current().add(request) { error in if let error = error { print("❌❌ Failed to send welcome notification: \(error.localizedDescription)") - } else { - print("✅✅ Welcome notification sent!!") } } } @@ -105,15 +103,13 @@ class NotificationManager { content.body = "New memory exercises are available!! Strengthen your mind right now." content.sound = .default - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10800, repeats: true) // Every 3 hours + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10800, repeats: true) let request = UNNotificationRequest(identifier: "questionReminder", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print("❌❌ Failed to schedule recurring notification: \(error.localizedDescription)") - } else { - print("✅✅ Recurring notification scheduled every 3 hours!!") } } } diff --git a/recap/Managers/Family/QuestionsManager.swift b/recap/Managers/Family/QuestionsManager.swift old mode 100644 new mode 100755 index 93cc3dc..e3aadb1 --- a/recap/Managers/Family/QuestionsManager.swift +++ b/recap/Managers/Family/QuestionsManager.swift @@ -1,10 +1,11 @@ // -// QuestionsFetcher.swift +// QuestionsManager.swift // recap // -// Created by user@47 on 03/02/25. +// Created by s1834 on 03/02/25. // + import FirebaseFirestore class QuestionsManager { @@ -15,23 +16,10 @@ class QuestionsManager { self.verifiedUserDocID = verifiedUserDocID } - func startFetchingQuestions(completion: @escaping ([Question]) -> Void) { - fetchQuestions(completion: completion) - - timer = Timer.scheduledTimer(withTimeInterval: 86400.0, repeats: true) { _ in - self.fetchQuestions(completion: completion) - } - } - - func stopFetchingQuestions() { - timer?.invalidate() - timer = nil - } - + // MARK: - Fetch Questions func fetchQuestions(completion: @escaping ([Question]) -> Void) { let db = Firestore.firestore() let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") - print("Fetching questions for user: \(verifiedUserDocID)") userQuestionsRef.getDocuments { userSnapshot, userError in if let userError = userError { @@ -41,7 +29,6 @@ class QuestionsManager { } let existingQuestionIDs = Set(userSnapshot?.documents.map { $0.documentID } ?? []) - let questionsRef = db.collection("Questions") questionsRef.getDocuments { snapshot, error in if let error = error { @@ -51,20 +38,17 @@ class QuestionsManager { } guard let documents = snapshot?.documents else { - print("No new questions found") - completion([]) + self.fetchQuestionsByDateIfNeeded(completion: completion) return } - // Convert Firestore documents to Question objects and shuffle them var allQuestions = documents.compactMap { doc -> Question? in var question = self.convertToQuestion(doc: doc) - question?.id = doc.documentID // Assign Firestore document ID + question?.id = doc.documentID return question } - allQuestions.shuffle() // Shuffle before categorization + allQuestions.shuffle() - // Categorized lists var immediateMemoryQuestions: [Question] = [] var recentQuestions: [Question] = [] var remoteQuestions: [Question] = [] @@ -72,7 +56,6 @@ class QuestionsManager { for question in allQuestions { guard let questionID = question.id, !existingQuestionIDs.contains(questionID) else { continue } - // Categorize questions switch question.category.rawValue { case "immediateMemory": immediateMemoryQuestions.append(question) case "recentMemory": recentQuestions.append(question) @@ -81,21 +64,175 @@ class QuestionsManager { } } - // Pick questions from each category let selectedImmediate = Array(immediateMemoryQuestions.prefix(4)) let selectedRecent = Array(recentQuestions.prefix(2)) let selectedRemote = Array(remoteQuestions.prefix(1)) var finalQuestions = selectedImmediate + selectedRecent + selectedRemote -// print("Selected Questions: \(finalQuestions)") + if finalQuestions.isEmpty { + self.fetchQuestionsByDateIfNeeded(completion: completion) + } else { + self.sendQuestionsToUser(questions: finalQuestions) + completion(finalQuestions) + } + } + } + } + + func moveQuestionsToAskedAndDelete(completion: @escaping () -> Void) { + ensureQuestionsAskedExists { + let db = Firestore.firestore() + let userQuestionsRef = db.collection("users").document(self.verifiedUserDocID).collection("questions") + let calendar = Calendar.current + let date = Date() + + let year = calendar.component(.year, from: date) + let month = calendar.component(.month, from: date) + let day = calendar.component(.day, from: date) + + let monthFormatted = String(format: "%02d", month) // Ensure month is "01", "02", ..., "12" + let dayFormatted = String(format: "%02d", day) // Ensure day is "01", "02", ..., "31" + + let monthPath = "\(year)-\(monthFormatted)" + let dayPath = "\(year)-\(monthFormatted)-\(dayFormatted)" + + let questionsAskedRef = db.collection("users") + .document(self.verifiedUserDocID) + .collection("questionsAsked") + .document("\(year)") + .collection(monthPath) + .document(dayPath) + + userQuestionsRef.getDocuments { snapshot, error in + if let error = error { + print("❌ Error fetching user questions: \(error.localizedDescription)") + completion() + return + } + + guard let documents = snapshot?.documents, !documents.isEmpty else { + print("✅ No questions to move.") + completion() + return + } + + let batch = db.batch() + + for document in documents { + let questionID = document.documentID + let questionData = document.data() + + let questionAskedRef = questionsAskedRef.collection(questionID).document("data") + batch.setData(questionData, forDocument: questionAskedRef) + + let questionRef = userQuestionsRef.document(questionID) + batch.deleteDocument(questionRef) + } + + batch.commit { batchError in + if let batchError = batchError { + print("❌ Error committing batch: \(batchError.localizedDescription)") + } + completion() + } + } + } + } + + func ensureQuestionsAskedExists(completion: @escaping () -> Void) { + let db = Firestore.firestore() + let calendar = Calendar.current + let date = Date() + + let year = calendar.component(.year, from: date) + let month = calendar.component(.month, from: date) + let day = calendar.component(.day, from: date) + + let monthFormatted = String(format: "%02d", month) + let dayFormatted = String(format: "%02d", day) + + let monthPath = "\(year)-\(monthFormatted)" + let dayPath = "\(year)-\(monthFormatted)-\(dayFormatted)" + + let questionsAskedRef = db.collection("users") + .document(verifiedUserDocID) + .collection("questionsAsked") + .document("\(year)") + .collection(monthPath) + .document(dayPath) + + questionsAskedRef.getDocument { document, error in + if let error = error { + print("Error checking questionsAsked existence: \(error.localizedDescription)") + return + } - self.sendQuestionsToUser(questions: finalQuestions) - completion(finalQuestions) + if document?.exists == true { + completion() + } else { + questionsAskedRef.setData([:]) { error in + if let error = error { + print("Error creating questionsAsked: \(error.localizedDescription)") + } else { + completion() + } + } } } } + + func fetchQuestionsByDateIfNeeded(completion: @escaping ([Question]) -> Void) { + let db = Firestore.firestore() + let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") + + userQuestionsRef.getDocuments { snapshot, error in + if let error = error { + print("Error fetching user questions: \(error.localizedDescription)") + completion([]) + return + } + + if let documents = snapshot?.documents, !documents.isEmpty { + let questions = documents.compactMap { self.convertToQuestion(doc: $0) } + completion(questions) + return + } + + let calendar = Calendar.current + let date = Date() + let year = calendar.component(.year, from: date) + let month = calendar.component(.month, from: date) + let day = calendar.component(.day, from: date) + + let questionsAskedRef = db.collection("users") + .document(self.verifiedUserDocID) + .collection("questionsAsked") + .document("\(year)") + .collection("\(month)") + .document("\(year)-\(month)-\(day)") + + questionsAskedRef.collection("questions").getDocuments { snapshot, error in + if let error = error { + print("Error fetching questions from questionsAsked: \(error.localizedDescription)") + completion([]) + return + } + + guard let documents = snapshot?.documents else { + print("No questions found in questionsAsked.") + completion([]) + return + } + + let questions = documents.compactMap { self.convertToQuestion(doc: $0) } + completion(questions) + } + } + } + + private func sendQuestionsToUser(questions: [Question]) { let db = Firestore.firestore() let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") @@ -106,47 +243,14 @@ class QuestionsManager { continue } - userQuestionsRef.document(questionID).setData([ - "text": question.text, - "category": question.category.rawValue, - "subcategory": question.subcategory.rawValue, - "tag": question.tag, - "answerOptions": question.answerOptions, - "answers": question.answers, - "correctAnswers": question.correctAnswers, - "image": question.image ?? NSNull(), // Use NSNull for optional fields that are nil - "isAnswered": question.isAnswered, - "askInterval": question.askInterval, - "lastAsked": NSNull(), // Use NSNull() for null fields - "timesAsked": question.timesAsked, - "timesAnsweredCorrectly": question.timesAnsweredCorrectly, - "timeFrame": [ - "from": question.timeFrame.from, - "to": question.timeFrame.to - ], - "priority": question.priority, - "audio": question.audio ?? NSNull(), // Use NSNull for nil audio - "isActive": question.isActive, - "lastAnsweredCorrectly": question.lastAnsweredCorrectly ?? NSNull(), - "hint": question.hint ?? NSNull(), - "confidence": question.confidence ?? NSNull(), - "hardness": question.hardness, - "questionType": question.questionType.rawValue, - "addedAt": FieldValue.serverTimestamp(), // This is the timestamp when the question is added to the user - "createdAt": question.createdAt // This is the timestamp from the original question document - ], merge: true) { error in + userQuestionsRef.document(questionID).setData(["text": question.text, "category": question.category.rawValue, "subcategory": question.subcategory.rawValue, "tag": question.tag, "answerOptions": question.answerOptions, "answers": question.answers, "correctAnswers": question.correctAnswers, "image": question.image ?? NSNull(), "isAnswered": question.isAnswered, "askInterval": question.askInterval, "lastAsked": NSNull(), "timesAsked": question.timesAsked, "timesAnsweredCorrectly": question.timesAnsweredCorrectly, "timeFrame": [ "from": question.timeFrame.from, "to": question.timeFrame.to], "priority": question.priority, "audio": question.audio ?? NSNull(), "isActive": question.isActive, "lastAnsweredCorrectly": question.lastAnsweredCorrectly ?? NSNull(), "hint": question.hint ?? NSNull(), "confidence": question.confidence ?? NSNull(), "hardness": question.hardness, "questionType": question.questionType.rawValue, "addedAt": FieldValue.serverTimestamp(), "createdAt": question.createdAt], merge: true) { error in if let error = error { print("Error adding question to user: \(error.localizedDescription)") - } else { - print("Successfully added question \(questionID) to user") } } } } - - - private func convertToQuestion(doc: QueryDocumentSnapshot) -> Question? { let data = doc.data() @@ -161,28 +265,23 @@ class QuestionsManager { let answerOptions = data["answerOptions"] as? [String] ?? [] let answers = data["answers"] as? [String] ?? [] let correctAnswers = data["correctAnswers"] as? [String] ?? [] - let tag = data["tag"] as? String ?? "" let image = data["image"] as? String let audio = data["audio"] as? String let hint = data["hint"] as? String - let isAnswered = data["isAnswered"] as? Bool ?? false let isActive = data["isActive"] as? Bool ?? true - let askInterval = data["askInterval"] as? Int ?? 0 let timesAsked = data["timesAsked"] as? Int ?? 0 let timesAnsweredCorrectly = data["timesAnsweredCorrectly"] as? Int ?? 0 let priority = data["priority"] as? Int ?? 0 let hardness = data["hardness"] as? Int ?? 0 let confidence = data["confidence"] as? Int - let lastAsked = (data["lastAsked"] as? Timestamp)?.dateValue() let lastAnsweredCorrectly = (data["lastAnsweredCorrectly"] as? Timestamp)?.dateValue() - // Convert Date to String let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withInternetDateTime] // Standard format + dateFormatter.formatOptions = [.withInternetDateTime] let timeFrameData = data["timeFrame"] as? [String: Timestamp] let fromDate = timeFrameData?["from"]?.dateValue() ?? Date() @@ -196,30 +295,149 @@ class QuestionsManager { let createdAt = (data["createdAt"] as? Timestamp)?.dateValue() ?? Date() let addedAt: Timestamp? = data["addedAt"] as? Timestamp - let questionTypeString = data["questionType"] as? String ?? "" let questionType = QuestionType(rawValue: questionTypeString) ?? .singleCorrect - return Question( - text: text, - category: category, - subcategory: QuestionSubcategory(rawValue: subcategory) ?? .general, - tag: tag, - answerOptions: answerOptions, - answers: answers, - correctAnswers: correctAnswers, - image: image, - isAnswered: isAnswered, - askInterval: TimeInterval(askInterval), - timeFrame: timeFrame, - priority: priority, - audio: audio, - isActive: isActive, - hint: hint, - confidence: confidence, - hardness: hardness, - questionType: questionType - ) + return Question(text: text, category: category, subcategory: QuestionSubcategory(rawValue: subcategory) ?? .general, tag: tag, answerOptions: answerOptions, answers: answers, correctAnswers: correctAnswers, image: image, isAnswered: isAnswered, askInterval: TimeInterval(askInterval), timeFrame: timeFrame, priority: priority, audio: audio, isActive: isActive, hint: hint, confidence: confidence, hardness: hardness, questionType: questionType) } - } + +//import FirebaseFirestore +// +//class QuestionsManager { +// var verifiedUserDocID: String +// var timer: Timer? +// +// init(verifiedUserDocID: String) { +// self.verifiedUserDocID = verifiedUserDocID +// } +// +// func fetchQuestions(completion: @escaping ([Question]) -> Void) { +// let db = Firestore.firestore() +// let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") +// +// userQuestionsRef.getDocuments { userSnapshot, userError in +// if let userError = userError { +// print("Error fetching user questions: \(userError.localizedDescription)") +// completion([]) +// return +// } +// +// let existingQuestionIDs = Set(userSnapshot?.documents.map { $0.documentID } ?? []) +// +// let questionsRef = db.collection("Questions") +// questionsRef.getDocuments { snapshot, error in +// if let error = error { +// print("Error fetching new questions: \(error.localizedDescription)") +// completion([]) +// return +// } +// +// guard let documents = snapshot?.documents else { +// print("No new questions found") +// completion([]) +// return +// } +// +// var allQuestions = documents.compactMap { doc -> Question? in +// var question = self.convertToQuestion(doc: doc) +// question?.id = doc.documentID +// return question +// } +// allQuestions.shuffle() +// +// var immediateMemoryQuestions: [Question] = [] +// var recentQuestions: [Question] = [] +// var remoteQuestions: [Question] = [] +// +// for question in allQuestions { +// guard let questionID = question.id, !existingQuestionIDs.contains(questionID) else { continue } +// +// switch question.category.rawValue { +// case "immediateMemory": immediateMemoryQuestions.append(question) +// case "recentMemory": recentQuestions.append(question) +// case "remoteMemory": remoteQuestions.append(question) +// default: break +// } +// } +// +// let selectedImmediate = Array(immediateMemoryQuestions.prefix(4)) +// let selectedRecent = Array(recentQuestions.prefix(2)) +// let selectedRemote = Array(remoteQuestions.prefix(1)) +// +// var finalQuestions = selectedImmediate + selectedRecent + selectedRemote +// +// self.sendQuestionsToUser(questions: finalQuestions) +// completion(finalQuestions) +// } +// } +// } +// +// private func sendQuestionsToUser(questions: [Question]) { +// let db = Firestore.firestore() +// let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") +// +// for question in questions { +// guard let questionID = question.id, !questionID.isEmpty else { +// print("Skipping question due to empty ID: \(question)") +// continue +// } +// +// userQuestionsRef.document(questionID).setData(["text": question.text, "category": question.category.rawValue, "subcategory": question.subcategory.rawValue, "tag": question.tag, "answerOptions": question.answerOptions, "answers": question.answers, "correctAnswers": question.correctAnswers, "image": question.image ?? NSNull(), "isAnswered": question.isAnswered, "askInterval": question.askInterval, "lastAsked": NSNull(), "timesAsked": question.timesAsked, "timesAnsweredCorrectly": question.timesAnsweredCorrectly, "timeFrame": [ "from": question.timeFrame.from, "to": question.timeFrame.to], "priority": question.priority, "audio": question.audio ?? NSNull(), "isActive": question.isActive, "lastAnsweredCorrectly": question.lastAnsweredCorrectly ?? NSNull(), "hint": question.hint ?? NSNull(), "confidence": question.confidence ?? NSNull(), "hardness": question.hardness, "questionType": question.questionType.rawValue, "addedAt": FieldValue.serverTimestamp(), "createdAt": question.createdAt], merge: true) { error in +// if let error = error { +// print("Error adding question to user: \(error.localizedDescription)") +// } +// } +// } +// } +// +// private func convertToQuestion(doc: QueryDocumentSnapshot) -> Question? { +// let data = doc.data() +// +// guard let categoryString = data["category"] as? String, +// let category = QuestionCategory(rawValue: categoryString), +// let text = data["text"] as? String else { +// print("Invalid data for document \(doc.documentID)") +// return nil +// } +// +// let subcategory = data["subcategory"] as? String ?? "" +// let answerOptions = data["answerOptions"] as? [String] ?? [] +// let answers = data["answers"] as? [String] ?? [] +// let correctAnswers = data["correctAnswers"] as? [String] ?? [] +// let tag = data["tag"] as? String ?? "" +// let image = data["image"] as? String +// let audio = data["audio"] as? String +// let hint = data["hint"] as? String +// let isAnswered = data["isAnswered"] as? Bool ?? false +// let isActive = data["isActive"] as? Bool ?? true +// let askInterval = data["askInterval"] as? Int ?? 0 +// let timesAsked = data["timesAsked"] as? Int ?? 0 +// let timesAnsweredCorrectly = data["timesAnsweredCorrectly"] as? Int ?? 0 +// let priority = data["priority"] as? Int ?? 0 +// let hardness = data["hardness"] as? Int ?? 0 +// let confidence = data["confidence"] as? Int +// let lastAsked = (data["lastAsked"] as? Timestamp)?.dateValue() +// let lastAnsweredCorrectly = (data["lastAnsweredCorrectly"] as? Timestamp)?.dateValue() +// +// let dateFormatter = ISO8601DateFormatter() +// dateFormatter.formatOptions = [.withInternetDateTime] +// +// let timeFrameData = data["timeFrame"] as? [String: Timestamp] +// let fromDate = timeFrameData?["from"]?.dateValue() ?? Date() +// let toDate = timeFrameData?["to"]?.dateValue() ?? Date() +// +// let timeFrame = TimeFrame( +// from: dateFormatter.string(from: fromDate), +// to: dateFormatter.string(from: toDate) +// ) +// +// let createdAt = (data["createdAt"] as? Timestamp)?.dateValue() ?? Date() +// let addedAt: Timestamp? = data["addedAt"] as? Timestamp +// +// let questionTypeString = data["questionType"] as? String ?? "" +// let questionType = QuestionType(rawValue: questionTypeString) ?? .singleCorrect +// +// return Question(text: text, category: category, subcategory: QuestionSubcategory(rawValue: subcategory) ?? .general, tag: tag, answerOptions: answerOptions, answers: answers, correctAnswers: correctAnswers, image: image, isAnswered: isAnswered, askInterval: TimeInterval(askInterval), timeFrame: timeFrame, priority: priority, audio: audio, isActive: isActive, hint: hint, confidence: confidence, hardness: hardness, questionType: questionType) +// } +//} diff --git a/recap/Managers/FirebaseDecodableManager.swift b/recap/Managers/FirebaseDecodableManager.swift old mode 100644 new mode 100755 diff --git a/recap/Managers/FirebaseManager.swift b/recap/Managers/FirebaseManager.swift old mode 100644 new mode 100755 index 6d19d11..7023939 --- a/recap/Managers/FirebaseManager.swift +++ b/recap/Managers/FirebaseManager.swift @@ -44,6 +44,50 @@ class FirebaseManager { // completion(error) // } // } + func uploadFamilyMemberImage(patientId: String, imagePath: String, image: UIImage, completion: @escaping (String?, Error?) -> Void) { + let storageRef = storage.child(imagePath) + + guard let imageData = image.jpegData(compressionQuality: 0.8) else { + completion(nil, NSError(domain: "ImageError", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to convert image to data."])) + return + } + + // Upload image to Firebase Storage + storageRef.putData(imageData, metadata: nil) { metadata, error in + if let error = error { + completion(nil, error) + return + } + + // Get download URL + storageRef.downloadURL { url, error in + if let error = error { + completion(nil, error) + } else { + guard let imageUrl = url?.absoluteString else { + completion(nil, NSError(domain: "ImageError", code: 500, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) + return + } + completion(imageUrl, nil) // Return image URL immediately + + // Trigger Firestore update after image upload + let firestoreRef = self.firestore.collection(Constants.FirestoreKeys.usersCollection) + .document(patientId) + .collection(Constants.FirestoreKeys.familyMembersCollection) + .document(imagePath) + + firestoreRef.updateData(["imageURL": imageUrl]) { error in + if let error = error { + print("Failed to update Firestore document with image URL: \(error)") + } else { + print("Firestore document updated successfully with image URL") + } + } + } + } + } + } + func saveUserDetails(_ details: UserDetails, completion: @escaping (Error?) -> Void) { let userId = details.id let userData: [String: Any] = [ @@ -55,20 +99,21 @@ class FirebaseManager { "sex": details.sex, "bloodGroup": details.bloodGroup, "stage": details.stage, - "profileImageURL": details.profileImageURL ?? "" + "profileImageURL": details.profileImageURL ?? "", ] - + firestore.collection("users").document(userId).setData(userData) { error in completion(error) } } - + // func addFamilyMember(for patientId: String, member: FamilyMember, completion: @escaping (Error?) -> Void) { // let familyMemberData = member.dictionary // firestore.collection("users").document(patientId).collection("familyMembers").addDocument(data: familyMemberData) { error in // completion(error) // } // } + func addFamilyMember(for patientId: String, member: FamilyMember, completion: @escaping (Error?) -> Void) { let familyMemberData: [String: Any] = [ "name": member.name, @@ -77,11 +122,16 @@ class FirebaseManager { "relationship": member.relationship, "phone": member.phone, "linkedPatientId": patientId, // Link the family member to the patient + "imageName": member.imageName, + "imageURL": member.imageURL, ] - - firestore.collection("users").document(patientId).collection("family_members").addDocument(data: familyMemberData) { error in - completion(error) - } + firestore + .collection(Constants.FirestoreKeys.usersCollection) + .document(patientId) + .collection(Constants.FirestoreKeys.familyMembersCollection) + .addDocument(data: familyMemberData) { error in + completion(error) + } } func fetchUserDetails(userId: String, completion: @escaping (UserDetails?, Error?) -> Void) { @@ -101,6 +151,7 @@ class FirebaseManager { completion(userDetails, nil) } } + func fetchUserProfile(userId: String, completion: @escaping ([String: Any]?) -> Void) { firestore.collection("users").document(userId).getDocument { document, error in if let error = error { @@ -117,43 +168,53 @@ class FirebaseManager { completion(data) } } + func deleteFamilyMember(for patientId: String, memberId: String, completion: @escaping (Error?) -> Void) { - firestore.collection("users").document(patientId).collection("family_members").document(memberId).delete { error in - if let error = error { - print("Error deleting family member from Firebase: \(error.localizedDescription)") - } else { - print("Successfully deleted family member \(memberId)") + firestore + .collection(Constants.FirestoreKeys.usersCollection) + .document(patientId) + .collection(Constants.FirestoreKeys.familyMembersCollection) + .document(memberId) + .delete { error in + if let error = error { + print("Error deleting family member from Firebase: \(error.localizedDescription)") + } else { + print("Successfully deleted family member \(memberId)") + } + completion(error) } - completion(error) - } } + func fetchFamilyMembers(for patientId: String, completion: @escaping ([FamilyMember]?, Error?) -> Void) { - firestore.collection("users").document(patientId).collection("family_members").getDocuments { snapshot, error in - if let error = error { - completion(nil, error) - return + firestore + .collection(Constants.FirestoreKeys.usersCollection) + .document(patientId) + .collection(Constants.FirestoreKeys.familyMembersCollection) + .getDocuments { snapshot, error in + if let error = error { + completion(nil, error) + return + } + let familyMembers = snapshot?.documents.compactMap { doc -> FamilyMember? in + let data = doc.data() + return FamilyMember( + id: doc.documentID, // Use the actual Firestore document ID + name: data["name"] as? String ?? "", + relationship: data["relationship"] as? String ?? "", + phone: data["phone"] as? String ?? "", + email: data["email"] as? String ?? "", + password: data["password"] as? String ?? "", + imageName: data["imageName"] as? String ?? "", + imageURL: data["imageURL"] as? String ?? "" + ) + } + completion(familyMembers, nil) } + } - let familyMembers = snapshot?.documents.compactMap { doc -> FamilyMember? in - let data = doc.data() - return FamilyMember( - id: doc.documentID, // Use the actual Firestore document ID - name: data["name"] as? String ?? "", - relationship: data["relationship"] as? String ?? "", - phone: data["phone"] as? String ?? "", - email: data["email"] as? String ?? "", - password: data["password"] as? String ?? "", - imageName: data["imageName"] as? String ?? "", - imageURL: data["imageURL"] as? String ?? "" - ) - } - - completion(familyMembers, nil) + func uploadDocument(collectionPath: String, documentId: String, data: [String: Any], completion: @escaping (Error?) -> Void) { + firestore.collection(collectionPath).document(documentId).setData(data) { error in + completion(error) } } - func uploadDocument(collectionPath: String, documentId: String, data: [String: Any], completion: @escaping (Error?) -> Void) { - firestore.collection(collectionPath).document(documentId).setData(data) { error in - completion(error) - } - } } diff --git a/recap/Managers/LoginFunctions.swift b/recap/Managers/LoginFunctions.swift old mode 100644 new mode 100755 index 500eb70..e108aab --- a/recap/Managers/LoginFunctions.swift +++ b/recap/Managers/LoginFunctions.swift @@ -10,69 +10,75 @@ import FirebaseCore import FirebaseFirestore import GoogleSignIn import UIKit +import Lottie extension PatientLoginViewController { - @objc func rememberMeTapped() { - rememberMeButton.isSelected.toggle() - } - +// @objc func rememberMeTapped() { +// rememberMeButton.isSelected.toggle() +// } @objc func loginTapped() { print("Login tapped") - - guard let loginVC = self as? PatientLoginViewController else { return } - - let email = loginVC.emailField.text ?? "" - let password = loginVC.passwordField.text ?? "" - - Auth.auth().signIn(withEmail: email, password: password) { [weak loginVC] authResult, error in + + // Validate input fields + guard let email = emailField.text, !email.isEmpty, + let password = passwordField.text, !password.isEmpty else { + showAlert(message: "Please enter both email and password.") + return + } + + // Validate email format + if !isValidEmail(email) { + showAlert(message: "Please enter a valid email address.") + return + } + + // Show loading animation + let loadingAnimation = showLoadingAnimation() + + // Sign in with Firebase Auth + Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in + guard let self = self else { return } + if let error = error { - print("Login failed: \(error.localizedDescription)") - loginVC?.showAlert(message: "Invalid email or password.") + // Remove loading animation if there's an error + self.removeLoadingAnimation(loadingAnimation) + print("Login Error: \(error.localizedDescription)") + self.showAlert(message: "Login failed: \(error.localizedDescription)") return } - - guard let user = authResult?.user else { return } - let userId = user.uid // Get the patient ID from Firebase session - - FirebaseManager.shared.fetchUserDetails(userId: userId) { userDetails, error in - if let error = error { - print("Error fetching user details: \(error.localizedDescription)") - return - } - - if let userDetails = userDetails { - UserDefaultsStorageProfile.shared.saveProfile(details: userDetails.dictionary, image: nil) { [weak loginVC] success in - if success { - // Fetch family members - FirebaseManager.shared.fetchFamilyMembers(for: userId) { familyMembers, error in - if let familyMembers = familyMembers { - // Handle family members as needed - } - } - let mainVC = TabbarViewController() - loginVC?.navigationController?.setViewControllers([mainVC], animated: true) - } else { - print("Failed to save profile") - } - } - } else { - print("User profile not found.") - loginVC?.showAlert(message: "User profile not found.") - } + + guard let user = authResult?.user else { + // Remove loading animation if no user is found + self.removeLoadingAnimation(loadingAnimation) + self.showAlert(message: "Login unsuccessful. Please try again.") + return } + + // Store user ID in UserDefaults + let userId = user.uid + UserDefaults.standard.set(userId, forKey: Constants.UserDefaultsKeys.verifiedUserDocID) + UserDefaults.standard.set(email, forKey: "userEmail") + + // Use the existing fetchOrCreateUserProfile function for consistency + self.fetchOrCreateUserProfile(userId: userId, email: email, loadingAnimation: loadingAnimation) } } @objc func signupTapped() { -// let signupVC = PatientSignupViewController() -// navigationController?.pushViewController(signupVC, animated: true) - - let signupVC = patientInfo() + // Create and present the signup view controller + let signupVC = PatientSignupViewController() let nav = UINavigationController(rootViewController: signupVC) + // Present the signup view controller present(nav, animated: true) } + // Helper method for email validation + private func isValidEmail(_ email: String) -> Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: email) + } @objc func googleLoginTapped() { guard let clientID = FirebaseApp.app()?.options.clientID else { print("Firebase client ID not found") @@ -82,10 +88,19 @@ extension PatientLoginViewController { let config = GIDConfiguration(clientID: clientID) GIDSignIn.sharedInstance.configuration = config + // Show loading animation immediately before presenting Google Sign-In + let loadingAnimation = self.showLoadingAnimation() + loadingAnimation.isHidden = true // Initially hide it + GIDSignIn.sharedInstance.signIn(withPresenting: self) { [weak self] result, error in guard let self = self else { return } + // Make the loading animation visible once Google Sign-In screen dismisses + loadingAnimation.isHidden = false + if let error = error { + // Remove loading animation if there's an error + self.removeLoadingAnimation(loadingAnimation) print("Google Sign-In Error: \(error.localizedDescription)") self.showAlert(message: "Google Sign-In failed. Please try again.") return @@ -93,6 +108,8 @@ extension PatientLoginViewController { guard let user = result?.user, let idToken = user.idToken?.tokenString else { + // Remove loading animation if user retrieval fails + self.removeLoadingAnimation(loadingAnimation) print("Failed to retrieve Google user") self.showAlert(message: "Unable to retrieve user information.") return @@ -104,43 +121,79 @@ extension PatientLoginViewController { guard let self = self else { return } if let authError = authError { + // Remove loading animation if there's an authentication error + self.removeLoadingAnimation(loadingAnimation) print("Firebase Authentication Error: \(authError.localizedDescription)") self.showAlert(message: "Authentication failed. Please try again.") return } guard let firebaseUser = authResult?.user else { + // Remove loading animation if no user is found + self.removeLoadingAnimation(loadingAnimation) self.showAlert(message: "Login unsuccessful. Please try again.") return } let userId = firebaseUser.uid - self.fetchOrCreateUserProfile(userId: userId, email: firebaseUser.email ?? "") + UserDefaults.standard.set(userId, forKey: Constants.UserDefaultsKeys.verifiedUserDocID) + self.fetchOrCreateUserProfile(userId: userId, email: firebaseUser.email ?? "", loadingAnimation: loadingAnimation) } } } - private func fetchOrCreateUserProfile(userId: String, email: String) { + private func fetchOrCreateUserProfile(userId: String, email: String, loadingAnimation: LottieAnimationView) { let db = Firestore.firestore() db.collection("users").document(userId).getDocument { [weak self] document, error in guard let self = self else { return } if let error = error { + self.removeLoadingAnimation(loadingAnimation) print("Error fetching user profile: \(error.localizedDescription)") self.showAlert(message: "Failed to fetch user profile.") return } if let document = document, document.exists { - // Existing user profile found, navigate to main view - print("User profile fetched successfully") - let tabBarVC = TabbarViewController() - self.navigationController?.setViewControllers([tabBarVC], animated: true) + // Existing user profile found + let userData = document.data() ?? [:] + + // Check if profile is complete by verifying required fields + let requiredFields = ["firstName", "lastName", "dateOfBirth", "sex", "bloodGroup", "stage"] + let isProfileComplete = requiredFields.allSatisfy { field in + guard let value = userData[field] as? String else { return false } + return !value.isEmpty + } + + if isProfileComplete { + // Profile is complete, navigate to main view + print("User profile is complete, navigating to main view") + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.isPatientLoggedIn) + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.hasPatientCompletedProfile) + UserDefaults.standard.synchronize() + + self.removeLoadingAnimation(loadingAnimation) + let tabBarVC = TabbarViewController() + self.navigationController?.setViewControllers([tabBarVC], animated: true) + } else { + // Profile exists but is incomplete, navigate to profile completion + print("User profile is incomplete, navigating to profile completion") + self.removeLoadingAnimation(loadingAnimation) + let patientInfoVC = patientInfo() + // Set the delegate to SceneDelegate to handle navigation after profile completion + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + patientInfoVC.delegate = sceneDelegate + } + let nav = UINavigationController(rootViewController: patientInfoVC) + nav.modalPresentationStyle = .pageSheet // Change from .fullScreen to .pageSheet + self.present(nav, animated: true) + } } else { // New user, generate unique patient ID and save profile generateUniquePatientID { patientUID in guard let patientUID = patientUID else { + self.removeLoadingAnimation(loadingAnimation) print("Failed to generate unique Patient ID.") self.showAlert(message: "Unable to create profile. Please try again.") return @@ -158,18 +211,25 @@ extension PatientLoginViewController { "stage": "", "profileImageURL": "", "familyMembers": [], - "type": "patient" + "type": "patient", ] // Save the initial user profile to Firestore db.collection("users").document(userId).setData(initialData) { error in if let error = error { + self.removeLoadingAnimation(loadingAnimation) print("Error saving initial user profile: \(error.localizedDescription)") self.showAlert(message: "Failed to create profile. Please try again.") } else { print("New user profile created successfully") + self.removeLoadingAnimation(loadingAnimation) let patientInfoVC = patientInfo() + // Set the delegate to SceneDelegate to handle navigation after profile completion + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + patientInfoVC.delegate = sceneDelegate + } let nav = UINavigationController(rootViewController: patientInfoVC) + nav.modalPresentationStyle = .pageSheet // Change from .fullScreen to .pageSheet self.present(nav, animated: true) } } @@ -177,7 +237,7 @@ extension PatientLoginViewController { } } } - + private func fetchUserProfileAndNavigate(userId: String) { let db = Firestore.firestore() @@ -190,20 +250,50 @@ extension PatientLoginViewController { if let document = document, document.exists, let userData = document.data() { print("User profile fetched successfully: \(userData)") - - UserDefaultsStorageProfile.shared.saveProfile(details: userData, image: nil) { [weak self] success in - if success { - let mainVC = TabbarViewController() - self?.navigationController?.setViewControllers([mainVC], animated: true) - } else { - print("Failed to save profile") - self?.showAlert(message: "Failed to save user profile locally.") + + // Check if profile is complete by verifying required fields + let requiredFields = ["firstName", "lastName", "dateOfBirth", "sex", "bloodGroup", "stage"] + let isProfileComplete = requiredFields.allSatisfy { field in + guard let value = userData[field] as? String else { return false } + return !value.isEmpty + } + + if isProfileComplete { + // Profile is complete, navigate to main view + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.isPatientLoggedIn) + UserDefaults.standard.set(true, forKey: Constants.UserDefaultsKeys.hasPatientCompletedProfile) + UserDefaults.standard.synchronize() + + UserDefaultsStorageProfile.shared.saveProfile(details: userData, image: nil) { [weak self] success in + if success { + let mainVC = TabbarViewController() + self?.navigationController?.setViewControllers([mainVC], animated: true) + } else { + print("Failed to save profile") + self?.showAlert(message: "Failed to save user profile locally.") + } } + } else { + // Profile exists but is incomplete, navigate to profile completion + print("User profile is incomplete, navigating to profile completion") + let patientInfoVC = patientInfo() + // Set the delegate to SceneDelegate to handle navigation after profile completion + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + patientInfoVC.delegate = sceneDelegate + } + let nav = UINavigationController(rootViewController: patientInfoVC) + nav.modalPresentationStyle = .pageSheet // Change from .fullScreen to .pageSheet + self.present(nav, animated: true) } } else { print("User profile not found. Redirecting to profile setup.") let patientInfoVC = patientInfo() + // Set the delegate to SceneDelegate to handle navigation after profile completion + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + patientInfoVC.delegate = sceneDelegate + } let nav = UINavigationController(rootViewController: patientInfoVC) + nav.modalPresentationStyle = .pageSheet // Change from .fullScreen to .pageSheet self.present(nav, animated: true) } } @@ -220,7 +310,14 @@ extension PatientLoginViewController { GIDSignIn.sharedInstance.signOut() // Clear user session and local storage - UserDefaults.standard.removeObject(forKey: "hasCompletedProfile") + UserDefaults.standard + .removeObject( + forKey: Constants.UserDefaultsKeys.hasPatientCompletedProfile + ) + UserDefaults.standard + .removeObject( + forKey: Constants.UserDefaultsKeys.isPatientLoggedIn + ) UserDefaultsStorageProfile.shared.clearProfile() // Animate the swipe down effect diff --git a/recap/Managers/Resuable.swift b/recap/Managers/Resuable.swift new file mode 100755 index 0000000..05de694 --- /dev/null +++ b/recap/Managers/Resuable.swift @@ -0,0 +1,23 @@ +// +// Resuable.swift +// recap +// +// Created by user@47 on 08/03/25. +// + +import UIKit + +extension UIView { + func applyGradientBackground() { + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [ + UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0).cgColor, + UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0).cgColor + ] + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 1, y: 1) + gradientLayer.frame = self.bounds + + self.layer.insertSublayer(gradientLayer, at: 0) + } +} diff --git a/recap/Managers/patientUID.swift b/recap/Managers/patientUID.swift old mode 100644 new mode 100755 diff --git a/recap/Models/.DS_Store b/recap/Models/.DS_Store old mode 100644 new mode 100755 index 838ed96..ad9015a Binary files a/recap/Models/.DS_Store and b/recap/Models/.DS_Store differ diff --git a/recap/Models/DataModels/.DS_Store b/recap/Models/DataModels/.DS_Store old mode 100644 new mode 100755 diff --git a/recap/Models/DataModels/DemoCreds.swift b/recap/Models/DataModels/DemoCreds.swift old mode 100644 new mode 100755 diff --git a/recap/Models/DataModels/FamilyDataModel.swift b/recap/Models/DataModels/FamilyDataModel.swift old mode 100644 new mode 100755 diff --git a/recap/Models/DataModels/GamesDataModel.swift b/recap/Models/DataModels/GamesDataModel.swift old mode 100644 new mode 100755 index 051878a..0758233 --- a/recap/Models/DataModels/GamesDataModel.swift +++ b/recap/Models/DataModels/GamesDataModel.swift @@ -15,8 +15,8 @@ struct Games: Identifiable, Equatable { } let gamesDemo = [ - Games(imageName: "wordGame", name: "Word Games", description: "Boost verbal memory", screenName: "GeoSorterViewController"), - Games(imageName: "cardGame", name: "Card Games", description: "Enhance memory function", screenName: "CardGameViewController"), -// Games(imageName: "puzzleGame", name: "Puzzles", description: "Increase memory agility", screenName: "PuzzleGameViewController"), + Games(imageName: "geoGusser", name: "Geo Sorter", description: "Boost memory", screenName: "GeoSorterViewController"), +// Games(imageName: "cardGame", name: "Card Games", description: "Enhance memory function", screenName: "CardGameViewController"), + Games(imageName: "MemoryMatch", name: "Match Mania", description: "Increase memory agility", screenName: "MemoryGameViewController"), // Games(imageName: "sortGame", name: "Sort Objects", description: "Boost spatial memory", screenName: "SortObjectsViewController") ] diff --git a/recap/Models/DataModels/TimeFrame.swift b/recap/Models/DataModels/TimeFrame.swift old mode 100644 new mode 100755 diff --git a/recap/Models/DataModels/UserDetails.swift b/recap/Models/DataModels/UserDetails.swift old mode 100644 new mode 100755 diff --git a/recap/Models/DataModels/appColors.swift b/recap/Models/DataModels/appColors.swift new file mode 100644 index 0000000..9ff15ae --- /dev/null +++ b/recap/Models/DataModels/appColors.swift @@ -0,0 +1,160 @@ +// +// appColors.swift +// recap +// +// Created by admin70 on 25/03/25. +// +//import UIKit +//struct AppColors { +// // MARK: - Primary Colors +// static let primaryButtonColor = iconColor.withAlphaComponent(0.2) +// static let savedButtonColor = iconColor.withAlphaComponent(0.1) +// +// +// // MARK: - Background Gradient Colors +// static let gradientStartColor = UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0) +// static let gradientEndColor = UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0) // Soft rose-pink +// +// +// // MARK: - Card Background Colors +// // static let cardBackgroundColor = UIColor(appHex: "#EDE7F6") // Lavender Mist for card backgrounds +// static let cardBackgroundColor = UIColor(hex: "#F4F6FF") +// // MARK: - icon Colors +// static let iconColor = UIColor(hex: "#0B8494") // Teal-blue base +// +// // MARK: - Icon and Symbol Colors +// static let selectedIconColor = iconColor // Same as base for consistency +// +// +// // MARK: - Text Colors +// static let primaryButtonTextColor = iconColor +// +// // MARK: - Text Colors +// static let primaryTextColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0) // Dark charcoal for better readability +// static let secondaryTextColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) // Medium gray for secondary text +// static let inverseTextColor = UIColor.white // White text for use on darker backgrounds +// // MARK: - Button Colors +// +// static let secondaryButtonColor = iconColor.withAlphaComponent(0.1) +// static let secondaryButtonTextColor = iconColor +// +// +// // MARK: - Interactive Colors +// static let highlightColor = iconColor.withAlphaComponent(0.3) +// static let errorColor = UIColor(red: 0.90, green: 0.40, blue: 0.40, alpha: 1.0) // Keeping error red +// static let successColor = UIColor(red: 0.40, green: 0.80, blue: 0.40, alpha: 1.0) +// +// // MARK: - Gradient Layer for Background +// static func createAppBackgroundGradientLayer() -> CAGradientLayer { +// let gradientLayer = CAGradientLayer() +// gradientLayer.colors = [ +// AppColors.gradientStartColor.cgColor, +// AppColors.gradientEndColor.cgColor, +// ] +// gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) +// gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) +// return gradientLayer +// } +// +// // MARK: - Accessibility Helpers +// static func getContrastingTextColor(for backgroundColor: UIColor) -> UIColor { +// // Simple luminance calculation to determine text color +// let luminance = (0.299 * backgroundColor.components.red + +// 0.587 * backgroundColor.components.green + +// 0.114 * backgroundColor.components.blue) +// return luminance > 0.5 ? primaryTextColor : inverseTextColor +// } +//} +// +//// Extension to help with color component access +//extension UIColor { +// var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { +// var r: CGFloat = 0 +// var g: CGFloat = 0 +// var b: CGFloat = 0 +// var a: CGFloat = 0 +// getRed(&r, green: &g, blue: &b, alpha: &a) +// return (r, g, b, a) +// } +//} + +//import UIKit +// +//struct AppColors { +// // MARK: - Purple Themed Colors +// static let primaryButtonColor = UIColor(appHex: "#6A0DAD") // Royal Purple for primary buttons +// static let savedButtonColor = UIColor(appHex: "#7E57C2") // Soft Lavender Purple for saved buttons +// +// // MARK: - Background Gradient Colors (Unchanged) +// static let gradientStartColor = UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0) // Light teal +// static let gradientEndColor = UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0) // Soft rose-pink +// +// // MARK: - Card Background Colors +//// static let cardBackgroundColor = UIColor(appHex: "#EDE7F6") // Lavender Mist for card backgrounds +// static let cardBackgroundColor = UIColor(appHex: "#FFFFFF") +// +// // MARK: - Icon and Symbol Colors +// static let iconColor = UIColor(appHex: "#6A0DAD") // Royal Purple for icons +// static let selectedIconColor = UIColor(appHex: "#4A148C") // Deep Purple for selected icons +// +// // MARK: - Text Colors +// static let primaryTextColor = UIColor(appHex: "#4A148C") // Deep Purple for primary text +// static let secondaryTextColor = UIColor(appHex: "#6A0DAD") // Royal Purple for secondary text +// static let inverseTextColor = UIColor.white // White text for darker backgrounds +// +// // MARK: - Button Colors +// static let primaryButtonTextColor = UIColor.white // White text on primary buttons +// static let secondaryButtonColor = UIColor(appHex: "#D1C4E9") // Light Lavender for secondary buttons +// static let secondaryButtonTextColor = UIColor(appHex: "#6A0DAD") // Medium Purple for text on secondary buttons +// +// // MARK: - Interactive Colors +// static let highlightColor = UIColor(appHex: "#9575CD") // Lavender Purple for highlights +// static let errorColor = UIColor(appHex: "#D500F9") // Bright Purple for errors +// static let successColor = UIColor(appHex: "#9C27B0") // Strong Purple for success messages +// +// // MARK: - Gradient Layer for Background +// static func createAppBackgroundGradientLayer() -> CAGradientLayer { +// let gradientLayer = CAGradientLayer() +// gradientLayer.colors = [ +// AppColors.gradientStartColor.cgColor, +// AppColors.gradientEndColor.cgColor, +// ] +// gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) +// gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) +// return gradientLayer +// } +// +// // MARK: - Accessibility Helpers +// static func getContrastingTextColor(for backgroundColor: UIColor) -> UIColor { +// let luminance = (0.299 * backgroundColor.components.red + +// 0.587 * backgroundColor.components.green + +// 0.114 * backgroundColor.components.blue) +// return luminance > 0.5 ? primaryTextColor : inverseTextColor +// } +//} +// +//// MARK: - UIColor Hex Extension +//extension UIColor { +// convenience init(appHex hex: String) { +// var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) +// hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") +// +// var rgb: UInt64 = 0 +// Scanner(string: hexSanitized).scanHexInt64(&rgb) +// +// let red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 +// let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 +// let blue = CGFloat(rgb & 0x0000FF) / 255.0 +// +// self.init(red: red, green: green, blue: blue, alpha: 1.0) +// } +// +// var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { +// var r: CGFloat = 0 +// var g: CGFloat = 0 +// var b: CGFloat = 0 +// var a: CGFloat = 0 +// getRed(&r, green: &g, blue: &b, alpha: &a) +// return (r, g, b, a) +// } +//} diff --git a/recap/Models/DataModels/color.swift b/recap/Models/DataModels/color.swift old mode 100644 new mode 100755 index fb5d849..eb98cae --- a/recap/Models/DataModels/color.swift +++ b/recap/Models/DataModels/color.swift @@ -1,27 +1,69 @@ // -// color.swift +// Color.swift // Recap // -// Created by admin70 on 30/11/24. +// Created by khushi on 30/11/24. // - +import UIKit import SwiftUI +struct ColorTheme { + static let primary = UIColor(hex: "#6A0DAD") + static let secondary = UIColor(hex: "#B19CD9") + static let accent = UIColor(hex: "#D8BFD8") + static let background = UIColor(hex: "#F3E5F5") + static let textLight = UIColor(hex: "#4A148C") +} + +extension UIColor { + convenience init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } +} + extension Color { static let customLightRed = Color(red: 1.0, green: 0.5725, blue: 0.5412) static let customLightPurple = Color(red: 0.5373, green: 0.4745, blue: 1.0) - // Custom Blue (#4B70F5) - static let customBlue = Color(red: 0.2941, green: 0.4392, blue: 0.9608) - - // Custom Green (#74C25A) - static let customGreen = Color(red: 0.4549, green: 0.7618, blue: 0.3529) + static let customBlue = Color(red: 0.2941, green: 0.4392, blue: 0.9608) + static let customGreen = Color(red: 0.4549, green: 0.7618, blue: 0.3529) + static let customLightBlueGray = Color(red: 0.7255, green: 0.8078, blue: 0.8588) + static let customLightGray = Color(red: 0.8902, green: 0.8667, blue: 0.8667) + static let customSoftYellow = Color(red: 1.0, green: 0.9019, blue: 0.4275) + + static let primary = Color(UIColor(hex: "#6A0DAD")) + static let secondary = Color(UIColor(hex: "#B19CD9")) + static let accent = Color(UIColor(hex: "#D8BFD8")) + static let background = Color(UIColor(hex: "#F3E5F5")) + static let textLight = Color(UIColor(hex: "#4A148C")) +} + +extension UIButton { + func addGradient(colors: [CGColor], startPoint: CGPoint, endPoint: CGPoint) { + let gradientLayer = CAGradientLayer() + gradientLayer.colors = colors + gradientLayer.startPoint = startPoint + gradientLayer.endPoint = endPoint + gradientLayer.frame = bounds + gradientLayer.cornerRadius = layer.cornerRadius - // Custom Light Blue-Gray (#B9CEDB) - static let customLightBlueGray = Color(red: 0.7255, green: 0.8078, blue: 0.8588) + // Remove existing gradient layers to avoid duplicates + layer.sublayers?.removeAll(where: { $0 is CAGradientLayer }) - // Custom Light Gray (#E3DDDD) - static let customLightGray = Color(red: 0.8902, green: 0.8667, blue: 0.8667) - // Custom Soft Yellow (#FFE66D) - static let customSoftYellow = Color(red: 1.0, green: 0.9019, blue: 0.4275) + layer.insertSublayer(gradientLayer, at: 0) + } } diff --git a/recap/Models/DataModels/rapidMemory.swift b/recap/Models/DataModels/rapidMemory.swift old mode 100644 new mode 100755 diff --git a/recap/Models/Family/ArticlesDataModel.swift b/recap/Models/Family/ArticlesDataModel.swift old mode 100644 new mode 100755 index 24617b7..c4cada1 --- a/recap/Models/Family/ArticlesDataModel.swift +++ b/recap/Models/Family/ArticlesDataModel.swift @@ -2,7 +2,7 @@ // ArticlesDataModel.swift // Recap // -// Created by admin70 on 05/11/24. +// Created by khushi on 05/11/24. // import UIKit diff --git a/recap/Models/Family/ImmediateDataModel.swift b/recap/Models/Family/ImmediateDataModel.swift old mode 100644 new mode 100755 index 39f0594..2418f13 --- a/recap/Models/Family/ImmediateDataModel.swift +++ b/recap/Models/Family/ImmediateDataModel.swift @@ -1,7 +1,12 @@ // +// ImmediateDataModel.swift +// recap +// +// Created by s1834 on 26/02/25. +// import Foundation -import SwiftUI +import Firebase struct ImmediateMemoryData: Identifiable { let id = UUID() @@ -11,6 +16,159 @@ struct ImmediateMemoryData: Identifiable { var status: MemoryStatus } -let immediateMemoryData = [ - ImmediateMemoryData(date: Date(), correctAnswers: 8, incorrectAnswers: 2, status: .improving), -] +func evaluateAndStoreMemoryReport(for verifiedUserDocID: String, completion: @escaping () -> Void) { + let db = Firestore.firestore() + let userRef = db.collection("users").document(verifiedUserDocID) + let immediateMemoryRef = userRef.collection("reports").document("immediateMemory") + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let dateString = dateFormatter.string(from: Date()) + let dateRef = immediateMemoryRef.collection(dateString).document("summary") + + immediateMemoryRef.setData(["initialized": true], merge: true) { error in + if let error = error { + print("❌❌ Error creating 'immediateMemory' collection: \(error.localizedDescription)") + completion() + return + } + + let userQuestionsRef = userRef.collection("questions") + userQuestionsRef.getDocuments { snapshot, error in + if let error = error { + print("❌❌ Error fetching existing questions: \(error.localizedDescription)") + completion() + return + } + + guard let snapshot = snapshot, !snapshot.isEmpty else { + print("⚠️⚠️ No existing questions found.") + completion() + return + } + + var correctCount = 0 + var incorrectCount = 0 + + for document in snapshot.documents { + let data = document.data() + let correctAnswers = data["correctAnswers"] as? [String] ?? [] + let userAnswers = data["Answers"] as? [String] ?? [] + let matchCount = userAnswers.filter { correctAnswers.contains($0) }.count + if matchCount == userAnswers.count && matchCount > 0 { + correctCount += 1 + } else { + incorrectCount += 1 + } + } + + let reportData: [String: Any] = [ + "correctAnswers": correctCount, + "incorrectAnswers": incorrectCount, + "timestamp": Timestamp(date: Date()) + ] + + dateRef.setData(reportData, merge: true) { error in + if let error = error { + print("❌❌ Error storing memory report: \(error.localizedDescription)") + } + completion() + } + } + } +} + +func fetchImmediateMemoryData(for verifiedUserDocID: String, completion: @escaping ([ImmediateMemoryData]) -> Void) { + let db = Firestore.firestore() + + // Reference to the user's reports collection + let reportsPath = db.collection("users").document(verifiedUserDocID) + .collection("reports").document("immediateMemory") + + // First approach: Query all documents that contain memory data + // We need to use a different approach since documentReference.listCollections is not available + + // Get the base path for reports + let basePath = "users/\(verifiedUserDocID)/reports/immediateMemory" + + // Create a date formatter for consistent date handling + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let todayDateString = dateFormatter.string(from: Date()) + + // We'll use a known pattern to fetch all date collections + // This requires knowing the date range to check + + // Get dates for the last 90 days to check + var datesToCheck: [String] = [] + let calendar = Calendar.current + let today = Date() + + for dayOffset in 0..<90 { + if let date = calendar.date(byAdding: .day, value: -dayOffset, to: today) { + let dateString = dateFormatter.string(from: date) + datesToCheck.append(dateString) + } + } + + // Use dispatch group to handle multiple async calls + let group = DispatchGroup() + var allMemoryData: [ImmediateMemoryData] = [] + + // Check each potential date path + for dateString in datesToCheck { + group.enter() + + let datePath = "\(basePath)/\(dateString)/summary" + let docRef = db.document(datePath) + + docRef.getDocument { (document, error) in + defer { group.leave() } + + if let error = error { + print("❌ Error checking \(dateString): \(error.localizedDescription)") + return + } + + guard let document = document, document.exists, let data = document.data() else { + // No data for this date, which is expected for most dates + return + } + + // We found data for this date! + let timestamp = data["timestamp"] as? Timestamp + let date: Date + + if let timestamp = timestamp { + date = timestamp.dateValue() + } else if let parsedDate = dateFormatter.date(from: dateString) { + date = parsedDate + } else { + date = today + } + + let correctAnswers = data["correctAnswers"] as? Int ?? 0 + let incorrectAnswers = data["incorrectAnswers"] as? Int ?? 0 + let status: MemoryStatus = correctAnswers >= incorrectAnswers ? .improving : .declining + + let memoryData = ImmediateMemoryData( + date: date, + correctAnswers: correctAnswers, + incorrectAnswers: incorrectAnswers, + status: status + ) + + // Add to our results + allMemoryData.append(memoryData) + print("✅ Found data for \(dateString): correct=\(correctAnswers), incorrect=\(incorrectAnswers)") + } + } + + // When all checks complete, sort and return the data + group.notify(queue: .main) { + // Sort by date, most recent first + let sortedData = allMemoryData.sorted(by: { $0.date > $1.date }) + print("✅ Successfully fetched \(sortedData.count) memory data entries") + completion(sortedData) + } +} diff --git a/recap/Models/Family/RecentDataModel.swift b/recap/Models/Family/RecentDataModel.swift old mode 100644 new mode 100755 index d2b2b9e..e4d0500 --- a/recap/Models/Family/RecentDataModel.swift +++ b/recap/Models/Family/RecentDataModel.swift @@ -2,30 +2,246 @@ // RecentDataModel.swift // Recap // -// Created by admin70 on 29/10/24. +// Created by khushi on 29/10/24. // import Foundation -import SwiftUI +import FirebaseFirestore struct RecentMemoryData: Identifiable { let id = UUID() - let week: String + let day: String let correctAnswers: Int let incorrectAnswers: Int - var status: MemoryStatus { - return calculateAverageStatus(correctAnswers: correctAnswers, incorrectAnswers: incorrectAnswers).status - } } -// Sample data -let recentMemoryData = [ - RecentMemoryData(week: "Mon", correctAnswers: 18, incorrectAnswers: 2), - RecentMemoryData(week: "Tue", correctAnswers: 12, incorrectAnswers: 8), - RecentMemoryData(week: "Wed", correctAnswers: 10, incorrectAnswers: 10), - RecentMemoryData(week: "Thu", correctAnswers: 14, incorrectAnswers: 6), - RecentMemoryData(week: "Fri", correctAnswers: 11, incorrectAnswers: 9), - RecentMemoryData(week: "Sat", correctAnswers: 9, incorrectAnswers: 11), - RecentMemoryData(week: "Sun", correctAnswers: 14, incorrectAnswers: 6) -] +class RecentMemoryDataModel: ObservableObject { + @Published var recentMemoryData: [RecentMemoryData] = [] + @Published var selectedWeekIndex: Int = 0 + var availableWeeks: [(String, [Date])] = [] + + func fetchWeeks(for verifiedUserDocID: String, selectedMonth: String) { + let db = Firestore.firestore() + let recentMemoryRef = db.collection("users").document(verifiedUserDocID).collection("reports").document("recentMemory").collection(selectedMonth) + + recentMemoryRef.getDocuments { snapshot, error in + if let error = error { + print("❌❌ Error checking month existence: \(error.localizedDescription)") + return + } + + if snapshot?.documents.isEmpty == true { + print("⚠️⚠️ Month \(selectedMonth) does not exist. Generating weeks and creating month entry.") + guard let weeks = self.generateWeeks(for: selectedMonth) else { return } + self.availableWeeks = weeks + self.createMonthInFirestore(for: verifiedUserDocID, selectedMonth: selectedMonth, weeks: weeks) + } else { + guard let weeks = self.generateWeeks(for: selectedMonth) else { return } + self.availableWeeks = weeks + self.selectedWeekIndex = 0 + self.fetchRecentMemoryData(for: verifiedUserDocID, selectedMonth: selectedMonth) + } + } + } + + private func createMonthInFirestore(for verifiedUserDocID: String, selectedMonth: String, weeks: [(String, [Date])]) { + let db = Firestore.firestore() + let recentMemoryRef = db.collection("users").document(verifiedUserDocID).collection("reports").document("recentMemory").collection(selectedMonth) + let batch = db.batch() + let allDaysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + let defaultScores: [String: Int] = ["correct": 0, "incorrect": 0] + + for (weekRange, days) in weeks { + let weekDocRef = recentMemoryRef.document(weekRange) + var daysDict: [String: [String: Int]] = [:] + for day in allDaysOfWeek { + daysDict[day] = defaultScores + } + for date in days { + let dayName = getWeekdayName(from: date) + daysDict[dayName] = defaultScores + } + batch.setData(["days": daysDict], forDocument: weekDocRef, merge: true) + } + + batch.commit { error in + if let error = error { + print("❌❌ Error creating month structure: \(error.localizedDescription)") + } else { + self.selectedWeekIndex = 0 + self.fetchRecentMemoryData(for: verifiedUserDocID, selectedMonth: selectedMonth) + } + } + } + + private func generateWeeks(for month: String) -> [(String, [Date])]? { + let calendar = Calendar.current + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM" + guard let startOfMonth = formatter.date(from: month) else { return nil } + + var weeks: [(String, [Date])] = [] + var currentWeek: [Date] = [] + var weekStart: Date? + + let range = calendar.range(of: .day, in: .month, for: startOfMonth) ?? 1..<1 + for day in range { + guard let date = calendar.date(byAdding: .day, value: day - 1, to: startOfMonth) else { continue } + let weekday = calendar.component(.weekday, from: date) + + if weekday == 1 || day == range.lowerBound { + if !currentWeek.isEmpty { + let weekRange = formatWeekRange(from: currentWeek.first!, to: currentWeek.last!) + weeks.append((weekRange, currentWeek)) + } + currentWeek = [] + weekStart = date + } + currentWeek.append(date) + } + + if !currentWeek.isEmpty, let weekStart = weekStart { + let weekRange = formatWeekRange(from: weekStart, to: currentWeek.last!) + weeks.append((weekRange, currentWeek)) + weeks.append((weekRange, currentWeek)) + } + return weeks + } + + private func formatWeekRange(from startDate: Date, to endDate: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "dd" + return "\(formatter.string(from: startDate))-\(formatter.string(from: endDate))" + } + func fetchRecentMemoryData(for verifiedUserDocID: String, selectedMonth: String) { + guard !availableWeeks.isEmpty else { + print("⚠️⚠️ No available weeks found for month: \(selectedMonth)") + return + } + + let (weekRange, days) = availableWeeks[selectedWeekIndex] + let allDaysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + + var weeklyData = allDaysOfWeek.reduce(into: [String: RecentMemoryData]()) { dict, day in + dict[day] = RecentMemoryData(day: day, correctAnswers: 0, incorrectAnswers: 0) + } + + fetchAndProcessImmediateMemoryData(for: verifiedUserDocID, selectedMonth: selectedMonth, weekRange: weekRange, days: days) { updatedDaysDict, updatedWeeklyData in + self.recentMemoryData = allDaysOfWeek.map { day in + updatedWeeklyData[day] ?? RecentMemoryData(day: day, correctAnswers: 0, incorrectAnswers: 0) + } + } + } + + private func fetchAndProcessImmediateMemoryData(for verifiedUserDocID: String, selectedMonth: String, weekRange: String, days: [Date], completion: @escaping ([String: [String: Int]], [String: RecentMemoryData]) -> Void) { + let db = Firestore.firestore() + let weekDocRef = db.collection("users").document(verifiedUserDocID).collection("reports").document("recentMemory").collection(selectedMonth).document(weekRange) + + let group = DispatchGroup() + let allDaysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + + let shortToFullDayMap: [String: String] = [ + "Sun": "Sunday", "Mon": "Monday", "Tue": "Tuesday", "Wed": "Wednesday", "Thu": "Thursday", "Fri": "Friday", "Sat": "Saturday"] + + var tempDaysDict = allDaysOfWeek.reduce(into: [String: [String: Int]]()) { dict, day in + dict[day] = ["correct": 0, "incorrect": 0] + } + + var tempWeeklyData = allDaysOfWeek.reduce(into: [String: RecentMemoryData]()) { dict, day in + dict[day] = RecentMemoryData(day: day, correctAnswers: 0, incorrectAnswers: 0) + } + + group.enter() + weekDocRef.getDocument { snapshot, error in + defer { group.leave() } + if let error = error { + print("❌❌ Error fetching existing week data: \(error.localizedDescription)") + return + } + + if let existingData = snapshot?.data()?["days"] as? [String: [String: Int]] { + for (day, scores) in existingData { + let fullDay = shortToFullDayMap[day] ?? day + tempDaysDict[fullDay] = scores + } + } else { + print("⚠️⚠️ No existing data found for week range: \(weekRange)") + } + } + + for date in days { + let dateString = formatDate(date) + let fullDayName = getWeekdayName(from: date) + let shortDayName = String(fullDayName.prefix(3)) + + let immediateMemoryRef = db.collection("users").document(verifiedUserDocID).collection("reports").document("immediateMemory").collection(dateString).document("summary") + + group.enter() + immediateMemoryRef.getDocument { snapshot, error in + defer { group.leave() } + + if let error = error { + print("❌❌ Firestore fetch error on \(dateString): \(error.localizedDescription)") + return + } + + let data = snapshot?.data() + let correctAnswers = data?["correctAnswers"] as? Int ?? 0 + let incorrectAnswers = data?["incorrectAnswers"] as? Int ?? 0 + + if correctAnswers != 0 || incorrectAnswers != 0 { + tempDaysDict[fullDayName] = ["correct": correctAnswers, "incorrect": incorrectAnswers] + tempWeeklyData[fullDayName] = RecentMemoryData(day: fullDayName, correctAnswers: correctAnswers, incorrectAnswers: incorrectAnswers) + } + } + } + + group.notify(queue: .main) { + let filteredDaysDict = tempDaysDict.filter { allDaysOfWeek.contains($0.key) } + + self.recentMemoryData = allDaysOfWeek.map { day in + tempWeeklyData[day] ?? RecentMemoryData(day: day, correctAnswers: 0, incorrectAnswers: 0) + } + + self.updateFirestoreWithProcessedData(for: verifiedUserDocID, selectedMonth: selectedMonth, weekRange: weekRange, daysDict: filteredDaysDict) + } + } + + private func updateFirestoreWithProcessedData(for verifiedUserDocID: String, selectedMonth: String, weekRange: String, daysDict: [String: [String: Int]]) { + let db = Firestore.firestore() + let weekDocRef = db.collection("users").document(verifiedUserDocID).collection("reports").document("recentMemory").collection(selectedMonth).document(weekRange) + + weekDocRef.setData(["days": daysDict], merge: true) { error in + if let error = error { + print("❌ Error updating Firestore with processed data: \(error.localizedDescription)") + } + } + } + + func goToNextWeek(for verifiedUserDocID: String, selectedMonth: String) { + if selectedWeekIndex < availableWeeks.count - 1 { + selectedWeekIndex += 1 + fetchRecentMemoryData(for: verifiedUserDocID, selectedMonth: selectedMonth) + } + } + + func goToPreviousWeek(for verifiedUserDocID: String, selectedMonth: String) { + if selectedWeekIndex > 0 { + selectedWeekIndex -= 1 + fetchRecentMemoryData(for: verifiedUserDocID, selectedMonth: selectedMonth) + } + } + + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter.string(from: date) + } + + private func getWeekdayName(from date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE" + return formatter.string(from: date) + } +} diff --git a/recap/Models/Family/RemoteDataModel.swift b/recap/Models/Family/RemoteDataModel.swift old mode 100644 new mode 100755 index e208a5f..2dc6c95 --- a/recap/Models/Family/RemoteDataModel.swift +++ b/recap/Models/Family/RemoteDataModel.swift @@ -2,78 +2,197 @@ // RemoteDataModel.swift // Recap // -// Created by admin70 on 29/10/24. +// Created by khushi on 29/10/24. // +import Foundation +import FirebaseFirestore +struct RemoteMemoryData: Identifiable, Equatable { + let id = UUID() + let day: String + let correctAnswers: Int + let incorrectAnswers: Int -let mayWeeklyData: [RecentMemoryData] = [ - RecentMemoryData(week: "Mon", correctAnswers: 18, incorrectAnswers: 2), - RecentMemoryData(week: "Tue", correctAnswers: 12, incorrectAnswers: 8), - RecentMemoryData(week: "Wed", correctAnswers: 10, incorrectAnswers: 10), - RecentMemoryData(week: "Thu", correctAnswers: 14, incorrectAnswers: 6), - RecentMemoryData(week: "Fri", correctAnswers: 11, incorrectAnswers: 9), - RecentMemoryData(week: "Sat", correctAnswers: 9, incorrectAnswers: 11), - RecentMemoryData(week: "Sun", correctAnswers: 14, incorrectAnswers: 6) -] + static func == (lhs: RemoteMemoryData, rhs: RemoteMemoryData) -> Bool { + return lhs.day == rhs.day && + lhs.correctAnswers == rhs.correctAnswers && + lhs.incorrectAnswers == rhs.incorrectAnswers + } +} -let juneWeeklyData: [RecentMemoryData] = [ - RecentMemoryData(week: "Mon", correctAnswers: 20, incorrectAnswers: 0), - RecentMemoryData(week: "Tue", correctAnswers: 15, incorrectAnswers: 5), - RecentMemoryData(week: "Wed", correctAnswers: 14, incorrectAnswers: 6), - RecentMemoryData(week: "Thu", correctAnswers: 18, incorrectAnswers: 2), - RecentMemoryData(week: "Fri", correctAnswers: 16, incorrectAnswers: 4), - RecentMemoryData(week: "Sat", correctAnswers: 13, incorrectAnswers: 7), - RecentMemoryData(week: "Sun", correctAnswers: 17, incorrectAnswers: 3) +let DEFAULT_MEMORY_DATA: [String: Any] = [ + "correct": 0, + "incorrect": 0, + "averageScore": 0.0, + "createdAt": Timestamp() ] -struct MonthlyMemoryData { - let month: String - let weeklyData: [RecentMemoryData] - +func fetchRemoteMemoryData(for userID: String, month: String, completion: @escaping ([RemoteMemoryData]) -> Void) { + let db = Firestore.firestore() + let monthRef = db.collection("users").document(userID).collection("reports").document("remoteMemory").collection(month) + + ensureSubcollectionExists(for: userID, collection: "remoteMemory", month: month) { exists in + guard exists else { + completion([]) + return + } + + monthRef.getDocuments { snapshot, error in + if let error = error { + print("❌❌ Error fetching remote memory data: \(error.localizedDescription)") + completion([]) + return + } + + guard let documents = snapshot?.documents, !documents.isEmpty else { + completion([]) + return + } + + var fetchedData: [RemoteMemoryData] = [] + + for document in documents { + let data = document.data() + let day = document.documentID + + if let correct = data["correct"] as? Int, + let incorrect = data["incorrect"] as? Int { + + let entry = RemoteMemoryData(day: day, correctAnswers: correct, incorrectAnswers: incorrect) + fetchedData.append(entry) + } + } + + if fetchedData.isEmpty { + fetchedData = (1...30).map { day in + RemoteMemoryData(day: String(format: "%02d", day), correctAnswers: 0, incorrectAnswers: 0) + } + } + completion(fetchedData) + } + } +} + - var monthlyAverageScore: Double { - let totalWeeklyScores = weeklyData.reduce(0) { $0 + (Double($1.correctAnswers) / Double($1.correctAnswers + $1.incorrectAnswers)) * 100 } - return totalWeeklyScores / Double(weeklyData.count) +func fetchAndCalculateRecentMemory(for userID: String, month: String, completion: @escaping ([RemoteMemoryData]) -> Void) { + ensureSubcollectionExists(for: userID, collection: "recentMemory", month: month) { exists in + guard exists else { + completion([]) + return + } + + let db = Firestore.firestore() + let recentMemoryRef = db.collection("users").document(userID) + .collection("reports").document("recentMemory") + .collection(month) + + recentMemoryRef.getDocuments { snapshot, error in + if let error = error { + print("❌❌ Error fetching recent memory data: \(error.localizedDescription)") + completion([]) + return + } + + var weeklyData: [RemoteMemoryData] = [] + + for document in snapshot?.documents ?? [] { + let data = document.data() + + if let days = data["days"] as? [String: [String: Any]] { + for (day, values) in days { + let correct = values["correct"] as? Int ?? 0 + let incorrect = values["incorrect"] as? Int ?? 0 + + let memoryData = RemoteMemoryData(day: day, correctAnswers: correct, incorrectAnswers: incorrect) + weeklyData.append(memoryData) + } + } + } + completion(weeklyData) + } } } -let monthlyMemoryData: [MonthlyMemoryData] = [ - MonthlyMemoryData(month: "May", weeklyData: mayWeeklyData), - MonthlyMemoryData(month: "June", weeklyData: juneWeeklyData) -] +func calculateFromRecentMemory(weeklyData: [RemoteMemoryData]) -> Double { + guard !weeklyData.isEmpty else { + return 0.0 + } + let totalCorrect = weeklyData.reduce(0) { $0 + $1.correctAnswers } + let totalIncorrect = weeklyData.reduce(0) { $0 + $1.incorrectAnswers } + let totalAnswers = totalCorrect + totalIncorrect + let averageScore = totalAnswers > 0 ? (Double(totalCorrect) / Double(totalAnswers)) * 100 : 0.0 + return averageScore +} +func ensureSubcollectionExists(for userID: String, collection: String, month: String, completion: @escaping (Bool) -> Void) { + let db = Firestore.firestore() + let reportsRef = db.collection("users").document(userID).collection("reports").document(collection).collection(month) -import Foundation + reportsRef.document("summary").getDocument { snapshot, error in + if let error = error { + print("❌❌ Error checking \(collection)/\(month): \(error.localizedDescription)") + completion(false) + return + } -struct MonthlyReport: Identifiable { - let id = UUID() - let date: Date - let correctAnswers: Int + if snapshot?.exists == false { + var defaultData = DEFAULT_MEMORY_DATA + defaultData["createdAt"] = Timestamp() + + reportsRef.document("summary").setData(defaultData) { error in + if let error = error { + print("❌❌ Error: Failed to create summary: \(error.localizedDescription)") + completion(false) + } else { + completion(true) + } + } + } else { + completion(true) + } + } } -let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - return formatter -}() +func saveRemoteMemoryData(userID: String, month: String, weeklyData: [RemoteMemoryData]) { + let db = Firestore.firestore() + let monthRef = db.collection("users").document(userID).collection("reports").document("remoteMemory").collection(month) -let startDate = formatter.date(from: "2024-11-01")! -let endDate = formatter.date(from: "2024-11-30")! + let totalCorrect = weeklyData.reduce(0) { $0 + $1.correctAnswers } + let totalIncorrect = weeklyData.reduce(0) { $0 + $1.incorrectAnswers } + let totalAnswers = totalCorrect + totalIncorrect + let averageScore = totalAnswers > 0 ? (Double(totalCorrect) / Double(totalAnswers)) * 100 : 0.0 -func generateFakeMonthlyData(startDate: Date, endDate: Date) -> [MonthlyReport] { - var reports: [MonthlyReport] = [] - var currentDate = startDate - - while currentDate <= endDate { - let randomCorrectAnswers = Int.random(in: 0...20) - let report = MonthlyReport(date: currentDate, correctAnswers: randomCorrectAnswers) - reports.append(report) + let summaryRef = monthRef.document("summary") + + summaryRef.getDocument { snapshot, _ in + let existingCreatedAt = snapshot?.data()?["createdAt"] ?? Timestamp() + let summaryData: [String: Any] = ["correct": totalCorrect, "incorrect": totalIncorrect, "averageScore": averageScore, "createdAt": existingCreatedAt] + + summaryRef.setData(summaryData, merge: true) { error in + if let error = error { + print("❌❌ Error saving remote memory summary: \(error.localizedDescription)") + } + } + } + + let allDays = (1...31).map { String(format: "%02d", $0) } + let existingDataDict = Dictionary(uniqueKeysWithValues: weeklyData.map { ($0.day, $0) }) + + for day in allDays { + let dayRef = monthRef.document(day) - currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)! + dayRef.getDocument { snapshot, _ in + let existingCreatedAt = snapshot?.data()?["createdAt"] ?? Timestamp() + let memoryData = existingDataDict[day] ?? RemoteMemoryData(day: day, correctAnswers: 0, incorrectAnswers: 0) + + let dayData: [String: Any] = ["correct": memoryData.correctAnswers, "incorrect": memoryData.incorrectAnswers, "createdAt": existingCreatedAt] + + dayRef.setData(dayData, merge: true) { error in + if let error = error { + print("❌ Error saving data for \(memoryData.day): \(error.localizedDescription)") + } + } + } } - - return reports } - -let novemberReports = generateFakeMonthlyData(startDate: startDate, endDate: endDate) diff --git a/recap/Models/Family/StreakService.swift b/recap/Models/Family/StreakService.swift old mode 100644 new mode 100755 index e18e08c..541f02c --- a/recap/Models/Family/StreakService.swift +++ b/recap/Models/Family/StreakService.swift @@ -2,8 +2,9 @@ // StreakService.swift // recap // -// Created by user@47 on 06/02/25. +// Created by s1834 on 06/02/25. // + import Foundation import FirebaseFirestore @@ -14,30 +15,80 @@ struct Streak: Codable { class StreakService { private let db = Firestore.firestore() - private var verifiedUserDocID: String - + private let verifiedUserDocID: String + + var streakDataFetched: ((Int, Int, Int) -> Void)? + init(verifiedUserDocID: String) { self.verifiedUserDocID = verifiedUserDocID - print("✅ StreakService initialized with User Doc ID: \(verifiedUserDocID)") } - - func updateStreak(for day: Int, with value: Bool) { - let date = getFormattedDate(for: day) - let yearMonth = date.prefix(7) // YYYY-MM + + func ensureCurrentMonthExists(completion: @escaping () -> Void) { + let yearMonth = getCurrentYearMonth() + let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(yearMonth) - let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(String(yearMonth)) + streakDocRef.getDocument { (document, error) in + if let error = error { + print("Error checking streak document: \(error.localizedDescription)") + return + } + + if document?.exists == true { + completion() + } else { + self.createMonthStreakDocument(yearMonth: yearMonth, completion: completion) + } + } + } + + private func getCurrentYearMonth() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM" + return dateFormatter.string(from: Date()) + } + + private func createMonthStreakDocument(yearMonth: String, completion: @escaping () -> Void) { + let totalDays = getDaysInMonth(yearMonth: yearMonth) + let streakData = (1...totalDays).reduce(into: [String: Bool]()) { dict, day in + dict["\(yearMonth)-\(String(format: "%02d", day))"] = false + } - streakDocRef.updateData([ - date: value - ]) { error in + db.collection("users").document(verifiedUserDocID).collection("streaks").document(yearMonth).setData(streakData) { error in if let error = error { - print("Error updating streak: \(error.localizedDescription)") + print("❌❌ Error creating monthly streak document: \(error.localizedDescription)") } else { - print("Streak updated successfully") + completion() } } } - + + private func getDaysInMonth(yearMonth: String) -> Int { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM" + guard let date = dateFormatter.date(from: yearMonth) else { return 30 } + + let calendar = Calendar.current + let range = calendar.range(of: .day, in: .month, for: date) + return range?.count ?? 30 + } + + func updateStreak(for day: Int, with value: Bool) { + ensureCurrentMonthExists { + let date = self.getFormattedDate(for: day) + let yearMonth = date.prefix(7) + + let streakDocRef = self.db.collection("users").document(self.verifiedUserDocID).collection("streaks").document(String(yearMonth)) + + streakDocRef.updateData([ + date: value + ]) { error in + if let error = error { + print("❌❌ Error updating streak: \(error.localizedDescription)") + } + } + } + } + private func getFormattedDate(for day: Int) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" @@ -46,99 +97,144 @@ class StreakService { let date = calendar.date(byAdding: .day, value: day, to: today)! return formatter.string(from: date) } - + + func updateStreakForToday(with value: Bool) { + ensureCurrentMonthExists { + let today = self.getFormattedDateForToday() + let yearMonth = today.prefix(7) + + let streakDocRef = self.db.collection("users").document(self.verifiedUserDocID).collection("streaks").document(String(yearMonth)) + + streakDocRef.setData([today: value], merge: true) { error in + if let error = error { + print("❌❌ Error updating streak for today: \(error.localizedDescription)") + } + } + } + } + + private func getFormattedDateForToday() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let today = Date() + return formatter.string(from: today) + } + func getStreaksForUser(yearMonth: String, completion: @escaping (Streak?) -> Void) { let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(yearMonth) streakDocRef.getDocument { (document, error) in if let error = error { - print("❌ Error getting streaks: \(error.localizedDescription)") + print("❌❌ Error getting streaks: \(error.localizedDescription)") completion(nil) } else if let document = document, document.exists { - print("✅ Successfully fetched streaks for \(yearMonth)") - if let streakData = document.data() as? [String: Bool] { let streak = Streak(streakDates: streakData) completion(streak) } else { - print("⚠️ No valid streak data found.") + print("⚠️⚠️ No valid streak data found.") completion(nil) } } else { - print("⚠️ No streak document found for \(yearMonth)") + print("⚠️⚠️ No streak document found for \(yearMonth)") completion(nil) } } } - func updateStreak(yearMonth: String, date: String, isStreakCompleted: Bool) { - let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(yearMonth) + func fetchAndUpdateStreakStats() { + ensureStreaksCoreExists { + let coreRef = self.db.collection("users").document(self.verifiedUserDocID).collection("streaksCore").document("streakData") - streakDocRef.updateData([date: isStreakCompleted]) { error in - if let error = error { - print("Error updating streak: \(error.localizedDescription)") - } else { - print("Streak updated successfully.") + coreRef.getDocument { document, error in + if let error = error { + print("Error fetching streak data: \(error.localizedDescription)") + return + } + + if let document = document, let data = document.data() { + let maxStreak = data["maxStreak"] as? Int ?? 0 + let currentStreak = data["currentStreak"] as? Int ?? 0 + let activeDays = data["activeDays"] as? Int ?? 0 + let answeredToday = data["answeredToday"] as? Bool ?? false + let lastAnsweredDate = (data["lastAnsweredDate"] as? Timestamp)?.dateValue() ?? Date.distantPast + let totalQuestionsAnswered = data["totalQuestionsAnswered"] as? Int ?? 0 + let correctAnswers = data["correctAnswers"] as? Int ?? 0 + let longestBreak = data["longestBreak"] as? Int ?? 0 + + self.streakDataFetched?(maxStreak, currentStreak, activeDays) + + self.calculateStreakStats(maxStreak: maxStreak, answeredToday: answeredToday, currentStreak: currentStreak, activeDays: activeDays, lastAnsweredDate: lastAnsweredDate, totalQuestionsAnswered: totalQuestionsAnswered, correctAnswers: correctAnswers, longestBreak: longestBreak) + } } } } - func ensureStreakDocumentExists(yearMonth: String) { - let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(yearMonth) + private func ensureStreaksCoreExists(completion: @escaping () -> Void) { + let coreRef = db.collection("users").document(verifiedUserDocID).collection("streaksCore").document("streakData") - streakDocRef.getDocument { (document, error) in + coreRef.getDocument { document, error in if let error = error { - print("Error checking streak document: \(error.localizedDescription)") + print("Error checking streaksCore existence: \(error.localizedDescription)") return } - if document == nil { - streakDocRef.setData([:]) { error in + if document?.exists == true { + completion() + } else { + coreRef.setData(["initialized": true], merge: true) { error in if let error = error { - print("Error creating streak document: \(error.localizedDescription)") + print("Error creating streaksCore: \(error.localizedDescription)") } else { - print("Streak document created successfully.") + print("streaksCore initialized") + completion() } } } } } - - func createOrUpdateMonthlyStreak(yearMonth: String, streak: Streak) { - let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(yearMonth) - - streakDocRef.setData(streak.streakDates) { error in - if let error = error { - print("Error updating monthly streak: \(error.localizedDescription)") - } else { - print("Monthly streak successfully updated.") - } - } - } - - func updateStreaksForUser(streaks: [String: Bool], completion: @escaping (Bool) -> Void) { - let streakDocRef = db.collection("users").document(verifiedUserDocID).collection("streaks").document(getCurrentYearMonth()) + + func calculateStreakStats(maxStreak: Int, answeredToday: Bool, currentStreak: Int, activeDays: Int, lastAnsweredDate: Date, totalQuestionsAnswered: Int, correctAnswers: Int, longestBreak: Int) { + let calendar = Calendar.current + let today = Date() + + var newCurrentStreak = currentStreak + var newMaxStreak = maxStreak + var newActiveDays = activeDays + var newAnsweredToday = answeredToday + var newLongestBreak = longestBreak + var newTotalQuestionsAnswered = totalQuestionsAnswered + 1 + var newCorrectAnswers = correctAnswers - guard !streaks.isEmpty else { - print("Error: Streaks data is empty.") - completion(false) - return + let daysSinceLastAnswer = calendar.dateComponents([.day], from: lastAnsweredDate, to: today).day ?? 0 + + if calendar.isDateInToday(lastAnsweredDate) { + newAnsweredToday = true + } else if daysSinceLastAnswer == 1 { + newCurrentStreak += 1 + newActiveDays += 1 + newAnsweredToday = true + } else { + newCurrentStreak = currentStreak + newAnsweredToday = false + newLongestBreak = max(newLongestBreak, daysSinceLastAnswer) } - streakDocRef.setData(streaks) { error in - if let error = error { - print("Error uploading streaks: \(error)") - completion(false) - } else { - completion(true) + newMaxStreak = max(newMaxStreak, newCurrentStreak) + + updateStreakData(maxStreak: newMaxStreak, answeredToday: newAnsweredToday, currentStreak: newCurrentStreak, activeDays: newActiveDays, totalQuestionsAnswered: newTotalQuestionsAnswered, correctAnswers: newCorrectAnswers, longestBreak: newLongestBreak) + } + + + func updateStreakData(maxStreak: Int, answeredToday: Bool, currentStreak: Int, activeDays: Int, totalQuestionsAnswered: Int, correctAnswers: Int, longestBreak: Int) { + ensureStreaksCoreExists { + let coreRef = self.db.collection("users").document(self.verifiedUserDocID).collection("streaksCore").document("streakData") + + coreRef.setData(["maxStreak": maxStreak, "answeredToday": answeredToday, "currentStreak": currentStreak, "activeDays": activeDays, "totalQuestionsAnswered": totalQuestionsAnswered, "correctAnswers": correctAnswers, "longestBreak": longestBreak, "lastAnsweredDate": Timestamp(date: Date())], merge: true) { error in + if let error = error { + print("Error updating streak data: \(error.localizedDescription)") + } } } } - - private func getCurrentYearMonth() -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM" - return dateFormatter.string(from: Date()) -// return "2025-02" - } } diff --git a/recap/Models/Family/memoryStatus.swift b/recap/Models/Family/memoryStatus.swift old mode 100644 new mode 100755 index ebf12de..5eba4aa --- a/recap/Models/Family/memoryStatus.swift +++ b/recap/Models/Family/memoryStatus.swift @@ -2,7 +2,7 @@ // status.swift // Recap // -// Created by admin70 on 30/11/24. +// Created by khushi on 30/11/24. // @@ -15,8 +15,8 @@ enum MemoryStatus: String { } func calculateAverageStatus(correctAnswers: Int, incorrectAnswers: Int) -> (average: Double, status: MemoryStatus) { let totalAnswers = correctAnswers + incorrectAnswers - guard totalAnswers > 0 else { return (0.0, .declining) } // no division by 0 - + guard totalAnswers > 0 else { return (0.0, .declining) } + let average = (Double(correctAnswers) / Double(totalAnswers)) * 100 let status: MemoryStatus diff --git a/recap/Models/Family/questionsSchema.swift b/recap/Models/Family/questionsSchema.swift old mode 100644 new mode 100755 index 7919af9..a1c4e92 --- a/recap/Models/Family/questionsSchema.swift +++ b/recap/Models/Family/questionsSchema.swift @@ -2,21 +2,19 @@ // questionsSchema.swift // recap // -// Created by user@47 on 06/02/25. +// Created by s1834 on 06/02/25. // import UIKit import Foundation import FirebaseFirestore -// MARK: - Question Categories enum QuestionCategory: String, Codable { case immediateMemory case recentMemory case remoteMemory } -// MARK: - Question Subcategories (Expanded) enum QuestionSubcategory: String, Codable { case dailyRoutine case general @@ -26,17 +24,16 @@ enum QuestionSubcategory: String, Codable { case hobbies case musicMovies case familyAdded - case nutrition // New - case fitnessExercise // New - case socialInteraction // New - case moodEmotion // New - case personalHygiene // New - case medicationManagement // New - case cognitiveExercises // New - case outdoorActivities // New + case nutrition + case fitnessExercise + case socialInteraction + case moodEmotion + case personalHygiene + case medicationManagement + case cognitiveExercises + case outdoorActivities } -// MARK: - Question Types (Expanded) enum QuestionType: String, Codable { case multipleChoice case singleCorrect @@ -54,7 +51,6 @@ enum QuestionType: String, Codable { case sequence } -// MARK: - Question Model struct Question: Identifiable, Codable { @DocumentID var id: String? var text: String @@ -63,7 +59,7 @@ struct Question: Identifiable, Codable { var tag: String? var answerOptions: [String] var answers: [String] - var correctAnswers: [String]? // Supports multiple correct answers + var correctAnswers: [String]? var image: String? var isAnswered: Bool var askInterval: TimeInterval @@ -78,8 +74,8 @@ struct Question: Identifiable, Codable { var isActive: Bool var lastAnsweredCorrectly: Date? var hint: String? - var confidence: Int? // Tracks confidence level (1-10) - var hardness: Int // Difficulty level (1-5) + var confidence: Int? + var hardness: Int var questionType: QuestionType init( @@ -128,138 +124,3 @@ struct Question: Identifiable, Codable { self.questionType = questionType } } - -// MARK: - Daily Questions View Controller -class DailyQuestionsViewController: UIViewController { - - var activityIndicator: UIActivityIndicatorView? - - override func viewDidLoad() { - super.viewDidLoad() - setupActivityIndicator() - addQuestionsToFirestore() - } - - func setupActivityIndicator() { - activityIndicator = UIActivityIndicatorView(style: .large) - activityIndicator?.center = view.center - activityIndicator?.hidesWhenStopped = true - view.addSubview(activityIndicator!) - activityIndicator?.startAnimating() - } - - func addQuestionsToFirestore() { - let db = Firestore.firestore() - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm" - - let questions: [Question] = [ - Question( - text: "Did you take your morning medication today?", - category: .immediateMemory, - subcategory: .medicationManagement, - tag: "Medication", - answerOptions: ["Yes", "No", "Not Sure"], - answers: [], - correctAnswers: [], - image: nil, - isAnswered: false, - askInterval: 14400, - timeFrame: TimeFrame(from: "06:00", to: "11:59"), // Only time, stored as Timestamp - priority: 1, - audio: nil, - isActive: true, - hint: "Check your medication box!", - confidence: nil, - hardness: 2, - questionType: .yesNo - ), - - Question( - text: "What did you eat for breakfast this morning?", - category: .immediateMemory, - subcategory: .nutrition, - tag: "Eating", - answerOptions: ["Cereal", "Eggs", "Toast", "Fruits", "Other"], // Added meaningful options - answers: [], - correctAnswers: [], - image: nil, - isAnswered: false, - askInterval: 21600, - timeFrame: TimeFrame(from: "06:00", to: "11:00"), // Only time, stored as Timestamp - priority: 1, - audio: nil, - isActive: true, - hint: "Think back to your morning meal!", - confidence: nil, - hardness: 3, - questionType: .multipleChoice // Changed from `openEnded` to `multipleChoice` to match the options - ) - ] - - - - - for question in questions { - var questionData: [String: Any] = [ - "text": question.text, - "category": question.category.rawValue, - "subcategory": question.subcategory.rawValue, - "tag": question.tag ?? NSNull(), - "answerOptions": question.answerOptions, - "answers": question.answers, - "correctAnswers": question.correctAnswers ?? NSNull(), - "image": question.image ?? NSNull(), - "isAnswered": question.isAnswered, - "askInterval": question.askInterval, - "lastAsked": question.lastAsked ?? NSNull(), - "timesAsked": question.timesAsked, - "timesAnsweredCorrectly": question.timesAnsweredCorrectly, - "timeFrame": [ - "from": dateFormatter.string(from: question.timeFrame.from.dateValue()), - "to": dateFormatter.string(from: question.timeFrame.to.dateValue()) - ], - "createdAt": question.createdAt, - "addedAt": NSNull(), - "priority": question.priority, - "audio": question.audio ?? NSNull(), - "isActive": question.isActive, - "lastAnsweredCorrectly": question.lastAnsweredCorrectly ?? NSNull(), - "hint": question.hint ?? NSNull(), - "confidence": question.confidence ?? NSNull(), - "hardness": question.hardness, - "questionType": question.questionType.rawValue - ] - - db.collection("Questions").addDocument(data: questionData) { error in - if let error = error { - print("Error adding question: \(error.localizedDescription)") - } else { - print("Question successfully added to Firestore.") - } - } - } - - DispatchQueue.main.async { - self.activityIndicator?.stopAnimating() - } - } -} - -// MARK: - Utility Functions -func shouldAskQuestion(question: Question, currentDate: Date) -> Bool { - guard let lastAsked = question.lastAsked else { - return true - } - return currentDate.timeIntervalSince(lastAsked) >= question.askInterval -} - -func updateAskInterval(for question: inout Question, wasAnsweredCorrectly: Bool) { - if wasAnsweredCorrectly { - question.askInterval *= 1.5 - question.timesAnsweredCorrectly += 1 - } else { - question.askInterval = max(3600, question.askInterval / 2) - } - question.timesAsked += 1 -} diff --git a/recap/Resources/.DS_Store b/recap/Resources/.DS_Store new file mode 100755 index 0000000..86a4d4e Binary files /dev/null and b/recap/Resources/.DS_Store differ diff --git a/recap/Resources/Animations/SuccessAnimation.json b/recap/Resources/Animations/SuccessAnimation.json new file mode 100755 index 0000000..ca2c11f --- /dev/null +++ b/recap/Resources/Animations/SuccessAnimation.json @@ -0,0 +1 @@ +{"v":"5.9.6","fr":60,"ip":0,"op":151,"w":1080,"h":1080,"nm":"25","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[100]},{"t":70,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[19,-17,0],"ix":1,"l":2},"s":{"a":0,"k":[28,28,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-145.251,0],[0,-145.251],[145.251,0],[0,145.251]],"o":[[145.251,0],[0,145.251],[-145.251,0],[0,-145.251]],"v":[[0,-263],[263,0],[0,263],[-263,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9961,0.9608,0.7412,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":526,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[19,-17],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[40.429]},{"t":70,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":424,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[543,541,0],"to":[0,-4.667,0],"ti":[0,4.667,0]},{"t":88,"s":[543,513,0]}],"ix":2,"l":2},"a":{"a":0,"k":[30.25,-10,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[100.5,20.5],[-40,20.5],[-40,-40.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9961,0.9608,0.7412,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":28,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":68,"s":[100]},{"t":88,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":52,"op":472,"st":52,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Circle 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[93,57,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[350,350],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.7056,0.3819,0.1344,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[93,57],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":418,"st":-2,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Elements - 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540.186,531.584,0],"ix":2,"l":2},"a":{"a":0,"k":[540.186,531.584,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540.491,539.286],"to":[62.083,-23.831],"ti":[-62.083,23.831]},{"t":110,"s":[912.989,396.297]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-21,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 116","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[539.474,539.474],"to":[-47.023,-47.023],"ti":[47.023,47.023]},{"t":110,"s":[257.339,257.339]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-487,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 115","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540.508,539.338],"to":[28.104,-60.269],"ti":[-28.104,60.269]},{"t":110,"s":[709.132,177.722]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-57,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 114","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-22.744,62.49],"ti":[22.744,-62.49]},{"t":109,"s":[404.01,914.19]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":200,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 113","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-28.104,-60.269],"ti":[28.104,60.269]},{"t":109,"s":[371.852,177.635]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-25,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 112","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-60.269,-28.104],"ti":[60.269,28.104]},{"t":109,"s":[178.86,370.628]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-425,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 111","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[28.104,60.269],"ti":[-28.104,-60.269]},{"t":109,"s":[709.101,900.869]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-205,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 110","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70,70],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-60.269,28.104],"ti":[60.269,-28.104]},{"t":109,"s":[178.86,707.877]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-115,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 9","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540,540],"to":[0,66.5],"ti":[0,-66.5]},{"t":110,"s":[540,939]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 1","np":1,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540,540],"to":[-47.023,47.023],"ti":[47.023,-47.023]},{"t":110,"s":[257.864,822.136]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-217,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 2","np":1,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540,540],"to":[50.942,42.745],"ti":[-50.942,-42.745]},{"t":110,"s":[845.652,796.472]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":48,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 3","np":1,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.372,539.846],"to":[-66.5,0],"ti":[66.5,0]},{"t":109,"s":[141.372,539.846]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":630,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 4","np":1,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540,540],"to":[66.5,0],"ti":[-66.5,0]},{"t":109,"s":[939,540]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 5","np":1,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540,540],"to":[50.942,-42.745],"ti":[-50.942,42.745]},{"t":109,"s":[845.652,283.528]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-310,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 6","np":1,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.18,538.981],"to":[62.49,22.744],"ti":[-62.49,-22.744]},{"t":109,"s":[915.117,675.447]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-250,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 7","np":1,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540,540],"to":[0,-66.5],"ti":[0,66.5]},{"t":109,"s":[540,141]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70,70],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 8","np":1,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false}],"ip":74,"op":429,"st":69,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Elements - 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":159,"ix":10},"p":{"a":0,"k":[540.186,531.584,0],"ix":2,"l":2},"a":{"a":0,"k":[540.186,531.584,0],"ix":1,"l":2},"s":{"a":0,"k":[70,70,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540.491,539.286],"to":[62.083,-23.831],"ti":[-62.083,23.831]},{"t":117,"s":[912.989,396.297]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-21,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 116","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[539.474,539.474],"to":[-47.023,-47.023],"ti":[47.023,47.023]},{"t":117,"s":[257.339,257.339]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-487,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 115","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540.508,539.338],"to":[28.104,-60.269],"ti":[-28.104,60.269]},{"t":117,"s":[709.132,177.722]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-57,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 114","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-22.744,62.49],"ti":[22.744,-62.49]},{"t":116,"s":[404.01,914.19]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":200,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 113","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-28.104,-60.269],"ti":[28.104,60.269]},{"t":116,"s":[371.852,177.635]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-25,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 112","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-60.269,-28.104],"ti":[60.269,28.104]},{"t":116,"s":[178.86,370.628]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-425,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 111","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[28.104,60.269],"ti":[-28.104,-60.269]},{"t":116,"s":[709.101,900.869]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-205,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 110","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-60.269,28.104],"ti":[60.269,-28.104]},{"t":116,"s":[178.86,707.877]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-115,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 9","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540,540],"to":[0,66.5],"ti":[0,-66.5]},{"t":117,"s":[540,939]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 1","np":1,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540,540],"to":[-47.023,47.023],"ti":[47.023,-47.023]},{"t":117,"s":[257.864,822.136]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-217,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 2","np":1,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540,540],"to":[50.942,42.745],"ti":[-50.942,-42.745]},{"t":117,"s":[845.652,796.472]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":48,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 3","np":1,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.372,539.846],"to":[-66.5,0],"ti":[66.5,0]},{"t":116,"s":[141.372,539.846]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":630,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 4","np":1,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540,540],"to":[66.5,0],"ti":[-66.5,0]},{"t":116,"s":[939,540]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 5","np":1,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540,540],"to":[50.942,-42.745],"ti":[-50.942,42.745]},{"t":116,"s":[845.652,283.528]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-310,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 6","np":1,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.18,538.981],"to":[62.49,22.744],"ti":[-62.49,-22.744]},{"t":116,"s":[915.117,675.447]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-250,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 7","np":1,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540,540],"to":[0,-66.5],"ti":[0,66.5]},{"t":116,"s":[540,141]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 8","np":1,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false}],"ip":76,"op":436,"st":76,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/.DS_Store b/recap/Resources/Assets.xcassets/.DS_Store new file mode 100755 index 0000000..5008ddf Binary files /dev/null and b/recap/Resources/Assets.xcassets/.DS_Store differ diff --git a/recap/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/recap/Resources/Assets.xcassets/AccentColor.colorset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/recap/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/AppIcon.appiconset/Logo.png b/recap/Resources/Assets.xcassets/AppIcon.appiconset/Logo.png index 7b040ec..e83c731 100644 Binary files a/recap/Resources/Assets.xcassets/AppIcon.appiconset/Logo.png and b/recap/Resources/Assets.xcassets/AppIcon.appiconset/Logo.png differ diff --git a/recap/Resources/Assets.xcassets/AppIcon.appiconset/LogoDark.png b/recap/Resources/Assets.xcassets/AppIcon.appiconset/LogoDark.png index 2a7c39a..e7feb64 100644 Binary files a/recap/Resources/Assets.xcassets/AppIcon.appiconset/LogoDark.png and b/recap/Resources/Assets.xcassets/AppIcon.appiconset/LogoDark.png differ diff --git a/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Big Shoes Torso.png b/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Big Shoes Torso.png deleted file mode 100644 index 1c4a805..0000000 Binary files a/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Big Shoes Torso.png and /dev/null differ diff --git a/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Contents.json b/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Contents.json old mode 100644 new mode 100755 index b1b3e0f..bf62abf --- a/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Contents.json +++ b/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Big Shoes Torso.png", + "filename" : "read.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/read.png b/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/read.png new file mode 100644 index 0000000..7418866 Binary files /dev/null and b/recap/Resources/Assets.xcassets/BigShoesTorso.imageset/read.png differ diff --git a/recap/Resources/Assets.xcassets/Contents.json b/recap/Resources/Assets.xcassets/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/MemoryMatch.imageset/Contents.json b/recap/Resources/Assets.xcassets/MemoryMatch.imageset/Contents.json new file mode 100755 index 0000000..5fe848b --- /dev/null +++ b/recap/Resources/Assets.xcassets/MemoryMatch.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Memory Matching Game PNG.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/MemoryMatch.imageset/Memory Matching Game PNG.png b/recap/Resources/Assets.xcassets/MemoryMatch.imageset/Memory Matching Game PNG.png new file mode 100755 index 0000000..29c03d3 Binary files /dev/null and b/recap/Resources/Assets.xcassets/MemoryMatch.imageset/Memory Matching Game PNG.png differ diff --git a/recap/Resources/Assets.xcassets/SuccessAnimation.dataset/Contents.json b/recap/Resources/Assets.xcassets/SuccessAnimation.dataset/Contents.json new file mode 100755 index 0000000..5496adc --- /dev/null +++ b/recap/Resources/Assets.xcassets/SuccessAnimation.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "SuccessAnimation.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/SuccessAnimation.dataset/SuccessAnimation.json b/recap/Resources/Assets.xcassets/SuccessAnimation.dataset/SuccessAnimation.json new file mode 100755 index 0000000..ca2c11f --- /dev/null +++ b/recap/Resources/Assets.xcassets/SuccessAnimation.dataset/SuccessAnimation.json @@ -0,0 +1 @@ +{"v":"5.9.6","fr":60,"ip":0,"op":151,"w":1080,"h":1080,"nm":"25","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[100]},{"t":70,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[19,-17,0],"ix":1,"l":2},"s":{"a":0,"k":[28,28,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-145.251,0],[0,-145.251],[145.251,0],[0,145.251]],"o":[[145.251,0],[0,145.251],[-145.251,0],[0,-145.251]],"v":[[0,-263],[263,0],[0,263],[-263,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9961,0.9608,0.7412,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":526,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[19,-17],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[40.429]},{"t":70,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":424,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[543,541,0],"to":[0,-4.667,0],"ti":[0,4.667,0]},{"t":88,"s":[543,513,0]}],"ix":2,"l":2},"a":{"a":0,"k":[30.25,-10,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[100.5,20.5],[-40,20.5],[-40,-40.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9961,0.9608,0.7412,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":28,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":68,"s":[100]},{"t":88,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":52,"op":472,"st":52,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Circle 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[93,57,0],"ix":1,"l":2},"s":{"a":0,"k":[110,110,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[350,350],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.7056,0.3819,0.1344,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[93,57],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":418,"st":-2,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Elements - 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540.186,531.584,0],"ix":2,"l":2},"a":{"a":0,"k":[540.186,531.584,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540.491,539.286],"to":[62.083,-23.831],"ti":[-62.083,23.831]},{"t":110,"s":[912.989,396.297]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-21,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 116","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[539.474,539.474],"to":[-47.023,-47.023],"ti":[47.023,47.023]},{"t":110,"s":[257.339,257.339]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-487,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 115","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540.508,539.338],"to":[28.104,-60.269],"ti":[-28.104,60.269]},{"t":110,"s":[709.132,177.722]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-57,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 114","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-22.744,62.49],"ti":[22.744,-62.49]},{"t":109,"s":[404.01,914.19]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":200,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 113","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-28.104,-60.269],"ti":[28.104,60.269]},{"t":109,"s":[371.852,177.635]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-25,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 112","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-60.269,-28.104],"ti":[60.269,28.104]},{"t":109,"s":[178.86,370.628]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-425,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 111","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[28.104,60.269],"ti":[-28.104,-60.269]},{"t":109,"s":[709.101,900.869]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-205,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 110","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70,70],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.476,539.252],"to":[-60.269,28.104],"ti":[60.269,-28.104]},{"t":109,"s":[178.86,707.877]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-115,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 9","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540,540],"to":[0,66.5],"ti":[0,-66.5]},{"t":110,"s":[540,939]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 1","np":1,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540,540],"to":[-47.023,47.023],"ti":[47.023,-47.023]},{"t":110,"s":[257.864,822.136]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-217,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 2","np":1,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[540,540],"to":[50.942,42.745],"ti":[-50.942,-42.745]},{"t":110,"s":[845.652,796.472]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":48,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":70,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":71,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":105,"s":[100]},{"t":115,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 3","np":1,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.372,539.846],"to":[-66.5,0],"ti":[66.5,0]},{"t":109,"s":[141.372,539.846]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":630,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 4","np":1,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540,540],"to":[66.5,0],"ti":[-66.5,0]},{"t":109,"s":[939,540]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 5","np":1,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540,540],"to":[50.942,-42.745],"ti":[-50.942,42.745]},{"t":109,"s":[845.652,283.528]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-310,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 6","np":1,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540.18,538.981],"to":[62.49,22.744],"ti":[-62.49,-22.744]},{"t":109,"s":[915.117,675.447]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-250,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 7","np":1,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":69,"s":[540,540],"to":[0,-66.5],"ti":[0,66.5]},{"t":109,"s":[540,141]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[70,70],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":69,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":70,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":104,"s":[100]},{"t":114,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 8","np":1,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false}],"ip":74,"op":429,"st":69,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Elements - 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":159,"ix":10},"p":{"a":0,"k":[540.186,531.584,0],"ix":2,"l":2},"a":{"a":0,"k":[540.186,531.584,0],"ix":1,"l":2},"s":{"a":0,"k":[70,70,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540.491,539.286],"to":[62.083,-23.831],"ti":[-62.083,23.831]},{"t":117,"s":[912.989,396.297]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-21,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 116","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[539.474,539.474],"to":[-47.023,-47.023],"ti":[47.023,47.023]},{"t":117,"s":[257.339,257.339]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-487,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 115","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540.508,539.338],"to":[28.104,-60.269],"ti":[-28.104,60.269]},{"t":117,"s":[709.132,177.722]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-57,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 114","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-22.744,62.49],"ti":[22.744,-62.49]},{"t":116,"s":[404.01,914.19]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":200,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 113","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-28.104,-60.269],"ti":[28.104,60.269]},{"t":116,"s":[371.852,177.635]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-25,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 112","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-60.269,-28.104],"ti":[60.269,28.104]},{"t":116,"s":[178.86,370.628]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-425,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 111","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[28.104,60.269],"ti":[-28.104,-60.269]},{"t":116,"s":[709.101,900.869]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-205,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 110","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.476,539.252],"to":[-60.269,28.104],"ti":[60.269,-28.104]},{"t":116,"s":[178.86,707.877]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-115,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 9","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-35],[0,35]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.8667,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540,540],"to":[0,66.5],"ti":[0,-66.5]},{"t":117,"s":[540,939]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 1","np":1,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540,540],"to":[-47.023,47.023],"ti":[47.023,-47.023]},{"t":117,"s":[257.864,822.136]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-217,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 2","np":1,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.8667,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":77,"s":[540,540],"to":[50.942,42.745],"ti":[-50.942,-42.745]},{"t":117,"s":[845.652,796.472]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":48,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":77,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":78,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":112,"s":[100]},{"t":122,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 3","np":1,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.372,539.846],"to":[-66.5,0],"ti":[66.5,0]},{"t":116,"s":[141.372,539.846]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":630,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 4","np":1,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.122,-1.172],[0,0],[0.279,-0.516],[0,0],[1.172,2.122],[0,0],[0.516,0.279],[0,0],[-2.122,1.172],[0,0],[-0.279,0.516],[0,0],[-1.172,-2.122],[0,0],[-0.516,-0.279],[0,0]],"o":[[0,0],[-0.513,0.283],[0,0],[-1.154,2.131],[0,0],[-0.283,-0.513],[0,0],[-2.131,-1.154],[0,0],[0.513,-0.283],[0,0],[1.154,-2.131],[0,0],[0.283,0.513],[0,0],[2.131,1.154]],"v":[[31.518,2.561],[13.717,12.392],[12.503,13.615],[2.819,31.496],[-2.561,31.518],[-12.392,13.717],[-13.615,12.503],[-31.496,2.819],[-31.518,-2.561],[-13.717,-12.392],[-12.503,-13.615],[-2.819,-31.496],[2.561,-31.518],[12.392,-13.717],[13.615,-12.503],[31.496,-2.819]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4431,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540,540],"to":[66.5,0],"ti":[-66.5,0]},{"t":116,"s":[939,540]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 5","np":1,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6004,0.5416,0.1596,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":34,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540,540],"to":[50.942,-42.745],"ti":[-50.942,42.745]},{"t":116,"s":[845.652,283.528]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-310,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 6","np":1,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.44],[15.44,0],[0,15.44]],"o":[[0,15.44],[-15.44,0],[0,-15.44]],"v":[[27.957,-13.979],[0,13.979],[-27.957,-13.979]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":39,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540.18,538.981],"to":[62.49,22.744],"ti":[-62.49,-22.744]},{"t":116,"s":[915.117,675.447]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-250,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 7","np":1,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.4333,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[540,540],"to":[0,-66.5],"ti":[0,66.5]},{"t":116,"s":[540,141]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":76,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":77,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[100]},{"t":121,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Elements - 8","np":1,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false}],"ip":76,"op":436,"st":76,"ct":1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/google.imageset/Contents.json b/recap/Resources/Assets.xcassets/Vecotr.imageset/Contents.json similarity index 89% rename from recap/Resources/Assets.xcassets/google.imageset/Contents.json rename to recap/Resources/Assets.xcassets/Vecotr.imageset/Contents.json index 17f6fd9..9c2c517 100644 --- a/recap/Resources/Assets.xcassets/google.imageset/Contents.json +++ b/recap/Resources/Assets.xcassets/Vecotr.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Image.png", + "filename" : "Vecotr.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/recap/Resources/Assets.xcassets/Vecotr.imageset/Vecotr.svg b/recap/Resources/Assets.xcassets/Vecotr.imageset/Vecotr.svg new file mode 100644 index 0000000..9f290ec --- /dev/null +++ b/recap/Resources/Assets.xcassets/Vecotr.imageset/Vecotr.svg @@ -0,0 +1,3 @@ + + + diff --git a/recap/Resources/Assets.xcassets/apple.imageset/Contents.json b/recap/Resources/Assets.xcassets/apple.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/apple.imageset/Image.png b/recap/Resources/Assets.xcassets/apple.imageset/Image.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/appleLogo.imageset/Contents.json b/recap/Resources/Assets.xcassets/appleLogo.imageset/Contents.json new file mode 100755 index 0000000..fcbfda0 --- /dev/null +++ b/recap/Resources/Assets.xcassets/appleLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Logo - SIWA - Logo-only - White.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/appleLogo.imageset/Logo - SIWA - Logo-only - White.svg b/recap/Resources/Assets.xcassets/appleLogo.imageset/Logo - SIWA - Logo-only - White.svg new file mode 100755 index 0000000..ebdf1b8 --- /dev/null +++ b/recap/Resources/Assets.xcassets/appleLogo.imageset/Logo - SIWA - Logo-only - White.svg @@ -0,0 +1,10 @@ + + + + White Logo Square + Created with Sketch. + + + + + \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/appleLogoWhite.imageset/Contents.json b/recap/Resources/Assets.xcassets/appleLogoWhite.imageset/Contents.json new file mode 100755 index 0000000..73d6ab4 --- /dev/null +++ b/recap/Resources/Assets.xcassets/appleLogoWhite.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Logo - SIWA - Logo-only - Black.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/appleLogoWhite.imageset/Logo - SIWA - Logo-only - Black.svg b/recap/Resources/Assets.xcassets/appleLogoWhite.imageset/Logo - SIWA - Logo-only - Black.svg new file mode 100755 index 0000000..789a03e --- /dev/null +++ b/recap/Resources/Assets.xcassets/appleLogoWhite.imageset/Logo - SIWA - Logo-only - Black.svg @@ -0,0 +1,10 @@ + + + + Black Logo Square + Created with Sketch. + + + + + \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/article.imageset/Contents.json b/recap/Resources/Assets.xcassets/article.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/article.imageset/article.png b/recap/Resources/Assets.xcassets/article.imageset/article.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/avatar.imageset/Contents.json b/recap/Resources/Assets.xcassets/avatar.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/avatar.imageset/_Messages-avatar.png b/recap/Resources/Assets.xcassets/avatar.imageset/_Messages-avatar.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/Game.imageset/Contents.json b/recap/Resources/Assets.xcassets/background-image.imageset/Contents.json old mode 100644 new mode 100755 similarity index 100% rename from recap/Resources/Assets.xcassets/Game.imageset/Contents.json rename to recap/Resources/Assets.xcassets/background-image.imageset/Contents.json diff --git a/recap/Resources/Assets.xcassets/Game.imageset/Image.png b/recap/Resources/Assets.xcassets/background-image.imageset/Image.png old mode 100644 new mode 100755 similarity index 100% rename from recap/Resources/Assets.xcassets/Game.imageset/Image.png rename to recap/Resources/Assets.xcassets/background-image.imageset/Image.png diff --git a/recap/Resources/Assets.xcassets/cardGame.imageset/Contents.json b/recap/Resources/Assets.xcassets/cardGame.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/cardGame.imageset/cardGame.png b/recap/Resources/Assets.xcassets/cardGame.imageset/cardGame.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_10.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_10.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_10.imageset/card_10.jpeg b/recap/Resources/Assets.xcassets/card_10.imageset/card_10.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_11.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_11.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_11.imageset/card_11.jpeg b/recap/Resources/Assets.xcassets/card_11.imageset/card_11.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_12.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_12.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_12.imageset/card_12.jpeg b/recap/Resources/Assets.xcassets/card_12.imageset/card_12.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_13.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_13.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_13.imageset/card_13.jpeg b/recap/Resources/Assets.xcassets/card_13.imageset/card_13.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_14.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_14.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_14.imageset/card_14.jpeg b/recap/Resources/Assets.xcassets/card_14.imageset/card_14.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_2.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_2.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_2.imageset/card_2.jpeg b/recap/Resources/Assets.xcassets/card_2.imageset/card_2.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_3.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_3.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_3.imageset/card_3.jpeg b/recap/Resources/Assets.xcassets/card_3.imageset/card_3.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_4.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_4.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_4.imageset/card_4.jpeg b/recap/Resources/Assets.xcassets/card_4.imageset/card_4.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_5.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_5.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_5.imageset/card_5.jpeg b/recap/Resources/Assets.xcassets/card_5.imageset/card_5.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_6.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_6.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_6.imageset/card_6.jpeg b/recap/Resources/Assets.xcassets/card_6.imageset/card_6.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_7.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_7.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_7.imageset/card_7.jpeg b/recap/Resources/Assets.xcassets/card_7.imageset/card_7.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_8.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_8.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_8.imageset/card_8.jpeg b/recap/Resources/Assets.xcassets/card_8.imageset/card_8.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_9.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_9.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_9.imageset/card_9.jpeg b/recap/Resources/Assets.xcassets/card_9.imageset/card_9.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_back.imageset/Contents.json b/recap/Resources/Assets.xcassets/card_back.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/card_back.imageset/card_1.jpeg b/recap/Resources/Assets.xcassets/card_back.imageset/card_1.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/cosmonaut.imageset/Contents.json b/recap/Resources/Assets.xcassets/cosmonaut.imageset/Contents.json old mode 100644 new mode 100755 diff --git "a/recap/Resources/Assets.xcassets/cosmonaut.imageset/\360\237\246\206 icon _cosmonaut_.png" "b/recap/Resources/Assets.xcassets/cosmonaut.imageset/\360\237\246\206 icon _cosmonaut_.png" old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/dj.imageset/Contents.json b/recap/Resources/Assets.xcassets/dj.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/dj.imageset/dj.png b/recap/Resources/Assets.xcassets/dj.imageset/dj.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/familyImg.imageset/Contents.json b/recap/Resources/Assets.xcassets/familyImg.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/familyImg.imageset/familyImg.png b/recap/Resources/Assets.xcassets/familyImg.imageset/familyImg.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/gameComplete.dataset/Contents.json b/recap/Resources/Assets.xcassets/gameComplete.dataset/Contents.json new file mode 100755 index 0000000..8645cf2 --- /dev/null +++ b/recap/Resources/Assets.xcassets/gameComplete.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "gameComplete.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/gameComplete.dataset/gameComplete.json b/recap/Resources/Assets.xcassets/gameComplete.dataset/gameComplete.json new file mode 100755 index 0000000..e6fb6a3 --- /dev/null +++ b/recap/Resources/Assets.xcassets/gameComplete.dataset/gameComplete.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":30,"ip":0,"op":71,"w":500,"h":500,"nm":"Trophy","ddd":0,"assets":[{"id":"comp_0","nm":"Pre-comp 3","fr":30,"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[391.176,345.588,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":2,"op":17,"st":2,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[344.118,294.118,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":1,"op":16,"st":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[151.471,317.647,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":7,"op":22,"st":7,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[104.412,266.176,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":6,"op":21,"st":6,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[342.647,145.588,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":4,"op":19,"st":4,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[295.588,94.118,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":3,"op":18,"st":3,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[133.824,122.059,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[30,30,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":1,"op":16,"st":1,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"Pre-comp 2","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[179.412,82.353,0],"ix":2,"l":2},"a":{"a":0,"k":[50,48.5,0],"ix":1,"l":2},"s":{"a":0,"k":[50,50,100],"ix":6,"l":2}},"ao":0,"w":100,"h":97,"ip":0,"op":15,"st":0,"bm":0}]},{"id":"comp_1","nm":"Pre-comp 2","fr":30,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.5,47,0],"ix":2,"l":2},"a":{"a":0,"k":[-142.5,-154,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-142.5,-154],[-101.5,-154]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[0]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":15,"st":-11,"bm":0}]},{"id":"comp_2","nm":"Pre-comp 1","fr":30,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":30,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":120,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":150,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":210,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":240,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":270,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4,"s":[60]},{"t":14,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 21","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":300,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":3,"s":[60]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 20","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":330,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[178,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":3,"s":[60]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[60]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":300,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":39,"op":61,"st":39,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":24,"op":46,"st":24,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Cup 3","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.371,-98.838,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.8,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.2,"y":0},"t":14,"s":[{"i":[[0,6.785],[0,0],[0,-11.667],[0,0],[0,55.777],[0,0]],"o":[[0,0],[0,8.035],[0,0],[0,54.652],[0,0],[0,-12.042]],"v":[[-0.25,-128.285],[-0.25,-128.285],[-0.25,-106.958],[-0.25,-21.652],[-0.25,-21.652],[-0.25,-106.958]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]},{"i":{"x":1,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]},{"i":{"x":0.223,"y":1},"o":{"x":0.2,"y":0},"t":31,"s":[{"i":[[0,6.785],[0,0],[0,-11.667],[0,0],[0,55.777],[0,0]],"o":[[0,0],[0,8.035],[0,0],[0,54.652],[0,0],[0,-12.042]],"v":[[-0.25,-128.285],[-0.25,-128.285],[-0.25,-106.958],[-0.25,-21.652],[-0.25,-21.652],[-0.25,-106.958]],"c":true}]},{"t":50,"s":[{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.8,-128.285],[49.3,-128.285],[70.626,-106.958],[62.096,-21.652],[-62.596,-21.652],[-71.126,-106.958]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Cup","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":31,"op":310,"st":10,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 7","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"t":24,"s":[{"i":[[3.191,-0.395],[2.304,-0.927],[2.095,-1.709],[1.788,-1.877],[0.908,-1.912],[-1.334,-6.312],[-2.779,-4.188],[-3.401,-3.602],[-3.548,-3.297],[-2.312,-2.352],[-2.506,-2.506],[-2.476,-2.535],[-0.232,-1.997],[0.723,-0.831],[0.267,-1.304],[-2.88,-0.857],[1.3,9.712],[4.203,4.76],[9.453,16.328],[-0.295,3.28],[-3.343,1.249],[-4.023,-0.951],[-1.8,-0.768],[-8.286,2.069],[-0.398,3.182],[3.129,3.445],[1.614,1.176],[1.189,0.657],[2.306,0.956],[2.086,0.582]],"o":[[-4.689,0.581],[-2.304,0.927],[-1.938,1.582],[-1.788,1.877],[-3.116,6.566],[1.334,6.312],[2.849,4.294],[3.401,3.602],[2.244,2.084],[2.312,2.352],[2.864,2.864],[2.476,2.535],[0.16,1.372],[-0.723,0.831],[-1.339,6.557],[13.183,3.921],[-1.018,-7.607],[-12.335,-13.97],[-1.509,-2.606],[0.413,-4.602],[3.955,-1.477],[2.275,0.538],[8.878,3.789],[3.458,-0.863],[0.467,-3.729],[-1.703,-1.875],[-1.654,-1.205],[-1.861,-1.028],[-2.371,-0.983],[-7.691,-2.147]],"v":[[-93,-111],[-102.945,-108.846],[-109,-105],[-114.773,-99.748],[-119,-94],[-120.921,-74.216],[-114,-58],[-104.525,-46.252],[-94,-36],[-87.197,-29.316],[-80,-22],[-71.526,-13.849],[-67,-7],[-68.18,-3.949],[-70,-1],[-64,12],[-47,-11],[-59,-30],[-99,-72],[-102,-81],[-94,-91],[-82,-91],[-75,-89],[-55,-79],[-48,-87],[-53,-98],[-58,-101],[-62,-105],[-69,-107],[-75,-110]],"c":true}],"h":1},{"t":25,"s":[{"i":[[6.363,-1.468],[2.979,-2.095],[1.84,-2.639],[0.408,-1.067],[0.408,-1.35],[0.465,-0.387],[0.082,-0.263],[-3.965,-7.542],[-4.029,-4.555],[-0.766,-0.479],[-0.438,-0.523],[-0.104,-0.568],[-0.27,-0.353],[-0.859,-0.529],[-0.842,-0.709],[-4.878,-5.799],[-0.092,-0.71],[0.419,-2.677],[-6.464,-0.238],[-1.53,2.112],[6.189,7.171],[5.82,5.82],[5.515,7.127],[-5.296,5.528],[-9.204,-2.345],[-3.834,-2.514],[-5.231,0.751],[-0.822,3.258],[5.25,2.566],[1.551,0.608]],"o":[[-4.243,0.979],[-2.98,2.096],[-0.962,1.38],[-0.408,1.067],[-0.057,0.189],[-0.465,0.387],[-2.889,9.276],[3.965,7.542],[0.497,0.561],[0.766,0.479],[0.313,0.374],[0.104,0.568],[1.053,1.378],[1.068,0.657],[6.494,5.469],[1.271,1.511],[0.356,2.738],[-1.191,7.598],[4.588,0.169],[8.605,-11.877],[-4.677,-5.419],[-6.151,-6.151],[-4.119,-5.322],[4.622,-4.825],[3.701,0.943],[4.525,2.967],[3.142,-0.451],[2.691,-10.661],[-1.826,-0.892],[-7.754,-3.037]],"v":[[-95,-111],[-105.802,-106.245],[-113,-99],[-114.915,-95.478],[-116,-92],[-116.981,-91.056],[-118,-90],[-114.689,-64.459],[-101,-46],[-98.956,-44.471],[-97,-43],[-96.468,-41.485],[-96,-40],[-92,-37],[-90,-35],[-71,-17],[-65,-8],[-68,-1],[-59,12],[-49,6],[-55,-29],[-72,-45],[-91,-66],[-97,-87],[-77,-91],[-66,-85],[-54,-79],[-46,-86],[-62,-106],[-67,-109]],"c":true}],"h":1},{"t":26,"s":[{"i":[[1.111,-0.113],[2.585,-1.009],[1.674,-1.559],[0.573,-0.084],[0.356,-0.336],[0.475,-0.926],[0.564,-0.79],[0.36,-0.281],[0.285,-0.493],[0.871,-2.718],[0.063,-2.618],[-3.733,-5.588],[-2.015,-2.521],[-0.872,-1.07],[-1.274,-1.411],[-7.648,-7.648],[-0.543,-5.007],[0.55,-2.542],[-2.362,-2.153],[-1.562,7.645],[2.913,4.566],[2.86,3.478],[9.12,11.123],[-1.11,7.282],[-2.157,0.726],[-4.835,-3.467],[-4.962,0.362],[-0.24,6.416],[7.573,3.176],[2.407,0.544]],"o":[[-3.987,0.405],[-2.585,1.009],[-0.352,0.328],[-0.573,0.084],[-0.536,0.507],[-0.475,0.926],[-0.273,0.382],[-0.36,0.281],[-1.452,2.508],[-0.871,2.718],[-0.253,10.508],[1.754,2.625],[0.961,1.203],[1.009,1.238],[6.895,7.635],[5.268,5.268],[0.253,2.337],[-0.918,4.241],[8.175,7.452],[1.806,-8.842],[-2.806,-4.398],[-8.695,-10.574],[-5.23,-6.378],[0.863,-5.666],[7.845,-2.641],[3.765,2.699],[4.671,-0.341],[0.327,-8.737],[-3.3,-1.384],[-5.338,-1.207]],"v":[[-85,-112],[-94.735,-109.865],[-101,-106],[-102.498,-105.506],[-104,-105],[-105.479,-102.712],[-107,-100],[-107.991,-99.083],[-109,-98],[-112.542,-90.083],[-114,-82],[-106,-58],[-100,-51],[-98,-47],[-94,-44],[-75,-23],[-63,-8],[-65,-1],[-63,9],[-43,-1],[-48,-23],[-58,-35],[-84,-62],[-94,-83],[-86,-92],[-64,-86],[-52,-79],[-43,-89],[-63,-108],[-72,-112]],"c":true}],"h":1},{"t":27,"s":[{"i":[[2.317,-0.535],[3.86,-4.04],[1.242,-4.613],[-2.12,-5.938],[-2.373,-3.37],[-0.492,-0.639],[-0.459,-0.632],[-0.8,-1.421],[-0.923,-1.114],[-0.951,-0.655],[-0.472,-0.507],[-0.081,-0.566],[-0.34,-0.374],[-1.019,-0.973],[-0.936,-1.02],[-0.997,-1.18],[-0.954,-1.127],[-0.458,-4.534],[0.591,-1.92],[-7.875,-0.144],[-0.943,9.517],[4.79,6.294],[7.791,10.593],[-2.16,8.125],[-1.994,0.531],[-4.781,-3.242],[-0.608,-0.61],[-0.949,-0.628],[-0.752,8.522],[12.133,3.114]],"o":[[-5.317,1.227],[-3.86,4.04],[-1.851,6.879],[2.12,5.938],[0.571,0.811],[0.492,0.639],[0.875,1.203],[0.8,1.421],[0.723,0.873],[0.951,0.655],[0.337,0.361],[0.081,0.566],[0.99,1.09],[1.018,0.973],[1.058,1.152],[0.997,1.18],[3.481,4.111],[0.271,2.68],[-2.255,7.329],[7.212,0.132],[1.112,-11.222],[-8.19,-10.762],[-4.476,-6.085],[0.814,-3.063],[5.149,-1.372],[0.641,0.434],[1.172,1.175],[7.025,4.649],[0.85,-9.635],[-5.404,-1.387]],"v":[[-82,-111],[-96.057,-102.54],[-104,-89],[-102.668,-69.368],[-95,-55],[-93.416,-52.866],[-92,-51],[-89.536,-46.934],[-87,-43],[-84.312,-40.725],[-82,-39],[-81.502,-37.509],[-81,-36],[-77.959,-32.947],[-75,-30],[-71.923,-26.481],[-69,-23],[-58,-8],[-60,-2],[-51,12],[-38,-5],[-48,-30],[-76,-62],[-84,-85],[-77,-92],[-60,-87],[-57,-85],[-55,-81],[-38,-88],[-65,-111]],"c":true}],"h":1},{"t":28,"s":[{"i":[[9.692,-1.405],[1.755,-0.653],[1.212,-0.989],[0.612,-0.799],[1.064,-1.588],[0.956,-1.834],[0.829,-2.651],[-1.471,-5.599],[-2.124,-4.18],[-2.534,-3.99],[-2.617,-3.613],[-1.064,-1.14],[-0.856,-1.216],[-1.667,-2.958],[-0.212,-2.155],[0.684,-2.633],[-1.441,-2.449],[-1.846,-1.035],[-3.412,0.49],[-1.764,4.555],[0.994,6.213],[1.271,2.573],[1.579,2.614],[5.214,6.995],[2.346,5.732],[-9.454,1.216],[-1.712,-1.097],[-10.861,3.613],[-0.352,2.926],[4.889,4.64]],"o":[[-2.767,0.401],[-1.755,0.653],[-1.489,1.215],[-0.612,0.799],[-1.348,2.013],[-0.956,1.834],[-2.008,6.423],[1.471,5.599],[2.249,4.426],[2.534,3.99],[0.888,1.226],[1.064,1.14],[2.12,3.012],[1.667,2.958],[0.297,3.021],[-0.684,2.633],[0.206,0.35],[1.846,1.035],[3.939,-0.566],[1.764,-4.555],[-0.335,-2.095],[-1.271,-2.573],[-3.799,-6.286],[-5.214,-6.995],[-3.248,-7.936],[1.752,-0.225],[5.75,3.685],[2.926,-0.973],[0.526,-4.371],[-6.865,-6.516]],"v":[[-63,-111],[-69.666,-109.441],[-74,-107],[-76.82,-104.28],[-79,-101],[-82.389,-95.478],[-85,-89],[-85.099,-70.817],[-79,-56],[-71.776,-43.39],[-64,-32],[-60.976,-28.493],[-58,-25],[-52.068,-15.857],[-49,-8],[-50.358,0.429],[-50,8],[-46.904,10.63],[-39,12],[-30.301,3.736],[-29,-13],[-31.567,-20.11],[-36,-28],[-50.589,-48.416],[-63,-68],[-59,-92],[-53,-89],[-34,-79],[-28,-87],[-36,-101]],"c":true}],"h":1},{"t":29,"s":[{"i":[[-2.683,7.317],[-0.869,-1.25],[-0.8,-0.658],[-1.031,-0.204],[-1.563,0.111],[-1.735,1.264],[-0.45,1.828],[3.728,4.807],[2.323,0.76],[3.865,-2.557],[1.585,-3.719],[-0.715,-8.537],[-2.363,-6.29],[-1.877,-4.04],[-1.844,-4.454],[-1.186,-3.102],[-0.134,-2.887],[0.496,-1.449],[0.053,-1.394],[-1.552,-2.05],[-2.93,-0.213],[-1.523,0.346],[-0.883,0.689],[-0.327,1.358],[-0.442,1.593],[1.215,6.113],[1.85,4.387],[0.684,1.501],[0.533,1.264],[2.41,7.866]],"o":[[1.239,1.98],[0.869,1.25],[0.8,0.658],[1.031,0.204],[1.61,-0.114],[1.735,-1.264],[1.234,-5.011],[-3.728,-4.807],[-5.956,-1.948],[-3.865,2.557],[-3.224,7.564],[0.715,8.537],[1.648,4.387],[1.877,4.04],[1.12,2.706],[1.186,3.102],[0.038,0.811],[-0.496,1.449],[-0.136,3.585],[1.552,2.05],[1.025,0.075],[1.523,-0.346],[1.249,-0.974],[0.327,-1.358],[1.71,-6.16],[-1.215,-6.113],[-0.73,-1.733],[-0.684,-1.501],[-3.044,-7.221],[-2.41,-7.866]],"v":[[-33,-87],[-29.914,-82.19],[-27.486,-79.364],[-24.816,-78.105],[-21,-78],[-15.63,-80.215],[-12,-85],[-17.332,-100.689],[-28,-110],[-42.778,-108.25],[-51,-98],[-54.191,-73.044],[-49,-50],[-43.647,-37.55],[-38,-25],[-34.26,-16.136],[-32,-7],[-32.932,-3.437],[-34,1],[-31.799,9.529],[-25,13],[-20.893,12.572],[-17,11],[-14.895,7.464],[-14,3],[-13.83,-15.829],[-19,-32],[-21.148,-36.851],[-23,-41],[-32.295,-63.928]],"c":true}],"h":1},{"t":30,"s":[{"i":[[3.333,-0.976],[1.133,-1.074],[0.545,-1.572],[0.264,-2.087],[0.291,-2.619],[-0.238,-6.751],[-0.732,-6.624],[-0.753,-6.186],[-0.301,-5.438],[0.011,-2.415],[-0.042,-2.238],[-0.282,-1.704],[-0.709,-0.814],[-1.027,-0.436],[-1.371,-0.226],[-1.409,0.198],[-1.139,0.835],[0.122,7.953],[0.958,8.662],[0.318,3.74],[0.375,3.844],[0.571,4.011],[-0.7,2.69],[-0.723,0.668],[-0.748,0.937],[-0.514,0.926],[-0.22,1.386],[0.663,2.516],[0.717,1.6],[2.195,1.193]],"o":[[-2.029,0.594],[-1.133,1.074],[-0.545,1.572],[-0.264,2.087],[-0.729,6.568],[0.238,6.751],[0.732,6.624],[0.753,6.186],[0.124,2.236],[-0.011,2.415],[0.042,2.238],[0.282,1.704],[0.377,0.433],[1.027,0.436],[1.371,0.226],[1.409,-0.198],[3.447,-2.528],[-0.122,-7.953],[-0.353,-3.197],[-0.318,-3.74],[-0.416,-4.266],[-0.571,-4.011],[0.361,-1.39],[0.723,-0.668],[0.726,-0.91],[0.514,-0.926],[0.37,-2.338],[-0.663,-2.516],[-1.885,-4.206],[-2.195,-1.193]],"v":[[-11,-109],[-15.667,-106.502],[-18.108,-102.537],[-19.245,-97.054],[-20,-90],[-20.619,-69.944],[-19.046,-49.805],[-16.7,-30.513],[-15,-13],[-14.877,-5.936],[-14.877,1.132],[-14.438,7.133],[-13,11],[-10.818,12.357],[-7.145,13.403],[-2.899,13.497],[1,12],[5.304,-4.9],[3,-31],[2.016,-41.514],[1,-53],[-0.837,-65.682],[-1,-76],[0.711,-78.84],[3,-81],[4.88,-83.643],[6,-87],[5.316,-94.554],[3,-101],[-2.914,-108.886]],"c":true}],"h":1},{"t":31,"s":[{"i":[[3.441,-0.647],[1.283,-0.953],[0.833,-1.422],[0.508,-1.758],[0.31,-1.96],[-0.087,-1.457],[-0.408,-1.039],[-0.617,-0.938],[-0.714,-1.153],[-0.141,-1.047],[0.131,-1.095],[0.206,-1.125],[0.086,-1.137],[0.415,-3.836],[0.451,-3.58],[0.436,-3.579],[0.369,-3.834],[0.163,-4.005],[-0.565,-3.399],[-1.82,-1.944],[-3.604,0.359],[-1.357,1.89],[-0.311,2.89],[0.076,3.305],[-0.195,3.135],[-0.864,6.178],[-0.881,7.07],[-0.327,7.107],[0.798,6.288],[2.455,2.453]],"o":[[-1.859,0.35],[-1.283,0.953],[-0.833,1.422],[-0.508,1.758],[-0.346,2.191],[0.087,1.457],[0.408,1.039],[0.617,0.938],[0.608,0.981],[0.141,1.047],[-0.131,1.095],[-0.206,1.125],[-0.328,4.346],[-0.415,3.836],[-0.451,3.58],[-0.436,3.579],[-0.363,3.764],[-0.163,4.005],[0.565,3.399],[1.82,1.944],[3.06,-0.305],[1.357,-1.89],[0.311,-2.89],[-0.076,-3.305],[0.275,-4.432],[0.864,-6.178],[0.881,-7.07],[0.327,-7.107],[-0.741,-5.836],[-2.455,-2.453]],"v":[[11,-109],[6.318,-107.013],[3.176,-103.416],[1.196,-98.612],[0,-93],[-0.361,-87.608],[0.409,-83.943],[1.975,-81.057],[4,-78],[5.074,-74.953],[5.041,-71.735],[4.487,-68.399],[4,-65],[2.874,-52.791],[1.563,-41.731],[0.22,-31.056],[-1,-20],[-1.921,-8.134],[-1.45,3.184],[1.995,11.41],[10,14],[16.461,10.561],[18.798,3.245],[18.986,-6.194],[19,-16],[20.851,-32.129],[23.61,-52.216],[25.564,-73.694],[25,-94],[20.025,-106.362]],"c":true}],"h":1},{"t":32,"s":[{"i":[[10.012,-0.857],[0.663,-0.066],[0.696,-0.133],[0.685,-0.25],[0.629,-0.419],[1.161,-1.785],[0.997,-2.24],[0.588,-2.086],[-0.068,-1.321],[-1.507,-1.219],[-0.877,-0.316],[-1.658,-0.093],[-0.11,-0.173],[0.768,-3.552],[0.803,-3.414],[1.144,-4.168],[1.192,-4.1],[1.234,-4.969],[-0.056,-4.277],[-2.293,-3.72],[-5.767,1.698],[-1.257,0.847],[-0.419,0.742],[0.306,2.867],[-0.16,2.647],[-0.757,2.689],[-0.84,3.026],[-1.59,5.162],[-1.1,4.811],[1.605,11.555]],"o":[[-0.585,0.05],[-0.663,0.066],[-0.696,0.133],[-0.685,0.25],[-1.079,0.718],[-1.161,1.785],[-0.997,2.24],[-0.588,2.086],[0.093,1.806],[1.507,1.219],[1.258,0.453],[1.658,0.093],[0.876,1.379],[-0.768,3.552],[-1.23,5.23],[-1.144,4.168],[-1.119,3.847],[-1.234,4.969],[0.062,4.817],[2.293,3.72],[-0.445,0.131],[1.257,-0.847],[1.063,-1.884],[-0.306,-2.867],[0.134,-2.219],[0.757,-2.689],[1.538,-5.541],[1.59,-5.162],[2.433,-10.639],[-1.605,-11.555]],"v":[[27,-109],[25.116,-108.839],[23.066,-108.553],[20.982,-107.991],[19,-107],[15.579,-103.093],[12.28,-96.903],[9.841,-90.262],[9,-85],[11.912,-80.383],[16,-78],[20.861,-77.29],[24,-77],[23.76,-69.026],[21,-58],[17.471,-44.153],[14,-32],[10.119,-18.323],[8,-4],[11.222,9.887],[23,14],[24.852,12.655],[28,10],[28.678,2.572],[28,-6],[29.471,-13.395],[32,-22],[36.828,-38.047],[41,-53],[43.334,-89.622]],"c":true}],"h":1},{"t":33,"s":[{"i":[[-1.692,-0.766],[1.471,-6.219],[2.553,-5.943],[1.166,-2.647],[1.155,-2.664],[1.283,-2.686],[0.627,-2.395],[-0.895,-5.6],[-2.8,-1.331],[-2.452,0.856],[-0.736,1.221],[0.438,2.954],[-0.248,2.689],[-1.182,2.599],[-0.96,2.214],[-0.473,1.359],[-0.575,1.331],[-0.379,0.478],[-0.227,0.489],[-0.098,0.91],[-0.285,0.66],[-1.26,2.459],[-0.88,2.66],[-0.699,3.206],[-0.248,3.282],[3.214,7.139],[7.793,-0.124],[1.212,-0.836],[-16.354,-1.393],[-1.724,3.235]],"o":[[1.434,6.183],[-1.471,6.218],[-1.18,2.747],[-1.166,2.647],[-1.24,2.86],[-1.283,2.686],[-1.379,5.271],[0.895,5.6],[3.456,1.643],[2.452,-0.856],[1.506,-2.497],[-0.438,-2.954],[0.312,-3.39],[1.182,-2.599],[0.535,-1.233],[0.473,-1.359],[0.258,-0.599],[0.379,-0.478],[0.325,-0.701],[0.098,-0.91],[1.079,-2.504],[1.26,-2.459],[0.958,-2.895],[0.698,-3.206],[0.647,-8.57],[-3.214,-7.139],[-2.471,0.04],[-7.887,5.438],[7.4,0.63],[0.183,-0.343]],"v":[[38,-85],[37.49,-66.32],[31,-48],[27.481,-39.938],[24,-32],[20.04,-23.651],[17,-16],[16.366,1.455],[22,13],[31.04,13.648],[36,10],[36.944,1.644],[36,-7],[38.514,-15.882],[42,-23],[43.47,-26.927],[45,-31],[46.023,-32.582],[47,-34],[47.53,-36.53],[48,-39],[51.649,-46.383],[55,-54],[57.532,-63.209],[59,-73],[55.33,-98.02],[39,-110],[28,-106],[26,-77],[36,-83]],"c":true}],"h":1},{"t":34,"s":[{"i":[[14.095,-1.272],[0.937,-0.322],[0.989,-0.507],[0.677,-0.062],[0.614,-0.406],[1.107,-1.421],[0.922,-1.224],[0.408,-0.14],[0.201,-0.216],[0.756,-2.55],[-0.329,-1.4],[-2.06,-1.454],[-3.378,0.441],[-2.23,3.005],[-1.843,-1.444],[-0.405,-1.932],[-0.025,-1.995],[0.392,-1.914],[0.456,-1.6],[2.132,-3.599],[2.472,-4.571],[0.616,-1.544],[0.771,-1.406],[0.656,-1.147],[0.679,-1.473],[-14.013,-1.683],[-0.906,4.713],[-0.286,3.517],[-3.246,5.959],[-1.098,19.932]],"o":[[-2.264,0.204],[-0.937,0.322],[-0.678,0.347],[-0.677,0.062],[-1.651,1.092],[-1.107,1.421],[-0.169,0.224],[-0.407,0.14],[-1.305,1.401],[-0.756,2.55],[0.382,1.624],[2.06,1.454],[3.235,-0.423],[2.23,-3.005],[0.353,0.276],[0.405,1.932],[0.022,1.745],[-0.392,1.914],[-2.002,7.031],[-2.132,3.6],[-0.767,1.416],[-0.616,1.544],[-0.631,1.151],[-0.757,1.324],[-5.008,10.858],[6.28,0.754],[0.78,-4.06],[0.405,-4.988],[8.39,-15.404],[1.187,-21.552]],"v":[[47,-109],[42.544,-108.226],[40,-107],[37.952,-106.544],[36,-106],[31.953,-102.099],[29,-98],[28.025,-97.494],[27,-97],[23.774,-90.499],[23,-84],[26.753,-78.951],[35,-77],[43.044,-83.9],[49,-88],[50.246,-84.288],[51,-78],[50.358,-72.392],[49,-67],[42.853,-52.155],[36,-41],[34.003,-36.492],[32,-32],[29,-29],[27,-24],[32,14],[44,6],[42,-6],[49,-23],[71,-75]],"c":true}],"h":1},{"t":35,"s":[{"i":[[8.043,-0.886],[1.416,-0.578],[2.278,-1.352],[1.236,-0.627],[0.831,-0.763],[0.311,-0.547],[0.347,-0.438],[0.627,-2.982],[-3.135,-2.767],[-3.115,2.252],[-2.844,2.136],[-1.146,-0.796],[-0.7,-3.858],[2.241,-5.103],[2.163,-3.381],[0.336,-0.745],[0.319,-0.495],[4.168,-7.075],[0.392,-6.585],[-1.84,-4.18],[-4.893,0],[-1.309,1.377],[-0.485,1.491],[0.519,2.006],[-0.269,2.498],[-1.51,2.797],[-1.745,2.835],[-3.084,4.604],[-2.081,10.716],[5.726,6.344]],"o":[[-3.056,0.337],[-1.416,0.578],[-1.225,0.727],[-1.236,0.627],[-0.408,0.374],[-0.311,0.547],[-2.626,3.307],[-0.627,2.981],[4.694,4.144],[3.115,-2.252],[2.716,-2.041],[1.146,0.796],[1.126,6.206],[-2.241,5.103],[-0.337,0.526],[-0.336,0.745],[-3.936,6.116],[-4.168,7.075],[-0.255,4.279],[1.841,4.18],[3.279,0],[1.309,-1.377],[0.806,-2.478],[-0.519,-2.006],[0.213,-1.978],[1.511,-2.797],[3.564,-5.791],[5.859,-8.748],[2.529,-13.025],[-5.149,-5.706]],"v":[[54,-109],[47.916,-107.762],[43,-105],[39.204,-103.026],[36,-101],[34.955,-99.548],[34,-98],[28.68,-88.595],[32,-80],[43.387,-78.79],[52,-87],[57.513,-88.924],[60,-82],[57.466,-64.882],[50,-52],[48.986,-49.976],[48,-48],[34.842,-27.852],[27,-7],[29.139,6.709],[39,14],[45.596,11.618],[48,7],[47.903,0.515],[47,-6],[49.851,-13.357],[55,-22],[67,-40],[80,-70],[72,-102]],"c":true}],"h":1},{"t":36,"s":[{"i":[[6.95,-0.74],[2.905,-1.098],[1.338,-1.282],[2.561,-3.354],[-1.377,-4.54],[-1.524,-1.099],[-2.451,-0.109],[-1.063,0.784],[-0.925,0.989],[-2.672,1.205],[-0.427,0.231],[-1.115,-0.377],[-0.221,-2.138],[2.655,-4.292],[0.852,-1.278],[0.353,-0.47],[0.554,-0.76],[0.376,-0.501],[0.552,-0.759],[1.638,-2.261],[2.119,-8.747],[-11.571,0.349],[0.102,-0.212],[-0.6,5.533],[-3.865,5.614],[-3.36,5.017],[-2.959,7.022],[-0.68,2.933],[8.858,3.993],[0.432,0.229]],"o":[[-3.041,0.324],[-2.905,1.098],[-2.132,2.042],[-2.561,3.354],[0.454,1.496],[1.524,1.099],[2.821,0.126],[1.064,-0.784],[2.484,-2.655],[0.357,-0.161],[1.608,-0.869],[1.776,0.601],[0.807,7.818],[-0.965,1.561],[-0.298,0.447],[-0.636,0.848],[-0.344,0.472],[-0.635,0.846],[-1.758,2.419],[-5.704,7.876],[-2.125,8.771],[5.64,-0.17],[2.416,-5.024],[0.401,-3.692],[4.493,-6.525],[4.477,-6.685],[1.175,-2.788],[3.523,-15.198],[-0.36,-0.162],[-3.543,-1.875]],"v":[[60,-109],[50.723,-106.719],[44,-103],[35.868,-94.873],[33,-83],[36.002,-78.96],[42,-77],[47.422,-78.164],[50,-81],[56,-86],[57,-88],[64,-90],[69,-80],[60,-57],[58,-52],[56,-51],[55,-48],[53,-47],[52,-44],[46,-37],[32,-11],[44,14],[53,9],[51,-6],[60,-21],[73,-39],[84,-59],[88,-68],[76,-105],[75,-107]],"c":true}],"h":1},{"t":37,"s":[{"i":[[6.233,-0.744],[3.673,-1.795],[2.536,-2.251],[1.463,-2.357],[-0.21,-2.367],[-1.895,-1.574],[-2.184,-0.066],[-3.996,3.091],[-3.467,-1.045],[-0.259,-4.087],[3.554,-5.557],[3.616,-4.531],[1.087,-12.139],[-9.903,1.954],[-0.682,3.684],[-0.71,4.488],[-0.964,1.734],[-0.939,1.446],[-0.345,0.461],[-0.554,0.76],[-0.376,0.501],[-0.596,0.775],[-1.455,2.193],[-0.623,0.94],[-0.623,0.94],[-0.506,0.737],[-0.962,1.445],[-0.981,1.538],[0.282,9.538],[7.628,4.07]],"o":[[-4.773,0.57],[-3.673,1.795],[-1.358,1.206],[-1.463,2.357],[0.234,2.637],[1.896,1.574],[6.258,0.189],[4.147,-3.208],[2.255,0.68],[0.397,6.272],[-3.899,6.097],[-9.421,11.805],[-1.145,12.792],[3.806,-0.751],[0.803,-4.341],[0.086,-0.546],[0.913,-1.643],[0.285,-0.438],[0.636,-0.848],[0.344,-0.472],[0.653,-0.87],[2.166,-2.819],[1.103,-1.662],[1.103,-1.662],[0.595,-0.897],[0.809,-1.177],[0.873,-1.312],[4.282,-6.717],[-0.374,-12.63],[-4.306,-2.297]],"v":[[65,-109],[52.323,-105.261],[43,-99],[38.324,-93.371],[36,-86],[39.537,-79.572],[46,-77],[57,-84],[70,-90],[76,-79],[68,-59],[56,-43],[35,-8],[50,14],[57,6],[55,-7],[58,-11],[60,-16],[62,-17],[63,-20],[65,-21],[66,-24],[74,-33],[77,-37],[80,-41],[82,-43],[84,-48],[87,-52],[96,-79],[81,-107]],"c":true}],"h":1},{"t":38,"s":[{"i":[[1.647,-0.211],[3.205,-1.278],[3.453,-2.54],[2.196,-2.853],[-1.45,-4.627],[-1.536,-1.121],[-2.416,-0.112],[-2.608,2.496],[-2.576,1.201],[-2.905,-1.487],[-0.312,-4.948],[1.855,-3.591],[1.77,-2.606],[2.135,-2.77],[2.131,-2.486],[4.115,-5.865],[0,-7.076],[-9.381,0.969],[0.945,4.244],[-0.331,2.895],[-2.557,3.213],[-1.725,2.071],[-2.393,3.051],[-0.908,1.137],[-1.532,2.219],[-0.672,1.075],[-1.198,3.937],[3.55,7.75],[6.098,3.388],[1.056,0.329]],"o":[[-2.727,0.349],[-3.205,1.278],[-2.665,1.96],[-2.196,2.853],[0.439,1.403],[1.536,1.121],[3.993,0.185],[2.608,-2.496],[4.069,-1.897],[2.905,1.487],[0.191,3.028],[-1.855,3.591],[-2.331,3.432],[-2.135,2.77],[-5.541,6.463],[-4.115,5.865],[0,10.245],[8.784,-0.907],[-0.459,-2.063],[0.63,-5.51],[1.77,-2.224],[3.326,-3.993],[0.909,-1.159],[1.718,-2.153],[0.596,-0.863],[2.908,-4.652],[3.053,-10.033],[-3.703,-8.083],[-0.968,-0.538],[-4.187,-1.303]],"v":[[69,-109],[60.044,-106.643],[50,-101],[41.914,-94.001],[40,-83],[43.017,-79.032],[49,-77],[58.562,-81.461],[66,-88],[76.817,-88.634],[82,-79],[78.971,-68.684],[73,-59],[66.35,-49.791],[60,-42],[44.844,-23.959],[38,-5],[52,14],[60,1],[58,-6],[68,-20],[73,-27],[82,-37],[84,-41],[90,-47],[92,-51],[100,-66],[98,-93],[86,-106],[82,-109]],"c":true}],"h":1},{"t":39,"s":[{"i":[[6.89,-0.815],[3.45,-1.257],[3.629,-2.669],[2.123,-2.867],[-1.331,-4.445],[-1.321,-1.204],[-2.45,-0.294],[-2.437,1.703],[-1.607,-13.824],[3.698,-5.29],[0.365,-0.487],[0.611,-0.78],[0.417,-0.448],[0.686,-0.803],[2.043,-2.273],[2.527,-10.638],[-10.055,1.039],[-0.351,0.734],[-0.86,5.47],[-1.778,2.471],[-3.623,4.033],[-1.799,1.999],[-1.727,2.054],[-0.83,1.103],[-0.403,0.433],[-0.679,0.936],[-0.711,1.062],[0.53,11.664],[4.486,4.612],[1.94,1.026]],"o":[[-2.573,0.304],[-3.45,1.257],[-2.84,2.088],[-2.123,2.867],[0.267,0.889],[1.321,1.204],[6.346,0.762],[8.352,-5.838],[0.651,5.598],[-0.322,0.461],[-0.659,0.878],[-0.361,0.46],[-0.697,0.749],[-2.476,2.9],[-10.39,11.564],[-3.517,14.807],[4.645,-0.48],[2.721,-5.69],[0.214,-1.358],[3.702,-5.144],[2.269,-2.525],[1.844,-2.05],[0.988,-1.175],[0.337,-0.448],[0.745,-0.8],[0.829,-1.142],[4.953,-7.395],[-0.454,-9.995],[-2.485,-2.555],[-5.035,-2.663]],"v":[[73,-109],[63.792,-106.773],[53,-101],[44.872,-93.767],[43,-83],[45.362,-79.554],[51,-77],[64,-84],[87,-80],[79,-60],[77,-59],[76,-56],[74,-55],[73,-52],[66,-45],[42,-12],[55,14],[63,9],[61,-7],[66,-14],[77,-27],[83,-34],[89,-40],[91,-44],[93,-45],[94,-48],[97,-51],[107,-80],[97,-101],[91,-107]],"c":true}],"h":1},{"t":40,"s":[{"i":[[4.56,-0.479],[2.024,-0.262],[1.95,-0.748],[0.607,-0.565],[0.799,-0.444],[1.906,-1.337],[1.585,-2.101],[0.779,-1.504],[-0.362,-2.331],[-4.248,-0.316],[-3.926,2.486],[-1.573,-12.837],[4.023,-5.229],[6.173,-6.924],[2.693,-4.652],[0.019,-5.058],[-8.53,1.272],[-0.963,3.04],[0.206,2.491],[-0.396,3.016],[-1.573,2.228],[-3.55,3.963],[-1.701,1.835],[-3.405,4.389],[-0.712,0.971],[-1.534,2.568],[-1.537,4.72],[-0.22,2.206],[6.333,4.201],[0.665,0.404]],"o":[[-1.954,0.206],[-2.024,0.262],[-0.772,0.296],[-0.607,0.565],[-2.79,1.549],[-1.906,1.337],[-1.247,1.653],[-0.779,1.504],[0.802,5.157],[6.652,0.495],[9.988,-6.325],[0.794,6.479],[-5.783,7.516],[-4.146,4.65],[-2.608,4.505],[-0.039,10.342],[5.007,-0.747],[0.615,-1.941],[-0.203,-2.447],[0.19,-1.452],[3.567,-5.052],[2.286,-2.552],[3.581,-3.864],[0.608,-0.783],[2.013,-2.745],[2.329,-3.899],[0.731,-2.245],[1.388,-13.905],[-0.823,-0.546],[-5.657,-3.436]],"v":[[77,-109],[70.996,-108.407],[65,-107],[63.02,-105.611],[61,-104],[54.097,-99.914],[49,-95],[45.793,-90.508],[45,-85],[54,-77],[67,-85],[91,-80],[82,-59],[60,-35],[48,-20],[43,-5],[58,14],[65,7],[66,2],[63,-6],[68,-13],[79,-26],[85,-33],[97,-45],[99,-49],[104,-56],[109,-67],[111,-74],[99,-104],[97,-106]],"c":true}],"h":1},{"t":41,"s":[{"i":[[5.891,-0.619],[7.054,-4.41],[-0.705,-7.194],[-5.031,-0.092],[-2.824,1.828],[-0.657,0.394],[-2.013,0.636],[-2.125,-0.552],[-0.418,-5.349],[2.56,-3.348],[0.67,-0.82],[2.544,-2.78],[1.253,-1.437],[1.207,-1.257],[0,-14.229],[-8.933,0.442],[-0.698,1.155],[0.579,3.606],[-0.339,2.536],[-0.446,0.479],[-0.686,0.877],[-0.919,0.987],[-0.682,0.766],[-5.785,6.618],[-0.808,0.985],[-0.432,0.464],[-0.687,0.872],[-1.654,2.521],[-1.364,6.519],[8.91,4.888]],"o":[[-6.357,0.668],[-5.62,3.514],[0.498,5.084],[5.931,0.109],[0.694,-0.449],[1.764,-1.058],[2.338,-0.739],[2.344,0.608],[0.506,6.467],[-0.612,0.801],[-2.183,2.67],[-1.231,1.345],[-1.246,1.429],[-12.272,12.774],[0,8.527],[2.795,-0.138],[0.685,-1.133],[-0.352,-2.197],[0.226,-1.696],[0.726,-0.779],[1.372,-1.753],[0.681,-0.732],[7.305,-8.207],[0.724,-0.829],[0.388,-0.473],[0.724,-0.777],[1.893,-2.404],[3.54,-5.395],[3.454,-16.502],[-6.286,-3.448]],"v":[[80,-109],[59,-102],[47,-86],[57,-77],[68,-84],[70,-85],[77,-89],[87,-90],[95,-79],[86,-61],[85,-58],[77,-51],[74,-46],[70,-42],[45,-5],[59,14],[67,10],[68,1],[65,-6],[69,-11],[70,-14],[75,-18],[76,-21],[96,-40],[98,-44],[100,-45],[101,-48],[107,-55],[114,-71],[99,-106]],"c":true}],"h":1},{"t":42,"s":[{"i":[[5.581,-0.632],[7.165,-4.053],[-1.142,-7.166],[-1.553,-1.381],[-2.421,-0.18],[-1.8,1.565],[-1.521,0.984],[-3.539,1.312],[-2.811,-0.562],[-1.738,-1.872],[-0.263,-3.155],[2.61,-4.713],[9.48,-10.878],[1.233,-1.504],[1.8,-3.22],[0.368,-5.272],[-1.843,-2.869],[-6.806,1.534],[-0.796,2.513],[0.246,2.439],[-0.489,3.636],[-0.368,0.49],[-0.662,0.792],[-3.438,3.791],[-5.613,8.462],[-1.561,2.724],[-1.17,6.131],[7.022,5.403],[0.481,0.217],[0.411,0.238]],"o":[[-5.48,0.621],[-7.166,4.053],[0.366,2.296],[1.553,1.381],[3.279,0.244],[1.8,-1.565],[2.031,-1.314],[3.54,-1.312],[1.785,0.357],[1.738,1.872],[0.371,4.452],[-8.341,15.06],[-1.324,1.52],[-2.438,2.974],[-1.597,2.857],[-0.327,4.68],[1.338,2.084],[3.671,-0.828],[0.639,-2.018],[-0.224,-2.216],[0.192,-1.43],[0.677,-0.903],[3.676,-4.395],[7.576,-8.354],[1.656,-2.497],[2.724,-4.753],[2.594,-13.588],[-0.41,-0.315],[-0.349,-0.157],[-5.679,-3.284]],"v":[[82,-109],[60.534,-101.908],[49,-85],[51.959,-79.413],[58,-77],[65.318,-79.579],[70,-84],[78.915,-88.407],[89,-90],[94.642,-86.599],[98,-79],[94,-68],[61,-32],[58,-27],[51,-19],[47,-7],[50,8],[63,14],[69,7],[70,2],[67,-6],[71,-11],[72,-14],[85,-27],[108,-52],[112,-59],[117,-72],[106,-104],[104,-104],[103,-106]],"c":true}],"h":1},{"t":43,"s":[{"i":[[4.927,-0.558],[3.542,-1.123],[3.644,-2.146],[2.59,-2.62],[-0.351,-3.759],[-1.728,-1.527],[-1.838,-0.203],[-1.693,1.27],[-1.334,1.242],[-1.264,0.661],[-0.923,0.407],[-1.021,0.334],[-1.016,0.265],[-2.77,-1.549],[-1.102,-4.061],[2.077,-3.671],[2.572,-3.066],[2.696,-2.589],[1.977,-1.977],[2.091,-10.911],[-10.779,1.307],[-1.057,1.787],[-1.051,5.198],[-2.459,2.941],[-0.93,1.141],[-3.209,2.975],[-2.158,2.158],[-2.093,2.308],[-2.789,7.123],[11.877,6.689]],"o":[[-3.189,0.361],[-3.542,1.123],[-2.991,1.761],[-2.59,2.62],[0.251,2.69],[1.728,1.527],[2.895,0.32],[1.693,-1.27],[1.02,-0.95],[1.264,-0.661],[0.9,-0.398],[1.021,-0.334],[4.816,-1.257],[2.77,1.549],[0.882,3.25],[-2.077,3.671],[-2.239,2.668],[-2.696,2.589],[-9.81,9.81],[-2.071,10.81],[1.08,-0.131],[3.745,-6.328],[0.488,-2.412],[0.944,-1.129],[2.981,-3.657],[2.545,-2.359],[2.037,-2.037],[5.832,-6.43],[6.585,-16.816],[-5.88,-3.312]],"v":[[84,-109],[73.841,-106.839],[63,-102],[53.993,-95.498],[50,-86],[53.309,-79.635],[59,-77],[65.671,-78.829],[70,-83],[73.573,-85.407],[77,-87],[79.913,-88.099],[83,-89],[94.285,-88.489],[100,-80],[97.591,-69.362],[90,-59],[82.303,-50.981],[75,-44],[49,-10],[63,14],[70,10],[69,-7],[75,-14],[77,-18],[87,-28],[95,-34],[101,-41],[118,-64],[105,-106]],"c":true}],"h":1},{"t":44,"s":[{"i":[[5.115,-0.566],[0.943,-0.154],[1.191,-0.267],[0.914,-0.327],[1.32,-0.463],[1.941,-0.604],[1.595,-1.01],[0.14,-0.424],[0.226,-0.169],[0.793,-0.671],[0.484,-0.704],[-9.232,-0.443],[-2.197,1.257],[-5.522,-1.014],[-0.547,-6.508],[2.724,-4.107],[3.049,-2.882],[3.51,-5.292],[1.328,-2.294],[0.363,-1.894],[-1.376,-2.868],[-5.947,0.179],[-0.63,1.987],[-1.123,6.386],[-1.55,1.785],[-9.427,10.506],[-1.063,1.345],[-1.763,2.75],[0.099,8.822],[7.131,4.016]],"o":[[-1.038,0.115],[-0.943,0.154],[-1.026,0.231],[-0.914,0.327],[-1.639,0.575],[-1.941,0.604],[-0.215,0.136],[-0.14,0.424],[-0.907,0.68],[-1.156,0.978],[-5.414,7.879],[4.964,0.238],[4.374,-2.503],[3.374,0.62],[0.679,8.082],[-3.097,4.669],[-5.604,5.298],[-2.105,3.173],[-1.447,2.499],[-1.606,8.373],[2.312,4.819],[6.404,-0.193],[2.077,-6.55],[0.135,-0.768],[10.333,-11.897],[0.89,-0.991],[2.309,-2.921],[3.774,-5.887],[-0.135,-11.968],[-6.041,-3.402]],"v":[[86,-109],[83.114,-108.614],[80,-108],[77.22,-107.174],[74,-106],[68.467,-104.326],[63,-102],[62.508,-101.025],[62,-100],[59,-99],[55,-95],[61,-77],[75,-85],[93,-90],[103,-79],[91,-58],[78,-44],[61,-28],[54,-19],[50,-11],[52,6],[63,14],[72,7],[70,-7],[75,-14],[107,-45],[109,-49],[115,-56],[123,-79],[107,-106]],"c":true}],"h":1},{"t":45,"s":[{"i":[[0.261,-0.024],[8.017,-4.31],[-2.441,-8.274],[-1.328,-1.18],[-2.397,-0.288],[-2.528,1.81],[-1.946,1.114],[-3.131,1.036],[-3.078,-0.516],[-1.994,-1.612],[-0.453,-3.289],[2.299,-3.506],[2.393,-2.681],[2.722,-2.635],[2.206,-2.206],[2.769,-2.663],[2.362,-2.786],[2.18,-3.757],[0.046,-4.341],[-2.666,-3.677],[-4.385,0.463],[1.056,5.63],[-0.76,4],[-2.669,2.987],[-6.78,6.435],[-3.89,11.92],[3.755,5.161],[2.881,1.299],[0.428,0.231],[7.572,0.345]],"o":[[-5.371,0.485],[-8.018,4.31],[0.293,0.992],[1.328,1.18],[2.942,0.353],[2.528,-1.81],[2.397,-1.371],[3.131,-1.036],[1.571,0.263],[1.994,1.612],[0.59,4.296],[-2.299,3.506],[-2.904,3.254],[-2.722,2.635],[-2.564,2.564],[-2.769,2.663],[-2.232,2.632],[-2.181,3.757],[-0.05,4.755],[2.666,3.677],[7.589,-0.801],[-0.441,-2.352],[0.115,-0.608],[6.683,-7.478],[10.619,-10.079],[3.595,-11.016],[-2.432,-3.343],[-0.358,-0.161],[-4.269,-2.305],[-0.985,-0.045]],"v":[[88,-109],[64.641,-101.842],[53,-83],[55.422,-79.472],[61,-77],[69.247,-79.9],[76,-85],[84.489,-88.916],[94,-90],[99.839,-87.269],[104,-80],[100.738,-68.289],[93,-59],[84.476,-50.214],[77,-43],[68.849,-35.167],[61,-27],[53.861,-17.282],[50,-5],[54.174,8.414],[65,14],[73,1],[71,-7],[77,-15],[99,-36],[123,-69],[118,-97],[110,-104],[109,-106],[90,-110]],"c":true}],"h":1},{"t":46,"s":[{"i":[[0.261,-0.024],[3.609,-1.108],[3.998,-2.245],[-0.756,-8.148],[-4.586,-0.172],[-3.519,2.276],[-3.334,0.953],[-4.742,-2.039],[-0.147,-6.015],[1.516,-2.355],[5.212,-4.928],[2.644,-2.52],[2.302,-2.807],[1.252,-1.683],[0.027,-7.326],[-9.201,1.116],[1.156,7.206],[-0.386,2.527],[-2.006,2.278],[-4.659,4.126],[-1.031,0.939],[-4.081,4.977],[-0.432,0.464],[-0.685,0.891],[-1.167,2.216],[0.124,6.681],[1.678,2.899],[2.982,2.713],[0.823,0.444],[6.03,0.275]],"o":[[-3.566,0.322],[-3.609,1.108],[-6.202,3.483],[0.531,5.729],[7.273,0.272],[2.833,-1.832],[3.97,-1.135],[1.682,0.723],[0.086,3.512],[-5.448,8.46],[-2.964,2.802],[-3.241,3.089],[-1.422,1.734],[-3.514,4.726],[-0.035,9.359],[6.251,-0.758],[-0.396,-2.471],[0.249,-1.627],[4.982,-5.658],[1.278,-1.132],[5.173,-4.711],[0.388,-0.473],[0.73,-0.785],[2.117,-2.754],[2.635,-5.004],[-0.117,-6.315],[-2.072,-3.579],[-2.029,-1.847],[-4.47,-2.413],[-0.985,-0.045]],"v":[[89,-109],[78.324,-106.942],[67,-102],[53,-86],[63,-77],[73,-83],[84,-88],[99,-89],[106,-78],[101,-67],[78,-42],[69,-35],[62,-26],[58,-22],[51,-5],[66,14],[74,1],[71,-6],[77,-13],[93,-29],[97,-32],[112,-49],[114,-50],[115,-53],[121,-60],[126,-79],[121,-94],[115,-101],[110,-106],[91,-110]],"c":true}],"h":1},{"t":47,"s":[{"i":[[0.256,-0.022],[2.254,-0.585],[3.206,-1.332],[1.791,-0.633],[1.243,-0.803],[0.459,-0.344],[0.796,-0.69],[0.578,-2.536],[-5.412,-0.37],[-5.389,2.961],[-5.612,-7.141],[4.373,-4.9],[5.997,-4.909],[3.55,-3.74],[1.212,-6.251],[-3.531,-3.523],[-4.555,1.079],[-0.436,0.887],[-0.723,4.885],[-1.207,1.819],[-1.929,1.28],[-0.83,0.751],[-7.632,9.27],[-2.211,5.316],[-0.595,2.568],[2.654,4.923],[0.466,0.486],[3.364,1.516],[0.428,0.231],[8.55,0.39]],"o":[[-3.78,0.33],[-2.254,0.585],[-1.595,0.663],[-1.791,0.633],[-0.437,0.283],[-0.915,0.687],[-2.477,2.147],[-1.81,7.935],[5.485,0.375],[9.229,-5.071],[7.221,9.187],[-7.227,8.097],[-4.679,3.831],[-5.316,5.6],[-1.874,9.669],[2.82,2.814],[1.501,-0.355],[3.521,-7.157],[0.248,-1.678],[1.85,-2.789],[1.033,-0.685],[9.235,-8.355],[4.474,-5.434],[0.69,-1.658],[2.05,-8.853],[-0.787,-1.461],[-2.878,-3.004],[-0.358,-0.161],[-4.426,-2.389],[-0.981,-0.045]],"v":[[90,-109],[81.569,-107.752],[74,-105],[68.736,-103.105],[64,-101],[63,-99],[60,-98],[54,-90],[63,-77],[78,-85],[104,-85],[96,-60],[77,-41],[65,-29],[52,-11],[57,10],[68,14],[74,9],[72,-6],[79,-15],[86,-22],[88,-24],[112,-48],[124,-64],[126,-71],[123,-92],[121,-96],[112,-104],[111,-106],[92,-110]],"c":true}],"h":1},{"t":48,"s":[{"i":[[5.707,-0.548],[3.607,-1.102],[4.005,-2.249],[2.819,-2.546],[-0.378,-4.074],[-1.662,-1.648],[-2.151,-0.238],[-1.707,1.271],[-1.321,1.23],[-4.305,1.431],[-2.995,-0.388],[-2.217,-1.571],[-0.439,-4.495],[2.592,-3.463],[2.102,-2.328],[3.975,-3.638],[4.112,-3.924],[2.697,-3.058],[1.5,-3.18],[0.57,-2.285],[-0.267,-2.771],[-10.363,1.257],[1.29,7.424],[-0.455,2.952],[-2.67,2.951],[-4.807,4.372],[-3.133,3.133],[-1.813,14.684],[3.516,5.047],[5.309,2.786]],"o":[[-3.566,0.342],[-3.607,1.102],[-3.101,1.742],[-2.819,2.546],[0.201,2.169],[1.662,1.648],[2.852,0.316],[1.707,-1.271],[1.786,-1.663],[4.305,-1.431],[1.695,0.22],[2.216,1.571],[0.471,4.82],[-2.592,3.463],[-4.989,5.524],[-3.975,3.638],[-2.714,2.59],[-2.697,3.058],[-0.985,2.088],[-0.571,2.285],[0.413,4.288],[6.161,-0.747],[-0.406,-2.339],[0.384,-2.489],[5.442,-6.015],[2.741,-2.493],[10.566,-10.566],[1.415,-11.462],[-3.734,-5.361],[-5.857,-3.073]],"v":[[90,-109],[79.329,-106.93],[68,-102],[58.391,-95.749],[54,-86],[57.038,-80.051],[63,-77],[69.648,-78.841],[74,-83],[84.094,-88.039],[96,-90],[102.442,-87.706],[107,-79],[102.93,-66.631],[95,-58],[81.841,-44.8],[70,-34],[61.59,-25.442],[55,-16],[52.561,-9.512],[52,-2],[67,14],[75,1],[72,-6],[79,-14],[94,-29],[102,-37],[127,-73],[121,-96],[111,-106]],"c":true}],"h":1},{"t":49,"s":[{"i":[[3.726,-0.269],[3.866,-1.088],[4.144,-2.403],[2.823,-2.612],[-0.276,-3.822],[-1.586,-1.63],[-2.499,-0.282],[-1.548,1.293],[-1.59,1.192],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.118,-3.707],[1.76,-2.908],[6.757,-6.211],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-3.098,4.195],[3.88,12.637],[0.571,0.761],[0.508,0.74],[2.927,1.694],[0.556,0.251]],"o":[[-3.391,0.245],[-3.866,1.088],[-2.984,1.73],[-2.823,2.612],[0.159,2.197],[1.586,1.63],[3.218,0.363],[1.548,-1.293],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[2.825,2.405],[0.108,3.373],[-5.399,8.92],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[4.571,-3.776],[5.652,-7.653],[-0.646,-2.104],[-0.616,-0.821],[-3.524,-5.128],[-0.784,-0.454],[-4.8,-2.162]],"v":[[91,-109],[80.065,-107.118],[68,-102],[58.555,-95.569],[54,-86],[56.744,-80.063],[63,-77],[69.721,-78.833],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[118,-54],[127,-87],[123,-93],[122,-96],[112,-105],[110,-107]],"c":true}],"h":1},{"t":50,"s":[{"i":[[10.014,-0.756],[0.334,0.01],[0.332,-0.013],[7.34,-4.098],[-0.047,-5.375],[-1.561,-1.849],[-2.735,-0.328],[-1.553,1.304],[-1.559,1.17],[-0.564,0.12],[-0.354,0.225],[-4.846,0.868],[-3.148,-2.679],[-0.605,-0.782],[-0.453,-0.912],[2.39,-3.712],[5.022,-4.916],[1.428,-1.092],[0.716,-0.65],[4.753,-5.254],[0.052,-8.184],[-9.639,1.902],[0.967,6.027],[-0.365,2.287],[-0.108,0.144],[-0.519,0.745],[-3.935,3.656],[-4.823,4.433],[-3.771,12.303],[9.393,5.781]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.69,0.232],[-7.339,4.098],[0.02,2.276],[1.561,1.849],[3.227,0.388],[1.553,-1.304],[0.392,-0.294],[0.564,-0.12],[3.47,-2.209],[4.846,-0.868],[1.127,0.96],[0.605,0.782],[2.799,5.636],[-4.487,6.967],[-2.004,1.962],[-0.743,0.568],[-5.103,4.635],[-4.541,5.021],[-0.059,9.36],[6.182,-1.22],[-0.434,-2.705],[0.005,-0.029],[0.621,-0.828],[3.433,-4.925],[6.122,-5.688],[10.354,-9.517],[4.825,-15.744],[-6.583,-4.051]],"v":[[91,-109],[90.001,-108.991],[89,-109],[67.197,-101.857],[54,-87],[56.464,-80.539],[63,-77],[69.751,-78.832],[74,-83],[75.529,-83.551],[77,-84],[90.242,-89.166],[103,-87],[105.506,-84.464],[107,-82],[103,-67],[88,-50],[82,-44],[79,-43],[63,-27],[52,-5],[68,14],[75,1],[72,-6],[74,-7],[75,-10],[86,-21],[101,-35],[126,-68],[114,-104]],"c":true}],"h":1},{"t":51,"s":[{"i":[[10.014,-0.756],[0.334,0.01],[0.332,-0.013],[7.385,-4.171],[-0.434,-6.076],[-1.652,-1.692],[-2.25,-0.27],[-3.046,2.238],[-3.59,1.436],[-3.744,0.083],[-2.191,-1.865],[-0.881,-1.501],[-0.057,-1.79],[1.061,-1.93],[0.82,-1.273],[2.519,-2.714],[2.61,-2.555],[1.071,-1.118],[0.714,-0.546],[0.716,-0.65],[4.921,-5.441],[0,-8.425],[-9.948,1.963],[0.967,6.027],[-0.361,2.289],[-1.893,2.135],[-2.46,2.285],[-4.891,4.496],[-3.77,12.301],[9.393,5.781]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.894,0.24],[-7.385,4.171],[0.141,1.96],[1.652,1.692],[3.227,0.388],[3.046,-2.238],[3.834,-1.534],[3.744,-0.083],[1.418,1.208],[0.881,1.501],[0.064,2.005],[-1.061,1.93],[-2.312,3.591],[-2.519,2.714],[-1.002,0.981],[-1.071,1.118],[-0.743,0.568],[-5.167,4.692],[-4.504,4.98],[0,9.142],[6.182,-1.22],[-0.434,-2.706],[0.105,-0.668],[2.217,-2.501],[6.163,-5.726],[10.351,-9.515],[4.825,-15.744],[-6.583,-4.051]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.754,-101.877],[54,-86],[56.918,-80.233],[63,-77],[72.227,-80.632],[82,-87],[93.732,-89.55],[103,-87],[106.521,-82.937],[108,-78],[106.163,-71.951],[103,-67],[95.723,-57.723],[88,-50],[84.784,-46.674],[82,-44],[79,-43],[63,-27],[52,-5],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[126,-68],[114,-104]],"c":true}],"h":1},{"t":52,"s":[{"i":[[12.38,-0.935],[0.334,0.01],[0.332,-0.013],[7.395,-4.231],[-0.401,-5.84],[-1.589,-1.631],[-2.495,-0.281],[-1.556,1.296],[-1.569,1.177],[-0.708,0.451],[-6.344,-5.401],[-0.118,-3.707],[1.553,-2.412],[2.109,-2.261],[3.479,-3.032],[1.015,-0.925],[4.884,-5.399],[0.457,-0.597],[0.675,-0.881],[0.965,-1.73],[-4.653,-6.739],[-6.717,1.325],[0.967,6.027],[-0.361,2.289],[-1.893,2.135],[-2.46,2.285],[-4.813,4.442],[-3.763,12.278],[3.547,5.784],[3.366,3.062]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.822,0.237],[-7.395,4.231],[0.151,2.196],[1.589,1.631],[3.209,0.362],[1.556,-1.296],[0.784,-0.588],[6.92,-4.404],[2.825,2.405],[0.127,3.97],[-1.794,2.785],[-3.802,4.076],[-1.461,1.273],[-5.345,4.868],[-0.563,0.622],[-0.636,0.832],[-1.303,1.701],[-4.265,7.65],[2.215,3.208],[6.182,-1.22],[-0.434,-2.706],[0.105,-0.668],[2.217,-2.501],[6.121,-5.687],[10.358,-9.559],[2.752,-8.978],[-2.654,-4.329],[-7.422,-6.753]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.832,-101.702],[54,-86],[56.742,-80.064],[63,-77],[69.73,-78.846],[74,-83],[77,-84],[103,-87],[108,-78],[103,-67],[96,-58],[84,-46],[79,-43],[63,-27],[61,-25],[60,-22],[56,-18],[56,8],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[126,-68],[124,-93],[117,-101]],"c":true}],"h":1},{"t":53,"s":[{"i":[[12.304,-0.929],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.586,-1.63],[-2.499,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[1.066,-1.956],[0.799,-1.242],[3.42,-3.683],[2.926,-2.55],[1.015,-0.925],[4.644,-5.08],[-8.281,-11.995],[-6.717,1.325],[0.967,6.027],[-0.361,2.289],[-1.837,2.073],[-2.369,2.222],[-4.859,4.485],[13.718,22.37],[3.366,3.062]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.586,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.061,1.934],[-1.066,1.956],[-2.394,3.718],[-3.42,3.683],[-1.461,1.273],[-5.408,4.924],[-7.65,8.367],[2.215,3.208],[6.182,-1.22],[-0.434,-2.706],[0.097,-0.617],[2.18,-2.459],[6.391,-5.993],[13.758,-12.697],[-2.654,-4.329],[-7.482,-6.808]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.146,-71.981],[103,-67],[93.899,-55.624],[84,-46],[79,-43],[63,-27],[56,8],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[124,-93],[117,-101]],"c":true}],"h":1},{"t":54,"s":[{"i":[[12.304,-0.929],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.586,-1.63],[-2.499,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[1.066,-1.956],[0.799,-1.242],[3.42,-3.683],[2.926,-2.55],[1.015,-0.925],[4.644,-5.08],[-8.281,-11.995],[-6.717,1.325],[0.967,6.027],[-0.361,2.289],[-1.837,2.073],[-2.369,2.222],[-4.843,4.499],[13.699,22.339],[3.366,3.062]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.586,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.061,1.934],[-1.066,1.956],[-2.394,3.718],[-3.42,3.683],[-1.461,1.273],[-5.408,4.924],[-7.65,8.367],[2.215,3.208],[6.182,-1.22],[-0.434,-2.706],[0.097,-0.617],[2.18,-2.459],[6.39,-5.993],[13.777,-12.8],[-2.654,-4.329],[-7.482,-6.808]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.146,-71.981],[103,-67],[93.899,-55.624],[84,-46],[79,-43],[63,-27],[56,8],[68,14],[75,1],[72,-6],[78,-14],[86,-21],[101,-35],[124,-93],[117,-101]],"c":true}],"h":1},{"t":55,"s":[{"i":[[12.3,-0.929],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.586,-1.63],[-2.499,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-6.344,-5.401],[-0.118,-3.707],[1.809,-2.989],[6.743,-6.198],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.831,2.095],[-2.428,2.305],[-3.88,3.668],[-1.006,0.754],[-0.796,0.79],[-2.045,2.458],[-2.43,5.656],[-0.59,2.441],[2.858,4.937],[2.61,2.375]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.586,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[6.92,-4.404],[2.825,2.405],[0.107,3.347],[-5.399,8.921],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.213,-1.349],[2.331,-2.667],[5.725,-5.434],[1.796,-1.698],[0.952,-0.714],[2.279,-2.261],[4.543,-5.459],[0.729,-1.698],[2.288,-9.461],[-1.972,-3.406],[-7.468,-6.795]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[99,-34],[104,-39],[107,-40],[113,-48],[125,-64],[127,-71],[123,-94],[117,-101]],"c":true}],"h":1},{"t":56,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.404,-4.223],[-0.424,-5.87],[-1.585,-1.63],[-2.5,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.118,-3.707],[1.809,-2.989],[6.743,-6.198],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-5.434,5.059],[-0.68,0.664],[-2.148,2.482],[-1.904,2.579],[0.202,10.903],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.404,4.223],[0.159,2.197],[1.585,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[2.825,2.405],[0.107,3.347],[-5.399,8.921],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[7.595,-7.21],[0.69,-0.642],[2.343,-2.289],[2.151,-2.486],[4.975,-6.737],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.828,-101.724],[54,-86],[56.744,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[103,-38],[106,-39],[112,-47],[118,-54],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1},{"t":57,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.406,-4.223],[-0.432,-5.871],[-1.584,-1.63],[-2.502,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[0.787,-1.708],[0.904,-1.495],[6.663,-6.125],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-0.666,0.747],[0.293,15.788],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.406,4.223],[0.162,2.198],[1.584,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.053,1.673],[-0.787,1.708],[-5.368,8.869],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[0.788,-0.651],[8.101,-9.083],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.821,-101.725],[54,-86],[56.745,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.719,-72.866],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[109,-43],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1},{"t":58,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.406,-4.223],[-0.432,-5.871],[-1.584,-1.63],[-2.502,-0.282],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[0.787,-1.708],[0.904,-1.495],[6.663,-6.125],[3.75,-10.485],[-0.208,-3.713],[-1.091,-1.383],[2.067,12.881],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-0.666,0.747],[0.293,15.788],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.806,0.237],[-7.406,4.223],[0.162,2.198],[1.584,1.63],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.053,1.673],[-0.787,1.708],[-5.368,8.869],[-11.244,10.335],[-2.003,5.601],[0.162,2.889],[6.774,8.581],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[0.788,-0.651],[8.101,-9.083],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[66.821,-101.725],[54,-86],[56.745,-80.063],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.719,-72.866],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[109,-43],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1},{"t":59,"s":[{"i":[[0.233,-0.018],[0.334,0.01],[0.332,-0.013],[7.34,-4.074],[-0.087,-5.471],[-1.568,-1.799],[-2.689,-0.303],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.118,-3.707],[1.665,-2.75],[6.591,-6.058],[3.695,-10.333],[-0.208,-3.713],[-1.136,-1.439],[2.07,12.899],[-0.361,2.289],[-2.204,2.522],[-2.383,2.262],[-7.197,5.946],[-3.098,4.195],[0.194,10.463],[2.607,2.721],[3.203,1.444],[0.428,0.231],[6.327,0.288]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.729,0.234],[-7.339,4.074],[0.04,2.501],[1.568,1.799],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[2.825,2.405],[0.102,3.205],[-5.468,9.033],[-11.226,10.319],[-2.003,5.601],[0.162,2.892],[6.756,8.557],[-0.434,-2.706],[0.214,-1.36],[2.256,-2.581],[8.329,-7.906],[4.571,-3.776],[4.943,-6.693],[-0.111,-5.958],[-2.958,-3.088],[-0.358,-0.161],[-4.365,-2.357],[-0.963,-0.044]],"v":[[91,-109],[90.001,-108.991],[89,-109],[67.138,-101.928],[54,-87],[56.513,-80.352],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[108,-78],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[118,-54],[128,-79],[122,-96],[113,-104],[112,-106],[93,-110]],"c":true}],"h":1},{"t":60,"s":[{"i":[[12.291,-0.928],[0.334,0.01],[0.332,-0.013],[7.34,-4.074],[-0.087,-5.471],[-1.568,-1.799],[-2.689,-0.303],[-1.556,1.296],[-1.569,1.177],[-0.564,0.12],[-0.354,0.225],[-4.842,0.875],[-3.172,-2.701],[-0.882,-1.486],[-0.059,-1.853],[0.805,-1.756],[0.833,-1.375],[6.591,-6.058],[3.695,-10.333],[-0.208,-3.713],[-1.136,-1.439],[2.07,12.899],[-0.361,2.289],[-1.78,2.037],[-2.428,2.305],[-7.457,6.16],[-0.666,0.747],[0.293,15.788],[1.709,2.951],[2.701,2.457]],"o":[[-0.331,0.025],[-0.334,-0.01],[-5.729,0.234],[-7.339,4.074],[0.04,2.501],[1.568,1.799],[3.209,0.362],[1.556,-1.296],[0.392,-0.294],[0.564,-0.12],[3.46,-2.202],[4.842,-0.875],[1.412,1.203],[0.882,1.486],[0.051,1.602],[-0.805,1.756],[-5.468,9.033],[-11.226,10.319],[-2.003,5.601],[0.162,2.892],[6.756,8.557],[-0.434,-2.706],[0.207,-1.314],[2.331,-2.667],[8.916,-8.463],[0.788,-0.651],[8.101,-9.083],[-0.118,-6.386],[-1.971,-3.404],[-7.448,-6.777]],"v":[[91,-109],[90.001,-108.991],[89,-109],[67.138,-101.928],[54,-87],[56.513,-80.352],[63,-77],[69.73,-78.846],[74,-83],[75.529,-83.551],[77,-84],[90.216,-89.177],[103,-87],[106.515,-82.988],[108,-78],[106.663,-72.829],[104,-68],[82,-45],[54,-13],[52,-3],[56,9],[75,1],[72,-6],[78,-13],[85,-20],[107,-41],[109,-43],[128,-79],[123,-94],[117,-101]],"c":true}],"h":1}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.28,0.664,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":31,"op":300,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 4","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.28,0.664,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[43.313,-47.836],"ix":2},"a":{"a":0,"k":[43.313,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":24,"op":31,"st":10,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 1","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.28,0.664,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[43.313,-47.836],"ix":2},"a":{"a":0,"k":[43.313,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18,"st":10,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 5","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.28,0.664,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-78.173,-47.836],"ix":2},"a":{"a":0,"k":[-78.173,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":24,"st":10,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Cup 2","parent":15,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.55,-73.91],[49.55,-73.91],[70.876,-52.583],[62.346,32.723],[-62.346,32.723],[-70.876,-52.583]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Cup","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Star 4 :M","parent":15,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[-225,-6.953,0],"to":[75,0,0],"ti":[-75,0,0]},{"t":50,"s":[225,-6.953,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24.984,188.998,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-200.016,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star 4","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-50.016,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star 3","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[99.984,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.278,-3.874],[6.547,-0.032],[5.316,3.822],[2.054,6.217],[-1.993,6.237],[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237]],"o":[[-5.278,3.874],[-6.547,0.032],[-5.316,-3.822],[-2.054,-6.217],[1.993,-6.237],[5.278,-3.874],[6.547,-0.033],[5.316,3.822],[2.054,6.217],[-1.993,6.237]],"v":[[19.304,28.834],[0.146,23.68],[-18.962,29.022],[-19.98,9.209],[-30.965,-7.313],[-12.436,-14.404],[-0.118,-29.957],[12.352,-14.526],[30.95,-7.617],[20.128,9.011]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,188.998],"ix":2},"a":{"a":0,"k":[249.984,188.998],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Star","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Black Stand 2","parent":14,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-24.605,0],[0,0],[18.303,0]],"o":[[-18.303,0],[0,0],[24.605,0],[0,0]],"v":[[-42.653,-29.114],[-53.962,29.114],[53.962,29.114],[42.653,-29.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0314,0.3412,0.6275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Black Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"White Stand 4 :M","parent":14,"tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[-225,-1.544,0],"to":[75,0,0],"ti":[-75,0,0]},{"t":50,"s":[225,-1.544,0]}],"ix":2,"l":2},"a":{"a":0,"k":[24.984,347.302,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-200.016,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand 4","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[-50.016,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand 3","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[99.984,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand 2","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.323,0],[0,0],[-1.582,-4.024],[0,0],[4.323,0],[0,0],[-1.582,4.024],[0,0]],"o":[[0,0],[4.323,0],[0,0],[1.582,4.024],[0,0],[-4.323,0],[0,0],[1.582,-4.024]],"v":[[-25.949,-12.268],[25.998,-12.268],[33.803,-4.464],[37.313,4.464],[31.758,12.268],[-32.174,12.268],[-37.263,4.464],[-33.753,-4.464]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[249.984,347.302],"ix":2},"a":{"a":0,"k":[249.984,347.302],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"White Stand","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Black Stand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"k":[{"s":[90],"t":2,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[88.052],"t":3,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[83.09],"t":4,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[75.985],"t":5,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[67.277],"t":6,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[57.336],"t":7,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[46.447],"t":8,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[34.86],"t":9,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[10.836],"t":11,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":12,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-6.514],"t":13,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-10.253],"t":14,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-11.772],"t":15,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-11.657],"t":16,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-10.457],"t":17,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-8.646],"t":18,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-6.599],"t":19,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-4.592],"t":20,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-2.804],"t":21,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-1.336],"t":22,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.223],"t":23,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.544],"t":24,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.006],"t":25,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.219],"t":26,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.245],"t":27,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[1.142],"t":28,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.963],"t":29,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.75],"t":30,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.535],"t":31,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.34],"t":32,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.176],"t":33,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.049],"t":34,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.04],"t":35,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.097],"t":36,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.125],"t":37,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.132],"t":38,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.124],"t":39,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.107],"t":40,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.085],"t":41,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.062],"t":42,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.041],"t":43,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.023],"t":44,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.008],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.002],"t":46,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.009],"t":47,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.013],"t":48,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.014],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.013],"t":50,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.012],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.01],"t":52,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.007],"t":53,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.005],"t":54,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.003],"t":55,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0.001],"t":56,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":57,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":58,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":59,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":60,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":61,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":62,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":63,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[-0.001],"t":65,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":66,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":67,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":68,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":69,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"p":{"k":[{"s":[138.235,254.547,0],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[143.584,250.368,0],"t":1,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[157.812,240.556,0],"t":2,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[179.791,229.215,0],"t":3,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[209.087,221.759,0],"t":4,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[243.189,225.873,0],"t":5,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[274.404,246.799,0],"t":6,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[294.84,281.274,0],"t":7,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[299.502,322.507,0],"t":8,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[282.589,360.014,0],"t":9,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.984,377.959,0],"t":10,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[228.111,384.013,0],"t":11,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[215.555,387.488,0],"t":12,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[210.454,388.9,0],"t":13,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[210.841,388.792,0],"t":14,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[214.869,387.678,0],"t":15,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[220.951,385.994,0],"t":16,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[227.823,384.092,0],"t":17,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[234.564,382.227,0],"t":18,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[240.567,380.565,0],"t":19,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[245.498,379.201,0],"t":20,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.235,378.166,0],"t":21,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[251.813,377.453,0],"t":22,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[253.364,377.023,0],"t":23,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[254.079,376.826,0],"t":24,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[254.164,376.802,0],"t":25,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[253.818,376.898,0],"t":26,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[253.217,377.064,0],"t":27,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[252.503,377.262,0],"t":28,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[251.782,377.461,0],"t":29,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[251.126,377.643,0],"t":30,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[250.576,377.795,0],"t":31,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[250.15,377.913,0],"t":32,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.849,377.996,0],"t":33,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.66,378.049,0],"t":34,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[249.909,377.98,0],"t":42,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}],"l":2},"a":{"a":0,"k":[0,29.114,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"t":10,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"Elastic Controller","np":5,"mn":"Pseudo/MDS Elastic Controller","ix":1,"en":1,"ef":[{"ty":0,"nm":"Amplitude","mn":"Pseudo/MDS Elastic Controller-0001","ix":1,"v":{"a":0,"k":20,"ix":1}},{"ty":0,"nm":"Frequency","mn":"Pseudo/MDS Elastic Controller-0002","ix":2,"v":{"a":0,"k":40,"ix":2}},{"ty":0,"nm":"Decay","mn":"Pseudo/MDS Elastic Controller-0003","ix":3,"v":{"a":0,"k":60,"ix":3}}]},{"ty":5,"nm":"Elastic Controller 2","np":5,"mn":"Pseudo/MDS Elastic Controller","ix":2,"en":1,"ef":[{"ty":0,"nm":"Amplitude","mn":"Pseudo/MDS Elastic Controller-0001","ix":1,"v":{"a":0,"k":20,"ix":1}},{"ty":0,"nm":"Frequency","mn":"Pseudo/MDS Elastic Controller-0002","ix":2,"v":{"a":0,"k":40,"ix":2}},{"ty":0,"nm":"Decay","mn":"Pseudo/MDS Elastic Controller-0003","ix":3,"v":{"a":0,"k":60,"ix":3}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-24.605,0],[0,0],[18.303,0]],"o":[[-18.303,0],[0,0],[24.605,0],[0,0]],"v":[[-42.653,-29.114],[-53.962,29.114],[53.962,29.114],[42.653,-29.114]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0314,0.3412,0.6275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Black Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Cup","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-152.895,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-11.815,0],[0,0],[1.176,-11.756],[0,0],[5.492,54.916],[0,0]],"o":[[0,0],[11.815,0],[0,0],[-5.492,54.916],[0,0],[-1.176,-11.756]],"v":[[-49.55,-73.91],[49.55,-73.91],[70.876,-52.583],[62.346,32.723],[-62.346,32.723],[-70.876,-52.583]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.24,0.6453,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Cup","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Stand","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-56.636,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[19.235,36.65],[0,0],[-15.853,-38.082],[0,0]],"o":[[0,0],[-20.405,35.342],[0,0],[17.561,-38.659]],"v":[[-33.841,-56.55],[33.841,-56.55],[25.31,56.55],[-25.31,56.55]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.28,0.664,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Stand","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":310,"st":10,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 3","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.28,0.664,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[43.313,-47.836],"ix":2},"a":{"a":0,"k":[43.313,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":24,"st":10,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 6","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.28,0.664,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-78.173,-47.836],"ix":2},"a":{"a":0,"k":[-78.173,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":24,"op":310,"st":10,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 2","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.016,54.049,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[0,0],[11.928,-26.533],[-4,-20],[1.5,-2]],"o":[[-4.5,-7],[-12.25,27.25],[0.88,4.401],[-1.5,2]],"v":[[-64.5,-87],[-116.25,-85.75],[-62.5,-7],[-65.5,4]],"c":false}]},{"t":50,"s":[{"i":[[0,0],[-11.928,-26.533],[4,-20],[-1.5,-2]],"o":[[4.5,-7],[12.25,27.25],[-0.88,4.401],[1.5,2]],"v":[[64.42,-87],[116.17,-85.75],[62.42,-7],[65.42,4]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.28,0.664,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-78.173,-47.836],"ix":2},"a":{"a":0,"k":[-78.173,-47.836],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18,"st":10,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"Pre-comp 1","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":60,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":16,"op":316,"st":16,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"Pre-comp 1","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":11,"op":311,"st":11,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/geoGusser.imageset/Contents.json b/recap/Resources/Assets.xcassets/geoGusser.imageset/Contents.json new file mode 100755 index 0000000..dbc563f --- /dev/null +++ b/recap/Resources/Assets.xcassets/geoGusser.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "art-work-globe.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/geoGusser.imageset/art-work-globe.png b/recap/Resources/Assets.xcassets/geoGusser.imageset/art-work-globe.png new file mode 100755 index 0000000..88f7ae1 Binary files /dev/null and b/recap/Resources/Assets.xcassets/geoGusser.imageset/art-work-globe.png differ diff --git a/recap/Resources/Assets.xcassets/google.imageset/Image.png b/recap/Resources/Assets.xcassets/google.imageset/Image.png deleted file mode 100644 index 68f7b21..0000000 Binary files a/recap/Resources/Assets.xcassets/google.imageset/Image.png and /dev/null differ diff --git a/recap/Resources/Assets.xcassets/googleLogo.imageset/Contents.json b/recap/Resources/Assets.xcassets/googleLogo.imageset/Contents.json new file mode 100644 index 0000000..b6d406d --- /dev/null +++ b/recap/Resources/Assets.xcassets/googleLogo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Google Icons.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/googleLogo.imageset/Google Icons.svg b/recap/Resources/Assets.xcassets/googleLogo.imageset/Google Icons.svg new file mode 100644 index 0000000..c0669b3 --- /dev/null +++ b/recap/Resources/Assets.xcassets/googleLogo.imageset/Google Icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/loadingAnimation.dataset/Contents.json b/recap/Resources/Assets.xcassets/loadingAnimation.dataset/Contents.json new file mode 100755 index 0000000..514fa5c --- /dev/null +++ b/recap/Resources/Assets.xcassets/loadingAnimation.dataset/Contents.json @@ -0,0 +1,12 @@ +{ + "data" : [ + { + "filename" : "loadingAnimation.json", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/recap/Resources/Assets.xcassets/loadingAnimation.dataset/loadingAnimation.json b/recap/Resources/Assets.xcassets/loadingAnimation.dataset/loadingAnimation.json new file mode 100755 index 0000000..4b47634 --- /dev/null +++ b/recap/Resources/Assets.xcassets/loadingAnimation.dataset/loadingAnimation.json @@ -0,0 +1 @@ +{"v":"5.7.8","fr":29.9700012207031,"ip":0,"op":91.000003706506,"w":1080,"h":1080,"nm":"brain","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"right bro Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[299.56,170.306,0],"to":[0,-2.5,0],"ti":[0,0,0]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[299.56,155.306,0],"to":[0,0,0],"ti":[0,-2.5,0]},{"t":90.0000036657751,"s":[299.56,170.306,0]}],"ix":2,"l":2},"a":{"a":0,"k":[39.222,30.056,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-15.873,-20.112]],"o":[[0,0],[0,0]],"v":[[-19.222,0.944],[19.222,10.056]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.3042,0.4043,0.4758,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[39.222,30.056],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"left bro Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[166.049,170.235,0],"to":[0,-2.5,0],"ti":[0,0,0]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[166.049,155.235,0],"to":[0,0,0],"ti":[0,-2.5,0]},{"t":90.0000036657751,"s":[166.049,170.235,0]}],"ix":2,"l":2},"a":{"a":0,"k":[38.783,30.128,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-25.565,-11.185]],"o":[[0,0],[0,0]],"v":[[-18.782,10.127],[18.782,1.058]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.3042,0.4043,0.4758,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[38.783,30.128],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"left eye Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[172.231,246.678,0],"to":[0,-2.5,0],"ti":[0,0,0]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[172.231,231.678,0],"to":[0,0,0],"ti":[0,-2.5,0]},{"t":90.0000036657751,"s":[172.231,246.678,0]}],"ix":2,"l":2},"a":{"a":0,"k":[59.465,49.143,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-26.286,-1.428],[0,38.285]],"o":[[0,0],[0,0],[0,0]],"v":[[-28.152,-17.475],[-3.178,14.285],[29.464,-19.142]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2964,0.3939,0.4636,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[59.464,49.143],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"right eye Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[291.059,245.977,0],"to":[0,-2.5,0],"ti":[0,0,0]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[291.059,230.977,0],"to":[0,0,0],"ti":[0,-2.5,0]},{"t":90.0000036657751,"s":[291.059,245.977,0]}],"ix":2,"l":2},"a":{"a":0,"k":[60.208,47.773,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-26.286,-1.428],[1.514,35.35]],"o":[[0,0],[0,0],[0,0]],"v":[[-28.896,-17.773],[-3.922,13.987],[28.694,-17.577]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2964,0.3939,0.4636,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[60.208,47.773],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"mouth Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[232.162,290.25,0],"to":[0,-2.5,0],"ti":[0,0,0]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[232.162,275.25,0],"to":[0,0,0],"ti":[0,-2.5,0]},{"t":90.0000036657751,"s":[232.162,290.25,0]}],"ix":2,"l":2},"a":{"a":0,"k":[53.39,44.715,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-27.928,32.75]],"o":[[0,0],[0,0]],"v":[[-25.64,-17.015],[23.89,-16.964]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.2728,0.4679,0.6072,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[53.39,44.715],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body p","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[440.491,527.143,0],"to":[0,-8.833,0],"ti":[0,0,0]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[440.491,474.143,0],"to":[0,0,0],"ti":[0,-8.833,0]},{"t":90.0000036657751,"s":[440.491,527.143,0]}],"ix":2,"l":2},"a":{"a":0,"k":[230.615,213.678,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[18.858,-33.116]],"o":[[0,0],[0,0]],"v":[[-17.288,-26.299],[-1.57,26.299]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[18.858,-33.116]],"o":[[0,0],[0,0]],"v":[[-17.288,-26.299],[0.43,19.299]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[18.858,-33.116]],"o":[[0,0],[0,0]],"v":[[-17.288,-26.299],[-1.57,26.299]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[308.265,89.121],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-24.857,-51.233]],"o":[[0,0],[0,0]],"v":[[-32.285,4.563],[32.285,25.617]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-28.428,-49.767]],"o":[[0,0],[0,0]],"v":[[-32.285,4.563],[31.285,15.617]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-24.857,-51.233]],"o":[[0,0],[0,0]],"v":[[-32.285,4.563],[32.285,25.617]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[305.267,119.686],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-2.286,-41.714]],"o":[[0,0],[0,0]],"v":[[26.572,-36.857],[-24.286,36.857]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-9,-53.571]],"o":[[0,0],[0,0]],"v":[[26.572,-36.857],[-20.286,26.857]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-2.286,-41.714]],"o":[[0,0],[0,0]],"v":[[26.572,-36.857],[-24.286,36.857]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[350.41,148.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-18.477,-45.715]],"o":[[0,0],[0,0]],"v":[[18.705,-27.148],[-0.228,27.148]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-18.477,-45.715]],"o":[[0,0],[0,0]],"v":[[18.705,-27.148],[-2.228,21.148]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-18.477,-45.715]],"o":[[0,0],[0,0]],"v":[[18.705,-27.148],[-0.228,27.148]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[158.677,89.969],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[26.953,-47.91]],"o":[[0,0],[0,0]],"v":[[32.191,0.331],[-32.191,23.955]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[27.668,-45.767]],"o":[[0,0],[0,0]],"v":[[32.191,0.331],[-28.191,11.955]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[26.953,-47.91]],"o":[[0,0],[0,0]],"v":[[32.191,0.331],[-32.191,23.955]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[159.647,121.347],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-1.714,-48.857]],"o":[[0,0],[0,0]],"v":[[-27.428,-36.857],[27.428,36.857]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[11.429,-57.571]],"o":[[0,0],[0,0]],"v":[[-20.428,-32.857],[24.428,30.857]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-1.714,-48.857]],"o":[[0,0],[0,0]],"v":[[-27.428,-36.857],[27.428,36.857]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[114.267,148.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-51,15.428]],"o":[[0,0],[0,0]],"v":[[-4.5,28.272],[25.5,-28.272]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-55.571,-8.143]],"o":[[0,0],[0,0]],"v":[[-4.5,28.272],[25.5,-28.272]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-51,15.428]],"o":[[0,0],[0,0]],"v":[[-4.5,28.272],[25.5,-28.272]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[56.196,167.95],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-38.428,-43.286]],"o":[[0,0],[0,0]],"v":[[-45.928,21.643],[45.928,21.643]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-38.428,-43.286]],"o":[[0,0],[0,0]],"v":[[-40.928,22.643],[37.928,21.643]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-38.428,-43.286]],"o":[[0,0],[0,0]],"v":[[-45.928,21.643],[45.928,21.643]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[91.624,212.321],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-64.857,18]],"o":[[0,0],[0,0]],"v":[[-29.857,-33.143],[32.428,15.143]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-64.857,18]],"o":[[0,0],[0,0]],"v":[[-30.857,-46.143],[31.428,2.143]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-64.857,18]],"o":[[0,0],[0,0]],"v":[[-29.857,-33.143],[32.428,15.143]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[97.696,306.535],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[30.12,8.313]],"o":[[0,0],[0,0]],"v":[[20.739,-7.393],[-20.739,-0.92]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[30.12,8.313]],"o":[[0,0],[0,0]],"v":[[22.739,-20.393],[-18.739,-13.92]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[30.12,8.313]],"o":[[0,0],[0,0]],"v":[[20.739,-7.393],[-20.739,-0.92]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[194.457,339.928],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[61.714,-21.857]],"o":[[0,0],[0,0]],"v":[[2.286,-39.505],[-30.857,39.506]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[61.714,-21.857]],"o":[[0,0],[0,0]],"v":[[4.286,-52.505],[-28.857,26.506]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[61.714,-21.857]],"o":[[0,0],[0,0]],"v":[[2.286,-39.505],[-30.857,39.506]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[168.41,325.172],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-29.571,9.857]],"o":[[0,0],[0,0]],"v":[[-22.5,-8.357],[22.5,-1.5]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-29.571,9.857]],"o":[[0,0],[0,0]],"v":[[-25.5,-20.357],[20.5,-14.5]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-29.571,9.857]],"o":[[0,0],[0,0]],"v":[[-22.5,-8.357],[22.5,-1.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[272.195,340.892],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[-42.428,-19.286]],"o":[[0,0],[0,0]],"v":[[-6.643,-40.072],[21.214,40.072]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[-37.428,-34.143]],"o":[[0,0],[0,0]],"v":[[-3.643,-53.072],[12.214,32.072]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[-42.428,-19.286]],"o":[[0,0],[0,0]],"v":[[-6.643,-40.072],[21.214,40.072]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[301.339,324.607],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[65.143,17.572]],"o":[[0,0],[0,0]],"v":[[28.286,-33.215],[-32.572,15.643]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[65.143,17.572]],"o":[[0,0],[0,0]],"v":[[27.286,-46.215],[-33.572,2.643]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[65.143,17.572]],"o":[[0,0],[0,0]],"v":[[28.286,-33.215],[-32.572,15.643]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":9,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[370.981,306.607],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[46.286,-37.428]],"o":[[0,0],[0,0]],"v":[[42,20.428],[-42,17]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[46.286,-37.428]],"o":[[0,0],[0,0]],"v":[[42,20.428],[-35,16]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[46.286,-37.428]],"o":[[0,0],[0,0]],"v":[[42,20.428],[-42,17]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[372.124,213.535],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":0,"s":[{"i":[[0,0],[15.794,-44.259]],"o":[[0,0],[0,0]],"v":[[-23.571,-27.415],[7.778,27.415]],"c":false}]},{"i":{"x":0.336,"y":1},"o":{"x":0.61,"y":0},"t":45,"s":[{"i":[[0,0],[32.08,-65.687]],"o":[[0,0],[0,0]],"v":[[-23.571,-27.415],[7.778,27.415]],"c":false}]},{"t":90.0000036657751,"s":[{"i":[[0,0],[15.794,-44.259]],"o":[[0,0],[0,0]],"v":[[-23.571,-27.415],[7.778,27.415]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[408.267,168.808],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-35.5,10],[-102.857,-50.286],[-61.714,-105.714],[-16.517,-46.823],[-0.605,-1.562],[24,-35.265],[130.857,-34.286],[74.286,25.143],[34.857,54.286],[-30.857,112.571],[-29.313,56.317],[-0.545,1.182]],"o":[[0,0],[0,0],[0,0],[0.558,1.58],[3.349,8.636],[0,0],[0,0],[0,0],[0,0],[0,0],[0.6,-1.155],[2.852,-6.171]],"v":[[-147.348,-107.715],[3.509,-156.285],[152.652,-107.715],[207.805,-55.923],[209.583,-51.219],[198.938,25.428],[99.508,160.285],[1.223,185.999],[-96.491,159.142],[-191.348,22.572],[-201.052,-62.729],[-199.325,-66.23]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.3862,0.6889,0.9538,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[230.615,213.678],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"body b","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[230.517,212.86,0],"ix":2,"l":2},"a":{"a":0,"k":[248.355,230.113,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-38.234,10.77],[-110.777,-54.157],[-66.467,-113.854],[-17.789,-50.429],[-0.653,-1.682],[25.848,-37.981],[140.933,-36.926],[80.006,27.079],[37.541,58.466],[-33.233,121.24],[-31.57,60.654],[-0.588,1.272]],"o":[[0,0],[0,0],[0,0],[0.6,1.702],[3.606,9.301],[0,0],[0,0],[0,0],[0,0],[0,0],[0.647,-1.243],[3.071,-6.647]],"v":[[-158.695,-116.009],[3.778,-168.321],[164.406,-116.009],[223.807,-60.23],[225.721,-55.164],[214.255,27.386],[107.171,172.628],[1.317,200.322],[-103.922,171.397],[-206.083,24.309],[-216.535,-67.561],[-214.674,-71.33]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[248.354,230.113],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"left shoes Outlines","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[112.918,83.222,0],"ix":2,"l":2},"a":{"a":0,"k":[9.837,21.675,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.982,-1.071],[0,0],[-0.214,7.286],[0,0],[0.81,0.797]],"o":[[0,0],[0,0],[0,0],[0.03,-1.136],[-0.844,-0.83]],"v":[[-1.178,-30.643],[-4.178,30.214],[2.785,24.428],[4.148,-27.253],[2.986,-30.322]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.5531,0.5925,0.6269,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[39.246,33.386],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.17,-3.065],[0,0],[-4.765,0.108],[-3.191,-0.102],[-0.537,-0.697],[0,0],[-0.508,0],[0,0],[-0.039,0.738],[0,0],[0,0]],"o":[[0,0],[-0.264,4.758],[1.939,-0.044],[0.88,0.028],[0,0],[0.309,0.402],[0,0],[0.739,0],[0,0],[0,0],[-3.066,-0.154]],"v":[[-17.388,-26.754],[-18.888,1.426],[-11.427,9.889],[-4.424,10.064],[-2.181,11.207],[13.263,31.251],[14.56,31.889],[14.578,31.889],[15.966,30.573],[19.152,-29.961],[-12.701,-31.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[19.402,32.139],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"left foot Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":45,"s":[10]},{"t":90.0000036657751,"s":[0]}],"ix":10},"p":{"a":0,"k":[116.038,382.706,0],"ix":2,"l":2},"a":{"a":0,"k":[38.018,9.875,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-18.571,-4.571],[0,0],[0,0],[0,0],[6.286,23.143],[-9.614,15.857],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.072,-40.429],[-23.643,-4.858],[-13.071,17.142],[68.643,25.071],[66.714,48.5],[-17.643,40.857],[-47.357,13.428],[-43.528,-17.143],[-21.928,-50]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0.75,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[55.018,47.875],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"right shoes Outlines","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[3.158,82.735,0],"ix":2,"l":2},"a":{"a":0,"k":[38.837,20.675,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.982,-1.071],[0,0],[0.214,7.286],[0,0],[-0.81,0.797]],"o":[[0,0],[0,0],[0,0],[-0.03,-1.136],[0.844,-0.83]],"v":[[1.179,-30.643],[4.179,30.214],[-2.786,24.428],[-4.149,-27.253],[-2.986,-30.322]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.5531,0.5925,0.6269,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[4.428,33.386],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.17,-3.065],[0,0],[4.765,0.108],[3.191,-0.102],[0.537,-0.697],[0,0],[0.508,0],[0,0],[0.039,0.738],[0,0],[0,0]],"o":[[0,0],[0.264,4.758],[-1.939,-0.044],[-0.88,0.028],[0,0],[-0.309,0.402],[0,0],[-0.739,0],[0,0],[0,0],[3.066,-0.154]],"v":[[17.389,-26.754],[18.889,1.426],[11.427,9.889],[4.424,10.064],[2.181,11.207],[-13.263,31.251],[-14.56,31.889],[-14.578,31.889],[-15.966,30.573],[-19.152,-29.961],[12.701,-31.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24.272,32.14],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"right foot Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":45,"s":[-10]},{"t":90.0000036657751,"s":[0]}],"ix":10},"p":{"a":0,"k":[345.839,385.964,0],"ix":2,"l":2},"a":{"a":0,"k":[71.018,12.875,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[18.571,-4.571],[0,0],[0,0],[0,0],[-6.286,23.143],[9.614,15.857],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-0.072,-41.929],[21.643,-4.858],[11.071,17.142],[-47.643,21.321],[-45.714,44.75],[17.643,40.857],[47.357,13.428],[43.528,-17.143],[21.929,-51.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0.75,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[55.017,47.875],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"right hand Outlines","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[77.282,4.434,0],"ix":2,"l":2},"a":{"a":0,"k":[31.897,64.505,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.723,-0.806],[0,0],[0,0],[0.427,0.658],[-1.787,2.122],[0.664,0.186],[1.464,12.242],[-1.189,2.572],[-4.41,-1.161],[-0.905,-7.233],[-0.263,-0.647],[-2.017,0.236],[-0.114,1.632],[1.346,1.026],[1.309,0.072],[0,0],[1.529,4.004],[-4.066,2.863],[-2.759,0.196],[-2.094,-1.725],[0.357,3],[-9,-0.536],[0.643,-10.821],[-3.643,4.285],[-7.393,-4.822],[3.619,-12.968],[1.347,-2.432],[-3.919,1.435],[-1.172,0.093],[-1.414,-3.086],[7.607,-7.5],[15.141,-3.528]],"o":[[0,0],[0,0],[-0.746,-0.239],[-0.988,-1.524],[0.444,-0.526],[-4.062,-1.138],[-0.337,-2.813],[1.272,-2.753],[0,0],[0.086,0.693],[0.498,1.226],[1.625,-0.19],[0.065,-0.935],[-1.042,-0.794],[0,0],[-4.28,-0.234],[-1.114,-2.916],[2.261,-1.592],[2.817,-0.2],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-0.747,2.678],[-0.769,1.387],[1.104,-0.405],[2.325,-0.186],[0,0],[0,0],[-1.055,0.246]],"v":[[8.317,28.996],[2.253,35.755],[-23.32,28.566],[-25.125,27.177],[-25.712,20.664],[-26.157,19.155],[-41.31,1.808],[-40.149,-6.462],[-32.068,-10.791],[-22.917,-3.156],[-22.456,-1.122],[-18.895,1.479],[-15.776,-1.621],[-17.394,-4.683],[-21.037,-5.945],[-22.453,-6.023],[-32.161,-12.867],[-29.071,-22.057],[-21.344,-24.661],[-12.675,-23.005],[-10.639,-24.148],[-2.818,-33.827],[7.254,-24.934],[10.468,-23.755],[22.575,-26.755],[26.081,-11.784],[22.802,-4.146],[25.822,-2.8],[29.255,-3.577],[36.182,-0.291],[34.04,13.316],[11.077,27.451]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.897,36.005],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"right arm Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":45,"s":[-11]},{"t":90.0000036657751,"s":[0]}],"ix":10},"p":{"a":0,"k":[441.267,269.714,0],"ix":2,"l":2},"a":{"a":0,"k":[3.785,46.678,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-18.428,54.893],[0,0],[43.893,2.454]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-39.178,28.035],[42.036,-21.822],[17.464,-27.428],[-40.036,6.367]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[45.285,31.678],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"left hand Outlines","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[11.289,4.433,0],"ix":2,"l":2},"a":{"a":0,"k":[51.897,66.506,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.723,-0.806],[0,0],[0,0],[-0.427,0.658],[1.787,2.122],[-0.664,0.186],[-1.464,12.242],[1.189,2.572],[4.41,-1.161],[0.905,-7.233],[0.263,-0.647],[2.017,0.236],[0.114,1.632],[-1.346,1.026],[-1.309,0.072],[0,0],[-1.529,4.004],[4.066,2.863],[2.759,0.196],[2.094,-1.724],[-0.357,3],[9,-0.536],[-0.643,-10.822],[3.643,4.286],[7.393,-4.821],[-3.619,-12.968],[-1.347,-2.432],[3.919,1.435],[1.172,0.093],[1.414,-3.086],[-7.607,-7.5],[-15.141,-3.528]],"o":[[0,0],[0,0],[0.746,-0.24],[0.988,-1.524],[-0.444,-0.526],[4.062,-1.138],[0.337,-2.813],[-1.272,-2.753],[0,0],[-0.086,0.693],[-0.498,1.226],[-1.625,-0.19],[-0.065,-0.935],[1.042,-0.794],[0,0],[4.28,-0.234],[1.114,-2.916],[-2.261,-1.592],[-2.817,-0.2],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0.747,2.678],[0.769,1.387],[-1.104,-0.405],[-2.325,-0.186],[0,0],[0,0],[1.055,0.246]],"v":[[-8.317,28.996],[-2.253,35.755],[23.32,28.566],[25.125,27.177],[25.712,20.664],[26.157,19.156],[41.31,1.808],[40.149,-6.461],[32.068,-10.791],[22.917,-3.156],[22.456,-1.122],[18.895,1.479],[15.776,-1.62],[17.394,-4.682],[21.037,-5.945],[22.453,-6.023],[32.161,-12.867],[29.071,-22.057],[21.344,-24.661],[12.675,-23.005],[10.639,-24.148],[2.818,-33.827],[-7.254,-24.934],[-10.468,-23.755],[-22.575,-26.755],[-26.081,-11.784],[-22.802,-4.146],[-25.822,-2.799],[-29.255,-3.577],[-36.182,-0.291],[-34.04,13.316],[-11.077,27.451]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.851,0.8784,0.902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.897,36.006],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"left arm Outlines","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.336],"y":[1]},"o":{"x":[0.61],"y":[0]},"t":45,"s":[11]},{"t":90.0000036657751,"s":[0]}],"ix":10},"p":{"a":0,"k":[22.063,270.662,0],"ix":2,"l":2},"a":{"a":0,"k":[86.285,46.679,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[16.071,52.5],[0,0],[-48.428,4.689]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[41.929,26.035],[-42.786,-23.321],[-21.215,-29.178],[42.786,4.367]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.4983,0.7241,0.9217,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[45.285,31.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[440.725,812,0],"ix":2,"l":2},"a":{"a":0,"k":[-112,290,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.325,0.325,0.667],"y":[1,1,1]},"o":{"x":[0.726,0.726,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.325,0.325,0.667],"y":[1,1,1]},"o":{"x":[0.726,0.726,0.333],"y":[0,0,0]},"t":45,"s":[83,100,100]},{"t":90.0000036657751,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[436,36],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1255,0.1529,0.1725,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-112,290],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[133.945,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":270.000010997325,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[692.56,540,0],"ix":2,"l":2},"a":{"a":0,"k":[540,540,0],"ix":1,"l":2},"s":{"a":0,"k":[138,138,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":1080,"ip":0,"op":270.000010997325,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/recap/Resources/Assets.xcassets/old man.imageset/Contents.json b/recap/Resources/Assets.xcassets/old man.imageset/Contents.json old mode 100644 new mode 100755 index 2f7d201..742694e --- a/recap/Resources/Assets.xcassets/old man.imageset/Contents.json +++ b/recap/Resources/Assets.xcassets/old man.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "old man_.png", + "filename" : "question.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/recap/Resources/Assets.xcassets/old man.imageset/old man_.png b/recap/Resources/Assets.xcassets/old man.imageset/old man_.png deleted file mode 100644 index ad6bdf5..0000000 Binary files a/recap/Resources/Assets.xcassets/old man.imageset/old man_.png and /dev/null differ diff --git a/recap/Resources/Assets.xcassets/old man.imageset/question.png b/recap/Resources/Assets.xcassets/old man.imageset/question.png new file mode 100644 index 0000000..c6d9a34 Binary files /dev/null and b/recap/Resources/Assets.xcassets/old man.imageset/question.png differ diff --git a/recap/Resources/Assets.xcassets/oldMan.imageset/Contents.json b/recap/Resources/Assets.xcassets/oldMan.imageset/Contents.json old mode 100644 new mode 100755 index 07dc404..742694e --- a/recap/Resources/Assets.xcassets/oldMan.imageset/Contents.json +++ b/recap/Resources/Assets.xcassets/oldMan.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "🦆 icon _old man_.png", + "filename" : "question.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/recap/Resources/Assets.xcassets/oldMan.imageset/question.png b/recap/Resources/Assets.xcassets/oldMan.imageset/question.png new file mode 100644 index 0000000..c6d9a34 Binary files /dev/null and b/recap/Resources/Assets.xcassets/oldMan.imageset/question.png differ diff --git "a/recap/Resources/Assets.xcassets/oldMan.imageset/\360\237\246\206 icon _old man_.png" "b/recap/Resources/Assets.xcassets/oldMan.imageset/\360\237\246\206 icon _old man_.png" deleted file mode 100644 index 48b5f8a..0000000 Binary files "a/recap/Resources/Assets.xcassets/oldMan.imageset/\360\237\246\206 icon _old man_.png" and /dev/null differ diff --git a/recap/Resources/Assets.xcassets/person_image.imageset/Contents.json b/recap/Resources/Assets.xcassets/person_image.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/person_image.imageset/WhatsApp Image 2024-11-18 at 10.46.45.jpeg b/recap/Resources/Assets.xcassets/person_image.imageset/WhatsApp Image 2024-11-18 at 10.46.45.jpeg old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/puzzleGame.imageset/Contents.json b/recap/Resources/Assets.xcassets/puzzleGame.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/puzzleGame.imageset/puzzleGame.png b/recap/Resources/Assets.xcassets/puzzleGame.imageset/puzzleGame.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/recapLogo.imageset/Contents.json b/recap/Resources/Assets.xcassets/recapLogo.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/recapLogo.imageset/recapLogo.png b/recap/Resources/Assets.xcassets/recapLogo.imageset/recapLogo.png index 931ba09..ce79578 100644 Binary files a/recap/Resources/Assets.xcassets/recapLogo.imageset/recapLogo.png and b/recap/Resources/Assets.xcassets/recapLogo.imageset/recapLogo.png differ diff --git a/recap/Resources/Assets.xcassets/sortGame.imageset/Contents.json b/recap/Resources/Assets.xcassets/sortGame.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/sortGame.imageset/sortGame.png b/recap/Resources/Assets.xcassets/sortGame.imageset/sortGame.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/sparkleLogo.imageset/Contents.json b/recap/Resources/Assets.xcassets/sparkleLogo.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/sparkleLogo.imageset/sparkleLogo.png b/recap/Resources/Assets.xcassets/sparkleLogo.imageset/sparkleLogo.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/wordGame.imageset/Contents.json b/recap/Resources/Assets.xcassets/wordGame.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/recap/Resources/Assets.xcassets/wordGame.imageset/wordGame.png b/recap/Resources/Assets.xcassets/wordGame.imageset/wordGame.png old mode 100644 new mode 100755 diff --git a/recap/Resources/Constants.swift b/recap/Resources/Constants.swift new file mode 100755 index 0000000..7d43fe5 --- /dev/null +++ b/recap/Resources/Constants.swift @@ -0,0 +1,154 @@ +// +// Constants.swift +// recap +// +// Created by Diptayan Jash on 05/02/25. +// + +import Foundation +import UIKit + +enum Constants { + enum BGs{ + static let GreyBG = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1) + } + + enum UserDefaultsKeys { + static let HasCompletedOnboarding = "HasCompletedOnboarding" + + static let isFamilyMemberLoggedIn = "isUserLoggedIn" + static let hasPatientCompletedProfile = "hasCompletedProfile" + + static let verifiedUserDocID = "verifiedUserDocID" + + static let familyMemberDetails = "familyMemberDetails" + static let familyMemberImageURL = "familyMemberImageURL" + + static let isPatientLoggedIn = "isPatientLoggedIn" + static let patientDetails = "patientDetails" + } + + enum FirestoreKeys { + static let usersCollection = "users" + static let familyMembersCollection = "family_members" + } + + enum StorageKeys { + } + + enum paddingKeys{ + static let DefaultPaddingLeft = 16.0 + static let DefaultPaddingRight = -16.0 + static let DefaultPaddingTop = 10.0 + static let DefaultPaddingBottom = -16.0 + } + enum CardSize{ + static let DefaultCardHeight = 160.0 + static let DefaultCardWidth = 180.0 + static let DefaultCardCornerRadius = 15.0 + } + enum ButtonStyle{ + static let DefaultButtonHeight = 56.0 + static let DefaultButtonWidth = 120.0 + static let DefaultButtonCornerRadius = 15.0 + static let DefaultButtonFontSize = 12.0 + static let DefaultButtonFont = UIFont.boldSystemFont(ofSize: 18) + static let DefaultButtonBackgroundColor = UIColor.systemBlue.withAlphaComponent(0.2) + static let DefaultButtonTextColor = UIColor.systemBlue + } + + enum FontandColors { + // Font definitions + static let titleFont = UIFont.systemFont(ofSize: 20, weight: .bold) // SF Pro Bold + static let subtitleFont = UIFont.systemFont(ofSize: 18, weight: .regular) // SF Pro Display Regular + static let descriptionFont = UIFont.systemFont(ofSize: 16, weight: .light) // SF Pro Display Light + + // Colors (converted to UIColor for immediate use) + static let titleColor = UIColor.black + static let subtitleColor = UIColor.gray + static let descriptionColor = UIColor.systemGray2 + static let chevronName = "chevron.right" + static let chevronColor = UIColor.gray + + static let defaultshadowColor = UIColor.black.cgColor + static let defaultshadowOpacity = 0.1 + static let defaultshadowOffset = CGSize(width: 0, height: 2) + static let defaultshadowRadius = 2.0 + } + + enum NotificationNames { + static let FamilyMemberAdded = "FamilyMemberAdded" + } +} +struct AppColors { + // MARK: - Primary Colors + static let primaryButtonColor = iconColor.withAlphaComponent(0.2) + static let savedButtonColor = iconColor.withAlphaComponent(0.1) + + + // MARK: - Background Gradient Colors + static let gradientStartColor = UIColor(red: 0.69, green: 0.88, blue: 0.88, alpha: 1.0) + static let gradientEndColor = UIColor(red: 0.94, green: 0.74, blue: 0.80, alpha: 1.0) // Soft rose-pink + + + // MARK: - Card Background Colors + // static let cardBackgroundColor = UIColor(appHex: "#EDE7F6") // Lavender Mist for card backgrounds + static let cardBackgroundColor = UIColor(hex: "#F4F6FF") + // MARK: - icon Colors + static let iconColor = UIColor(hex: "#0B8494") // Teal-blue base + + // MARK: - Icon and Symbol Colors + static let selectedIconColor = iconColor // Same as base for consistency + + + // MARK: - Text Colors + static let primaryButtonTextColor = iconColor + + // MARK: - Text Colors + static let primaryTextColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0) // Dark charcoal for better readability + static let secondaryTextColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1.0) // Medium gray for secondary text + static let inverseTextColor = UIColor.white // White text for use on darker backgrounds + // MARK: - Button Colors + + static let secondaryButtonColor = iconColor.withAlphaComponent(0.1) + static let secondaryButtonTextColor = iconColor + + + // MARK: - Interactive Colors + static let highlightColor = iconColor.withAlphaComponent(0.3) + static let errorColor = UIColor(red: 0.90, green: 0.40, blue: 0.40, alpha: 1.0) // Keeping error red + static let successColor = UIColor(red: 0.40, green: 0.80, blue: 0.40, alpha: 1.0) + + // MARK: - Gradient Layer for Background + static func createAppBackgroundGradientLayer() -> CAGradientLayer { + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [ + AppColors.gradientStartColor.cgColor, + AppColors.gradientEndColor.cgColor, + ] + gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) + gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + return gradientLayer + } + + // MARK: - Accessibility Helpers + static func getContrastingTextColor(for backgroundColor: UIColor) -> UIColor { + // Simple luminance calculation to determine text color + let luminance = (0.299 * backgroundColor.components.red + + 0.587 * backgroundColor.components.green + + 0.114 * backgroundColor.components.blue) + return luminance > 0.5 ? primaryTextColor : inverseTextColor + } +} + +// Extension to help with color component access +extension UIColor { + var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + getRed(&r, green: &g, blue: &b, alpha: &a) + return (r, g, b, a) + } +} diff --git a/recap/Resources/GoogleService-Info.plist b/recap/Resources/GoogleService-Info.plist old mode 100644 new mode 100755 index e1876ae..a9d8cbe --- a/recap/Resources/GoogleService-Info.plist +++ b/recap/Resources/GoogleService-Info.plist @@ -3,9 +3,9 @@ CLIENT_ID - 434967820660-bg8mr7r7qg62vbghtp7pkojqk72lqubb.apps.googleusercontent.com + 434967820660-bc03v9oovo92hnstjrvs5gb9d4lm4ovb.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.434967820660-bg8mr7r7qg62vbghtp7pkojqk72lqubb + com.googleusercontent.apps.434967820660-bc03v9oovo92hnstjrvs5gb9d4lm4ovb API_KEY AIzaSyCWgPB9nNjOgG3vVm7peJIOmnnyYgrVFR0 GCM_SENDER_ID @@ -13,24 +13,24 @@ PLIST_VERSION 1 BUNDLE_ID - in.recap.recap + com.srmist.recap PROJECT_ID recap-c5f09 STORAGE_BUCKET recap-c5f09.firebasestorage.app IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:434967820660:ios:6dc972bd2e611ca5921421 + 1:434967820660:ios:b6b874544954fb49921421 DATABASE_URL https://recap-c5f09-default-rtdb.firebaseio.com - + \ No newline at end of file diff --git a/recap/Resources/GoogleService-Info2.plist b/recap/Resources/GoogleService-Info2.plist deleted file mode 100644 index fe63b62..0000000 --- a/recap/Resources/GoogleService-Info2.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CLIENT_ID - 897493696741-nq8pdb29e2iqvlouogipppogu6t5c1gl.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.897493696741-nq8pdb29e2iqvlouogipppogu6t5c1gl - API_KEY - AIzaSyBrMl2kn3Qe_ej8DFwDDvYRn3-ZDi6MVUI - GCM_SENDER_ID - 897493696741 - PLIST_VERSION - 1 - BUNDLE_ID - in.djdiptayan.recap - PROJECT_ID - recap-37d0c - STORAGE_BUCKET - recap-37d0c.firebasestorage.app - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:897493696741:ios:c6b82c1ca9c5ba446dd40c - DATABASE_URL - https://recap-37d0c-default-rtdb.firebaseio.com - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - com.googleusercontent.apps.897493696741-nq8pdb29e2iqvlouogipppogu6t5c1gl - - - - - \ No newline at end of file diff --git a/recap/Resources/Info.plist b/recap/Resources/Info.plist old mode 100644 new mode 100755 index e9e7854..ef812c8 --- a/recap/Resources/Info.plist +++ b/recap/Resources/Info.plist @@ -13,7 +13,7 @@ firebase Auth CFBundleURLSchemes - com.googleusercontent.apps.434967820660-bg8mr7r7qg62vbghtp7pkojqk72lqubb + com.googleusercontent.apps.434967820660-bc03v9oovo92hnstjrvs5gb9d4lm4ovb diff --git a/recap/Resources/Pacifico-Regular.ttf b/recap/Resources/Pacifico-Regular.ttf old mode 100644 new mode 100755 diff --git a/recap/Resources/bn-IN.lproj/Localizable.strings b/recap/Resources/bn-IN.lproj/Localizable.strings new file mode 100755 index 0000000..695451e --- /dev/null +++ b/recap/Resources/bn-IN.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* + Localizable.strings + recap + + Created by Diptayan Jash on 11/02/25. + +*/ +"choose_language" = "আপনার ভাষা চয়ন করুন"; +"language_subtitle" = "অ্যাপ ইন্টারফেসের জন্য আপনার পছন্দের ভাষা নির্বাচন করুন"; diff --git a/recap/Resources/en.lproj/Localizable.strings b/recap/Resources/en.lproj/Localizable.strings new file mode 100755 index 0000000..09427f7 --- /dev/null +++ b/recap/Resources/en.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* + Localizable.strings + recap + + Created by Diptayan Jash on 11/02/25. + +*/ +"choose_language" = "Choose Your Language"; +"language_subtitle" = "Select the language you prefer for the app interface"; diff --git a/recap/Resources/hi.lproj/Localizable.strings b/recap/Resources/hi.lproj/Localizable.strings new file mode 100755 index 0000000..936736d --- /dev/null +++ b/recap/Resources/hi.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* + Localizable.strings + recap + + Created by Diptayan Jash on 11/02/25. + +*/ +"choose_language" = "अपनी भाषा चुनें"; +"language_subtitle" = "ऐप इंटरफ़ेस के लिए अपनी पसंदीदा भाषा चुनें"; diff --git a/recap/ViewModels/.DS_Store b/recap/ViewModels/.DS_Store old mode 100644 new mode 100755 diff --git a/recap/ViewModels/AnimatedGradientBackground.swift b/recap/ViewModels/AnimatedGradientBackground.swift new file mode 100755 index 0000000..eb345f2 --- /dev/null +++ b/recap/ViewModels/AnimatedGradientBackground.swift @@ -0,0 +1,126 @@ +// +// AnimatedGradientBackground.swift +// recap +// +// Created by Diptayan Jash on 04/03/25. +// + +import Foundation +import UIKit + +class AnimatedGradientBackground: NSObject { + // Container layer for the animation + private let gradientLayer = CAGradientLayer() + + // Array of gradient colors to cycle through + private let colorSets: [[CGColor]] = [ + // Health app-like colors - soothing blues to pinks to purples + [ + UIColor(red: 0.42, green: 0.55, blue: 0.97, alpha: 1.0).cgColor, + UIColor(red: 0.72, green: 0.53, blue: 0.93, alpha: 1.0).cgColor, + ], + [ + UIColor(red: 0.72, green: 0.53, blue: 0.93, alpha: 1.0).cgColor, + UIColor(red: 0.94, green: 0.42, blue: 0.71, alpha: 1.0).cgColor, + ], + [ + UIColor(red: 0.94, green: 0.42, blue: 0.71, alpha: 1.0).cgColor, + UIColor(red: 0.40, green: 0.76, blue: 0.93, alpha: 1.0).cgColor, + ], + [ + UIColor(red: 0.40, green: 0.76, blue: 0.93, alpha: 1.0).cgColor, + UIColor(red: 0.42, green: 0.55, blue: 0.97, alpha: 1.0).cgColor, + ], + ] + + private var colorIndex = 0 + private var rotationAnimation: CABasicAnimation? + private var extendedFrame: CGRect = .zero + + // Setup gradient background on view with extended size for scrolling + func setupGradient(for view: UIView, scrollableHeight: CGFloat = 1000) { + // Create an extended frame that's taller than the view for scrolling + extendedFrame = CGRect(x: 0, y: -scrollableHeight / 2, + width: view.bounds.width, + height: view.bounds.height + scrollableHeight) + + // Configure the gradient layer + gradientLayer.colors = colorSets[colorIndex] + gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0) + gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + gradientLayer.frame = extendedFrame + + // Insert at the bottom of the layer stack + view.layer.insertSublayer(gradientLayer, at: 0) + + // Start the animation + animateGradient() + animateRotation() + } + + // Update gradient position based on scroll offset + func updateForScrollPosition(yOffset: CGFloat) { + CATransaction.begin() + CATransaction.setDisableActions(true) + // Move gradient layer based on scroll + let newPosition = extendedFrame.origin.y + (yOffset * 0.5) // Parallax effect (0.5 = half speed) + gradientLayer.frame.origin.y = newPosition + CATransaction.commit() + } + + // Called when device orientation changes or view size changes + func updateGradientFrame(for view: UIView, scrollableHeight: CGFloat = 1000) { + extendedFrame = CGRect(x: 0, y: -scrollableHeight / 2, + width: view.bounds.width, + height: view.bounds.height + scrollableHeight) + + CATransaction.begin() + CATransaction.setDisableActions(true) + gradientLayer.frame = extendedFrame + CATransaction.commit() + } + + // Animate between gradient colors + private func animateGradient() { + // Move to next color set + colorIndex = (colorIndex + 1) % colorSets.count + + // Create animation + let colorChangeAnimation = CABasicAnimation(keyPath: "colors") + colorChangeAnimation.duration = 4.0 // Animation duration + colorChangeAnimation.toValue = colorSets[colorIndex] + colorChangeAnimation.fillMode = .forwards + colorChangeAnimation.isRemovedOnCompletion = false + colorChangeAnimation.delegate = self + + // Apply animation + gradientLayer.add(colorChangeAnimation, forKey: "colorChange") + } + + // Add rotation animation to the gradient + private func animateRotation() { + // Create rotation animation + let fullRotation = CGFloat.pi * 2 + let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotationAnimation.fromValue = 0 + rotationAnimation.toValue = fullRotation + rotationAnimation.duration = 20.0 // Slower rotation + rotationAnimation.repeatCount = .infinity + + // Add rotation animation + gradientLayer.add(rotationAnimation, forKey: "rotationAnimation") + + self.rotationAnimation = rotationAnimation + } +} + +// MARK: - Animation Delegate + +extension AnimatedGradientBackground: CAAnimationDelegate { + func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { + if flag && anim == gradientLayer.animation(forKey: "colorChange") { + gradientLayer.colors = colorSets[colorIndex] + animateGradient() + } + } +} diff --git a/recap/ViewModels/MemoryViewModel.swift b/recap/ViewModels/MemoryViewModel.swift old mode 100644 new mode 100755 diff --git a/recap/ViewModels/showAlert.swift b/recap/ViewModels/showAlert.swift old mode 100644 new mode 100755 index 1e7acbf..a7059cb --- a/recap/ViewModels/showAlert.swift +++ b/recap/ViewModels/showAlert.swift @@ -15,8 +15,9 @@ extension UIViewController { message: message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "ok", style: .default) { _ in - self.dismiss(animated: true) + alert.addAction(UIAlertAction(title: actionTitle, style: .default) { _ in + // Only call completion if provided, don't dismiss the view controller + completion?() }) present(alert, animated: true) } diff --git a/recap/ViewModels/userDefaultStorage.swift b/recap/ViewModels/userDefaultStorage.swift old mode 100644 new mode 100755 index 36b0a87..9931b3f --- a/recap/ViewModels/userDefaultStorage.swift +++ b/recap/ViewModels/userDefaultStorage.swift @@ -141,14 +141,6 @@ class UserDefaultsStorageFamilyMember: FamilyStorageProtocol { return members } -// func getFamilyMemberImage(for id: UUID) -> UIImage? { -// guard let imageDict = defaults.dictionary(forKey: Keys.familyImages) as? [String: Data], -// let imageData = imageDict[id.uuidString] else { -// return nil -// } -// return UIImage(data: imageData) -// } - func clearFamilyData() { defaults.removeObject(forKey: Keys.familyMembers) defaults.removeObject(forKey: Keys.familyImages) diff --git a/recap/Views/.DS_Store b/recap/Views/.DS_Store old mode 100644 new mode 100755 index ffa0cd6..f92451e Binary files a/recap/Views/.DS_Store and b/recap/Views/.DS_Store differ diff --git a/recap/Views/ActivityCell.swift b/recap/Views/ActivityCell.swift old mode 100644 new mode 100755 index fcf8135..d8d698c --- a/recap/Views/ActivityCell.swift +++ b/recap/Views/ActivityCell.swift @@ -13,7 +13,7 @@ class ActivityCell: UITableViewCell { private let iconImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit - imageView.layer.cornerRadius = 15 + imageView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius imageView.clipsToBounds = true imageView.widthAnchor.constraint(equalToConstant: 100).isActive = true imageView.heightAnchor.constraint(equalToConstant: 100).isActive = true @@ -55,7 +55,7 @@ class ActivityCell: UITableViewCell { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none - contentView.layer.cornerRadius = 10 + contentView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius contentView.layer.shadowColor = UIColor.black.cgColor contentView.layer.shadowOpacity = 0.1 contentView.layer.shadowOffset = CGSize(width: 0, height: 2) diff --git a/recap/Views/AddFamilyMemberViewController.swift b/recap/Views/AddFamilyMemberViewController.swift old mode 100644 new mode 100755 index ef23292..d83e27a --- a/recap/Views/AddFamilyMemberViewController.swift +++ b/recap/Views/AddFamilyMemberViewController.swift @@ -1,12 +1,17 @@ -import PhotosUI import FirebaseAuth +import PhotosUI import UIKit -class AddFamilyMemberViewController: UIViewController, UIImagePickerControllerDelegate & UINavigationControllerDelegate { +class AddFamilyMemberViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate & UINavigationControllerDelegate { private var selectedImage: UIImage? private var storage: FamilyStorageProtocol private let dataUploadManager: DataUploadManager - + private let activityIndicator = UIActivityIndicatorView(style: .large) + + struct ValidationError: Error { + let message: String + } + init( storage: FamilyStorageProtocol = UserDefaultsStorageFamilyMember.shared, dataUploadManager: DataUploadManager = DataUploadManager() @@ -15,13 +20,13 @@ class AddFamilyMemberViewController: UIViewController, UIImagePickerControllerDe self.dataUploadManager = dataUploadManager super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { storage = UserDefaultsStorageFamilyMember.shared dataUploadManager = DataUploadManager() super.init(coder: coder) } - + private let relationshipOptions = RelationshipCategory.allCases private let relationshipPicker = UIPickerView() private let pickerToolbar: UIToolbar = { @@ -29,210 +34,356 @@ class AddFamilyMemberViewController: UIViewController, UIImagePickerControllerDe toolbar.sizeToFit() return toolbar }() - + private let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false return scrollView }() - + private let contentView: UIView = { let view = UIView() return view }() - + private let profileImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.backgroundColor = .systemGray6 imageView.image = UIImage(systemName: "person.circle.fill") - imageView.tintColor = .systemGray3 + imageView.tintColor = AppColors.primaryButtonColor imageView.isUserInteractionEnabled = true return imageView }() - + private let addPhotoButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Add Photo", for: .normal) button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + button.setTitleColor(AppColors.iconColor, for: .normal) return button }() - + private let nameTextField: UITextField = { let textField = UITextField() textField.placeholder = "Full Name" - textField.borderStyle = .roundedRect - textField.font = .systemFont(ofSize: 17) + textField.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) + textField.leftViewMode = .always + textField.backgroundColor = .systemGray6 return textField }() - + private let relationshipTextField: UITextField = { let textField = UITextField() textField.placeholder = "Relationship (e.g., Son, Daughter)" - textField.borderStyle = .roundedRect - textField.font = .systemFont(ofSize: 17) + textField.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) + textField.leftViewMode = .always + textField.backgroundColor = .systemGray6 return textField }() - + private let phoneTextField: UITextField = { let textField = UITextField() textField.placeholder = "Phone Number" - textField.borderStyle = .roundedRect - textField.font = .systemFont(ofSize: 17) + textField.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) + textField.leftViewMode = .always + textField.backgroundColor = .systemGray6 textField.keyboardType = .phonePad return textField }() - + + private func validatePhone(_ phone: String) -> Bool { + // Remove any non-numeric characters from the phone number + let digitsOnly = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + // Check if the resulting string is exactly 10 digits + return digitsOnly.count == 10 + } + private let emailTextField: UITextField = { let textField = UITextField() textField.placeholder = "Email Address" - textField.borderStyle = .roundedRect - textField.font = .systemFont(ofSize: 17) + textField.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) + textField.leftViewMode = .always + textField.backgroundColor = .systemGray6 textField.keyboardType = .emailAddress textField.autocapitalizationType = .none return textField }() - + + private func validateEmail(_ email: String) -> Bool { + // Email validation + let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex) + return emailPredicate.evaluate(with: email) + } + private let passwordTextField: UITextField = { let textField = UITextField() textField.placeholder = "Password" - textField.borderStyle = .roundedRect - textField.font = .systemFont(ofSize: 17) + textField.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) + textField.leftViewMode = .always + textField.backgroundColor = .systemGray6 + textField.autocapitalizationType = .none textField.isSecureTextEntry = true return textField }() - + + private func validatePassword(_ password: String) -> (isValid: Bool, message: String) { + // Password validation rules + let minLength = 4 + let hasUppercase = password.range(of: "[A-Z]", options: .regularExpression) != nil + let hasLowercase = password.range(of: "[a-z]", options: .regularExpression) != nil + let hasSpecialCharacter = password.range(of: "[!@#$%^&*(),.?\":{}|<>]", options: .regularExpression) != nil + + if password.count < minLength { + return (false, "Password must be at least \(minLength) characters long") + } + if !hasUppercase { + return (false, "Password must contain at least one uppercase letter") + } + if !hasLowercase { + return (false, "Password must contain at least one lowercase letter") + } + if !hasSpecialCharacter { + return (false, "Password must contain at least one special character") + } + + return (true, "") + } + + private func validateInputs() throws { + guard let name = nameTextField.text, !name.isEmpty else { + throw ValidationError(message: "Please enter a name") + } + + guard let relationship = relationshipTextField.text, !relationship.isEmpty else { + throw ValidationError(message: "Please select a relationship") + } + + guard let phone = phoneTextField.text, !phone.isEmpty else { + throw ValidationError(message: "Please enter a phone number") + } + + if !validatePhone(phone) { + throw ValidationError(message: "Phone number must be exactly 10 digits") + } + + guard let email = emailTextField.text, !email.isEmpty else { + throw ValidationError(message: "Please enter an email address") + } + + if !validateEmail(email) { + throw ValidationError(message: "Please enter a valid email address") + } + + guard let password = passwordTextField.text, !password.isEmpty else { + throw ValidationError(message: "Please enter a password") + } + + let passwordValidation = validatePassword(password) + if !passwordValidation.isValid { + throw ValidationError(message: passwordValidation.message) + } + + guard profileImageView.image != nil else { + throw ValidationError(message: "Please select a profile image") + } + } + private lazy var addButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Add Family Member", for: .normal) button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) - button.backgroundColor = UIColor.systemGreen.withAlphaComponent(0.2) - button.setTitleColor(.systemGreen, for: .normal) - button.layer.cornerRadius = 12 + button.backgroundColor = AppColors.primaryButtonColor + button.setTitleColor(AppColors.iconColor, for: .normal) + button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius button.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside) return button }() - + + deinit { + NotificationCenter.default.removeObserver(self) + } + override func viewDidLoad() { super.viewDidLoad() setupUI() setupGestures() setupRelationshipPicker() - + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + let doneButton = UIBarButtonItem(title: "Cancel", style: .done, target: self, action: #selector(closeButtonTapped)) navigationItem.rightBarButtonItem = doneButton + doneButton.tintColor = AppColors.iconColor if let sheet = sheetPresentationController { sheet.prefersGrabberVisible = true sheet.prefersEdgeAttachedInCompactHeight = true } - + + nameTextField.delegate = self + relationshipTextField.delegate = self + phoneTextField.delegate = self + emailTextField.delegate = self + passwordTextField.delegate = self + let Dismisskeyboard = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) view.addGestureRecognizer(Dismisskeyboard) } - + + func textFieldDidBeginEditing(_ textField: UITextField) { + let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0) // adjust bottom inset if needed + scrollView.contentInset = contentInsets + scrollView.scrollIndicatorInsets = contentInsets + + // Scroll the text field into view + var aRect = view.frame + aRect.size.height -= 200 // Adjust based on your keyboard height or screen size + if !aRect.contains(textField.frame.origin) { + let scrollPoint = CGPoint(x: 0, y: textField.frame.origin.y - 20) + scrollView.setContentOffset(scrollPoint, animated: true) + } + } + + @objc func keyboardWillShow(notification: Notification) { + // Adjust the scroll view's content insets to account for the keyboard + if let userInfo = notification.userInfo, + let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + let keyboardHeight = keyboardFrame.height + // Adjust the content inset to move the view above the keyboard + UIView.animate(withDuration: 0.3) { + self.scrollView.contentInset.bottom = keyboardHeight + self.scrollView.scrollIndicatorInsets.bottom = keyboardHeight + } + } + } + + @objc func keyboardWillHide(notification: Notification) { + // Reset the content insets when the keyboard disappears + UIView.animate(withDuration: 0.3) { + self.scrollView.contentInset.bottom = 0 + self.scrollView.scrollIndicatorInsets.bottom = 0 + } + } + @objc func dismissKeyboard() { view.endEditing(true) } - + + private func setupActivityIndicator() { + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(activityIndicator) + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + private func setupUI() { view.backgroundColor = .systemBackground title = "Add Family Member" - + view.addSubview(scrollView) scrollView.addSubview(contentView) - + [scrollView, contentView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } - + [profileImageView, addPhotoButton, nameTextField, relationshipTextField, phoneTextField, emailTextField, passwordTextField, addButton].forEach { contentView.addSubview($0) $0.translatesAutoresizingMaskIntoConstraints = false } - + profileImageView.layer.cornerRadius = 50 - + NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), // Correctly set the bottom constraint contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - + profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), profileImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), profileImageView.widthAnchor.constraint(equalToConstant: 100), profileImageView.heightAnchor.constraint(equalToConstant: 100), - + addPhotoButton.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 12), addPhotoButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - + nameTextField.topAnchor.constraint(equalTo: addPhotoButton.bottomAnchor, constant: 24), nameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), nameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), nameTextField.heightAnchor.constraint(equalToConstant: 44), - + relationshipTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 16), relationshipTextField.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor), relationshipTextField.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor), relationshipTextField.heightAnchor.constraint(equalToConstant: 44), - + phoneTextField.topAnchor.constraint(equalTo: relationshipTextField.bottomAnchor, constant: 16), phoneTextField.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor), phoneTextField.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor), phoneTextField.heightAnchor.constraint(equalToConstant: 44), - + emailTextField.topAnchor.constraint(equalTo: phoneTextField.bottomAnchor, constant: 16), emailTextField.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor), emailTextField.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor), emailTextField.heightAnchor.constraint(equalToConstant: 44), - + passwordTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16), passwordTextField.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor), passwordTextField.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor), passwordTextField.heightAnchor.constraint(equalToConstant: 44), - + // Fix the bottom spacing between the addButton and contentView bottom addButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 32), addButton.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor), addButton.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor), addButton.heightAnchor.constraint(equalToConstant: 50), - addButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) + addButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24), ]) } - + private func setupGestures() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(profileImageTapped)) profileImageView.addGestureRecognizer(tapGesture) - + addPhotoButton.addTarget(self, action: #selector(profileImageTapped), for: .touchUpInside) } - + private func setupRelationshipPicker() { relationshipPicker.delegate = self relationshipPicker.dataSource = self relationshipTextField.inputView = relationshipPicker relationshipTextField.inputAccessoryView = pickerToolbar - + let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(donePickingRelationship)) let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) pickerToolbar.setItems([flexibleSpace, doneButton], animated: false) } - + @objc private func donePickingRelationship() { let selectedRow = relationshipPicker.selectedRow(inComponent: 0) let selectedRelationship = relationshipOptions[selectedRow] relationshipTextField.text = selectedRelationship.rawValue relationshipTextField.resignFirstResponder() } - + @objc private func profileImageTapped() { let picker = UIImagePickerController() picker.delegate = self @@ -241,84 +392,116 @@ class AddFamilyMemberViewController: UIViewController, UIImagePickerControllerDe // picker.delegate = self present(picker, animated: true) } - + @objc private func closeButtonTapped() { dismiss(animated: true) } + @objc private func addButtonTapped() { - guard let name = nameTextField.text, !name.isEmpty, - let relationship = relationshipTextField.text, !relationship.isEmpty, - let phone = phoneTextField.text, !phone.isEmpty, - let email = emailTextField.text, !email.isEmpty, - let password = passwordTextField.text, !password.isEmpty else { - showAlert(title: "Missing Information", message: "Please fill in all fields") - return - } - let newMemberId = UUID().uuidString - var imagePath = "" - - // Get the current patient's ID - guard let patientId = Auth.auth().currentUser?.uid else { - showAlert(title: "Error", message: "Patient not logged in.") - return - } - - if let image = profileImageView.image { - imagePath = saveImageToLocalDirectory(image: image, for: newMemberId) - } - - let newMember = FamilyMember( - id: UUID().uuidString, - name: name, - relationship: relationship, - phone: phone, - email: email, - password: password, -// imageName: "", - imageName: newMemberId, -// imageURL: "" - imageURL: imagePath - ) - - // Save locally - storage.saveFamilyMember(newMember, image: profileImageView.image) { [weak self] success in - if success { - NotificationCenter.default.post(name: Notification.Name("FamilyMemberAdded"), object: nil) - self?.showAlert(title: "Success", message: "Family member added successfully") - } else { - self?.showAlert(title: "Error", message: "Failed to save family member") + do { + try validateInputs() + + // Proceed with adding family member if validation is successful + guard let name = nameTextField.text, + let relationship = relationshipTextField.text, + let phone = phoneTextField.text, + let email = emailTextField.text, + let password = passwordTextField.text, + let image = profileImageView.image, + let patientId = Auth.auth().currentUser?.uid else { + return } - } - - // Upload to Firestore - dataUploadManager.addFamilyMember(for: patientId, member: newMember) { [weak self] error in - DispatchQueue.main.async { + + // Clean phone number + let cleanPhone = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + + let newMemberId = UUID().uuidString + let imagePath = "\(patientId)/FAMILY_IMGS/\(newMemberId).jpg" + + disableUIForUpload() + + FirebaseManager.shared.uploadFamilyMemberImage(patientId: patientId, imagePath: imagePath, image: image) { [weak self] imageURL, error in + guard let self = self else { return } + if let error = error { - self?.showAlert(title: "Error", message: "Failed to save family member: \(error.localizedDescription)") - } else { - self?.showAlert(title: "Success", message: "Family member added successfully") { - self?.dismiss(animated: true) // Automatically dismiss view + self.enableUIAfterUpload() + self.showAlert(title: "Upload Error", message: "Failed to upload image: \(error.localizedDescription)", retry: true) + return + } + + guard let imageURL = imageURL else { + self.enableUIAfterUpload() + self.showAlert(title: "Error", message: "Could not retrieve image URL.", retry: true) + return + } + + let newMember = FamilyMember( + id: newMemberId, + name: name, + relationship: relationship, + phone: cleanPhone, + email: email.trimmingCharacters(in: .whitespacesAndNewlines), + password: password, + imageName: newMemberId, + imageURL: imageURL + ) + + self.dataUploadManager.addFamilyMember(for: patientId, member: newMember) { error in + DispatchQueue.main.async { + self.enableUIAfterUpload() + if let error = error { + self.showAlert(title: "Error", message: "Failed to save family member: \(error.localizedDescription)", retry: true) + } else { + self.showSuccessAnimation { + self.showAlert(title: "Success", message: "Family member added successfully") { + self.dismiss(animated: true) + } + } + } } } } + } catch let validationError as ValidationError { + showAlert(title: "Validation Error", message: validationError.message, retry: true) + } catch { + showAlert(title: "Error", message: "An unexpected error occurred", retry: true) } } - private func saveImageToLocalDirectory(image: UIImage, for memberId: String) -> String { - let fileManager = FileManager.default - guard let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { - return "" - } - let imageFileName = "\(memberId).jpg" - let fileURL = documentsDirectory.appendingPathComponent(imageFileName) + private func disableUIForUpload() { + addButton.isEnabled = false + addButton.setTitle("Adding \(nameTextField.text ?? "family")", for: .normal) + activityIndicator.startAnimating() + } - if let imageData = image.jpegData(compressionQuality: 0.8) { - try? imageData.write(to: fileURL) - } + // Re-enable UI elements after upload + private func enableUIAfterUpload() { + addButton.isEnabled = true + addButton.setTitle("Add Family Member", for: .normal) + activityIndicator.stopAnimating() + } - return fileURL.path + // Success animation (simple scale animation) + private func showSuccessAnimation(completion: @escaping () -> Void) { + UIView.animate(withDuration: 0.3, animations: { + self.profileImageView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) + }, completion: { _ in + UIView.animate(withDuration: 0.3, animations: { + self.profileImageView.transform = .identity + }, completion: { _ in + completion() + }) + }) } + private func showAlert(title: String, message: String, retry: Bool = false) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let okAction = UIAlertAction(title: retry ? "Retry" : "OK", style: .default, handler: nil) + alertController.addAction(okAction) + + present(alertController, animated: true) + } } // MARK: - PHPickerViewControllerDelegate @@ -337,10 +520,10 @@ extension AddFamilyMemberViewController: PHPickerViewControllerDelegate { } } } - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { picker.dismiss(animated: true) - + if let editedImage = info[.editedImage] as? UIImage { profileImageView.image = editedImage } else if let originalImage = info[.originalImage] as? UIImage { diff --git a/recap/Views/AnimationViews/GameCompletionViewController.swift b/recap/Views/AnimationViews/GameCompletionViewController.swift new file mode 100755 index 0000000..2b06fc0 --- /dev/null +++ b/recap/Views/AnimationViews/GameCompletionViewController.swift @@ -0,0 +1,446 @@ +// +// GameCompletionViewController.swift +// recap +// +// Created by Diptayan Jash on 06/03/25. +// + +import UIKit +import Lottie + +class GameCompletionViewController: UIViewController { + // Completion details + var secondsElapsed: Int = 0 + var moves: Int = 0 + var score: Int = 0 // Added score property + + // Closures for actions + var onPlayAgainTapped: (() -> Void)? + var onExitTapped: (() -> Void)? + var onShareTapped: (() -> Void)? + + private let animationView: LottieAnimationView = { + guard let animation = LottieAnimation.named("gameComplete", bundle: .main) else { + fatalError("Lottie file not found") + } + + let lottieView = LottieAnimationView(animation: animation) + lottieView.contentMode = .scaleAspectFit + lottieView.loopMode = .loop + lottieView.translatesAutoresizingMaskIntoConstraints = false + return lottieView + }() + + // UI Components + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 20 + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOffset = CGSize(width: 0, height: 4) + view.layer.shadowRadius = 10 + view.layer.shadowOpacity = 0.3 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let confettiView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "confetti")) + imageView.contentMode = .scaleAspectFill + imageView.alpha = 0.6 + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let successLabel: UILabel = { + let label = UILabel() + label.text = "Congratulations!" + label.font = UIFont.systemFont(ofSize: 28, weight: .bold) + label.textColor = .systemIndigo + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let starStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.distribution = .fillEqually + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private let completionDescriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 18, weight: .medium) + label.textColor = .darkGray + label.textAlignment = .center + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let statsContainerView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.systemGray6 + view.layer.cornerRadius = 12 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let timeIconLabel: UILabel = { + let label = UILabel() + label.text = "⏱️" + label.font = UIFont.systemFont(ofSize: 24) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let timeLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .medium) + label.textColor = .darkGray + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let movesIconLabel: UILabel = { + let label = UILabel() + label.text = "🔄" + label.font = UIFont.systemFont(ofSize: 24) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let movesLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .medium) + label.textColor = .darkGray + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let scoreIconLabel: UILabel = { + let label = UILabel() + label.text = "🏆" + label.font = UIFont.systemFont(ofSize: 24) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let scoreLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16, weight: .medium) + label.textColor = .darkGray + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let buttonsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 15 + stackView.distribution = .fillEqually + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var playAgainButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Play Again", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor + button.setTitleColor(.systemGreen, for: .normal) + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(playAgainTapped), for: .touchUpInside) + return button + }() + + private lazy var exitButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Exit", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor + button.setTitleColor(.systemRed, for: .normal) + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(exitTapped), for: .touchUpInside) + return button + }() + + private lazy var shareButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal) + button.tintColor = .systemBlue + button.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(shareTapped), for: .touchUpInside) + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupStarRating() + updateStats() + + // Add a subtle pulsing effect to the success label + animateSuccessLabel() + + // Accessibility announcement + UIAccessibility.post(notification: .announcement, argument: "Congratulations! You completed the game!") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + playLottieAnimation() + } + + private func setupUI() { + view.backgroundColor = UIColor(white: 0, alpha: 0.6) + + view.addSubview(containerView) + containerView.addSubview(confettiView) + containerView.addSubview(animationView) + containerView.addSubview(successLabel) + containerView.addSubview(starStackView) + containerView.addSubview(completionDescriptionLabel) + containerView.addSubview(statsContainerView) + containerView.addSubview(buttonsStackView) + containerView.addSubview(shareButton) + + // Add stats elements to stats container + statsContainerView.addSubview(timeIconLabel) + statsContainerView.addSubview(timeLabel) + statsContainerView.addSubview(movesIconLabel) + statsContainerView.addSubview(movesLabel) + statsContainerView.addSubview(scoreIconLabel) + statsContainerView.addSubview(scoreLabel) + + // Add buttons to stack view + buttonsStackView.addArrangedSubview(playAgainButton) + buttonsStackView.addArrangedSubview(exitButton) + + NSLayoutConstraint.activate( +[ + containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + containerView.leadingAnchor + .constraint( + equalTo: view.leadingAnchor, + constant: Constants.paddingKeys.DefaultPaddingLeft + ), + containerView.trailingAnchor + .constraint( + equalTo: view.trailingAnchor, + constant: Constants.paddingKeys.DefaultPaddingRight + ), + containerView.widthAnchor.constraint(equalToConstant: 340), + containerView.heightAnchor.constraint(equalToConstant: 480), + + confettiView.topAnchor.constraint(equalTo: containerView.topAnchor), + confettiView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + confettiView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + confettiView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + + animationView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20), + animationView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + animationView.widthAnchor.constraint(equalToConstant: 150), + animationView.heightAnchor.constraint(equalToConstant: 150), + + successLabel.topAnchor.constraint(equalTo: animationView.bottomAnchor, constant: 5), + successLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + + starStackView.topAnchor.constraint(equalTo: successLabel.bottomAnchor, constant: 10), + starStackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + starStackView.heightAnchor.constraint(equalToConstant: 30), + + completionDescriptionLabel.topAnchor.constraint(equalTo: starStackView.bottomAnchor, constant: 10), + completionDescriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), + completionDescriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), + + statsContainerView.topAnchor.constraint(equalTo: completionDescriptionLabel.bottomAnchor, constant: 15), + statsContainerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), + statsContainerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), + statsContainerView.heightAnchor.constraint(equalToConstant: 80), + + // Time stats + timeIconLabel.leadingAnchor.constraint(equalTo: statsContainerView.leadingAnchor, constant: 20), + timeIconLabel.centerYAnchor.constraint(equalTo: statsContainerView.centerYAnchor), + + timeLabel.leadingAnchor.constraint(equalTo: timeIconLabel.trailingAnchor, constant: 8), + timeLabel.centerYAnchor.constraint(equalTo: timeIconLabel.centerYAnchor), + + // Moves stats + movesIconLabel.centerXAnchor.constraint(equalTo: statsContainerView.centerXAnchor), + movesIconLabel.centerYAnchor.constraint(equalTo: statsContainerView.centerYAnchor), + + movesLabel.leadingAnchor.constraint(equalTo: movesIconLabel.trailingAnchor, constant: 8), + movesLabel.centerYAnchor.constraint(equalTo: movesIconLabel.centerYAnchor), + + // Score stats + scoreIconLabel.trailingAnchor.constraint(equalTo: statsContainerView.trailingAnchor, constant: -60), + scoreIconLabel.centerYAnchor.constraint(equalTo: statsContainerView.centerYAnchor), + + scoreLabel.leadingAnchor.constraint(equalTo: scoreIconLabel.trailingAnchor, constant: 8), + scoreLabel.centerYAnchor.constraint(equalTo: scoreIconLabel.centerYAnchor), + + buttonsStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -25), + buttonsStackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + buttonsStackView.widthAnchor.constraint(equalToConstant: 280), + buttonsStackView.heightAnchor.constraint(equalToConstant: Constants.ButtonStyle.DefaultButtonHeight), + + // Share button + shareButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 15), + shareButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -15), + shareButton.widthAnchor.constraint(equalToConstant: 40), + shareButton.heightAnchor.constraint(equalToConstant: 40) + ] +) + } + + private func setupStarRating() { + // Calculate star rating based on moves and time + let starCount = calculateStarRating() + + // Clear any existing stars + starStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + + // Add stars based on rating + for i in 1...3 { + let imageView = UIImageView() + if i <= starCount { + imageView.image = UIImage(systemName: "star.fill") + imageView.tintColor = .systemYellow + } else { + imageView.image = UIImage(systemName: "star") + imageView.tintColor = .systemGray3 + } + imageView.contentMode = .scaleAspectFit + starStackView.addArrangedSubview(imageView) + + // Add a bounce animation for the stars + imageView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) + UIView.animate(withDuration: 0.5, delay: Double(i) * 0.2, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: { + imageView.transform = .identity + }) + } + } + + private func calculateStarRating() -> Int { + // This is just an example - adjust the logic based on your game mechanics + if moves < 10 && secondsElapsed < 30 { + return 3 + } else if moves < 20 && secondsElapsed < 60 { + return 2 + } else { + return 1 + } + } + + private func updateStats() { + // Format time + let minutes = secondsElapsed / 60 + let seconds = secondsElapsed % 60 + timeLabel.text = String(format: "%d:%02d", minutes, seconds) + + // Set moves + movesLabel.text = "\(moves)" + + // Calculate score - adjust formula as needed + score = max(100 - (moves * 1) - (secondsElapsed / 5), 0) + scoreLabel.text = "\(score)" + + // Update description + completionDescriptionLabel.text = "You've mastered the challenge!" + } + + private func animateSuccessLabel() { + let animation = CAKeyframeAnimation(keyPath: "transform.scale") + animation.values = [1.0, 1.1, 1.0] + animation.keyTimes = [0, 0.5, 1] + animation.duration = 1.5 + animation.repeatCount = .infinity + successLabel.layer.add(animation, forKey: "pulse") + } + + private func playLottieAnimation() { + animationView.play { [weak self] _ in + // Add a subtle bounce to the container when animation completes + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: { + self?.containerView.transform = CGAffineTransform(scaleX: 1.03, y: 1.03) + }) { _ in + UIView.animate(withDuration: 0.2) { + self?.containerView.transform = .identity + } + } + } + } + + @objc private func playAgainTapped() { + // Add a tactile feedback + let generator = UIImpactFeedbackGenerator(style: .medium) + generator.impactOccurred() + + // Scale animation + UIView.animate(withDuration: 0.1, animations: { + self.playAgainButton.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + }) { _ in + UIView.animate(withDuration: 0.1, animations: { + self.playAgainButton.transform = CGAffineTransform.identity + }) { _ in + self.dismiss(animated: true) { + self.onPlayAgainTapped?() + } + } + } + } + + @objc private func exitTapped() { + // Add a tactile feedback + let generator = UIImpactFeedbackGenerator(style: .medium) + generator.impactOccurred() + + // Scale animation + UIView.animate(withDuration: 0.1, animations: { + self.exitButton.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + }) { _ in + UIView.animate(withDuration: 0.1, animations: { + self.exitButton.transform = CGAffineTransform.identity + }) { _ in + self.dismiss(animated: true) { + self.onExitTapped?() + } + } + } + } + + @objc private func shareTapped() { + // Add a tactile feedback + let generator = UIImpactFeedbackGenerator(style: .medium) + generator.impactOccurred() + + // Create a screenshot to share + UIGraphicsBeginImageContextWithOptions(containerView.bounds.size, false, 0.0) + containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true) + let screenshot = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + // Items to share + let items: [Any] = [ + "I just completed the game with a score of \(score)! 🎮", + screenshot as Any + ].compactMap { $0 } + + let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil) + activityVC.popoverPresentationController?.sourceView = shareButton + present(activityVC, animated: true) + + onShareTapped?() + } +} diff --git a/recap/Views/AnimationViews/LoadingAnimationManager.swift b/recap/Views/AnimationViews/LoadingAnimationManager.swift new file mode 100755 index 0000000..b37474f --- /dev/null +++ b/recap/Views/AnimationViews/LoadingAnimationManager.swift @@ -0,0 +1,72 @@ +// +// LoadingAnimationManager.swift +// recap +// +// Created by Diptayan Jash on 05/03/25. +// +import Foundation +import Lottie +import UIKit + +class LoadingAnimationManager { + static let shared = LoadingAnimationManager() + + private init() {} + + func showLoadingAnimation(on view: UIView, name: String = "loadingAnimation") -> LottieAnimationView { + guard let animation = LottieAnimation.named("loadingAnimation", bundle: .main) else { + fatalError("Lottie file not found") + } + let lottieView = LottieAnimationView(animation: animation) + lottieView.contentMode = .scaleAspectFit + lottieView.loopMode = .loop + lottieView.translatesAutoresizingMaskIntoConstraints = false + + let backgroundView = UIView() + backgroundView.backgroundColor = UIColor.white.withAlphaComponent(0.8) + backgroundView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(backgroundView) + view.addSubview(lottieView) + + NSLayoutConstraint.activate([ + backgroundView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + NSLayoutConstraint.activate([ + lottieView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + lottieView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + lottieView.widthAnchor.constraint(equalToConstant: 250), + lottieView.heightAnchor.constraint(equalToConstant: 250), + ]) + + // Play animation + lottieView.play() + + return lottieView + } + + func removeLoadingAnimation(_ animationView: LottieAnimationView) { + animationView.stop() + + // Remove both the animation view and its background + if let backgroundView = animationView.superview?.subviews.first(where: { $0 is UIView && $0 != animationView }) { + backgroundView.removeFromSuperview() + } + animationView.removeFromSuperview() + } +} + +// Optional: Extension to make it easier to use in view controllers +extension UIViewController { + func showLoadingAnimation(name: String = "LoadingAnimation") -> LottieAnimationView { + return LoadingAnimationManager.shared.showLoadingAnimation(on: view, name: name) + } + + func removeLoadingAnimation(_ animationView: LottieAnimationView) { + LoadingAnimationManager.shared.removeLoadingAnimation(animationView) + } +} diff --git a/recap/Views/AnimationViews/SuccessQuestionsViewController.swift b/recap/Views/AnimationViews/SuccessQuestionsViewController.swift new file mode 100755 index 0000000..10ea789 --- /dev/null +++ b/recap/Views/AnimationViews/SuccessQuestionsViewController.swift @@ -0,0 +1,225 @@ +// +// SuccessQuestionsViewController.swift +// recap +// +// Created by Diptayan Jash on 05/03/25. +// + +import Lottie +import UIKit + +class SuccessQuestionsViewController: UIViewController { + // Closure to handle exit action + var onExitTapped: (() -> Void)? + + private let animationView: LottieAnimationView = { + guard let animation = LottieAnimation.named("SuccessAnimation", bundle: .main) else { + fatalError("Lottie file not found") + } + + let lottieView = LottieAnimationView(animation: animation) + lottieView.contentMode = .scaleAspectFit + lottieView.loopMode = .loop + lottieView.translatesAutoresizingMaskIntoConstraints = false + return lottieView + }() + + // Confetti Emitter Layer + private lazy var confettiLayer: CAEmitterLayer = { + let emitterLayer = CAEmitterLayer() + emitterLayer.emitterPosition = CGPoint(x: view.bounds.width / 2, y: -50) + emitterLayer.emitterShape = .line + emitterLayer.emitterSize = CGSize(width: view.bounds.width, height: 1) + + // Create confetti cells + let colors: [UIColor] = [ + .systemRed, .systemBlue, .systemGreen, .systemYellow, + .systemPurple, .systemOrange, .systemPink, + ] + + let cells = colors.map { color -> CAEmitterCell in + let cell = CAEmitterCell() + cell.birthRate = 10 + cell.lifetime = 10 + cell.velocity = CGFloat.random(in: 100 ... 300) + cell.velocityRange = 50 + cell.emissionLongitude = .pi + cell.spinRange = 5 + cell.scale = 0.1 + cell.scaleRange = 0.2 + cell.color = color.cgColor + + // Create a confetti-like shape + UIGraphicsBeginImageContextWithOptions(CGSize(width: 10, height: 10), false, 0) + let context = UIGraphicsGetCurrentContext() + context?.setFillColor(color.cgColor) + context?.fill(CGRect(x: 0, y: 0, width: 10, height: 10)) + context?.rotate(by: .pi / 4) + context?.fill(CGRect(x: 0, y: 0, width: 10, height: 10)) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + cell.contents = image?.cgImage + return cell + } + + emitterLayer.emitterCells = cells + return emitterLayer + }() + + // UI Components + private let backgroundView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(white: 1, alpha: 0.9) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 20 + view.layer.shadowColor = Constants.FontandColors.defaultshadowColor + view.layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + view.layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + view.layer.shadowOpacity = Float( + Constants.FontandColors.defaultshadowOpacity + ) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let successTitleLabel: UILabel = { + let label = UILabel() + label.text = "Well Done!" + label.font = UIFont.systemFont(ofSize: 24, weight: .bold) + label.textColor = .black + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let successDescriptionLabel: UILabel = { + let label = UILabel() + label.text = "You've completed all your questions for now.\nKeep up the great work!" + label.font = Constants.FontandColors.descriptionFont + label.textColor = Constants.FontandColors.descriptionColor + label.numberOfLines = 0 + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var exitButton: UIButton = { + var configuration = UIButton.Configuration.filled() + configuration.baseBackgroundColor = UIColor.systemOrange + configuration.cornerStyle = .large + + let button = UIButton(configuration: configuration) + button.setTitle("Continue to Home", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(exitButtonTapped), for: .touchUpInside) + return button + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + setupConstraints() + playLottieAnimation() + } + + private func playLottieAnimation() { + animationView.play() + } + + private func setupViews() { + // Setup background + view.backgroundColor = .clear + view.layer.addSublayer(confettiLayer) + + // Add views + view.addSubview(backgroundView) + backgroundView.addSubview(containerView) + containerView.addSubview(animationView) + containerView.addSubview(successTitleLabel) + containerView.addSubview(successDescriptionLabel) + containerView.addSubview(exitButton) + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + backgroundView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + containerView.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor), + containerView.widthAnchor.constraint(equalToConstant: 320), + containerView.heightAnchor.constraint(equalToConstant: 450), + + animationView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20), + animationView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + animationView.widthAnchor.constraint(equalToConstant: 200), + animationView.heightAnchor.constraint(equalToConstant: 200), + + successTitleLabel.topAnchor.constraint(equalTo: animationView.bottomAnchor, constant: 20), + successTitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), + successTitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), + + successDescriptionLabel.topAnchor.constraint(equalTo: successTitleLabel.bottomAnchor, constant: 10), + successDescriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20), + successDescriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20), + + exitButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -30), + exitButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + exitButton.widthAnchor.constraint(equalToConstant: 200), + exitButton.heightAnchor.constraint(equalToConstant: 50), + ]) + } + + private func animateSuccessView() { + // Confetti animation + confettiLayer.birthRate = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.confettiLayer.birthRate = 10 + } + + // Container animation + containerView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + containerView.alpha = 0 + + // Spring animation for container + UIView.animate(withDuration: 0.8, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: .curveEaseInOut) { + self.containerView.transform = .identity + self.containerView.alpha = 1 + } + + // Fade out confetti after 3 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + UIView.animate(withDuration: 1) { + self.confettiLayer.birthRate = 0 + } + } + } + + @objc private func exitButtonTapped() { + // Animate out with confetti + UIView.animate(withDuration: 0.5, animations: { + self.containerView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.containerView.alpha = 0 + self.confettiLayer.birthRate = 0 + }) { _ in + self.dismiss(animated: false) { + self.onExitTapped?() + } + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + confettiLayer.removeFromSuperlayer() + } +} diff --git a/recap/Views/Articles/ArticleDetailViewController.swift b/recap/Views/Articles/ArticleDetailViewController.swift old mode 100644 new mode 100755 index ce059d5..6292cef --- a/recap/Views/Articles/ArticleDetailViewController.swift +++ b/recap/Views/Articles/ArticleDetailViewController.swift @@ -1,33 +1,41 @@ +// +// ArticleTableViewCell.swift +// Recap +// +// Created by khushi on 22/03/25. +// + import UIKit import SafariServices class ArticleDetailViewController: UIViewController { private let article: Article - + + // MARK: - UI Elements + private let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.showsVerticalScrollIndicator = false return scrollView }() - - private let contentStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 20 - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView + + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view }() - + private let imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 12 imageView.clipsToBounds = true + imageView.layer.cornerRadius = 0 // Full width image imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - + private let titleLabel: UILabel = { let label = UILabel() label.font = UIFont.boldSystemFont(ofSize: 24) @@ -35,89 +43,200 @@ class ArticleDetailViewController: UIViewController { label.translatesAutoresizingMaskIntoConstraints = false return label }() - - private let contentLabel: UILabel = { + + private let metadataView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let authorLabel: UILabel = { let label = UILabel() - label.font = UIFont.systemFont(ofSize: 16) - label.numberOfLines = 0 + label.font = UIFont.italicSystemFont(ofSize: 14) + label.textColor = .gray label.translatesAutoresizingMaskIntoConstraints = false return label }() - - private let authorLabel: UILabel = { + + private let readTimeLabel: UILabel = { let label = UILabel() - label.font = UIFont.italicSystemFont(ofSize: 14) + label.font = UIFont.systemFont(ofSize: 14) label.textColor = .gray label.translatesAutoresizingMaskIntoConstraints = false return label }() - + + private let dividerView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.gray.withAlphaComponent(0.3) + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let contentLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + private let readMoreButton: UIButton = { let button = UIButton(type: .system) - button.setTitle("Read More", for: .normal) + button.backgroundColor = AppColors.iconColor + button.setTitle("Read Full Article", for: .normal) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + button.layer.cornerRadius = 12 button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(openLink), for: .touchUpInside) return button }() - + + // MARK: - Initialization + init(article: Article) { self.article = article super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() - + setupView() + setupConstraints() + configureWithArticle() + setupActions() + } + + // MARK: - Setup + + private func setupView() { view.backgroundColor = .white - title = article.title - setupLayout() + navigationItem.largeTitleDisplayMode = .never - // Set article details - imageView.image = article.image - titleLabel.text = article.title - contentLabel.text = article.content - authorLabel.text = "By \(article.author)" - } - - private func setupLayout() { - // Add scroll view to the main view + // Add scroll view to main view view.addSubview(scrollView) - // Add stack view to the scroll view - scrollView.addSubview(contentStackView) - - // Add image, title, content, author and button to the stack view - contentStackView.addArrangedSubview(imageView) - contentStackView.addArrangedSubview(titleLabel) - contentStackView.addArrangedSubview(contentLabel) - contentStackView.addArrangedSubview(authorLabel) - contentStackView.addArrangedSubview(readMoreButton) + // Add content view to scroll view + scrollView.addSubview(contentView) - // Set up constraints + // Add elements to content view + contentView.addSubview(imageView) + contentView.addSubview(titleLabel) + contentView.addSubview(metadataView) + metadataView.addSubview(authorLabel) + metadataView.addSubview(readTimeLabel) + contentView.addSubview(dividerView) + contentView.addSubview(contentLabel) + contentView.addSubview(readMoreButton) + } + + private func setupConstraints() { NSLayoutConstraint.activate([ + // Scroll View scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20), - contentStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20), - contentStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20), - contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -20), - contentStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40), + // Content View + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + // Image View - Full width at the top + imageView.topAnchor.constraint(equalTo: contentView.topAnchor), + imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + imageView.heightAnchor.constraint(equalToConstant: 250), + + // Title Label + titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 24), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + // Metadata View + metadataView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), + metadataView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + metadataView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + metadataView.heightAnchor.constraint(equalToConstant: 20), + + // Author Label + authorLabel.leadingAnchor.constraint(equalTo: metadataView.leadingAnchor), + authorLabel.centerYAnchor.constraint(equalTo: metadataView.centerYAnchor), + + // Read Time Label + readTimeLabel.trailingAnchor.constraint(equalTo: metadataView.trailingAnchor), + readTimeLabel.centerYAnchor.constraint(equalTo: metadataView.centerYAnchor), + + // Divider + dividerView.topAnchor.constraint(equalTo: metadataView.bottomAnchor, constant: 16), + dividerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + dividerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + dividerView.heightAnchor.constraint(equalToConstant: 1), - imageView.heightAnchor.constraint(equalToConstant: 200) + // Content Label + contentLabel.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 16), + contentLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + // Read More Button + readMoreButton.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 32), + readMoreButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + readMoreButton.widthAnchor.constraint(equalToConstant: 200), + readMoreButton.heightAnchor.constraint(equalToConstant: 50), + readMoreButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -32) ]) } - + + private func setupActions() { + readMoreButton.addTarget(self, action: #selector(openLink), for: .touchUpInside) + } + + // MARK: - Configuration + + private func configureWithArticle() { + title = "" // Clear the title to focus on the content + imageView.image = article.image + titleLabel.text = article.title + authorLabel.text = "By \(article.author)" + contentLabel.text = article.content + readTimeLabel.text = calculateReadTime(for: article.content) + } + + // MARK: - Actions + @objc private func openLink() { if let url = URL(string: article.link) { let safariVC = SFSafariViewController(url: url) present(safariVC, animated: true, completion: nil) } } + + // MARK: - Helper Methods + + private func calculateReadTime(for text: String) -> String { + // Average reading speed: 200-250 words per minute + let wordsPerMinute = 150 + + // Count words in the text + let words = text.components(separatedBy: .whitespacesAndNewlines) + let wordCount = words.filter { !$0.isEmpty }.count + + // Calculate reading time in minutes + let readTimeMinutes = max(1, Int(ceil(Double(wordCount) / Double(wordsPerMinute)))) + + if readTimeMinutes == 1 { + return "1 min read" + } else { + return "\(readTimeMinutes) mins read" + } + } } diff --git a/recap/Views/Articles/ArticleTableViewCell.swift b/recap/Views/Articles/ArticleTableViewCell.swift old mode 100644 new mode 100755 index 83da259..9533127 --- a/recap/Views/Articles/ArticleTableViewCell.swift +++ b/recap/Views/Articles/ArticleTableViewCell.swift @@ -13,11 +13,11 @@ class ArticleTableViewCell: UITableViewCell { private let containerView: UIView = { let view = UIView() view.backgroundColor = .white - view.layer.cornerRadius = 10 - view.layer.shadowColor = UIColor.black.cgColor - view.layer.shadowOpacity = 0.2 + view.layer.cornerRadius = 16 + view.layer.shadowColor = Constants.FontandColors.defaultshadowColor + view.layer.shadowOpacity = 0.1 view.layer.shadowOffset = CGSize(width: 0, height: 2) - view.layer.shadowRadius = 5 + view.layer.shadowRadius = 4 view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -25,15 +25,16 @@ class ArticleTableViewCell: UITableViewCell { private let articleImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 10 imageView.clipsToBounds = true + imageView.layer.cornerRadius = 16 + imageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() private let titleLabel: UILabel = { let label = UILabel() - label.font = UIFont.boldSystemFont(ofSize: 16) + label.font = Constants.FontandColors.titleFont label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false return label @@ -41,50 +42,77 @@ class ArticleTableViewCell: UITableViewCell { private let subtitleLabel: UILabel = { let label = UILabel() - label.font = UIFont.systemFont(ofSize: 14) - label.textColor = .gray - label.numberOfLines = 0 + label.font = Constants.FontandColors.subtitleFont + label.textColor = Constants.FontandColors.subtitleColor + label.numberOfLines = 2 label.translatesAutoresizingMaskIntoConstraints = false return label }() + // MARK: - Initialization + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + setupConstraints() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + private func setupViews() { + backgroundColor = .clear + selectionStyle = .none + contentView.addSubview(containerView) containerView.addSubview(articleImageView) containerView.addSubview(titleLabel) containerView.addSubview(subtitleLabel) + } + private func setupConstraints() { NSLayoutConstraint.activate([ - containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), + // Container View + containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), + // Article Image View articleImageView.topAnchor.constraint(equalTo: containerView.topAnchor), articleImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), articleImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), articleImageView.heightAnchor.constraint(equalToConstant: 180), - - titleLabel.topAnchor.constraint(equalTo: articleImageView.bottomAnchor, constant: 10), - titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10), - titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10), - - subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), - subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10), - subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10), - subtitleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10) + + // Title Label + titleLabel.topAnchor.constraint(equalTo: articleImageView.bottomAnchor, constant: 16), + titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + + // Subtitle Label + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + subtitleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + subtitleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + subtitleLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16) ]) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + // MARK: - Configuration func configure(with article: Article) { articleImageView.image = article.image titleLabel.text = article.title subtitleLabel.text = String(article.content.prefix(100)) } + + // For when you need to prepare the cell for reuse + override func prepareForReuse() { + super.prepareForReuse() + articleImageView.image = nil + titleLabel.text = nil + subtitleLabel.text = nil + } } diff --git a/recap/Views/CardCell.swift b/recap/Views/CardCell.swift deleted file mode 100644 index 5e39e5b..0000000 --- a/recap/Views/CardCell.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// CardCell.swift -// recap -// -// Created by Diptayan Jash on 05/11/24. -// - -import UIKit - -class CardCell: UICollectionViewCell { - static let identifier = "CardCell" - - private let iconImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.layer.cornerRadius = 8 - imageView.clipsToBounds = true - return imageView - }() - - private let titleLabel: UILabel = { - let label = UILabel() - label.font = .boldSystemFont(ofSize: 16) - label.textColor = .black - label.textAlignment = .left - label.numberOfLines = 0 - return label - }() - - private let descriptionLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 14) - label.textColor = .gray - label.textAlignment = .left - label.numberOfLines = 0 - return label - }() - - private let dividerLine: UIView = { - let view = UIView() - view.backgroundColor = .lightGray.withAlphaComponent(0.3) - return view - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - contentView.layer.cornerRadius = 10 - contentView.layer.shadowColor = UIColor.black.cgColor - contentView.layer.shadowOpacity = 0.2 - contentView.layer.shadowOffset = CGSize(width: 0, height: 1) - contentView.layer.shadowRadius = 2 - contentView.backgroundColor = .white - - contentView.clipsToBounds = false - contentView.layer.masksToBounds = false - - contentView.addSubview(iconImageView) - contentView.addSubview(dividerLine) - contentView.addSubview(titleLabel) - contentView.addSubview(descriptionLabel) - - setupConstraints() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(with title: String, description: String, image: UIImage?) { - titleLabel.text = title - descriptionLabel.text = description - iconImageView.image = image - } - - // MARK: - Setup Constraints - private func setupConstraints() { - iconImageView.translatesAutoresizingMaskIntoConstraints = false - titleLabel.translatesAutoresizingMaskIntoConstraints = false - dividerLine.translatesAutoresizingMaskIntoConstraints = false - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - iconImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), - iconImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - iconImageView.widthAnchor.constraint(equalToConstant: 90), - iconImageView.heightAnchor.constraint(equalToConstant: 90), - - dividerLine.topAnchor.constraint(equalTo: iconImageView.bottomAnchor, constant: 10), - dividerLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - dividerLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - dividerLine.heightAnchor.constraint(equalToConstant: 1), - - titleLabel.topAnchor.constraint(equalTo: dividerLine.bottomAnchor, constant: 8), - titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - - descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), - descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), - descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), - descriptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10) - ]) - } -} diff --git a/recap/Views/Charts/ImmediateReportDetailViewController.swift b/recap/Views/Charts/ImmediateReportDetailViewController.swift old mode 100644 new mode 100755 index fe1705b..5940053 --- a/recap/Views/Charts/ImmediateReportDetailViewController.swift +++ b/recap/Views/Charts/ImmediateReportDetailViewController.swift @@ -4,125 +4,873 @@ // // Created by admin70 on 13/11/24. // + +//import SwiftUI +//import Charts +// +//struct ImmediateReportDetailViewController: View { +// @State private var immediateMemoryData: [ImmediateMemoryData] = [] +// @State private var selectedDate: Date? = nil +// private let verifiedUserDocID: String +// +// init(verifiedUserDocID: String) { +// self.verifiedUserDocID = verifiedUserDocID +// } +// +// var body: some View { +// ZStack { +// // Modern gradient background that's softer and more subtle +// LinearGradient( +// gradient: Gradient(colors: [ +// Color(.systemBackground), +// Color(.systemGray6) +// ]), +// startPoint: .top, +// endPoint: .bottom +// ) +// .ignoresSafeArea() +// +// ScrollView { +// VStack(spacing: 24) { +// // Data summary card +// summaryCard +// +// // Date selector and chart +// if !immediateMemoryData.isEmpty { +// dateSelector +// memoryChartCard +// } else { +// emptyStateView +// } +// +// // Information section +// informationCard +// } +// .padding() +// } +// } +// .navigationTitle("Immediate Memory") +// .onAppear { +// loadMemoryData() +// } +// } +// +// // MARK: - Components +// +// var summaryCard: some View { +// VStack(alignment: .leading, spacing: 16) { +// HStack { +// VStack(alignment: .leading) { +// Text("Overall Performance") +// .font(.headline) +// .foregroundColor(.primary) +// +// Text(calculateOverallScore()) +// .font(.system(size: 36, weight: .bold, design: .rounded)) +// .foregroundColor(scoreColor()) +// } +// +// Spacer() +// +// Image(systemName: "brain.fill") +// .font(.system(size: 32)) +// .foregroundColor(scoreColor().opacity(0.8)) +// .frame(width: 60, height: 60) +// .background( +// Circle() +// .fill(scoreColor().opacity(0.1)) +// ) +// } +// +// if !immediateMemoryData.isEmpty { +// Text("Based on \(getTotalAnswers()) questions over \(immediateMemoryData.count) sessions") +// .font(.subheadline) +// .foregroundColor(.secondary) +// } +// } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color(.systemBackground)) +// .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) +// ) +// } +// +// var dateSelector: some View { +// ScrollView(.horizontal, showsIndicators: false) { +// HStack(spacing: 12) { +// ForEach(immediateMemoryData) { data in +// Button(action: { +// withAnimation(.spring()) { +// selectedDate = data.date +// } +// }) { +// Text(data.date.formatted(.dateTime.day().month(.abbreviated))) +// .font(.system(.subheadline, design: .rounded)) +// .padding(.horizontal, 16) +// .padding(.vertical, 8) +// .background( +// Capsule() +// .fill(isDateSelected(data.date) ? +// Color.accentColor : Color(.systemGray6)) +// ) +// .foregroundColor(isDateSelected(data.date) ? +// .white : .primary) +// } +// } +// } +// .padding(.horizontal, 4) +// } +// } +// +// // Helper function to check if a date is selected +// private func isDateSelected(_ date: Date) -> Bool { +// if let selectedDate = selectedDate { +// return Calendar.current.isDate(date, inSameDayAs: selectedDate) +// } +// return false +// } +// +// var memoryChartCard: some View { +// let displayData = selectedDate == nil ? +// immediateMemoryData : +// immediateMemoryData.filter { $0.date == selectedDate } +// +// return VStack(alignment: .leading, spacing: 16) { +// if !displayData.isEmpty { +// ForEach(displayData) { data in +// VStack(alignment: .leading, spacing: 12) { +// Text(data.date.formatted(.dateTime.weekday(.wide).day().month().year())) +// .font(.headline) +// .foregroundColor(.primary) +// +// HStack(alignment: .center) { +// // Modern donut chart +// ModernDonutChartView( +// correctAnswers: data.correctAnswers, +// incorrectAnswers: data.incorrectAnswers +// ) +// .frame(width: 160, height: 160) +// +// VStack(alignment: .leading, spacing: 16) { +// StatItemView( +// title: "Correct", +// value: "\(data.correctAnswers)", +// color: Color.green +// ) +// +// StatItemView( +// title: "Incorrect", +// value: "\(data.incorrectAnswers)", +// color: Color.red +// ) +// +// if data.correctAnswers + data.incorrectAnswers > 0 { +// StatItemView( +// title: "Accuracy", +// value: "\(Int(Double(data.correctAnswers) / Double(data.correctAnswers + data.incorrectAnswers) * 100))%", +// color: Color.blue +// ) +// } +// } +// .padding(.leading, 8) +// } +// } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color(.systemBackground)) +// .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 4) +// ) +// } +// } +// } +// } +// +// var emptyStateView: some View { +// VStack(spacing: 16) { +// Image(systemName: "brain.head.profile") +// .font(.system(size: 56)) +// .foregroundColor(.secondary.opacity(0.6)) +// .padding(.bottom, 8) +// +// Text("No Memory Data Available") +// .font(.headline) +// .foregroundColor(.primary) +// +// Text("Complete memory exercises to see your results here") +// .font(.subheadline) +// .foregroundColor(.secondary) +// .multilineTextAlignment(.center) +// } +// .frame(maxWidth: .infinity) +// .padding(.vertical, 40) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color(.systemBackground)) +// .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 4) +// ) +// } +// +// var informationCard: some View { +// VStack(alignment: .leading, spacing: 12) { +// Text("About Immediate Memory") +// .font(.headline) +// .foregroundColor(.primary) +// +// VStack(alignment: .leading, spacing: 12) { +// insightRow( +// icon: "timer", +// title: "Rapid Recall", +// description: "Your ability to remember information learned minutes ago" +// ) +// +// Divider() +// +// insightRow( +// icon: "brain.head.profile", +// title: "Working Memory", +// description: "Essential for problem-solving and everyday tasks" +// ) +// +// Divider() +// +// insightRow( +// icon: "arrow.up.forward", +// title: "Improvement Tips", +// description: "Practice attention to detail and focus during learning" +// ) +// } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 12) +// .fill(Color(.systemGray6)) +// ) +// } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color(.systemBackground)) +// .shadow(color: Color.black.opacity(0.05), radius: 8, x: 0, y: 4) +// ) +// } +// +// func insightRow(icon: String, title: String, description: String) -> some View { +// HStack(alignment: .top, spacing: 12) { +// Image(systemName: icon) +// .font(.system(size: 18)) +// .foregroundColor(.accentColor) +// .frame(width: 24, height: 24) +// +// VStack(alignment: .leading, spacing: 4) { +// Text(title) +// .font(.system(.subheadline, weight: .semibold)) +// .foregroundColor(.primary) +// +// Text(description) +// .font(.system(.footnote)) +// .foregroundColor(.secondary) +// .fixedSize(horizontal: false, vertical: true) +// } +// } +// } +// +// // MARK: - Helper Functions +// +// private func loadMemoryData() { +// fetchImmediateMemoryData(for: verifiedUserDocID) { data in +// DispatchQueue.main.async { +// self.immediateMemoryData = data.sorted(by: { $0.date > $1.date }) +// +// // Set default selected date to today if available, otherwise most recent +// let today = Date() +// let todayData = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: today) }) +// +// if let todayData = todayData { +// self.selectedDate = todayData.date +// } else if !data.isEmpty { +// self.selectedDate = data.first?.date +// } +// } +// } +// } +// +// private func calculateOverallScore() -> String { +// let totalCorrect = immediateMemoryData.reduce(0) { $0 + $1.correctAnswers } +// let totalQuestions = immediateMemoryData.reduce(0) { $0 + $1.correctAnswers + $1.incorrectAnswers } +// +// guard totalQuestions > 0 else { return "N/A" } +// +// let percentage = Double(totalCorrect) / Double(totalQuestions) * 100 +// return "\(Int(percentage))%" +// } +// +// private func getTotalAnswers() -> Int { +// immediateMemoryData.reduce(0) { $0 + $1.correctAnswers + $1.incorrectAnswers } +// } +// +// private func scoreColor() -> Color { +// let score = calculateOverallScore() +// guard score != "N/A" else { return .gray } +// +// let percentage = Int(score.dropLast()) ?? 0 +// +// switch percentage { +// case 0..<60: return .red +// case 60..<75: return .orange +// case 75..<90: return .blue +// default: return .green +// } +// } +//} +// +// +//// MARK: - Supporting Views +// +//struct ModernDonutChartView: View { +// let correctAnswers: Int +// let incorrectAnswers: Int +// +// @State private var animationProgress: Double = 0 +// +// var totalAnswers: Int { +// correctAnswers + incorrectAnswers +// } +// +// var correctRatio: Double { +// totalAnswers > 0 ? Double(correctAnswers) / Double(totalAnswers) : 0 +// } +// +// var body: some View { +// ZStack { +// // Background circle +// Circle() +// .stroke(Color(.systemGray5), lineWidth: 12) +// +// // Progress circle +// Circle() +// .trim(from: 0, to: correctRatio * animationProgress) +// .stroke( +// Color.green, +// style: StrokeStyle(lineWidth: 12, lineCap: .round) +// ) +// .rotationEffect(.degrees(-90)) +// .animation(.easeOut(duration: 1), value: animationProgress) +// +// // Inner circle with percentage +// VStack(spacing: 0) { +// Text("\(Int(correctRatio * 100 * animationProgress))%") +// .font(.system(size: 28, weight: .bold, design: .rounded)) +// .foregroundColor(.primary) +// .animation(.easeOut(duration: 1), value: animationProgress) +// +// Text("Correct") +// .font(.system(size: 12, weight: .medium)) +// .foregroundColor(.secondary) +// } +// } +// .onAppear { +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { +// withAnimation(.easeOut(duration: 1.0)) { +// animationProgress = 1.0 +// } +// } +// } +// } +//} +//struct DonutChartView: View { +// let correctAnswers: Int +// let incorrectAnswers: Int +// +// var body: some View { +// let totalAnswers = correctAnswers + incorrectAnswers +// let correctFraction = totalAnswers > 0 ? Double(correctAnswers) / Double(totalAnswers) : 0 +// let incorrectFraction = totalAnswers > 0 ? Double(incorrectAnswers) / Double(totalAnswers) : 0 +// +// ZStack { +// Circle() +// .stroke(Color.gray.opacity(0.2), lineWidth: 20) +// +// if totalAnswers > 0 { +// Circle() +// .trim(from: 0, to: correctFraction) +// .stroke(AngularGradient(gradient: Gradient(colors: [Color.customLightPurple]), center: .center), lineWidth: 40) +// .rotationEffect(.degrees(-90)) +// +// Circle() +// .trim(from: correctFraction, to: correctFraction + incorrectFraction) +// .stroke(AngularGradient(gradient: Gradient(colors: [Color.customLightRed]), center: .center), lineWidth: 40) +// .rotationEffect(.degrees(-90)) +// } else { +// Text("No memory data") +// .font(.subheadline) +// .foregroundColor(.gray) +// } +// +// Circle() +// .fill(Color.white) +// .frame(width: 180, height: 180) +// +// VStack { +// Text(totalAnswers > 0 ? "\(correctAnswers) / \(totalAnswers)" : "No Data") +// .font(.headline) +// if totalAnswers > 0 { +// Text("Correct") +// .font(.subheadline) +// } +// } +// } +// .frame(width: 190, height: 190) +// } +//} +//struct StatItemView: View { +// let title: String +// let value: String +// let color: Color +// +// var body: some View { +// HStack(spacing: 12) { +// Circle() +// .fill(color) +// .frame(width: 12, height: 12) +// +// Text(title) +// .font(.system(.subheadline, design: .rounded)) +// .foregroundColor(.secondary) +// +// Spacer() +// +// Text(value) +// .font(.system(size: 24, weight: .bold, design: .rounded)) +// .foregroundColor(.primary) +// } +// } +//} +// +// + +// +// immediateMemoryChart.swift +// recap_charts +// +// Created by admin70 on 13/11/24. +// + import SwiftUI +import Charts struct ImmediateReportDetailViewController: View { + @State private var immediateMemoryData: [ImmediateMemoryData] = [] + @State private var selectedDate: Date? = nil + private let verifiedUserDocID: String + + init(verifiedUserDocID: String) { + self.verifiedUserDocID = verifiedUserDocID + } + var body: some View { ZStack { - LinearGradient( - gradient: Gradient(colors: [Color(red: 0.8, green: 0.93, blue: 0.95), Color(red: 1.0, green: 0.88, blue: 0.88)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + + Color(AppColors.cardBackgroundColor) .ignoresSafeArea() ScrollView { - VStack(spacing: 20) { - HStack { - Spacer() - Text("Immediate Memory") - .font(.title) - .fontWeight(.bold) - Spacer() + VStack(spacing: Constants.paddingKeys.DefaultPaddingTop) { + // Data summary card + summaryCard + + // Date selector and chart + if !immediateMemoryData.isEmpty { + dateSelector + memoryChartCard + } else { + emptyStateView } - .padding(.horizontal) - - VStack(spacing: 15) { - ForEach(immediateMemoryData) { data in - VStack(spacing: 10) { - Text("Date: \(data.date, formatter: DateFormatter.shortDate)") - .font(.subheadline) - .padding(.top) + + // Information section + informationCard + } + .padding(.horizontal, Constants.paddingKeys.DefaultPaddingLeft) + .padding(.vertical, Constants.paddingKeys.DefaultPaddingTop) + } + } + .navigationTitle("Immediate Memory") + .onAppear { + loadMemoryData() + } + } + + // MARK: - Components + + var summaryCard: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + VStack(alignment: .leading) { + Text("Overall Performance") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + + Text(calculateOverallScore()) + .font(.system(size: 36, weight: .bold, design: .rounded)) + .foregroundColor(Color(AppColors.iconColor)) + } + + Spacer() + + Image(systemName: "brain.fill") + .font(.system(size: 32)) + .foregroundColor(Color(AppColors.iconColor)) + .frame(width: 60, height: 60) + .background( + Circle() + .fill(Color(AppColors.primaryButtonColor).opacity(0.8)) + ) + } + + if !immediateMemoryData.isEmpty { + Text("Based on \(getTotalAnswers()) questions over \(immediateMemoryData.count) sessions") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.iconColor)) + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: Constants.CardSize.DefaultCardCornerRadius) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(AppColors.primaryTextColor).opacity(CGFloat(Constants.FontandColors.defaultshadowOpacity)), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + } + + var dateSelector: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(immediateMemoryData) { data in + Button(action: { + withAnimation(.spring()) { + selectedDate = data.date + } + }) { + Text(data.date.formatted(.dateTime.day().month(.abbreviated))) + .font(.system(.subheadline, design: .rounded)) + .padding(.horizontal, Constants.paddingKeys.DefaultPaddingLeft) + .padding(.vertical, 8) + .background( + Capsule() + .fill(isDateSelected(data.date) ? + Color(AppColors.primaryButtonColor) : + Color(AppColors.secondaryButtonColor)) + ) + .foregroundColor(isDateSelected(data.date) ? + Color(AppColors.primaryButtonTextColor) : + Color(AppColors.primaryTextColor)) + } + } + } + .padding(.horizontal, 4) + } + } - // Integrating DonutChartView here - DonutChartView(correctAnswers: data.correctAnswers, incorrectAnswers: data.incorrectAnswers) - .frame(width: 300, height: 200) // Matches the frame size of DonutChartView - .padding() + // Helper function to check if a date is selected + private func isDateSelected(_ date: Date) -> Bool { + if let selectedDate = selectedDate { + return Calendar.current.isDate(date, inSameDayAs: selectedDate) + } + return false + } + + var memoryChartCard: some View { + let displayData = selectedDate == nil ? + immediateMemoryData : + immediateMemoryData.filter { $0.date == selectedDate } + + return VStack(alignment: .leading, spacing: 16) { + if !displayData.isEmpty { + ForEach(displayData) { data in + VStack(alignment: .leading, spacing: 12) { + Text(data.date.formatted(.dateTime.weekday(.wide).day().month().year())) + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + + HStack(alignment: .center) { + // Modern donut chart + ModernDonutChartView( + correctAnswers: data.correctAnswers, + incorrectAnswers: data.incorrectAnswers + ) + .frame( + width: Constants.CardSize.DefaultCardWidth, + height: Constants.CardSize.DefaultCardHeight + ) + + VStack(alignment: .leading, spacing: 16) { + StatItemView( + title: "Correct", + value: "\(data.correctAnswers)", + color: Color(AppColors.successColor) + ) + + StatItemView( + title: "Incorrect", + value: "\(data.incorrectAnswers)", + color: Color(AppColors.errorColor) + ) + + if data.correctAnswers + data.incorrectAnswers > 0 { + StatItemView( + title: "Accuracy", + value: "\(Int(Double(data.correctAnswers) / Double(data.correctAnswers + data.incorrectAnswers) * 100))%", + color: Color(AppColors.iconColor) + ) + } } - + .padding(.leading, 8) } } .padding() - .background(RoundedRectangle(cornerRadius: 15).fill(Color.white)) - .padding(.horizontal) - - VStack(alignment: .leading, spacing: 10) { - Text("About Immediate Insights") - .font(.headline) - .fontWeight(.bold) - - Text(""" - Your immediate memory helps you retain information learned just a few minutes ago. This section tracks how well you remember recent activities and conversations. - - Short-term memory is essential for processing recent information. Consistently performing well here indicates strong immediate recall abilities. - """) - .font(.body) - .foregroundColor(.black) - .padding() - .background(RoundedRectangle(cornerRadius: 15).fill(Color.white)) - .shadow(radius: 5) - } - .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: Constants.CardSize.DefaultCardCornerRadius) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(AppColors.primaryTextColor).opacity(CGFloat(Constants.FontandColors.defaultshadowOpacity)), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + } + } + } + } + + var emptyStateView: some View { + VStack(spacing: 16) { + Image(systemName: "brain.head.profile") + .font(.system(size: 56)) + .foregroundColor(Color(AppColors.secondaryTextColor).opacity(0.6)) + .padding(.bottom, 8) + + Text("No Memory Data Available") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + + Text("Complete memory exercises to see your results here") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 40) + .background( + RoundedRectangle(cornerRadius: Constants.CardSize.DefaultCardCornerRadius) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(AppColors.primaryTextColor).opacity(CGFloat(Constants.FontandColors.defaultshadowOpacity)), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + } + + var informationCard: some View { + VStack(alignment: .leading, spacing: 12) { + Text("About Immediate Memory") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + + VStack(alignment: .leading, spacing: 12) { + insightRow( + icon: "timer", + title: "Rapid Recall", + description: "Your ability to remember information learned minutes ago" + ) + + Divider() + + insightRow( + icon: "brain.head.profile", + title: "Working Memory", + description: "Essential for problem-solving and everyday tasks" + ) + + Divider() + + insightRow( + icon: "arrow.up.forward", + title: "Improvement Tips", + description: "Practice attention to detail and focus during learning" + ) + } + .padding() + .background( + RoundedRectangle(cornerRadius: Constants.CardSize.DefaultCardCornerRadius) + .fill(Color(AppColors.cardBackgroundColor)) + ) + } + .padding() + .background( + RoundedRectangle(cornerRadius: Constants.CardSize.DefaultCardCornerRadius) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(AppColors.primaryTextColor).opacity(CGFloat(Constants.FontandColors.defaultshadowOpacity)), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + } + + func insightRow(icon: String, title: String, description: String) -> some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(Color(AppColors.iconColor)) + .frame(width: 24, height: 24) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(Font(Constants.FontandColors.subtitleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + + Text(description) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + // MARK: - Helper Functions + + private func loadMemoryData() { + fetchImmediateMemoryData(for: verifiedUserDocID) { data in + DispatchQueue.main.async { + self.immediateMemoryData = data.sorted(by: { $0.date > $1.date }) + + // Set default selected date to today if available, otherwise most recent + let today = Date() + let todayData = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: today) }) + + if let todayData = todayData { + self.selectedDate = todayData.date + } else if !data.isEmpty { + self.selectedDate = data.first?.date } - .padding(.top) } } } + + private func calculateOverallScore() -> String { + let totalCorrect = immediateMemoryData.reduce(0) { $0 + $1.correctAnswers } + let totalQuestions = immediateMemoryData.reduce(0) { $0 + $1.correctAnswers + $1.incorrectAnswers } + + guard totalQuestions > 0 else { return "N/A" } + + let percentage = Double(totalCorrect) / Double(totalQuestions) * 100 + return "\(Int(percentage))%" + } + + private func getTotalAnswers() -> Int { + immediateMemoryData.reduce(0) { $0 + $1.correctAnswers + $1.incorrectAnswers } + } + + private func scoreColor() -> Color { + let score = calculateOverallScore() + guard score != "N/A" else { return Color(AppColors.secondaryTextColor) } + + let percentage = Int(score.dropLast()) ?? 0 + + switch percentage { + case 0..<60: return Color(AppColors.errorColor) + case 60..<75: return Color(.systemOrange) // No equivalent in AppColors, using systemOrange + case 75..<90: return Color(AppColors.iconColor) + default: return Color(AppColors.successColor) + } + } } -struct DonutChartView: View { +// MARK: - Supporting Views + +struct ModernDonutChartView: View { let correctAnswers: Int let incorrectAnswers: Int - + + @State private var animationProgress: Double = 0 + + var totalAnswers: Int { + correctAnswers + incorrectAnswers + } + + var correctRatio: Double { + totalAnswers > 0 ? Double(correctAnswers) / Double(totalAnswers) : 0 + } + var body: some View { - let totalAnswers = correctAnswers + incorrectAnswers - let correctFraction = totalAnswers > 0 ? Double(correctAnswers) / Double(totalAnswers) : 0 - let incorrectFraction = totalAnswers > 0 ? Double(incorrectAnswers) / Double(totalAnswers) : 0 - ZStack { + // Background circle Circle() - .stroke(Color.gray.opacity(0.2), lineWidth: 20) - - if correctFraction > 0 { - Circle() - .trim(from: 0, to: correctFraction) - .stroke(AngularGradient(gradient: Gradient(colors: [Color.customLightPurple]), center: .center), lineWidth: 40) - .rotationEffect(.degrees(-90)) - } - - if incorrectFraction > 0 { - Circle() - .trim(from: correctFraction, to: correctFraction + incorrectFraction) - .stroke(AngularGradient(gradient: Gradient(colors: [Color.customLightRed]), center: .center), lineWidth: 40) - .rotationEffect(.degrees(-90)) - } - + .stroke(Color(AppColors.secondaryTextColor).opacity(0.2), lineWidth: 12) + + // Progress circle Circle() - .fill(Color.white) - .frame(width: 180, height: 180) - - VStack { - Text("\(correctAnswers) / \(totalAnswers)") - .font(.headline) + .trim(from: 0, to: correctRatio * animationProgress) + .stroke( + Color(AppColors.successColor), + style: StrokeStyle(lineWidth: 12, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + .animation(.easeOut(duration: 1), value: animationProgress) + + // Inner circle with percentage + VStack(spacing: 0) { + Text("\(Int(correctRatio * 100 * animationProgress))%") + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundColor(Color(AppColors.primaryTextColor)) + .animation(.easeOut(duration: 1), value: animationProgress) + Text("Correct") - .font(.subheadline) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + withAnimation(.easeOut(duration: 1.0)) { + animationProgress = 1.0 + } } } - .frame(width: 190, height: 190) - } -} - -extension DateFormatter { - static var shortDate: DateFormatter { - let formatter = DateFormatter() - formatter.dateStyle = .short - return formatter } } -struct ImmediateReportDetailViewController_Previews: PreviewProvider { - static var previews: some View { - ImmediateReportDetailViewController() +struct StatItemView: View { + let title: String + let value: String + let color: Color + + var body: some View { + HStack(spacing: 12) { + Circle() + .fill(color) + .frame(width: 12, height: 12) + + Text(title) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + + Spacer() + + Text(value) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .foregroundColor(Color(AppColors.primaryTextColor)) + } } } +// Note: DonutChartView is not updated as it appears unused in the main view +// If needed, it can be updated similarly to use AppColors and Constants diff --git a/recap/Views/Charts/RecentReportDetailViewController.swift b/recap/Views/Charts/RecentReportDetailViewController.swift old mode 100644 new mode 100755 index c5e293b..8e3f5be --- a/recap/Views/Charts/RecentReportDetailViewController.swift +++ b/recap/Views/Charts/RecentReportDetailViewController.swift @@ -1,9 +1,280 @@ +//// +//// RecentMemoryChart.swift +//// recap_charts +//// +//// Created by admin70 on 13/11/24. +//// // -// RecentMemoryChart.swift -// recap_charts +//import SwiftUI +//import Charts // -// Created by admin70 on 13/11/24. +//struct BarChartView: View { +// var data: [RecentMemoryData] +// +// var body: some View { +// Chart { +// ForEach(data) { dayData in +// let shortDay = String(dayData.day.prefix(3)) +// let total = dayData.correctAnswers + dayData.incorrectAnswers +// +// BarMark( +// x: .value("Day", shortDay), +// y: .value("Correct", dayData.correctAnswers) +// ) +// .foregroundStyle(Color.customLightPurple.gradient) +// .cornerRadius(6) +// +// BarMark( +// x: .value("Day", shortDay), +// y: .value("Incorrect", dayData.incorrectAnswers) +// ) +// .foregroundStyle(Color.customLightRed.gradient) +// .cornerRadius(6) +// } +// } +// .chartYAxis { +// AxisMarks(position: .leading, values: .stride(by: 5)) { value in +// AxisValueLabel() +// .font(.caption) +// .foregroundStyle(Color.secondary) +// +// AxisGridLine() +// .foregroundStyle(Color.secondary.opacity(0.2)) +// } +// } +// .chartXAxis { +// AxisMarks { value in +// AxisValueLabel() +// .font(.caption) +// .foregroundStyle(Color.secondary) +// } +// } +// .chartForegroundStyleScale([ +// "Correct": Color.customLightPurple.gradient, +// "Incorrect": Color.customLightRed.gradient +// ]) +// .chartLegend(position: .bottom, alignment: .center, spacing: 20) +// .chartLegend(.visible) +// .frame(height: 250) +// .padding() +// } +//} // +//struct RecentReportDetailViewController: View { +// let verifiedUserDocID: String +// @StateObject private var recentMemoryDataModel = RecentMemoryDataModel() +// @Environment(\.colorScheme) var colorScheme +// @State private var animateChart = false +// +// var body: some View { +// ZStack { +// // Modern gradient background with subtle animation +// LinearGradient( +// gradient: Gradient(colors: [ +// Color(red: 0.95, green: 0.97, blue: 0.99), +// Color(red: 0.99, green: 0.95, blue: 0.97) +// ]), +// startPoint: .topLeading, +// endPoint: .bottomTrailing +// ) +// .ignoresSafeArea() +// +// ScrollView { +// VStack(spacing: 24) { +// +// // Summary card +// HStack(spacing: 16) { +// SummaryCard_recent( +// title: "Correct", +// value: String(recentMemoryDataModel.recentMemoryData.reduce(0) { $0 + $1.correctAnswers }), +// iconName: "checkmark.circle.fill", +// color: .customLightPurple +// ) +// +// SummaryCard_recent( +// title: "Incorrect", +// value: String(recentMemoryDataModel.recentMemoryData.reduce(0) { $0 + $1.incorrectAnswers }), +// iconName: "xmark.circle.fill", +// color: .customLightRed +// ) +// } +// .padding(.horizontal) +// +// // Week Navigation with improved UI +// HStack { +// Button(action: { +// withAnimation { +// recentMemoryDataModel.goToPreviousWeek(for: verifiedUserDocID, selectedMonth: getCurrentMonth()) +// animateChart = false +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { +// withAnimation(.easeInOut(duration: 0.5)) { +// animateChart = true +// } +// } +// } +// }) { +// Label("Previous", systemImage: "chevron.left") +// .font(.subheadline) +// .foregroundColor(recentMemoryDataModel.selectedWeekIndex > 0 ? .primary : .secondary) +// .padding(.vertical, 8) +// .padding(.horizontal, 16) +// .background(recentMemoryDataModel.selectedWeekIndex > 0 ? Color.primary.opacity(0.1) : Color.secondary.opacity(0.05)) +// .cornerRadius(8) +// } +// .disabled(recentMemoryDataModel.selectedWeekIndex == 0) +// +// Spacer() +// +// Text("Week \(recentMemoryDataModel.selectedWeekIndex + 1)") +// .font(.headline) +// .padding(.horizontal) +// .padding(.vertical, 8) +// .background( +// RoundedRectangle(cornerRadius: 8) +// .fill(Color.primary.opacity(0.05)) +// ) +// +// Spacer() +// +// Button(action: { +// withAnimation { +// recentMemoryDataModel.goToNextWeek(for: verifiedUserDocID, selectedMonth: getCurrentMonth()) +// animateChart = false +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { +// withAnimation(.easeInOut(duration: 0.5)) { +// animateChart = true +// } +// } +// } +// }) { +// Label("Next", systemImage: "chevron.right") +// .font(.subheadline) +// .foregroundColor(recentMemoryDataModel.selectedWeekIndex < recentMemoryDataModel.availableWeeks.count - 1 ? .primary : .secondary) +// .padding(.vertical, 8) +// .padding(.horizontal, 16) +// .background(recentMemoryDataModel.selectedWeekIndex < recentMemoryDataModel.availableWeeks.count - 1 ? Color.primary.opacity(0.1) : Color.secondary.opacity(0.05)) +// .cornerRadius(8) +// } +// .disabled(recentMemoryDataModel.selectedWeekIndex == recentMemoryDataModel.availableWeeks.count - 1) +// } +// .padding(.horizontal) +// +// // Bar Chart Section with enhanced styling +// VStack(alignment: .leading, spacing: 16) { +// Text("Daily Performance") +// .font(.headline) +// .padding(.horizontal) +// +// if animateChart { +// BarChartView(data: recentMemoryDataModel.recentMemoryData) +// .transition(.opacity) +// } else { +// BarChartView(data: recentMemoryDataModel.recentMemoryData) +// .opacity(0) +// } +// } +// .padding(.vertical) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5) +// ) +// .padding(.horizontal) +// +// // About Insights Section with modern card design +// VStack(alignment: .leading, spacing: 16) { +// HStack { +// Image(systemName: "brain.head.profile") +// .font(.title2) +// .foregroundColor(.purple) +// +// Text("About Recent Insights") +// .font(.title3) +// .fontWeight(.semibold) +// } +// +// Text(""" +// Your recent memory insights analyze your ability to recall information from the last week. This section evaluates how well you retain details from recent interactions, helping to monitor your short-term memory. +// """) +// .font(.subheadline) +// .foregroundColor(.secondary) +// +// HStack(alignment: .top, spacing: 12) { +// Image(systemName: "sparkles") +// .foregroundColor(.blue) +// .frame(width: 24, height: 24) +// +// Text("A strong performance in this area indicates healthy short-term recall. Identifying trends can provide insights into cognitive health over time.") +// .font(.subheadline) +// .foregroundColor(.primary) +// } +// } +// .padding(20) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5) +// ) +// .padding(.horizontal) +// } +// .padding(.bottom, 30) +// } +// } +// .onAppear { +// let currentMonth = getCurrentMonth() +// recentMemoryDataModel.fetchWeeks(for: verifiedUserDocID, selectedMonth: currentMonth) +// +// // Animate chart on appear +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { +// withAnimation(.easeInOut(duration: 0.8)) { +// animateChart = true +// } +// } +// } +// .navigationTitle("Recent Memory") +// } +// +// private func getCurrentMonth() -> String { +// let formatter = DateFormatter() +// formatter.dateFormat = "yyyy-MM" +// return formatter.string(from: Date()) +// } +//} +// +//// Helper view for summary cards +//struct SummaryCard_recent: View { +// var title: String +// var value: String +// var iconName: String +// var color: Color +// +// var body: some View { +// VStack(alignment: .leading, spacing: 8) { +// HStack { +// Image(systemName: iconName) +// .foregroundColor(color) +// .font(.system(size: 16, weight: .semibold)) +// +// Text(title) +// .font(.subheadline) +// .foregroundColor(.secondary) +// } +// +// Text(value) +// .font(.system(size: 28, weight: .bold)) +// .foregroundColor(.primary) +// } +// .frame(maxWidth: .infinity, alignment: .leading) +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: color.opacity(0.1), radius: 8, x: 0, y: 4) +// ) +// } +//} + + import SwiftUI import Charts @@ -14,108 +285,271 @@ struct BarChartView: View { var body: some View { Chart { ForEach(data) { dayData in + let shortDay = String(dayData.day.prefix(3)) + let total = dayData.correctAnswers + dayData.incorrectAnswers + BarMark( - x: .value("Week", dayData.week), - y: .value("Correct Answers", dayData.correctAnswers) + x: .value("Day", shortDay), + y: .value("Correct", dayData.correctAnswers) ) - .foregroundStyle(Color.customLightPurple) + .foregroundStyle(Color(AppColors.iconColor).opacity(0.8)) + .cornerRadius(CGFloat(Constants.CardSize.DefaultCardCornerRadius)) BarMark( - x: .value("Week", dayData.week), - y: .value("Incorrect Answers", dayData.incorrectAnswers) + x: .value("Day", shortDay), + y: .value("Incorrect", dayData.incorrectAnswers) ) - .foregroundStyle(Color.customLightRed) - .annotation(position: .top) { - Text("\(dayData.correctAnswers + dayData.incorrectAnswers)") - .font(.caption) - .foregroundColor(.primary) - } + .foregroundStyle(Color(AppColors.errorColor).gradient) + .cornerRadius(CGFloat(Constants.CardSize.DefaultCardCornerRadius)) } } .chartYAxis { - AxisMarks(position: .leading, values: .stride(by: 5)) + AxisMarks(position: .leading, values: .stride(by: 5)) { value in + AxisValueLabel() + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundStyle(Color(AppColors.secondaryTextColor)) + + AxisGridLine() + .foregroundStyle(Color(AppColors.secondaryTextColor).opacity(0.2)) + } + } + .chartXAxis { + AxisMarks { value in + AxisValueLabel() + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundStyle(Color(AppColors.secondaryTextColor)) + } } - .frame(width: 250, height: 250) - .padding() - .padding(.horizontal) + .chartForegroundStyleScale([ + "Correct": Color(AppColors.iconColor).gradient, + "Incorrect": Color(AppColors.errorColor).gradient + ]) + .chartLegend(position: .bottom, alignment: .center, spacing: 20) + .chartLegend(.visible) + .frame(height: 250) + .padding(.all, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) } } struct RecentReportDetailViewController: View { - var data: [RecentMemoryData] - + let verifiedUserDocID: String + @StateObject private var recentMemoryDataModel = RecentMemoryDataModel() + @Environment(\.colorScheme) var colorScheme + @State private var animateChart = false + var body: some View { ZStack { - // Background Gradient - LinearGradient( - gradient: Gradient(colors: [Color(red: 0.8, green: 0.93, blue: 0.95), Color(red: 1.0, green: 0.88, blue: 0.88)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + Color(AppColors.cardBackgroundColor) .ignoresSafeArea() ScrollView { - VStack(spacing: 20) { - // Title section + VStack(spacing: 24) { + HStack(spacing: 16) { + SummaryCard_recent( + title: "Correct", + value: String(recentMemoryDataModel.recentMemoryData.reduce(0) { $0 + $1.correctAnswers }), + iconName: "checkmark.circle.fill", + color: Color(AppColors.successColor) + ) + + SummaryCard_recent( + title: "Incorrect", + value: String(recentMemoryDataModel.recentMemoryData.reduce(0) { $0 + $1.incorrectAnswers }), + iconName: "xmark.circle.fill", + color: Color(AppColors.errorColor) + ) + } + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + HStack { + Button(action: { + withAnimation { + recentMemoryDataModel.goToPreviousWeek(for: verifiedUserDocID, selectedMonth: getCurrentMonth()) + animateChart = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation(.easeInOut(duration: 0.5)) { + animateChart = true + } + } + } + }) { + Label("Previous", systemImage: "chevron.left") + .font(Font(Constants.FontandColors.subtitleFont)) + .foregroundColor(recentMemoryDataModel.selectedWeekIndex > 0 ? Color(AppColors.primaryTextColor) : Color(AppColors.secondaryTextColor)) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + recentMemoryDataModel.selectedWeekIndex > 0 ? + Color(AppColors.secondaryButtonColor) : + Color(AppColors.secondaryTextColor).opacity(0.05) + ) + .cornerRadius(CGFloat(Constants.ButtonStyle.DefaultButtonCornerRadius)) + } + .disabled(recentMemoryDataModel.selectedWeekIndex == 0) + Spacer() - Text("Recent Memory") - .font(.title) - .fontWeight(.bold) + + Text("Week \(recentMemoryDataModel.selectedWeekIndex + 1)") + .font(Font(Constants.FontandColors.titleFont)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.ButtonStyle.DefaultButtonCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + ) + Spacer() + + Button(action: { + withAnimation { + recentMemoryDataModel.goToNextWeek(for: verifiedUserDocID, selectedMonth: getCurrentMonth()) + animateChart = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation(.easeInOut(duration: 0.5)) { + animateChart = true + } + } + } + }) { + Label("Next", systemImage: "chevron.right") + .font(Font(Constants.FontandColors.subtitleFont)) + .foregroundColor(recentMemoryDataModel.selectedWeekIndex < recentMemoryDataModel.availableWeeks.count - 1 ? Color(AppColors.primaryTextColor) : Color(AppColors.secondaryTextColor)) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + recentMemoryDataModel.selectedWeekIndex < recentMemoryDataModel.availableWeeks.count - 1 ? + Color(AppColors.secondaryButtonColor) : + Color(AppColors.secondaryTextColor).opacity(0.05) + ) + .cornerRadius(CGFloat(Constants.ButtonStyle.DefaultButtonCornerRadius)) + } + .disabled(recentMemoryDataModel.selectedWeekIndex == recentMemoryDataModel.availableWeeks.count - 1) } - .padding(.horizontal) - - // Bar Chart section - VStack(spacing: 15) { - BarChartView(data: data) - .frame(width: 300, height: 250) // Standardized dimensions - .padding() - .background(RoundedRectangle(cornerRadius: 15).fill(Color.white)) - .padding(.horizontal) - } - - // Chart Legend section - HStack { - Label("Correct", systemImage: "circle.fill") - .foregroundColor(.customLightPurple) - .font(.caption2) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + + VStack(alignment: .leading, spacing: 16) { + Text("Daily Performance") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) - Label("Incorrect", systemImage: "circle.fill") - .foregroundColor(.customLightRed) - .font(.caption2) + if animateChart { + BarChartView(data: recentMemoryDataModel.recentMemoryData) + .transition(.opacity) + } else { + BarChartView(data: recentMemoryDataModel.recentMemoryData) + .opacity(0) + } } - .padding(.horizontal) - - // About Insights section - VStack(alignment: .leading, spacing: 10) { - Text("About Recent Insights") - .font(.headline) - .fontWeight(.bold) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(cgColor: Constants.FontandColors.defaultshadowColor).opacity(Constants.FontandColors.defaultshadowOpacity), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: "brain.head.profile") + .font(.title2) + .foregroundColor(Color(AppColors.iconColor)) + + Text("About Recent Insights") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + } Text(""" Your recent memory insights analyze your ability to recall information from the last week. This section evaluates how well you retain details from recent interactions, helping to monitor your short-term memory. - - A strong performance in this area indicates healthy short-term recall. Identifying trends can provide insights into cognitive health over time. """) - .font(.body) - .foregroundColor(.black) - .padding() - .background( - RoundedRectangle(cornerRadius: 15) - .fill(Color.white) - ) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + + HStack(alignment: .top, spacing: 12) { + Image(systemName: "sparkles") + .foregroundColor(Color(AppColors.highlightColor)) + .frame(width: 24, height: 24) + + Text("A strong performance in this area indicates healthy short-term recall. Identifying trends can provide insights into cognitive health over time.") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } } - .padding(.horizontal) + .padding(CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(cgColor: Constants.FontandColors.defaultshadowColor).opacity(Constants.FontandColors.defaultshadowOpacity), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + } + .padding(.bottom, 30) + } + } + .onAppear { + let currentMonth = getCurrentMonth() + recentMemoryDataModel.fetchWeeks(for: verifiedUserDocID, selectedMonth: currentMonth) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + withAnimation(.easeInOut(duration: 0.8)) { + animateChart = true } - .padding() } } + .navigationTitle("Recent Memory") + } + + private func getCurrentMonth() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM" + return formatter.string(from: Date()) } } -struct RecentReportDetailViewController_Previews: PreviewProvider { - static var previews: some View { - RecentReportDetailViewController(data: recentMemoryData) +struct SummaryCard_recent: View { + var title: String + var value: String + var iconName: String + var color: Color + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: iconName) + .foregroundColor(color) + .font(.system(size: 16, weight: .semibold)) + + Text(title) + .font(Font(Constants.FontandColors.subtitleFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } + + Text(value) + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: color.opacity(0.1), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) } } diff --git a/recap/Views/Charts/RemoteReportDetailViewController.swift b/recap/Views/Charts/RemoteReportDetailViewController.swift old mode 100644 new mode 100755 index 142e9b6..a662963 --- a/recap/Views/Charts/RemoteReportDetailViewController.swift +++ b/recap/Views/Charts/RemoteReportDetailViewController.swift @@ -1,112 +1,1114 @@ +//// +//// RemoteReportDetailViewController.swift +//// recap_charts +//// +//// Created by admin70 on 13/11/24. +//// // -// Untitled.swift -// recap_charts +//import Charts +//import SwiftUI // -// Created by admin70 on 13/11/24. +//struct LineChartView: View { +// let data: [RemoteMemoryData] +// @State private var showChartData = false // +// var body: some View { +// VStack { +// Chart { +// ForEach(data) { report in +// if report.day != "Summary" { // Skip summary for detailed graph +// LineMark( +// x: .value("Day", report.day), +// y: .value("Correct Answers", showChartData ? report.correctAnswers : 0) +// ) +// .foregroundStyle(Color.customLightPurple.gradient) +// .lineStyle(StrokeStyle(lineWidth: 3, lineCap: .round)) +// .symbol { +// Circle() +// .fill(Color.customLightPurple) +// .frame(width: 8, height: 8) +// } +// .interpolationMethod(.catmullRom) +// +// LineMark( +// x: .value("Day", report.day), +// y: .value("Incorrect Answers", showChartData ? report.incorrectAnswers : 0) +// ) +// .foregroundStyle(Color.customLightRed.gradient) +// .lineStyle(StrokeStyle(lineWidth: 3, lineCap: .round)) +// .symbol { +// Circle() +// .fill(Color.customLightRed) +// .frame(width: 8, height: 8) +// } +// .interpolationMethod(.catmullRom) +// } +// } +// } +// .chartYAxis { +// AxisMarks(position: .leading) { _ in +// AxisValueLabel() +// .font(.caption) +// .foregroundStyle(Color.secondary) +// +// AxisGridLine() +// .foregroundStyle(Color.secondary.opacity(0.2)) +// } +// } +// .chartXAxis { +// AxisMarks { _ in +// AxisValueLabel() +// .font(.caption) +// .foregroundStyle(Color.secondary) +// } +// } +// .chartForegroundStyleScale([ +// "Correct Answers": Color.customLightPurple.gradient, +// "Incorrect Answers": Color.customLightRed.gradient, +// ]) +// .chartLegend(position: .bottom, alignment: .center, spacing: 20) +// .chartLegend(.visible) +// .animation(.easeInOut(duration: 1.0), value: showChartData) +// .frame(height: 250) +// .padding(.horizontal) +// .onAppear { +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { +// showChartData = true +// } +// } +// } +// } +//} +// +//struct RemoteReportDetailViewController: View { +// let verifiedUserDocID: String +// @State private var remoteMemoryData: [RemoteMemoryData] = [] +// @State private var isLoading = true +// @State private var selectedTimeframe = "Month" +// private let timeframeOptions = ["Week", "Month", "Quarter"] +// +// private var currentMonth: String { +// let formatter = DateFormatter() +// formatter.dateFormat = "yyyy-MM" +// return formatter.string(from: Date()) +// } +// +// var body: some View { +// ZStack { +// // Modern gradient background with subtle animation +// LinearGradient( +// gradient: Gradient(colors: [ +// Color(red: 0.95, green: 0.97, blue: 0.99), +// Color(red: 0.99, green: 0.95, blue: 0.97), +// ]), +// startPoint: .topLeading, +// endPoint: .bottomTrailing +// ) +// .ignoresSafeArea() +// +// ScrollView { +// VStack(spacing: 24) { +// // Timeframe selector +// HStack { +// ForEach(timeframeOptions, id: \.self) { option in +// Button(action: { +// withAnimation { +// selectedTimeframe = option +// } +// }) { +// Text(option) +// .fontWeight(selectedTimeframe == option ? .semibold : .regular) +// .foregroundColor(selectedTimeframe == option ? .white : .primary) +// .padding(.vertical, 8) +// .padding(.horizontal, 16) +// .background( +// RoundedRectangle(cornerRadius: 20) +// .fill(selectedTimeframe == option ? +// Color.customLightPurple : +// Color.primary.opacity(0.05)) +// ) +// } +// .buttonStyle(PlainButtonStyle()) +// } +// } +// .padding(.horizontal) +// +// // Stats summary cards +// if let summary = remoteMemoryData.first(where: { $0.day == "Summary" }) { +// HStack(spacing: 16) { +// // Correct answers card +// SummaryCard( +// title: "Correct", +// value: "\(summary.correctAnswers)", +// iconName: "checkmark.circle.fill", +// color: .customLightPurple +// ) +// +// // Incorrect answers card +// SummaryCard( +// title: "Incorrect", +// value: "\(summary.incorrectAnswers)", +// iconName: "xmark.circle.fill", +// color: .customLightRed +// ) +// } +// .padding(.horizontal) +// } +// +// // Chart section with modern card design +// VStack(alignment: .leading, spacing: 16) { +// Text("Memory Performance Trend") +// .font(.headline) +// .padding(.horizontal) +// +// if isLoading { +// HStack { +// Spacer() +// ProgressView() +// .scaleEffect(1.5) +// .padding() +// Spacer() +// } +// .frame(height: 250) +// } else if remoteMemoryData.isEmpty { +// HStack { +// Spacer() +// VStack(spacing: 16) { +// Image(systemName: "chart.line.downtrend.xyaxis") +// .font(.system(size: 48)) +// .foregroundColor(.secondary.opacity(0.6)) +// +// Text("No data available for this timeframe") +// .font(.subheadline) +// .foregroundColor(.secondary) +// } +// .padding() +// Spacer() +// } +// .frame(height: 250) +// } else { +// LineChartView(data: remoteMemoryData) +// } +// } +// .padding(.vertical) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5) +// ) +// .padding(.horizontal) +// +// // Performance insights card +// if let summary = remoteMemoryData.first(where: { $0.day == "Summary" }) { +// VStack(alignment: .leading, spacing: 16) { +// HStack { +// Image(systemName: "sparkles.rectangle.stack") +// .font(.title2) +// .foregroundColor(.customLightPurple) +// +// Text("Performance Insights") +// .font(.title3) +// .fontWeight(.semibold) +// } +// +// VStack(alignment: .leading, spacing: 10) { +// InsightRow( +// iconName: "arrow.up.right", +// color: .green, +// text: "Your highest performance was on day \(getBestPerformanceDay()) with \(getHighestCorrectAnswers()) correct answers." +// ) +// +// Divider() +// +// InsightRow( +// iconName: "chart.line.uptrend.xyaxis", +// color: .blue, +// text: "Overall accuracy rate: \(calculateAccuracyRate())%" +// ) +// } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 12) +// .fill(Color.gray.opacity(0.05)) +// ) +// } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5) +// ) +// .padding(.horizontal) +// } +// +// // About insights section with modern design +// VStack(alignment: .leading, spacing: 16) { +// HStack { +// Image(systemName: "brain.head.profile") +// .font(.title2) +// .foregroundColor(.purple) +// +// Text("About Remote Insights") +// .font(.title3) +// .fontWeight(.semibold) +// } +// +// Text("Remote memory refers to your ability to recall events or information from the distant past, typically over a week or more.") +// .font(.subheadline) +// .foregroundColor(.secondary) +// +// HStack(alignment: .top, spacing: 12) { +// Image(systemName: "chart.xyaxis.line") +// .foregroundColor(.blue) +// .frame(width: 24, height: 24) +// +// Text("By tracking your performance over time, we can observe trends that help you and your caregivers understand how well your long-term memory is functioning.") +// .font(.subheadline) +// .foregroundColor(.primary) +// } +// } +// .padding(20) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5) +// ) +// .padding(.horizontal) +// } +// .padding(.bottom, 30) +// } +// .navigationTitle("Remote Memory") +// } +// .onAppear { +// print("🟢 RemoteReportDetailViewController appeared. Fetching data...") +// +// // Simulate loading +// DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { +// fetchRemoteMemoryData(for: verifiedUserDocID, month: currentMonth) { data in +// DispatchQueue.main.async { +// self.isLoading = false +// +// if data.isEmpty { +// print("⚠️ No data found, setting default values.") +// self.remoteMemoryData = [RemoteMemoryData(day: "01", correctAnswers: 0, incorrectAnswers: 0)] +// } else { +// self.remoteMemoryData = data +// } +// print("📊 Final Remote Memory Data: \(self.remoteMemoryData)") +// } +// } +// } +// } +// } +// +// // Helper functions for insights +// private func getBestPerformanceDay() -> String { +// let dataPoints = remoteMemoryData.filter { $0.day != "Summary" } +// if let bestDay = dataPoints.max(by: { $0.correctAnswers < $1.correctAnswers }) { +// return bestDay.day +// } +// return "N/A" +// } +// +// private func getHighestCorrectAnswers() -> Int { +// let dataPoints = remoteMemoryData.filter { $0.day != "Summary" } +// if let bestDay = dataPoints.max(by: { $0.correctAnswers < $1.correctAnswers }) { +// return bestDay.correctAnswers +// } +// return 0 +// } +// +// private func calculateAccuracyRate() -> String { +// if let summary = remoteMemoryData.first(where: { $0.day == "Summary" }) { +// let total = summary.correctAnswers + summary.incorrectAnswers +// if total > 0 { +// let accuracy = Double(summary.correctAnswers) / Double(total) * 100 +// return String(format: "%.1f", accuracy) +// } +// } +// return "0.0" +// } +//} +// +//// Helper view for summary cards +//struct SummaryCard: View { +// var title: String +// var value: String +// var iconName: String +// var color: Color +// +// var body: some View { +// VStack(alignment: .leading, spacing: 8) { +// HStack { +// Image(systemName: iconName) +// .foregroundColor(color) +// .font(.system(size: 16, weight: .semibold)) +// +// Text(title) +// .font(.subheadline) +// .foregroundColor(.secondary) +// } +// +// Text(value) +// .font(.system(size: 28, weight: .bold)) +// .foregroundColor(.primary) +// } +// .frame(maxWidth: .infinity, alignment: .leading) +// .padding() +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: color.opacity(0.1), radius: 8, x: 0, y: 4) +// ) +// } +//} +// +//// Helper view for insight rows +//struct InsightRow: View { +// var iconName: String +// var color: Color +// var text: String +// +// var body: some View { +// HStack(alignment: .top, spacing: 12) { +// Image(systemName: iconName) +// .foregroundColor(color) +// .frame(width: 24, height: 24) +// +// Text(text) +// .font(.subheadline) +// .foregroundColor(.primary) +// .fixedSize(horizontal: false, vertical: true) +// } +// } +//} +// +//// Helper view for percentage change +//struct PercentageChangeView: View { +// var currentValue: Int +// var previousValue: Int +// var label: String +// +// private var percentageChange: Double { +// guard previousValue > 0 else { return 0 } +// return Double(currentValue - previousValue) / Double(previousValue) * 100 +// } +// +// private var isPositive: Bool { +// return percentageChange >= 0 +// } +// +// var body: some View { +// HStack(spacing: 4) { +// Image(systemName: isPositive ? "arrow.up.right" : "arrow.down.right") +// .font(.caption) +// .foregroundColor(isPositive ? .green : .red) +// +// Text("\(abs(percentageChange), specifier: "%.1f")%") +// .font(.caption) +// .foregroundColor(isPositive ? .green : .red) +// +// Text(label) +// .font(.caption) +// .foregroundColor(.secondary) +// } +// } +//} +// +//// Animated progress bar for visual metrics +//struct AnimatedProgressBar: View { +// var value: Double +// var maxValue: Double +// var color: Color +// @State private var width: Double = 0 +// +// private var percentage: Double { +// return min(value / maxValue, 1.0) +// } +// +// var body: some View { +// ZStack(alignment: .leading) { +// Rectangle() +// .fill(color.opacity(0.2)) +// .frame(height: 8) +// .cornerRadius(4) +// +// Rectangle() +// .fill(color) +// .frame(width: width, height: 8) +// .cornerRadius(4) +// } +// .onAppear { +// withAnimation(.easeInOut(duration: 1.0)) { +// width = percentage * UIScreen.main.bounds.width * 0.8 +// } +// } +// } +//} +// +//// Weekly trend view for compact display +//struct WeeklyTrendView: View { +// var data: [RemoteMemoryData] +// +// var body: some View { +// HStack(spacing: 4) { +// ForEach(data.prefix(7).filter { $0.day != "Summary" }, id: \.day) { day in +// let total = day.correctAnswers + day.incorrectAnswers +// let percentage = total > 0 ? Double(day.correctAnswers) / Double(total) : 0 +// +// VStack(spacing: 2) { +// Text(day.day) +// .font(.system(size: 10)) +// .foregroundColor(.secondary) +// +// Rectangle() +// .fill( +// LinearGradient( +// colors: [ +// Color.customLightPurple.opacity(0.7), +// Color.customLightPurple, +// ], +// startPoint: .bottom, +// endPoint: .top +// ) +// ) +// .frame(width: 20, height: max(percentage * 60, 4)) +// .cornerRadius(2) +// } +// } +// } +// .frame(height: 70) +// .padding(.vertical, 8) +// } +//} +// +//// Empty state view +//struct EmptyStateView: View { +// var message: String +// var iconName: String +// +// var body: some View { +// VStack(spacing: 16) { +// Image(systemName: iconName) +// .font(.system(size: 48)) +// .foregroundColor(.secondary.opacity(0.6)) +// +// Text(message) +// .font(.subheadline) +// .foregroundColor(.secondary) +// .multilineTextAlignment(.center) +// } +// .padding(32) +// .frame(maxWidth: .infinity) +// .background( +// RoundedRectangle(cornerRadius: 16) +// .fill(Color.white.opacity(0.8)) +// .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 5) +// ) +// } +//} +// +//// Helpful utility functions +//extension RemoteReportDetailViewController { +// func getPerformanceTrend() -> String { +// let regularData = remoteMemoryData.filter { $0.day != "Summary" } +// guard regularData.count >= 2 else { return "Insufficient data" } +// +// let firstHalf = Array(regularData.prefix(regularData.count / 2)) +// let secondHalf = Array(regularData.suffix(regularData.count / 2)) +// +// let firstHalfCorrect = firstHalf.reduce(0) { $0 + $1.correctAnswers } +// let secondHalfCorrect = secondHalf.reduce(0) { $0 + $1.correctAnswers } +// +// if secondHalfCorrect > firstHalfCorrect { +// return "Improving" +// } else if secondHalfCorrect < firstHalfCorrect { +// return "Declining" +// } else { +// return "Stable" +// } +// } +// +// func getColorForTrend() -> Color { +// let trend = getPerformanceTrend() +// switch trend { +// case "Improving": +// return .green +// case "Declining": +// return .red +// default: +// return .blue +// } +// } +//} +// +//// Tag view for categorization +//struct TagView: View { +// var text: String +// var color: Color +// +// var body: some View { +// Text(text) +// .font(.caption) +// .foregroundColor(.white) +// .padding(.horizontal, 8) +// .padding(.vertical, 4) +// .background(color) +// .cornerRadius(12) +// } +//} -import SwiftUI import Charts +import SwiftUI struct LineChartView: View { - let data: [MonthlyReport] - let threshold: Int - + let data: [RemoteMemoryData] + @State private var showChartData = false + var body: some View { - Chart { - ForEach(data) { report in - // Line for correct answers - LineMark( - x: .value("Date", report.date, unit: .day), - y: .value("Correct Answers", report.correctAnswers) - ) - .foregroundStyle(.blue) - .lineStyle(StrokeStyle(lineWidth: 2)) + VStack { + Chart { + ForEach(data) { report in + if report.day != "Summary" { + LineMark( + x: .value("Day", report.day), + y: .value("Correct Answers", showChartData ? report.correctAnswers : 0) + ) + .foregroundStyle(Color(AppColors.successColor).gradient) + .lineStyle(StrokeStyle(lineWidth: 3, lineCap: .round)) + .symbol { + Circle() + .fill(Color(AppColors.successColor)) + .frame(width: 8, height: 8) + } + .interpolationMethod(.catmullRom) + + LineMark( + x: .value("Day", report.day), + y: .value("Incorrect Answers", showChartData ? report.incorrectAnswers : 0) + ) + .foregroundStyle(Color(AppColors.errorColor).gradient) + .lineStyle(StrokeStyle(lineWidth: 3, lineCap: .round)) + .symbol { + Circle() + .fill(Color(AppColors.errorColor)) + .frame(width: 8, height: 8) + } + .interpolationMethod(.catmullRom) + } + } + } + .chartYAxis { + AxisMarks(position: .leading) { _ in + AxisValueLabel() + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundStyle(Color(AppColors.secondaryTextColor)) + + AxisGridLine() + .foregroundStyle(Color(AppColors.secondaryTextColor).opacity(0.2)) + } + } + .chartXAxis { + AxisMarks { _ in + AxisValueLabel() + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundStyle(Color(AppColors.secondaryTextColor)) + } + } + .chartForegroundStyleScale([ + "Correct Answers": Color(AppColors.successColor).gradient, + "Incorrect Answers": Color(AppColors.errorColor).gradient, + ]) + .chartLegend(position: .bottom, alignment: .center, spacing: 20) + .chartLegend(.visible) + .animation(.easeInOut(duration: 1.0), value: showChartData) + .frame(height: 250) + .padding(.all, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + showChartData = true + } } - - // Threshold line - RuleMark( - y: .value("Threshold", threshold) - ) - .foregroundStyle(.red) - } - .chartYAxis { - AxisMarks(position: .leading) } - .frame(width: 250, height: 250) // Standardized dimensions - .padding() - .padding(.horizontal) } } struct RemoteReportDetailViewController: View { - let monthlyData: [MonthlyReport] + let verifiedUserDocID: String + @State private var remoteMemoryData: [RemoteMemoryData] = [] + @State private var isLoading = true + @State private var selectedTimeframe = "Month" + private let timeframeOptions = ["Week", "Month", "Quarter"] + + private var currentMonth: String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM" + return formatter.string(from: Date()) + } var body: some View { ZStack { - LinearGradient( - gradient: Gradient(colors: [ - Color(red: 0.8, green: 0.93, blue: 0.95), - Color(red: 1.0, green: 0.88, blue: 0.88) - ]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + Color(AppColors.cardBackgroundColor) .ignoresSafeArea() ScrollView { - VStack(spacing: 20) { - // Title section + VStack(spacing: 24) { HStack { - Spacer() - Text("Remote Memory") - .font(.title) - .fontWeight(.bold) - Spacer() + ForEach(timeframeOptions, id: \.self) { option in + Button(action: { + withAnimation { + selectedTimeframe = option + } + }) { + Text(option) + .font(Font(Constants.FontandColors.subtitleFont)) + .fontWeight(selectedTimeframe == option ? .semibold : .regular) + .foregroundColor(selectedTimeframe == option ? Color(AppColors.inverseTextColor) : Color(AppColors.primaryTextColor)) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.ButtonStyle.DefaultButtonCornerRadius)) + .fill(selectedTimeframe == option ? + Color(AppColors.primaryButtonColor) : + Color(AppColors.secondaryButtonColor)) + ) + } + .buttonStyle(PlainButtonStyle()) + } } - .padding(.horizontal) - - // Line Chart section - VStack(spacing: 15) { - LineChartView(data: monthlyData, threshold: 15) - .frame(width: 300, height: 250) // Matches the chart dimensions from code 1 - .padding() - .background(RoundedRectangle(cornerRadius: 15).fill(Color.white)) - .padding(.horizontal) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + + if let summary = remoteMemoryData.first(where: { $0.day == "Summary" }) { + HStack(spacing: 16) { + SummaryCard( + title: "Correct", + value: "\(summary.correctAnswers)", + iconName: "checkmark.circle.fill", + color: Color(AppColors.successColor) + ) + + SummaryCard( + title: "Incorrect", + value: "\(summary.incorrectAnswers)", + iconName: "xmark.circle.fill", + color: Color(AppColors.errorColor) + ) + } + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) } - // About Insights section - VStack(alignment: .leading, spacing: 10) { - Text("About Remote Insights") - .font(.headline) - .fontWeight(.bold) - - Text(""" - Remote memory refers to your ability to recall events or information from the distant past, typically over a week or more. By tracking your performance over time, we can observe trends that help you and your caregivers understand how well your long-term memory is functioning. - """) - .font(.body) - .foregroundColor(.black) + VStack(alignment: .leading, spacing: 16) { + Text("Memory Performance Trend") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + + if isLoading { + HStack { + Spacer() + ProgressView() + .scaleEffect(1.5) + .padding() + Spacer() + } + .frame(height: 250) + } else if remoteMemoryData.isEmpty { + HStack { + Spacer() + VStack(spacing: 16) { + Image(systemName: "chart.line.downtrend.xyaxis") + .font(.system(size: 48)) + .foregroundColor(Color(AppColors.secondaryTextColor).opacity(0.6)) + + Text("No data available for this timeframe") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } + .padding() + Spacer() + } + .frame(height: 250) + } else { + LineChartView(data: remoteMemoryData) + } + } + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(cgColor: Constants.FontandColors.defaultshadowColor).opacity(Constants.FontandColors.defaultshadowOpacity), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + + if let summary = remoteMemoryData.first(where: { $0.day == "Summary" }) { + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: "sparkles.rectangle.stack") + .font(.title2) + .foregroundColor(Color(AppColors.iconColor)) + + Text("Performance Insights") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + } + + VStack(alignment: .leading, spacing: 10) { + InsightRow( + iconName: "arrow.up.right", + color: Color(AppColors.successColor), + text: "Your highest performance was on day \(getBestPerformanceDay()) with \(getHighestCorrectAnswers()) correct answers." + ) + + Divider() + + InsightRow( + iconName: "chart.line.uptrend.xyaxis", + color: Color(AppColors.iconColor), + text: "Overall accuracy rate: \(calculateAccuracyRate())%" + ) + } .padding() - .background(RoundedRectangle(cornerRadius: 15).fill(Color.white)) - .shadow(radius: 5) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor).opacity(0.8)) + ) + } + .padding(CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(cgColor: Constants.FontandColors.defaultshadowColor).opacity(Constants.FontandColors.defaultshadowOpacity), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) } - .padding(.horizontal) + + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: "brain.head.profile") + .font(.title2) + .foregroundColor(Color(AppColors.iconColor)) + + Text("About Remote Insights") + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + } + + Text("Remote memory refers to your ability to recall events or information from the distant past, typically over a week or more.") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + + HStack(alignment: .top, spacing: 12) { + Image(systemName: "chart.xyaxis.line") + .foregroundColor(Color(AppColors.iconColor)) + .frame(width: 24, height: 24) + + Text("By tracking your performance over time, we can observe trends that help you and your caregivers understand how well your long-term memory is functioning.") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } + } + .padding(CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(cgColor: Constants.FontandColors.defaultshadowColor).opacity(Constants.FontandColors.defaultshadowOpacity), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + } + .padding(.bottom, 30) + } + .navigationTitle("Remote Memory") + } + .onAppear { + print("🟢 RemoteReportDetailViewController appeared. Fetching data...") + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + fetchRemoteMemoryData(for: verifiedUserDocID, month: currentMonth) { data in + DispatchQueue.main.async { + self.isLoading = false + + if data.isEmpty { + print("⚠️ No data found, setting default values.") + self.remoteMemoryData = [RemoteMemoryData(day: "01", correctAnswers: 0, incorrectAnswers: 0)] + } else { + self.remoteMemoryData = data + } + print("📊 Final Remote Memory Data: \(self.remoteMemoryData)") + } + } + } + } + } + + private func getBestPerformanceDay() -> String { + let dataPoints = remoteMemoryData.filter { $0.day != "Summary" } + if let bestDay = dataPoints.max(by: { $0.correctAnswers < $1.correctAnswers }) { + return bestDay.day + } + return "N/A" + } + + private func getHighestCorrectAnswers() -> Int { + let dataPoints = remoteMemoryData.filter { $0.day != "Summary" } + if let bestDay = dataPoints.max(by: { $0.correctAnswers < $1.correctAnswers }) { + return bestDay.correctAnswers + } + return 0 + } + + private func calculateAccuracyRate() -> String { + if let summary = remoteMemoryData.first(where: { $0.day == "Summary" }) { + let total = summary.correctAnswers + summary.incorrectAnswers + if total > 0 { + let accuracy = Double(summary.correctAnswers) / Double(total) * 100 + return String(format: "%.1f", accuracy) + } + } + return "0.0" + } +} + +struct SummaryCard: View { + var title: String + var value: String + var iconName: String + var color: Color + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: iconName) + .foregroundColor(color) + .font(.system(size: 16, weight: .semibold)) + + Text(title) + .font(Font(Constants.FontandColors.subtitleFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } + + Text(value) + .font(Font(Constants.FontandColors.titleFont)) + .foregroundColor(Color(AppColors.primaryTextColor)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: color.opacity(0.1), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) + } +} + +struct InsightRow: View { + var iconName: String + var color: Color + var text: String + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: iconName) + .foregroundColor(color) + .frame(width: 24, height: 24) + + Text(text) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + .fixedSize(horizontal: false, vertical: true) + } + } +} + +struct PercentageChangeView: View { + var currentValue: Int + var previousValue: Int + var label: String + + private var percentageChange: Double { + guard previousValue > 0 else { return 0 } + return Double(currentValue - previousValue) / Double(previousValue) * 100 + } + + private var isPositive: Bool { + return percentageChange >= 0 + } + + var body: some View { + HStack(spacing: 4) { + Image(systemName: isPositive ? "arrow.up.right" : "arrow.down.right") + .font(.caption) + .foregroundColor(isPositive ? Color(AppColors.successColor) : Color(AppColors.errorColor)) + + Text("\(abs(percentageChange), specifier: "%.1f")%") + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(isPositive ? Color(AppColors.successColor) : Color(AppColors.errorColor)) + + Text(label) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + } + } +} + +struct AnimatedProgressBar: View { + var value: Double + var maxValue: Double + var color: Color + @State private var width: Double = 0 + + private var percentage: Double { + return min(value / maxValue, 1.0) + } + + var body: some View { + ZStack(alignment: .leading) { + Rectangle() + .fill(color.opacity(0.2)) + .frame(height: 8) + .cornerRadius(CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + + Rectangle() + .fill(color) + .frame(width: width, height: 8) + .cornerRadius(CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + } + .onAppear { + withAnimation(.easeInOut(duration: 1.0)) { + width = percentage * UIScreen.main.bounds.width * 0.8 + } + } + } +} + +struct WeeklyTrendView: View { + var data: [RemoteMemoryData] + + var body: some View { + HStack(spacing: 4) { + ForEach(data.prefix(7).filter { $0.day != "Summary" }, id: \.day) { day in + let total = day.correctAnswers + day.incorrectAnswers + let percentage = total > 0 ? Double(day.correctAnswers) / Double(total) : 0 + + VStack(spacing: 2) { + Text(day.day) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + + Rectangle() + .fill( + LinearGradient( + colors: [ + Color(AppColors.successColor).opacity(0.7), + Color(AppColors.successColor), + ], + startPoint: .bottom, + endPoint: .top + ) + ) + .frame(width: 20, height: max(percentage * 60, 4)) + .cornerRadius(CGFloat(Constants.CardSize.DefaultCardCornerRadius)) } - .padding(.top) } } + .frame(height: 70) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) } } -struct ContentView: View { +struct EmptyStateView: View { + var message: String + var iconName: String + var body: some View { - RemoteReportDetailViewController(monthlyData: novemberReports) + VStack(spacing: 16) { + Image(systemName: iconName) + .font(.system(size: 48)) + .foregroundColor(Color(AppColors.secondaryTextColor).opacity(0.6)) + + Text(message) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.secondaryTextColor)) + .multilineTextAlignment(.center) + } + .padding(CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: CGFloat(Constants.CardSize.DefaultCardCornerRadius)) + .fill(Color(AppColors.cardBackgroundColor)) + .shadow( + color: Color(cgColor: Constants.FontandColors.defaultshadowColor).opacity(Constants.FontandColors.defaultshadowOpacity), + radius: Constants.FontandColors.defaultshadowRadius, + x: Constants.FontandColors.defaultshadowOffset.width, + y: Constants.FontandColors.defaultshadowOffset.height + ) + ) } } -struct RemoteReportDetailViewController_Previews: PreviewProvider { - static var previews: some View { - RemoteReportDetailViewController(monthlyData: novemberReports) - .previewLayout(.device) +extension RemoteReportDetailViewController { + func getPerformanceTrend() -> String { + let regularData = remoteMemoryData.filter { $0.day != "Summary" } + guard regularData.count >= 2 else { return "Insufficient data" } + + let firstHalf = Array(regularData.prefix(regularData.count / 2)) + let secondHalf = Array(regularData.suffix(regularData.count / 2)) + + let firstHalfCorrect = firstHalf.reduce(0) { $0 + $1.correctAnswers } + let secondHalfCorrect = secondHalf.reduce(0) { $0 + $1.correctAnswers } + + if secondHalfCorrect > firstHalfCorrect { + return "Improving" + } else if secondHalfCorrect < firstHalfCorrect { + return "Declining" + } else { + return "Stable" + } + } + + func getColorForTrend() -> Color { + let trend = getPerformanceTrend() + switch trend { + case "Improving": + return Color(AppColors.successColor) + case "Declining": + return Color(AppColors.errorColor) + default: + return Color(AppColors.iconColor) + } + } +} + +struct TagView: View { + var text: String + var color: Color + + var body: some View { + Text(text) + .font(Font(Constants.FontandColors.descriptionFont)) + .foregroundColor(Color(AppColors.inverseTextColor)) + .padding(.horizontal, CGFloat(Constants.paddingKeys.DefaultPaddingLeft)) + .padding(.vertical, CGFloat(Constants.paddingKeys.DefaultPaddingTop)) + .background(color) + .cornerRadius(CGFloat(Constants.CardSize.DefaultCardCornerRadius)) } } diff --git a/recap/Views/DailyQuestions/AddQuestionViewController.swift b/recap/Views/DailyQuestions/AddQuestionViewController.swift old mode 100644 new mode 100755 index d43602c..9a3a30a --- a/recap/Views/DailyQuestions/AddQuestionViewController.swift +++ b/recap/Views/DailyQuestions/AddQuestionViewController.swift @@ -1,441 +1,848 @@ -// -// AddQuestionViewController.swift -// -// Created by admin70 on 13/11/24. -// - -import UIKit import FirebaseFirestore import FirebaseStorage +import UIKit + +class AddQuestionViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPickerViewDelegate, UIPickerViewDataSource { + var selectedImageURL: String? + var selectedAudioURL: String? + var selectedCategory: String? + var selectedTimeFrame: String? + + private let categoryToolbar = UIToolbar() + private let timeFrameToolbar = UIToolbar() -class AddQuestionViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - private var selectedImageURL: String? - private var selectedAudioURL: String? - // MARK: - UI Elements + // UI Components + let categoryTextField = UITextField() + let timeFrameTextField = UITextField() + let questionTextField = UITextField() + let imageView = UIImageView() + let addImageButton = UIButton(type: .system) + let cancelImageButton = UIButton(type: .system) + var optionTextFields: [UITextField] = [] + let saveButton = UIButton(type: .system) + let headerLabel = UILabel() - private let categoryButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Select Category", for: .normal) - button.setTitleColor(.white, for: .normal) - button.backgroundColor = .systemBlue - button.layer.cornerRadius = 10 - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - private let timeFrameButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Select Time Frame", for: .normal) - button.setTitleColor(.white, for: .normal) - button.backgroundColor = .systemBlue - button.layer.cornerRadius = 10 - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() + // Word limit constants + private let questionWordLimit = 50 + private let optionWordLimit = 20 - private let questionTextField: UITextField = { - let textField = UITextField() - textField.placeholder = "What did you eat?" - textField.backgroundColor = UIColor.systemGray6 - textField.layer.cornerRadius = 10 - textField.textAlignment = .center - textField.font = UIFont.systemFont(ofSize: 26) - textField.translatesAutoresizingMaskIntoConstraints = false - return textField - }() + // Data + let categories = ["Immediate", "Recent", "Remote"] + let timeFrame = ["Morning", "Afternoon", "Evening", "Night"] + + let categoryPicker = UIPickerView() + let timeFramePicker = UIPickerView() + + var verifiedUserDocID: String + required init(verifiedUserDocID: String) { + self.verifiedUserDocID = verifiedUserDocID + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + setupUI() + setupLayout() + setupActions() + setupPickers() + } - private let imageView: UIImageView = { - let imageView = UIImageView() + private func setupView() { +// view.backgroundColor = ColorTheme.background + view.backgroundColor = .white + title = "Add Question" + + // Setup navigation bar appearance + if let navigationBar = navigationController?.navigationBar { + navigationBar.prefersLargeTitles = true + navigationBar.tintColor = ColorTheme.primary + + // Remove navigation bar border + navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationBar.shadowImage = UIImage() + } + + // Add gesture recognizer to dismiss keyboard + let dismissKeyboard = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(dismissKeyboard) + } + + private func setupUI() { + // Configure header label + headerLabel.text = "Create a New Question" + headerLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + headerLabel.textColor = AppColors.primaryTextColor + headerLabel.translatesAutoresizingMaskIntoConstraints = false + + // Configure categoryTextField + categoryTextField.placeholder = "Select Category" + + categoryTextField.textAlignment = .left + styleTextField(categoryTextField) + categoryTextField.inputView = categoryPicker + categoryTextField.inputAccessoryView = categoryToolbar + categoryTextField.textColor = AppColors.primaryTextColor + + // Add icon to category text field + addLeftIconToTextField(categoryTextField, iconName: "folder.fill") + + // Configure timeFrameTextField + timeFrameTextField.placeholder = "Select Time Frame" + timeFrameTextField.textColor = AppColors.primaryTextColor + timeFrameTextField.textAlignment = .left + styleTextField(timeFrameTextField) + timeFrameTextField.inputView = timeFramePicker + timeFrameTextField.inputAccessoryView = timeFrameToolbar + + // Add icon to timeframe text field + addLeftIconToTextField(timeFrameTextField, iconName: "clock.fill") + + // Configure questionTextField + questionTextField.placeholder = "What did you eat? (max \(questionWordLimit) words)" + questionTextField.backgroundColor = AppColors.cardBackgroundColor + questionTextField.layer.cornerRadius = 15 + questionTextField.layer.borderWidth = 1 + questionTextField.layer.borderColor = AppColors.iconColor.cgColor + questionTextField.textAlignment = .left + questionTextField.font = UIFont.systemFont(ofSize: 18, weight: .medium) + questionTextField.translatesAutoresizingMaskIntoConstraints = false + questionTextField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: questionTextField.frame.height)) + questionTextField.leftViewMode = .always + questionTextField.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: questionTextField.frame.height)) + questionTextField.rightViewMode = .always + questionTextField.returnKeyType = .next + + // Add word limit to question field + questionTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + + // Configure imageView imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true - imageView.backgroundColor = UIColor.systemGray5 - imageView.layer.cornerRadius = 10 + imageView.backgroundColor = AppColors.secondaryTextColor + imageView.layer.cornerRadius = 15 + imageView.layer.borderWidth = 1 +// imageView.layer.borderColor = AppColors.iconColor.cgColor imageView.translatesAutoresizingMaskIntoConstraints = false imageView.isHidden = true - return imageView - }() - - private let addImageButton: UIButton = { - let button = UIButton(type: .system) - button.setImage(UIImage(systemName: "camera.fill"), for: .normal) - button.tintColor = .black - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - private let cancelImageButton: UIButton = { - let button = UIButton(type: .system) - button.setImage(UIImage(systemName: "xmark.circle"), for: .normal) - button.tintColor = .red // Red cross - button.backgroundColor = .white // White background - button.layer.cornerRadius = 12 // Round button - - button.translatesAutoresizingMaskIntoConstraints = false - button.isHidden = true - return button - - }() - private var optionTextFields: [UITextField] = { - var textFields = [UITextField]() + + // Configure addImageButton + addImageButton.setImage(UIImage(systemName: "camera.fill"), for: .normal) + addImageButton.tintColor = AppColors.secondaryTextColor + addImageButton.backgroundColor = UIColor.white + addImageButton.layer.cornerRadius = 20 + + addImageButton.layer.shadowOffset = CGSize(width: 0, height: 2) + addImageButton.layer.shadowRadius = 4 + addImageButton.layer.shadowOpacity = 0.1 + addImageButton.translatesAutoresizingMaskIntoConstraints = false + + // Configure cancelImageButton + cancelImageButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) + cancelImageButton.tintColor = UIColor.white + cancelImageButton.backgroundColor = UIColor(white: 0, alpha: 0.6) + cancelImageButton.layer.cornerRadius = 15 + cancelImageButton.translatesAutoresizingMaskIntoConstraints = false + cancelImageButton.isHidden = true + + // Configure optionTextFields - in a 2x2 grid for i in 1...4 { let textField = UITextField() - textField.placeholder = "Option \(i)" - textField.backgroundColor = UIColor.systemGray6 - textField.layer.cornerRadius = 10 - textField.font = UIFont.systemFont(ofSize: 20) + textField.placeholder = "Option \(i) (max \(optionWordLimit) words)" + textField.backgroundColor = UIColor.white + textField.layer.cornerRadius = 15 + textField.layer.borderWidth = 1 + textField.layer.borderColor = AppColors.secondaryButtonColor.cgColor + textField.font = UIFont.systemFont(ofSize: 16) textField.translatesAutoresizingMaskIntoConstraints = false - textFields.append(textField) - } - return textFields - }() - - private let saveButton: UIButton = { - let button = UIButton(type: .system) - button.setTitle("Save", for: .normal) - button.backgroundColor = UIColor.systemBlue - button.setTitleColor(.white, for: .normal) - button.layer.cornerRadius = 10 - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 22) - button.translatesAutoresizingMaskIntoConstraints = false - return button - }() - - // MARK: - Properties - - private let categories = ["Immediate", "Recent", "Remote"] - private var selectedCategory: String? { - didSet { - categoryButton.setTitle(selectedCategory ?? "Select Category", for: .normal) - } - } - - private let timeFrame = ["Morning", "Afternoon", "Evening", "Night"] - private var selectedTimeFrame: String? { - didSet { - timeFrameButton.setTitle(selectedTimeFrame ?? "Select Time Frame", for: .normal) + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: textField.frame.height)) + textField.leftViewMode = .always + textField.returnKeyType = .next + textField.tag = i // Set tag for identification + + // Add word limit to option field + textField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + +// // Add number badge +// let badgeView = UIView() +// badgeView.translatesAutoresizingMaskIntoConstraints = false +// badgeView.backgroundColor = AppColors.iconColor +// badgeView.layer.cornerRadius = 12 +// +// let numberLabel = UILabel() +// numberLabel.translatesAutoresizingMaskIntoConstraints = false +// numberLabel.text = "\(i)" +// numberLabel.textColor = .white +// numberLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold) +// numberLabel.textAlignment = .center +// +// badgeView.addSubview(numberLabel) +// +// NSLayoutConstraint.activate([ +// numberLabel.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor), +// numberLabel.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor) +// ]) +// +// textField.leftView = badgeView +// textField.leftViewMode = .always +// +// NSLayoutConstraint.activate([ +// badgeView.widthAnchor.constraint(equalToConstant: 24), +// badgeView.heightAnchor.constraint(equalToConstant: 24) +// ]) + + optionTextFields.append(textField) } + + // Configure saveButton + saveButton.setTitle("Save Question", for: .normal) + saveButton.backgroundColor = AppColors.primaryButtonColor + saveButton.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) + saveButton.layer.cornerRadius = 25 + saveButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + saveButton.translatesAutoresizingMaskIntoConstraints = false + + // Add shadow to saveButton + saveButton.layer.shadowColor = AppColors.primaryButtonColor.cgColor + saveButton.layer.shadowOffset = CGSize(width: 0, height: 4) + saveButton.layer.shadowRadius = 8 + saveButton.layer.shadowOpacity = 0.3 + + // Setup toolbars + categoryToolbar.sizeToFit() + timeFrameToolbar.sizeToFit() + categoryToolbar.barTintColor = AppColors.primaryTextColor + timeFrameToolbar.barTintColor = AppColors.primaryTextColor + + let categoryDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(categoryDonePressed)) + categoryDoneButton.tintColor = AppColors.primaryButtonColor + let categoryFlexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + categoryToolbar.setItems([categoryFlexSpace, categoryDoneButton], animated: false) + + let timeFrameDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(timeFrameDonePressed)) + timeFrameDoneButton.tintColor = AppColors.primaryButtonColor + let timeFrameFlexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + timeFrameToolbar.setItems([timeFrameFlexSpace, timeFrameDoneButton], animated: false) } - var verifiedUserDocID: String - - // Initialize with the already fetched verifiedUserDocID - required init(verifiedUserDocID: String) { - self.verifiedUserDocID = verifiedUserDocID - super.init(nibName: nil, bundle: nil) - } + private func styleTextField(_ textField: UITextField) { + textField.backgroundColor = UIColor.white + textField.textColor = AppColors.primaryTextColor + textField.layer.cornerRadius = 15 + textField.layer.borderWidth = 1 + textField.layer.borderColor = AppColors.secondaryButtonColor.cgColor + textField.font = UIFont.systemFont(ofSize: 16) + textField.translatesAutoresizingMaskIntoConstraints = false + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: textField.frame.height)) + textField.leftViewMode = .always + textField.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: textField.frame.height)) + textField.rightViewMode = .always - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Lifecycle + // Make sure it's enabled and user can interact with it + textField.isUserInteractionEnabled = true + textField.isEnabled = true + } - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - title = "Add Question" + private func addLeftIconToTextField(_ textField: UITextField, iconName: String) { + let iconContainer = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: textField.frame.height)) + let imageView = UIImageView(frame: CGRect(x: 15, y: 0, width: 20, height: 20)) + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage(systemName: iconName) + imageView.tintColor = AppColors.iconColor + iconContainer.addSubview(imageView) - categoryButton.addTarget(self, action: #selector(showCategoryPicker), for: .touchUpInside) - timeFrameButton.addTarget(self, action: #selector(showTimeFramePicker), for: .touchUpInside) - addImageButton.addTarget(self, action: #selector(selectImage), for: .touchUpInside) - saveButton.addTarget(self, action: #selector(saveQuestion), for: .touchUpInside) - cancelImageButton.addTarget(self, action: #selector(removeImage), for: .touchUpInside) - + // Center the icon vertically + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor), + imageView.leadingAnchor.constraint(equalTo: iconContainer.leadingAnchor, constant: 15), + imageView.widthAnchor.constraint(equalToConstant: 20), + imageView.heightAnchor.constraint(equalToConstant: 20) + ]) - setupLayout() + textField.leftView = iconContainer + textField.leftViewMode = .always } - - // MARK: - Layout Setup - + private func setupLayout() { + view.addSubview(headerLabel) view.addSubview(questionTextField) view.addSubview(addImageButton) view.addSubview(imageView) - optionTextFields.forEach { view.addSubview($0) } - view.addSubview(timeFrameButton) - view.addSubview(categoryButton) - view.addSubview(saveButton) view.addSubview(cancelImageButton) - - NSLayoutConstraint.activate([ - cancelImageButton.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -10), - cancelImageButton.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10), // Moved to right-top - cancelImageButton.widthAnchor.constraint(equalToConstant: 24), - cancelImageButton.heightAnchor.constraint(equalToConstant: 24) - ]) - + // Options grid layout - 2x2 + for i in 0..= 2 else { - showAlert(title: "⚠️ Incomplete Options", message: "Please provide at least 2 options. ✅✅") - return - } - - // Determine askInterval based on category, default is 6 hours (21600 sec) - let askInterval: Int - switch category { - case "Immediate": askInterval = 14400 // 4 hours - case "Recent": askInterval = 86400 // 1 day - case "Remote": askInterval = 31536000 // 1 year (configurable) - default: askInterval = 21600 // Default: 6 hours - } + @objc func textFieldDidChange(_ textField: UITextField) { + guard let text = textField.text else { return } - // Determine timeFrame range - let timeFrame: (from: String, to: String) - switch selectedTimeFrame { - case "Morning": timeFrame = ("06:00", "11:59") - case "Afternoon": timeFrame = ("12:00", "17:59") - case "Evening": timeFrame = ("18:00", "23:59") - case "Night": timeFrame = ("00:00", "05:59") - default: - showAlert(title: "⏰ Invalid Time Frame", message: "Please select a valid time frame.") - return - } - - // Handle optional image and audio URLs - let imageUrl = selectedImageURL ?? nil - let audioUrl = selectedAudioURL ?? nil - - // New Firestore schema - let newQuestion: [String: Any] = [ - "text": questionText, - "category": category.lowercased(), - "subcategory": "familyAdded", // Example, can be dynamic - "tag": "custom", - "answerOptions": filledOptions, - "answers": [], - "correctAnswers": [], - "image": imageUrl as Any, - "audio": audioUrl as Any, - "isAnswered": false, - "askInterval": askInterval, - "timeFrame": ["from": timeFrame.from, "to": timeFrame.to], - "priority": 10, - "isActive": true, - "hint": questionText, - "confidence": NSNull(), - "hardness": 2, - "questionType": "singleCorrect" - ] - - let db = Firestore.firestore() + let wordLimit = textField == questionTextField ? questionWordLimit : optionWordLimit + let words = text.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } - // Save to user's personal question list - db.collection("users").document(verifiedUserDocID).collection("questions").addDocument(data: newQuestion) { error in - if let error = error { - self.showAlert(title: "Error", message: "Failed to save question: \(error.localizedDescription)") - return - } + if words.count > wordLimit { + // Trim to word limit + let trimmedText = words[0.. Void - - init(categories: [String], completion: @escaping (String) -> Void) { - self.categories = categories - self.completion = completion - super.init(nibName: nil, bundle: nil) + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + if pickerView == categoryPicker { + return categories.count + } else { + return timeFrame.count + } + } + + // MARK: - UIPickerViewDelegate + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + if pickerView == categoryPicker { + return categories[row] + } else { + return timeFrame[row] + } } - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = UIColor(white: 0.9, alpha: 1) - pickerView.delegate = self - pickerView.dataSource = self - - let selectButton = UIButton(type: .system) - selectButton.setTitle("Done", for: .normal) - selectButton.addTarget(self, action: #selector(selectCategory), for: .touchUpInside) - - selectButton.translatesAutoresizingMaskIntoConstraints = false - pickerView.translatesAutoresizingMaskIntoConstraints = false + // Custom styling for picker view + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + let label = UILabel() + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 18, weight: .medium) + label.textColor = AppColors.iconColor - view.addSubview(pickerView) - view.addSubview(selectButton) + if pickerView == categoryPicker { + label.text = categories[row] + } else { + label.text = timeFrame[row] + } - NSLayoutConstraint.activate([ - pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - pickerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - pickerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8), - - selectButton.topAnchor.constraint(equalTo: pickerView.bottomAnchor, constant: 20), - selectButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - ]) + return label } - @objc private func selectCategory() { - let selectedRow = pickerView.selectedRow(inComponent: 0) - let selectedCategory = categories[selectedRow] - completion(selectedCategory) - dismiss(animated: true) + func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + return 40 } - - func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 } - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { categories.count } - func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - return categories[row] - } - +} + +#Preview { + AddQuestionViewController(verifiedUserDocID: "E4McfMAfgATYMSvzx43wm7r1WQ23") } -#Preview {AddQuestionViewController(verifiedUserDocID: "DT7GZI")} + +//import FirebaseFirestore +//import FirebaseStorage +//import UIKit +// +//class AddQuestionViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPickerViewDelegate, UIPickerViewDataSource { +// var selectedImageURL: String? +// var selectedAudioURL: String? +// var selectedCategory: String? +// var selectedTimeFrame: String? +// +// private let categoryToolbar = UIToolbar() +// private let timeFrameToolbar = UIToolbar() +// +// let categoryTextField = UITextField() // Replaced UIButton with UITextField +// let timeFrameTextField = UITextField() // Replaced UIButton with UITextField +// let questionTextField = UITextField() +// let imageView = UIImageView() +// let addImageButton = UIButton(type: .system) +// let cancelImageButton = UIButton(type: .system) +// var optionTextFields: [UITextField] = [] +// let saveButton = UIButton(type: .system) +// let categories = ["Immediate", "Recent", "Remote"] +// let timeFrame = ["Morning", "Afternoon", "Evening", "Night"] +// +// let categoryPicker = UIPickerView() +// let timeFramePicker = UIPickerView() +// +// var verifiedUserDocID: String +// required init(verifiedUserDocID: String) { +// self.verifiedUserDocID = verifiedUserDocID +// super.init(nibName: nil, bundle: nil) +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// override func viewDidLoad() { +// super.viewDidLoad() +// view.backgroundColor = .systemBackground +// title = "Add Question" +// +// setupUI() +// setupLayout() +// +// addImageButton.addTarget(self, action: #selector(selectImage), for: .touchUpInside) +// saveButton.addTarget(self, action: #selector(saveQuestion), for: .touchUpInside) +// cancelImageButton.addTarget(self, action: #selector(removeImage), for: .touchUpInside) +// +// let dismissKeyboard = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) +// view.addGestureRecognizer(dismissKeyboard) +// +// categoryPicker.delegate = self +// categoryPicker.dataSource = self +// timeFramePicker.delegate = self +// timeFramePicker.dataSource = self +// } +// +// private func setupUI() { +// // Configure categoryTextField +// categoryTextField.placeholder = "Select Category" +// categoryTextField.textAlignment = .center +// categoryTextField.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor +// categoryTextField.textColor = Constants.ButtonStyle.DefaultButtonTextColor +// categoryTextField.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius +// categoryTextField.font = Constants.ButtonStyle.DefaultButtonFont +// categoryTextField.translatesAutoresizingMaskIntoConstraints = false +// categoryTextField.inputView = categoryPicker // Set inputView to categoryPicker +// categoryTextField.inputAccessoryView = categoryToolbar // Set inputAccessoryView to categoryToolbar +// +// // Configure timeFrameTextField +// timeFrameTextField.placeholder = "Select Time Frame" +// timeFrameTextField.textAlignment = .center +// timeFrameTextField.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor +// timeFrameTextField.textColor = Constants.ButtonStyle.DefaultButtonTextColor +// timeFrameTextField.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius +// timeFrameTextField.font = Constants.ButtonStyle.DefaultButtonFont +// timeFrameTextField.translatesAutoresizingMaskIntoConstraints = false +// timeFrameTextField.inputView = timeFramePicker // Set inputView to timeFramePicker +// timeFrameTextField.inputAccessoryView = timeFrameToolbar // Set inputAccessoryView to timeFrameToolbar +// +// // Configure questionTextField +// questionTextField.placeholder = "What did you eat?" +// questionTextField.backgroundColor = UIColor.systemGray6 +// questionTextField.layer.cornerRadius = 10 +// questionTextField.textAlignment = .center +// questionTextField.font = UIFont.systemFont(ofSize: 26) +// questionTextField.translatesAutoresizingMaskIntoConstraints = false +// +// // Configure imageView +// imageView.contentMode = .scaleAspectFill +// imageView.clipsToBounds = true +// imageView.backgroundColor = UIColor.systemGray5 +// imageView.layer.cornerRadius = 10 +// imageView.translatesAutoresizingMaskIntoConstraints = false +// imageView.isHidden = true +// +// // Configure addImageButton +// addImageButton.setImage(UIImage(systemName: "camera.fill"), for: .normal) +// addImageButton.tintColor = .black +// addImageButton.translatesAutoresizingMaskIntoConstraints = false +// +// // Configure cancelImageButton +// cancelImageButton.setImage(UIImage(systemName: "xmark.circle"), for: .normal) +// cancelImageButton.tintColor = .red +// cancelImageButton.backgroundColor = .white +// cancelImageButton.layer.cornerRadius = 12 +// cancelImageButton.translatesAutoresizingMaskIntoConstraints = false +// cancelImageButton.isHidden = true +// +// // Configure optionTextFields +// for i in 1 ... 4 { +// let textField = UITextField() +// textField.placeholder = "Option \(i)" +// textField.backgroundColor = UIColor.systemGray6 +// textField.layer.cornerRadius = 10 +// textField.font = UIFont.systemFont(ofSize: 20) +// textField.translatesAutoresizingMaskIntoConstraints = false +// optionTextFields.append(textField) +// } +// +// // Configure saveButton +// saveButton.setTitle("Save", for: .normal) +// saveButton.backgroundColor = Constants.ButtonStyle.DefaultButtonBackgroundColor +// saveButton.setTitleColor(Constants.ButtonStyle.DefaultButtonTextColor, for: .normal) +// saveButton.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius +// saveButton.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont +// saveButton.translatesAutoresizingMaskIntoConstraints = false +// +// // Setup toolbars +// categoryToolbar.sizeToFit() +// timeFrameToolbar.sizeToFit() +// +// let categoryDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(categoryDonePressed)) +// let categoryFlexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) +// categoryToolbar.setItems([categoryFlexSpace, categoryDoneButton], animated: false) +// +// let timeFrameDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(timeFrameDonePressed)) +// let timeFrameFlexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) +// timeFrameToolbar.setItems([timeFrameFlexSpace, timeFrameDoneButton], animated: false) +// } +// +// @objc func categoryDonePressed() { +// let selectedRow = categoryPicker.selectedRow(inComponent: 0) +// selectedCategory = categories[selectedRow] +// categoryTextField.text = selectedCategory +// view.endEditing(true) // Dismiss the picker +// } +// +// @objc func timeFrameDonePressed() { +// let selectedRow = timeFramePicker.selectedRow(inComponent: 0) +// selectedTimeFrame = timeFrame[selectedRow] +// timeFrameTextField.text = selectedTimeFrame +// view.endEditing(true) // Dismiss the picker +// } +// +// private func setupLayout() { +// view.addSubview(questionTextField) +// view.addSubview(addImageButton) +// view.addSubview(imageView) +// optionTextFields.forEach { view.addSubview($0) } +// view.addSubview(saveButton) +// view.addSubview(cancelImageButton) +// +// NSLayoutConstraint.activate([ +// questionTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), +// questionTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), +// questionTextField.trailingAnchor.constraint(equalTo: addImageButton.leadingAnchor, constant: -10), +// questionTextField.heightAnchor.constraint(equalToConstant: 50), +// +// addImageButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), +// addImageButton.centerYAnchor.constraint(equalTo: questionTextField.centerYAnchor), +// addImageButton.widthAnchor.constraint(equalToConstant: 40), +// addImageButton.heightAnchor.constraint(equalToConstant: 40), +// +// imageView.topAnchor.constraint(equalTo: questionTextField.bottomAnchor, constant: 10), +// imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), +// imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), +// imageView.heightAnchor.constraint(equalToConstant: 200), +// +// cancelImageButton.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -10), +// cancelImageButton.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10), +// cancelImageButton.widthAnchor.constraint(equalToConstant: 24), +// cancelImageButton.heightAnchor.constraint(equalToConstant: 24), +// ]) +// +// for (index, optionTextField) in optionTextFields.enumerated() { +// NSLayoutConstraint.activate([ +// optionTextField.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: CGFloat(10 + index * 55)), +// optionTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), +// optionTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), +// optionTextField.heightAnchor.constraint(equalToConstant: 50), +// ]) +// } +// +// let buttonStack = UIStackView(arrangedSubviews: [categoryTextField, timeFrameTextField]) +// buttonStack.axis = .horizontal +// buttonStack.spacing = 20 +// buttonStack.alignment = .fill +// buttonStack.distribution = .fillEqually +// buttonStack.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(buttonStack) +// +// NSLayoutConstraint.activate([ +// buttonStack.topAnchor.constraint(equalTo: optionTextFields.last!.bottomAnchor, constant: 30), +// buttonStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), +// buttonStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), +// buttonStack.heightAnchor.constraint(equalToConstant: 50), +// +// saveButton.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 30), +// saveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), +// saveButton.widthAnchor.constraint(equalToConstant: 250), +// saveButton.heightAnchor.constraint(equalToConstant: 50), +// ]) +// } +// +// @objc private func selectImage() { +// let picker = UIImagePickerController() +// picker.delegate = self +// picker.allowsEditing = true +// picker.sourceType = .photoLibrary +// present(picker, animated: true) +// } +// +// @objc private func dismissKeyboard() { +// view.endEditing(true) +// } +// +// private func showAlert(title: String, message: String) { +// let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) +// alert.addAction(UIAlertAction(title: "OK", style: .default)) +// present(alert, animated: true) +// } +// +// func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { +// if let selectedImage = info[.editedImage] as? UIImage ?? info[.originalImage] as? UIImage { +// imageView.image = selectedImage +// imageView.isHidden = false +// cancelImageButton.isHidden = false +// } +// picker.dismiss(animated: true) +// } +// +// @objc private func removeImage() { +// imageView.image = nil +// imageView.isHidden = true +// cancelImageButton.isHidden = true +// } +// +// // MARK: - UIPickerViewDataSource +// +// func numberOfComponents(in pickerView: UIPickerView) -> Int { +// return 1 +// } +// +// func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { +// if pickerView == categoryPicker { +// return categories.count +// } else { +// return timeFrame.count +// } +// } +// +// // MARK: - UIPickerViewDelegate +// +// func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { +// if pickerView == categoryPicker { +// return categories[row] +// } else { +// return timeFrame[row] +// } +// } +//} diff --git a/recap/Views/DailyQuestions/AddQuestionsFirebase.swift b/recap/Views/DailyQuestions/AddQuestionsFirebase.swift new file mode 100755 index 0000000..310f5e1 --- /dev/null +++ b/recap/Views/DailyQuestions/AddQuestionsFirebase.swift @@ -0,0 +1,158 @@ +// +// AddQuestionsFirebase.swift +// recap +// +// Created by s1834 on 10/03/25. +// + +import FirebaseFirestore +import FirebaseStorage +import UIKit + +extension AddQuestionViewController { + private func setupLayout() { + view.addSubview(questionTextField) + view.addSubview(addImageButton) + view.addSubview(imageView) + optionTextFields.forEach { view.addSubview($0) } + view.addSubview(saveButton) + view.addSubview(cancelImageButton) + + NSLayoutConstraint.activate([ + cancelImageButton.topAnchor.constraint(equalTo: imageView.topAnchor, constant: -10), + cancelImageButton.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10), + cancelImageButton.widthAnchor.constraint(equalToConstant: 24), + cancelImageButton.heightAnchor.constraint(equalToConstant: 24), + ]) + + NSLayoutConstraint.activate([ + questionTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + questionTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + questionTextField.trailingAnchor.constraint(equalTo: addImageButton.leadingAnchor, constant: -10), + questionTextField.heightAnchor.constraint(equalToConstant: 50), + + addImageButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + addImageButton.centerYAnchor.constraint(equalTo: questionTextField.centerYAnchor), + addImageButton.widthAnchor.constraint(equalToConstant: 40), + addImageButton.heightAnchor.constraint(equalToConstant: 40), + + imageView.topAnchor.constraint(equalTo: questionTextField.bottomAnchor, constant: 10), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + imageView.heightAnchor.constraint(equalToConstant: 200), + ]) + + for (index, optionTextField) in optionTextFields.enumerated() { + NSLayoutConstraint.activate([ + optionTextField.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: CGFloat(10 + index * 55)), + optionTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + optionTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + optionTextField.heightAnchor.constraint(equalToConstant: 50), + ]) + } + + if let lastOption = optionTextFields.last { + // Use categoryTextField and timeFrameTextField instead of categoryButton and timeFrameButton + let buttonStack = UIStackView(arrangedSubviews: [categoryTextField, timeFrameTextField]) + buttonStack.axis = .horizontal // Set the axis explicitly + buttonStack.spacing = 20 + buttonStack.alignment = .fill // Set the alignment explicitly + buttonStack.distribution = .fillEqually // Set the distribution explicitly + buttonStack.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(buttonStack) + + NSLayoutConstraint.activate([ + buttonStack.topAnchor.constraint(equalTo: lastOption.bottomAnchor, constant: 30), + buttonStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + buttonStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + buttonStack.heightAnchor.constraint(equalToConstant: 50), + ]) + + NSLayoutConstraint.activate([ + saveButton.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 30), + saveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + saveButton.widthAnchor.constraint(equalToConstant: 250), + saveButton.heightAnchor.constraint(equalToConstant: 50), + ]) + } + } + + @objc func saveQuestion() { + guard let category = selectedCategory else { + showAlert(title: "⚠️⚠️ Missing Category", message: "Please select a category before saving.📌") + return + } + + guard let selectedTimeFrame = selectedTimeFrame else { + showAlert(title: "⏰⏰ Missing Time Frame", message: "Please select a time frame before saving.") + return + } + + let questionText = questionTextField.text ?? "" + let optionTexts = optionTextFields.map { $0.text ?? "" } + let filledOptions = optionTexts.filter { !$0.isEmpty } + + guard !questionText.isEmpty, !optionTexts.contains(where: { $0.isEmpty }) else { + showAlert(title: "📝📝 Incomplete Fields", message: "Please fill in all fields before saving.") + return + } + + guard filledOptions.count >= 2 else { + showAlert(title: "⚠️⚠️ Incomplete Options", message: "Please provide at least 2 options.") + return + } + + let askInterval: Int + switch category { + case "Immediate": askInterval = 14400 + case "Recent": askInterval = 86400 + case "Remote": askInterval = 31536000 + default: askInterval = 21600 + } + + let timeFrame: (from: String, to: String) + switch selectedTimeFrame { + case "Morning": timeFrame = ("06:00", "11:59") + case "Afternoon": timeFrame = ("12:00", "17:59") + case "Evening": timeFrame = ("18:00", "23:59") + case "Night": timeFrame = ("00:00", "05:59") + default: + showAlert(title: "⏰⏰ Invalid Time Frame", message: "Please select a valid time frame.") + return + } + + let imageUrl = selectedImageURL ?? nil + let audioUrl = selectedAudioURL ?? nil + let newQuestion: [String: Any] = ["text": questionText, "category": category.lowercased(), "subcategory": "familyAdded", "tag": "custom", "answerOptions": filledOptions, "answers": [], "correctAnswers": [], "image": imageUrl as Any, "audio": audioUrl as Any, "isAnswered": false, "askInterval": askInterval, "timeFrame": ["from": timeFrame.from, "to": timeFrame.to], "priority": 10, "isActive": true, "hint": questionText, "confidence": NSNull(), "hardness": 2, "questionType": "singleCorrect"] + + let db = Firestore.firestore() + db.collection("users").document(verifiedUserDocID).collection("questions").addDocument(data: newQuestion) { error in + if let error = error { + self.showAlert(title: "Error", message: "Failed to save question: \(error.localizedDescription)") + return + } + + db.collection("familyAddedQuestions").addDocument(data: newQuestion) { familyError in + if let familyError = familyError { + self.showAlert(title: "Error", message: "Question saved to user list, but failed to save in family collection: \(familyError.localizedDescription)") + } else { + self.saveButton.setTitle("Saved", for: .normal) + self.saveButton.backgroundColor = .systemGreen + // Show success alert and dismiss the view when "OK" is clicked + let alert = UIAlertController(title: "✅✅ Success", message: "Question saved successfully!!", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in + // Dismiss the view controller + self.dismiss(animated: true, completion: nil) + })) + self.present(alert, animated: true, completion: nil) + } + } + } + } + + private func showAlert(title: String, message: String) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) + } +} diff --git a/recap/Views/DailyQuestions/CategoryPickerViewController.swift b/recap/Views/DailyQuestions/CategoryPickerViewController.swift new file mode 100755 index 0000000..8a217b9 --- /dev/null +++ b/recap/Views/DailyQuestions/CategoryPickerViewController.swift @@ -0,0 +1,63 @@ +// +// CategoryPickerViewController.swift +// recap +// +// Created by s1834 on 11/03/25. +// + +import UIKit + +class CategoryPickerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { + private let pickerView = UIPickerView() + private let categories: [String] + private let completion: (String) -> Void + + init(categories: [String], completion: @escaping (String) -> Void) { + self.categories = categories + self.completion = completion + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor(white: 0.9, alpha: 1) + pickerView.delegate = self + pickerView.dataSource = self + + let selectButton = UIButton(type: .system) + selectButton.setTitle("Done", for: .normal) + selectButton.addTarget(self, action: #selector(selectCategory), for: .touchUpInside) + + selectButton.translatesAutoresizingMaskIntoConstraints = false + pickerView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(pickerView) + view.addSubview(selectButton) + + NSLayoutConstraint.activate([ + pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + pickerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + pickerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8), + + selectButton.topAnchor.constraint(equalTo: pickerView.bottomAnchor, constant: 20), + selectButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + ]) + } + + @objc private func selectCategory() { + let selectedRow = pickerView.selectedRow(inComponent: 0) + let selectedCategory = categories[selectedRow] + completion(selectedCategory) + dismiss(animated: true) + } + + func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 } + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { categories.count } + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return categories[row] + } +} diff --git a/recap/Views/DailyQuestions/DailyQuestionDetailViewController.swift b/recap/Views/DailyQuestions/DailyQuestionDetailViewController.swift old mode 100644 new mode 100755 index f56b1f9..f71c402 --- a/recap/Views/DailyQuestions/DailyQuestionDetailViewController.swift +++ b/recap/Views/DailyQuestions/DailyQuestionDetailViewController.swift @@ -2,7 +2,238 @@ // DailyQuestionDetailViewController.swift // Recap // -// Created by user@47 on 15/01/25. +// Created by s1834 on 15/01/25. +// + +// +//import UIKit +//import FirebaseFirestore +// +//class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, QuestionDetailDelegate { +// var verifiedUserDocID: String +// private var manager: QuestionsManager +// private var questions: [Question] = [] +// private var lastFetchTime: Date? +// private let fetchInterval: TimeInterval = 86400.0 +// +// private let tableView: UITableView = { +// let table = UITableView() +// table.translatesAutoresizingMaskIntoConstraints = false +// table.separatorStyle = .none +// table.rowHeight = UITableView.automaticDimension +// table.estimatedRowHeight = 120 +// return table +// }() +// +// init(verifiedUserDocID: String) { +// self.verifiedUserDocID = verifiedUserDocID +// self.manager = QuestionsManager(verifiedUserDocID: verifiedUserDocID) +// super.init(nibName: nil, bundle: nil) +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// var fetchTimer: Timer? +// +// override func viewDidLoad() { +// super.viewDidLoad() +// setupUI() +// loadQuestions() +// startFetchingQuestions() +// } +// +// private func setupUI() { +// view.backgroundColor = .white +// title = "Daily Question" +// +// navigationItem.rightBarButtonItem = UIBarButtonItem( +// barButtonSystemItem: .add, +// target: self, +// action: #selector(addQuestion) +// ) +// +// let captionLabel = UILabel() +// captionLabel.text = "Answer your loved one's daily questions anytime to support their memory journey." +// captionLabel.font = UIFont.systemFont(ofSize: 18) +// captionLabel.textColor = .gray +// captionLabel.numberOfLines = 0 +// captionLabel.translatesAutoresizingMaskIntoConstraints = false +// +// view.addSubview(captionLabel) +// view.addSubview(tableView) +// +// tableView.delegate = self +// tableView.dataSource = self +// tableView.register(QuestionCell.self, forCellReuseIdentifier: QuestionCell.identifier) +// +// NSLayoutConstraint.activate([ +// captionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), +// captionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), +// captionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), +// +// tableView.topAnchor.constraint(equalTo: captionLabel.bottomAnchor, constant: 16), +// tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) +// ]) +// } +// +// func startFetchingQuestions() { +// if fetchTimer == nil { +// fetchTimer = Timer.scheduledTimer(timeInterval: fetchInterval, target: self, selector: #selector(fetchNewQuestions), userInfo: nil, repeats: true) +// RunLoop.main.add(fetchTimer!, forMode: .common) +// } +// } +// +// @objc private func fetchNewQuestions() { +// self.manager.fetchQuestions { [weak self] (fetchedQuestions: [Question]) in +// guard let self = self else { return } +// +// self.questions = fetchedQuestions +// self.lastFetchTime = Date() +// let db = Firestore.firestore() +// let coreRef = db.collection("users").document(self.verifiedUserDocID).collection("core").document("analytics") +// +// coreRef.updateData([ +// "lastFetched": self.lastFetchTime ?? Date() +// ]) { error in +// if let error = error { +// print("❌❌ Error updating lastFetched timestamp: \(error.localizedDescription)") +// } +// } +// self.tableView.reloadData() +// } +// } +// +// @objc func loadQuestions() { +// let currentTime = Date() +// +// if let lastFetch = lastFetchTime, currentTime.timeIntervalSince(lastFetch) < fetchInterval { +// tableView.reloadData() +// return +// } +// +// evaluateAndStoreMemoryReport(for: verifiedUserDocID) { [weak self] in +// guard let self = self else { return } +// +// // Move questions to asked before fetching new ones +// self.manager.moveQuestionsToAskedAndDelete { +// let db = Firestore.firestore() +// let userQuestionsRef = db.collection("users").document(self.verifiedUserDocID).collection("questions") +// +// userQuestionsRef.getDocuments { (snapshot, error) in +// if let error = error { +// print("❌ Firestore error while checking questions collection: \(error.localizedDescription)") +// return +// } +// +// if let snapshot = snapshot, !snapshot.isEmpty { +// let filteredQuestions = snapshot.documents.compactMap { doc -> Question? in +// do { +// var question = try doc.data(as: Question.self) +// return question.correctAnswers?.isEmpty ?? true ? question : nil +// } catch { +// print("❌ Error decoding question: \(error)") +// return nil +// } +// } +// +// if !filteredQuestions.isEmpty { +// self.questions.append(contentsOf: filteredQuestions) +// self.tableView.reloadData() +// } else { +// self.fetchRandomQuestions(from: userQuestionsRef) +// } +// } else { +// self.fetchNewQuestions() +// } +// } +// } +// } +// } +// +// +// private func fetchRandomQuestions(from collectionRef: CollectionReference) { +// collectionRef.limit(to: 7).getDocuments { [weak self] (snapshot, error) in +// guard let self = self else { return } +// +// if let error = error { +// print("❌❌ Firestore error while fetching random questions: \(error.localizedDescription)") +// return +// } +// +// if let snapshot = snapshot { +// let randomQuestions = snapshot.documents.compactMap { doc in +// do { +// let question = try doc.data(as: Question.self) +// return question +// } catch { +// print("❌❌ Error decoding random question: \(error)") +// return nil +// } +// } +// self.questions.append(contentsOf: randomQuestions) +// self.tableView.reloadData() +// } +// } +// } +// +// func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { +// return questions.count +// } +// +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// guard let cell = tableView.dequeueReusableCell(withIdentifier: QuestionCell.identifier, for: indexPath) as? QuestionCell else { +// return UITableViewCell() +// } +// +// let question = questions[indexPath.row] +// cell.configure(with: question) +// +// return cell +// } +// +// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// tableView.deselectRow(at: indexPath, animated: true) +// let selectedQuestion = questions[indexPath.row] +// let questionDetailVC = QuestionDetailViewController(verifiedUserDocID: verifiedUserDocID) +// questionDetailVC.question = selectedQuestion +// questionDetailVC.delegate = self +// navigationController?.pushViewController(questionDetailVC, animated: true) +// } +// +// @objc private func addQuestion() { +// let addQuestionVC = AddQuestionViewController(verifiedUserDocID: self.verifiedUserDocID) +// let navController = UINavigationController(rootViewController: addQuestionVC) +// +// if let sheet = navController.sheetPresentationController { +// if #available(iOS 16.0, *) { +// sheet.detents = [.large()] +// } else { +// sheet.detents = [.medium()] +// } +// sheet.prefersGrabberVisible = true +// sheet.prefersEdgeAttachedInCompactHeight = true +// } +// present(navController, animated: true, completion: nil) +// } +// +// func didSubmitAnswer(for question: Question) { +// if let index = questions.firstIndex(where: { $0.id == question.id }) { +// var answeredQuestion = questions.remove(at: index) +// answeredQuestion.isAnswered = true +// questions.append(answeredQuestion) +// tableView.reloadData() +// } +// } +//} +// +// DailyQuestionDetailViewController.swift +// Recap +// +// Created by s1834 on 15/01/25. // @@ -10,11 +241,21 @@ import UIKit import FirebaseFirestore class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, QuestionDetailDelegate { - var question: Question? var verifiedUserDocID: String private var manager: QuestionsManager private var questions: [Question] = [] + private var lastFetchTime: Date? + private let fetchInterval: TimeInterval = 86400.0 + private let tableView: UITableView = { + let table = UITableView() + table.translatesAutoresizingMaskIntoConstraints = false + table.separatorStyle = .none + table.rowHeight = UITableView.automaticDimension + table.estimatedRowHeight = 120 + return table + }() + init(verifiedUserDocID: String) { self.verifiedUserDocID = verifiedUserDocID self.manager = QuestionsManager(verifiedUserDocID: verifiedUserDocID) @@ -25,56 +266,44 @@ class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, fatalError("init(coder:) has not been implemented") } - private let captionLabel: UILabel = { - let label = UILabel() - label.text = "Answer your loved one's daily questions anytime to support their memory journey." - label.font = UIFont.systemFont(ofSize: 18) - label.textColor = .gray - label.numberOfLines = 0 - label.textAlignment = .left - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private let tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.separatorStyle = .none - table.rowHeight = UITableView.automaticDimension - table.estimatedRowHeight = 120 - return table - }() - - // Timer property var fetchTimer: Timer? override func viewDidLoad() { super.viewDidLoad() + setupUI() + loadQuestions() + startFetchingQuestions() + } + + private func setupUI() { view.backgroundColor = .white title = "Daily Question" - + navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .add, target: self, action: #selector(addQuestion) ) + + let captionLabel = UILabel() + captionLabel.text = "Answer your loved one's daily questions anytime to support their memory journey." + captionLabel.font = UIFont.systemFont(ofSize: 18) + captionLabel.textColor = .gray + captionLabel.numberOfLines = 0 + captionLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(captionLabel) view.addSubview(tableView) - + tableView.delegate = self tableView.dataSource = self tableView.register(QuestionCell.self, forCellReuseIdentifier: QuestionCell.identifier) - - setupConstraints() - startFetchingQuestions() - } - - private func setupConstraints() { + NSLayoutConstraint.activate([ captionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), captionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), captionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - + tableView.topAnchor.constraint(equalTo: captionLabel.bottomAnchor, constant: 16), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), @@ -82,19 +311,106 @@ class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, ]) } - // MARK: - Start Timer to Fetch Questions Every 24 Hours - private func startFetchingQuestions() { - fetchTimer = Timer.scheduledTimer(timeInterval: 86400.0, target: self, selector: #selector(loadQuestions), userInfo: nil, repeats: true) - loadQuestions() // Fetch questions initially + func startFetchingQuestions() { + if fetchTimer == nil { + fetchTimer = Timer.scheduledTimer(timeInterval: fetchInterval, target: self, selector: #selector(fetchNewQuestions), userInfo: nil, repeats: true) + RunLoop.main.add(fetchTimer!, forMode: .common) + } } - // MARK: - Load Questions from Firestore + + @objc private func fetchNewQuestions() { + self.manager.fetchQuestions { [weak self] (fetchedQuestions: [Question]) in + guard let self = self else { return } + + self.questions = fetchedQuestions + self.lastFetchTime = Date() + let db = Firestore.firestore() + let coreRef = db.collection("users").document(self.verifiedUserDocID).collection("core").document("analytics") + + coreRef.updateData([ + "lastFetched": self.lastFetchTime ?? Date() + ]) { error in + if let error = error { + print("❌❌ Error updating lastFetched timestamp: \(error.localizedDescription)") + } + } + self.tableView.reloadData() + } + } + @objc func loadQuestions() { - manager.fetchQuestions { [weak self] (fetchedQuestions: [Question]) in - self?.questions = fetchedQuestions - self?.tableView.reloadData() + let currentTime = Date() + + if let lastFetch = lastFetchTime, currentTime.timeIntervalSince(lastFetch) < fetchInterval { + tableView.reloadData() + return + } + + evaluateAndStoreMemoryReport(for: verifiedUserDocID) { [weak self] in + guard let self = self else { return } + + // Move questions to asked before fetching new ones + self.manager.moveQuestionsToAskedAndDelete { + let db = Firestore.firestore() + let userQuestionsRef = db.collection("users").document(self.verifiedUserDocID).collection("questions") + + userQuestionsRef.getDocuments { (snapshot, error) in + if let error = error { + print("❌ Firestore error while checking questions collection: \(error.localizedDescription)") + return + } + + if let snapshot = snapshot, !snapshot.isEmpty { + let filteredQuestions = snapshot.documents.compactMap { doc -> Question? in + do { + var question = try doc.data(as: Question.self) + return question.correctAnswers?.isEmpty ?? true ? question : nil + } catch { + print("❌ Error decoding question: \(error)") + return nil + } + } + + if !filteredQuestions.isEmpty { + self.questions.append(contentsOf: filteredQuestions) + self.tableView.reloadData() + } else { + self.fetchRandomQuestions(from: userQuestionsRef) + } + } else { + self.fetchNewQuestions() + } + } + } } } - + + + private func fetchRandomQuestions(from collectionRef: CollectionReference) { + collectionRef.limit(to: 7).getDocuments { [weak self] (snapshot, error) in + guard let self = self else { return } + + if let error = error { + print("❌❌ Firestore error while fetching random questions: \(error.localizedDescription)") + return + } + + if let snapshot = snapshot { + let randomQuestions = snapshot.documents.compactMap { doc in + do { + let question = try doc.data(as: Question.self) + return question + } catch { + print("❌❌ Error decoding random question: \(error)") + return nil + } + } + self.questions.append(contentsOf: randomQuestions) + self.tableView.reloadData() + } + } + } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return questions.count } @@ -115,7 +431,7 @@ class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, let selectedQuestion = questions[indexPath.row] let questionDetailVC = QuestionDetailViewController(verifiedUserDocID: verifiedUserDocID) questionDetailVC.question = selectedQuestion - questionDetailVC.delegate = self + questionDetailVC.delegate = self navigationController?.pushViewController(questionDetailVC, animated: true) } @@ -135,7 +451,6 @@ class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, present(navController, animated: true, completion: nil) } - // MARK: - Delegate Method to Handle Answer Submission func didSubmitAnswer(for question: Question) { if let index = questions.firstIndex(where: { $0.id == question.id }) { var answeredQuestion = questions.remove(at: index) @@ -145,7 +460,6 @@ class DailyQuestionDetailViewController: UIViewController, UITableViewDelegate, } } } - -#Preview { - DailyQuestionDetailViewController(verifiedUserDocID: "DT7GZI") +#Preview{ + DailyQuestionDetailViewController(verifiedUserDocID: "E4McfMAfgATYMSvzx43wm7r1WQ23") } diff --git a/recap/Views/DailyQuestions/NextQuestionCardView.swift b/recap/Views/DailyQuestions/NextQuestionCardView.swift new file mode 100755 index 0000000..b01931f --- /dev/null +++ b/recap/Views/DailyQuestions/NextQuestionCardView.swift @@ -0,0 +1,444 @@ +// +// NextQuestionCardView.swift +// recap +// +// Created by s1834 on 13/03/25. +// +// +//#Preview{ +// NextQuestionCardView() +//} +// +//import UIKit +// +//protocol NextQuestionCardViewDelegate: AnyObject { +// func didTapNextQuestion() +// func didTapReadArticle() +//} +// +//class NextQuestionCardView: UIView { +// +// weak var delegate: NextQuestionCardViewDelegate? +// +// private let logoImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.image = UIImage(systemName: "sparkles") +// imageView.tintColor = .white +// imageView.contentMode = .scaleAspectFit +// imageView.translatesAutoresizingMaskIntoConstraints = false +// return imageView +// }() +// +// private let titleLabel: UILabel = { +// let label = UILabel() +// label.text = "What would you like to do next?" +// label.font = UIFont.systemFont(ofSize: 24, weight: .bold) +// label.textColor = .white +// label.textAlignment = .center +// label.numberOfLines = 0 +// label.translatesAutoresizingMaskIntoConstraints = false +// return label +// }() +// +// private let nextQuestionButton: UIButton = { +// let button = UIButton(type: .system) +// button.setTitle("Next Question", for: .normal) +// button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) +// button.setTitleColor(.white, for: .normal) +// button.backgroundColor = ColorTheme.primary +// button.layer.cornerRadius = 18 +// button.clipsToBounds = true +// button.translatesAutoresizingMaskIntoConstraints = false +// return button +// }() +// +// private let readArticleButton: UIButton = { +// let button = UIButton(type: .system) +// button.setTitle("Read Article", for: .normal) +// button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) +// button.setTitleColor(ColorTheme.textLight, for: .normal) +// button.backgroundColor = ColorTheme.accent +// button.layer.cornerRadius = 18 +// button.clipsToBounds = true +// button.translatesAutoresizingMaskIntoConstraints = false +// return button +// }() +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// setupUI() +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// private func setupUI() { +// // Apply gradient background directly to the view +// let gradientLayer = CAGradientLayer() +// gradientLayer.colors = [ +// AppColors.gradientStartColor.cgColor, +// AppColors.gradientEndColor.cgColor +// ] +// gradientLayer.locations = [0.0, 1.0] +// gradientLayer.startPoint = CGPoint(x: 0, y: 0) +// gradientLayer.endPoint = CGPoint(x: 1, y: 1) +// gradientLayer.cornerRadius = 24 +// gradientLayer.frame = bounds +// layer.insertSublayer(gradientLayer, at: 0) +// +// layer.cornerRadius = 24 +// layer.shadowColor = Constants.FontandColors.defaultshadowColor +// layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) +// layer.shadowOffset = Constants.FontandColors.defaultshadowOffset +// layer.shadowRadius = CGFloat(Constants.FontandColors.defaultshadowRadius) +// clipsToBounds = true +// +// // Setup content stack +// let headerStack = UIStackView(arrangedSubviews: [logoImageView, titleLabel]) +// headerStack.axis = .vertical +// headerStack.spacing = 12 +// headerStack.alignment = .center +// headerStack.translatesAutoresizingMaskIntoConstraints = false +// +// let buttonsStack = UIStackView(arrangedSubviews: [nextQuestionButton, readArticleButton]) +// buttonsStack.axis = .vertical +// buttonsStack.spacing = 20 +// buttonsStack.distribution = .fillEqually +// buttonsStack.translatesAutoresizingMaskIntoConstraints = false +// +// let mainStack = UIStackView(arrangedSubviews: [headerStack, buttonsStack]) +// mainStack.axis = .vertical +// mainStack.spacing = 28 +// mainStack.translatesAutoresizingMaskIntoConstraints = false +// +// addSubview(mainStack) +// +// NSLayoutConstraint.activate([ +// mainStack.topAnchor.constraint(equalTo: topAnchor, constant: 28), +// mainStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24), +// mainStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), +// mainStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -28), +// +// nextQuestionButton.heightAnchor.constraint(equalToConstant: 56), +// readArticleButton.heightAnchor.constraint(equalToConstant: 56), +// +// logoImageView.heightAnchor.constraint(equalToConstant: 60), +// logoImageView.widthAnchor.constraint(equalToConstant: 60) +// ]) +// +// // Button actions +// nextQuestionButton.addTarget(self, action: #selector(didTapNextQuestion), for: .touchUpInside) +// readArticleButton.addTarget(self, action: #selector(didTapReadArticle), for: .touchUpInside) +// +// // Touch feedback +// [nextQuestionButton, readArticleButton].forEach { button in +// button.addTarget(self, action: #selector(buttonTouchDown(_:)), for: .touchDown) +// button.addTarget(self, action: #selector(buttonTouchUp(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel]) +// } +// } +// +// +// @objc private func buttonTouchDown(_ sender: UIButton) { +// UIView.animate(withDuration: 0.1) { +// sender.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) +// } +// } +// +// @objc private func buttonTouchUp(_ sender: UIButton) { +// UIView.animate(withDuration: 0.1) { +// sender.transform = .identity +// } +// } +// +// @objc private func didTapNextQuestion() { +// animateButtonTap(nextQuestionButton) +// delegate?.didTapNextQuestion() +// } +// +// @objc private func didTapReadArticle() { +// animateButtonTap(readArticleButton) +// delegate?.didTapReadArticle() +// } +// +// private func animateButtonTap(_ button: UIButton) { +// UIView.animate(withDuration: 0.15, animations: { +// button.transform = CGAffineTransform(scaleX: 0.92, y: 0.92) +// }) { _ in +// UIView.animate(withDuration: 0.15) { +// button.transform = .identity +// } +// } +// } +//} + + +import UIKit + +protocol NextQuestionCardViewDelegate: AnyObject { + func didTapNextQuestion() + func didTapReadArticle() +} + +class NextQuestionCardView: UIView { + weak var delegate: NextQuestionCardViewDelegate? + + private let logoImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "sparkles") + imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + // Add a subtle glow effect + imageView.layer.shadowColor = UIColor.white.cgColor + imageView.layer.shadowOpacity = 0.5 + imageView.layer.shadowOffset = CGSize.zero + imageView.layer.shadowRadius = 8 + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "What would you like to do next?" + label.font = UIFont.preferredFont(forTextStyle: .title2).withWeight(.bold) // Dynamic type support + label.textColor = .white + label.textAlignment = .center + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + // Add subtle shadow for readability + label.layer.shadowColor = UIColor.black.cgColor + label.layer.shadowOffset = CGSize(width: 0, height: 1) + label.layer.shadowRadius = 2 + label.layer.shadowOpacity = 0.3 + // Accessibility + label.isAccessibilityElement = true + label.accessibilityLabel = "What would you like to do next?" + return label + }() + + private let nextQuestionButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Next Question", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.setTitleColor(.white, for: .normal) + button.backgroundColor = AppColors.iconColor + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.clipsToBounds = false + button.translatesAutoresizingMaskIntoConstraints = false + // Add shadow + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.2 + button.layer.shadowOffset = CGSize(width: 0, height: 2) + button.layer.shadowRadius = 4 + // Accessibility + button.isAccessibilityElement = true + button.accessibilityLabel = "Next Question" + button.accessibilityHint = "Double tap to go to the next question" + return button + }() + + private let readArticleButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Read Article", for: .normal) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.setTitleColor(.white, for: .normal) + button.backgroundColor = AppColors.iconColor + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.clipsToBounds = false + button.translatesAutoresizingMaskIntoConstraints = false + // Add shadow + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.2 + button.layer.shadowOffset = CGSize(width: 0, height: 2) + button.layer.shadowRadius = 4 + // Accessibility + button.isAccessibilityElement = true + button.accessibilityLabel = "Read Article" + button.accessibilityHint = "Double tap to read an article" + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + animateAppearance() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + // Apply gradient background directly to the view + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [ + AppColors.gradientStartColor.cgColor, + AppColors.gradientEndColor.cgColor + ] + gradientLayer.locations = [0.0, 1.0] + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 1, y: 1) + gradientLayer.cornerRadius = 24 + gradientLayer.frame = bounds + layer.insertSublayer(gradientLayer, at: 0) + + layer.cornerRadius = 24 + layer.shadowColor = Constants.FontandColors.defaultshadowColor + layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) + layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + layer.shadowRadius = CGFloat(Constants.FontandColors.defaultshadowRadius) + clipsToBounds = false + + // Add gradient to buttons + [nextQuestionButton, readArticleButton].forEach { button in + let gradient = CAGradientLayer() + gradient.colors = [UIColor.white.withAlphaComponent(0.2).cgColor, UIColor.clear.cgColor] + gradient.startPoint = CGPoint(x: 0.5, y: 0) + gradient.endPoint = CGPoint(x: 0.5, y: 1) + gradient.frame = CGRect(x: 0, y: 0, width: 300, height: 56) // Will adjust with layout + button.layer.insertSublayer(gradient, at: 0) + } + + // Setup content stack + let headerStack = UIStackView(arrangedSubviews: [logoImageView, titleLabel]) + headerStack.axis = .vertical + headerStack.spacing = 16 // Increased spacing + headerStack.alignment = .center + headerStack.translatesAutoresizingMaskIntoConstraints = false + + let buttonsStack = UIStackView(arrangedSubviews: [nextQuestionButton, readArticleButton]) + buttonsStack.axis = .vertical + buttonsStack.spacing = 24 // Increased spacing + buttonsStack.distribution = .fillEqually + buttonsStack.translatesAutoresizingMaskIntoConstraints = false + + let mainStack = UIStackView(arrangedSubviews: [headerStack, buttonsStack]) + mainStack.axis = .vertical + mainStack.spacing = 32 // Increased spacing + mainStack.translatesAutoresizingMaskIntoConstraints = false + + addSubview(mainStack) + + NSLayoutConstraint.activate([ + mainStack.topAnchor.constraint(equalTo: topAnchor, constant: 32), // Increased padding + mainStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28), + mainStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28), + mainStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32), // Increased padding + + nextQuestionButton.heightAnchor.constraint(equalToConstant: 56), + readArticleButton.heightAnchor.constraint(equalToConstant: 56), + + logoImageView.heightAnchor.constraint(equalToConstant: 60), + logoImageView.widthAnchor.constraint(equalToConstant: 60) + ]) + + // Button actions + nextQuestionButton.addTarget(self, action: #selector(didTapNextQuestion), for: .touchUpInside) + readArticleButton.addTarget(self, action: #selector(didTapReadArticle), for: .touchUpInside) + + // Touch feedback + [nextQuestionButton, readArticleButton].forEach { button in + button.addTarget(self, action: #selector(buttonTouchDown(_:)), for: .touchDown) + button.addTarget(self, action: #selector(buttonTouchUp(_:)), for: [.touchUpInside, .touchUpOutside, .touchCancel]) + } + } + + private func animateAppearance() { + // Fade-in animation for the card + alpha = 0 + transform = CGAffineTransform(translationX: 0, y: 20) + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseInOut) { + self.alpha = 1 + self.transform = .identity + } + + // Bounce animation for the logo + logoImageView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + UIView.animate(withDuration: 0.6, delay: 0.2, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.8, options: .curveEaseOut) { + self.logoImageView.transform = .identity + } + } + + @objc private func buttonTouchDown(_ sender: UIButton) { + UIView.animate(withDuration: 0.1) { + sender.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + } + } + + @objc private func buttonTouchUp(_ sender: UIButton) { + UIView.animate(withDuration: 0.1) { + sender.transform = .identity + } + } + + @objc private func didTapNextQuestion() { + animateButtonTap(nextQuestionButton) + delegate?.didTapNextQuestion() + } + + @objc private func didTapReadArticle() { + animateButtonTap(readArticleButton) + delegate?.didTapReadArticle() + } + + private func animateButtonTap(_ button: UIButton) { + // Create a ripple effect + let ripple = CAShapeLayer() + let rippleSize: CGFloat = 60 + let ripplePath = UIBezierPath(ovalIn: CGRect(x: button.bounds.midX - rippleSize / 2, y: button.bounds.midY - rippleSize / 2, width: rippleSize, height: rippleSize)) + ripple.path = ripplePath.cgPath + ripple.fillColor = UIColor.white.withAlphaComponent(0.5).cgColor + ripple.opacity = 0 + button.layer.addSublayer(ripple) + + let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") + scaleAnimation.fromValue = 0.5 + scaleAnimation.toValue = 2.0 + scaleAnimation.duration = 0.4 + + let opacityAnimation = CABasicAnimation(keyPath: "opacity") + opacityAnimation.fromValue = 1.0 + opacityAnimation.toValue = 0.0 + opacityAnimation.duration = 0.4 + + let animationGroup = CAAnimationGroup() + animationGroup.animations = [scaleAnimation, opacityAnimation] + animationGroup.duration = 0.4 + ripple.add(animationGroup, forKey: "ripple") + + // Scale animation for the button + UIView.animate(withDuration: 0.15, animations: { + button.transform = CGAffineTransform(scaleX: 0.92, y: 0.92) + }) { _ in + UIView.animate(withDuration: 0.15) { + button.transform = .identity + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + // Update gradient frame for the card + if let gradientLayer = layer.sublayers?.first as? CAGradientLayer { + gradientLayer.frame = bounds + } + // Update gradient frame for buttons + [nextQuestionButton, readArticleButton].forEach { button in + if let gradient = button.layer.sublayers?.first(where: { $0 is CAGradientLayer }) as? CAGradientLayer { + gradient.frame = button.bounds + } + } + } +} + +//#Preview { +// NextQuestionCardView() +//} + +// Extension to support dynamic font weight +extension UIFont { + func withWeight(_ weight: UIFont.Weight) -> UIFont { + let descriptor = fontDescriptor.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: weight]]) + return UIFont(descriptor: descriptor, size: 0) + } +} diff --git a/recap/Views/DailyQuestions/PatientQuestionFirebase.swift b/recap/Views/DailyQuestions/PatientQuestionFirebase.swift new file mode 100755 index 0000000..7134b75 --- /dev/null +++ b/recap/Views/DailyQuestions/PatientQuestionFirebase.swift @@ -0,0 +1,129 @@ +// +// PatientQuestionFirebase.swift +// recap +// +// Created by s1834 on 13/03/25. +// + +import FirebaseFirestore + +extension PatientQuestionViewController { + + func fetchQuestionsFromFirestore() { + let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") + userQuestionsRef.getDocuments { [weak self] (snapshot: QuerySnapshot?, error: Error?) in + guard let self = self else { return } + + if let error = error { + print("❌❌ Error fetching questions: \(error.localizedDescription)") + return + } + + self.questions = snapshot?.documents.compactMap { document in + var question = try? document.data(as: Question.self) + question?.id = document.documentID + return question + } ?? [] + + if self.questions.isEmpty { + self.showNoQuestionsReadyAlert() + return + } + + self.sortQuestions() + self.askAllQuestions() + } + } + + private func askAllQuestions() { + var displayedAny = false + + for question in questions { + if shouldAskQuestionAgain(for: question) { + displayQuestion(question) + displayedAny = true + break + } + } + + if !displayedAny { + showNoQuestionsReadyAlert() + } + } + + private func sortQuestions() { + self.questions.sort { (q1, q2) -> Bool in + if !q1.isAnswered && q2.isAnswered { return true } + if q1.isAnswered && !q2.isAnswered { return false } + return q1.priority < q2.priority + } + } + + private func shouldAskQuestionAgain(for question: Question) -> Bool { + if !question.isAnswered { + return true + } + + if let lastAsked = question.lastAsked { + let nextAskTime = lastAsked.addingTimeInterval(TimeInterval(question.askInterval)) + if Date() >= nextAskTime { + return true + } + } + return false + } + + + func updateStreakAndAnalytics() { + let streakService = StreakService(verifiedUserDocID: verifiedUserDocID) + streakService.updateStreakForToday(with: true) + + // Update lastAnswered in analytics + let analyticsRef = db.collection("users").document(verifiedUserDocID).collection("core").document("analytics") + analyticsRef.updateData(["lastAnswered": Date()]) { error in + if let error = error { + print("❌❌ Error updating lastAnswered: \(error.localizedDescription)") + } + } + + // Update streak data in streaksCore + let streaksCoresRef = db.collection("users").document(verifiedUserDocID).collection("streaksCore").document("streakData") + streaksCoresRef.getDocument { document, error in + if let error = error { + print("❌❌ Error fetching streak data: \(error.localizedDescription)") + return + } + + if let document = document, document.exists { + let data = document.data() + let maxStreak = data?["maxStreak"] as? Int ?? 0 + let currentStreak = data?["currentStreak"] as? Int ?? 0 + let lastAnsweredTimestamp = data?["lastAnswered"] as? Timestamp + let lastAnsweredDate = lastAnsweredTimestamp?.dateValue() + let today = Calendar.current.startOfDay(for: Date()) + let lastAnsweredDay = lastAnsweredDate.map { Calendar.current.startOfDay(for: $0) } + + if lastAnsweredDay != today { + let updatedMaxStreak = max(currentStreak + 1, maxStreak) + streaksCoresRef.updateData([ + "activeDays": FieldValue.increment(Int64(1)), + "currentStreak": FieldValue.increment(Int64(1)), + "maxStreak": updatedMaxStreak, + "lastAnswered": Timestamp(date: Date()) + ]) { error in + if let error = error { + print("❌❌ Error updating streak data: \(error.localizedDescription)") + } + } + } + } + } + } + + private func updateQuestionLastAsked(_ question: Question) { + let questionRef = db.collection("users").document(verifiedUserDocID).collection("questions").document(question.id!) + questionRef.updateData([ + "lastAsked": Date() + ]) + } +} diff --git a/recap/Views/DailyQuestions/PatientQuestionsViewController.swift b/recap/Views/DailyQuestions/PatientQuestionsViewController.swift old mode 100644 new mode 100755 index fc509b9..91afdd6 --- a/recap/Views/DailyQuestions/PatientQuestionsViewController.swift +++ b/recap/Views/DailyQuestions/PatientQuestionsViewController.swift @@ -1,10 +1,14 @@ // -// PatientQuestionsViewController.swift +// PatientQuestionViewController.swift // recap // -// Created by user@47 on 08/02/25. +// Created by s1834 on 08/02/25. // +#Preview{ + PatientQuestionViewController(verifiedUserDocID: "E4McfMAfgATYMSvzx43wm7r1WQ23") +} + import UIKit import FirebaseFirestore @@ -12,462 +16,640 @@ protocol PatientQuestionsDelegate: AnyObject { func didSubmitAnswer(for question: Question) } -class PatientQuestionsViewController: UIViewController { - +class PatientQuestionViewController: UIViewController, NextQuestionCardViewDelegate { + func didTapGoHome() { + dismissCard() + moveToNextQuestion() + navigationController?.popToRootViewController(animated: true) + } + + func didTapNextQuestion() { + dismissCard() + moveToNextQuestion() + } + + func didTapPlayGame() { + dismissCard() + moveToNextQuestion() + let playGameVC = PlayGameViewController() + navigationController?.pushViewController(playGameVC, animated: true) + } + + func didTapReadArticle() { + dismissCard() + moveToNextQuestion() + let articleVC = ArticleTableViewController() + navigationController?.pushViewController(articleVC, animated: true) + } + weak var delegate: PatientQuestionsDelegate? var question: Question? var selectedOptionButton: UIButton? var verifiedUserDocID: String var currentQuestionIndex = 0 - var questions: [Question] = [] // To hold fetched questions + var questions: [Question] = [] + + let db = Firestore.firestore() + + private var blurEffectView: UIVisualEffectView? + private var nextQuestionCardView: NextQuestionCardView? - private let db = Firestore.firestore() - init(verifiedUserDocID: String) { self.verifiedUserDocID = verifiedUserDocID super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // UI Components + + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.showsVerticalScrollIndicator = false + scrollView.translatesAutoresizingMaskIntoConstraints = false + return scrollView + }() + + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let questionCard: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 24 + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.08 + view.layer.shadowOffset = CGSize(width: 0, height: 6) + view.layer.shadowRadius = 12 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + private let questionLabel: UILabel = { let label = UILabel() - label.font = UIFont.boldSystemFont(ofSize: 22) - label.textColor = .black + label.font = UIFont.systemFont(ofSize: 22, weight: .bold) + label.textColor = AppColors.iconColor label.numberOfLines = 0 label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label }() - + private let questionImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true + imageView.layer.cornerRadius = 16 imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - - private let optionsContainer: UIStackView = { + + // Grid container for options + private let optionsGridContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + // Full width container for the last option when needed + private let fullWidthOptionContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let leftColumnStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical - stackView.spacing = 16 + stackView.spacing = 12 stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() - + + private let rightColumnStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 12 + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + private let submitButton: UIButton = { let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("Submit", for: .normal) - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.backgroundColor = AppColors.iconColor button.setTitleColor(.white, for: .normal) - button.backgroundColor = UIColor.systemBlue - button.layer.cornerRadius = 10 - button.translatesAutoresizingMaskIntoConstraints = false + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius return button }() private let footerLabel: UILabel = { let label = UILabel() label.text = "Keep going — each one sharpens your mind and warms hearts!" - label.font = UIFont.italicSystemFont(ofSize: 14) - label.textColor = .gray + label.font = UIFont.italicSystemFont(ofSize: 16) + label.textColor = AppColors.iconColor.withAlphaComponent(0.7) label.numberOfLines = 0 label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label }() - + + private let progressView: UIProgressView = { + let progress = UIProgressView(progressViewStyle: .bar) + progress.trackTintColor = AppColors.cardBackgroundColor + progress.progressTintColor = AppColors.highlightColor + progress.layer.cornerRadius = 4 + progress.clipsToBounds = true + progress.translatesAutoresizingMaskIntoConstraints = false + return progress + }() + + private let questionCountLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14, weight: .medium) + label.textColor = AppColors.iconColor.withAlphaComponent(0.7) + label.textAlignment = .right + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // Constraints that need to be activated/deactivated + private var fullWidthOptionTopConstraint: NSLayoutConstraint? + private var fullWidthOptionHeightConstraint: NSLayoutConstraint? + + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor(white: 0.95, alpha: 1.0) + view.backgroundColor = AppColors.cardBackgroundColor + setupNavigationBar() setupUI() - fetchQuestionsFromFirestore() // Fetch all questions for the user - } - - // MARK: - Fetch Questions from Firestore - - private func fetchQuestionsFromFirestore() { - let userQuestionsRef = db.collection("users").document(verifiedUserDocID).collection("questions") - - userQuestionsRef.getDocuments { [weak self] snapshot, error in - guard let self = self else { return } - - if let error = error { - print("🔥 Error fetching questions: \(error.localizedDescription)") - return - } - - // Extract data and ensure each question has an ID - self.questions = snapshot?.documents.compactMap { document in - var question = try? document.data(as: Question.self) - question?.id = document.documentID // Ensure the question has its Firestore document ID - return question - } ?? [] - - if self.questions.isEmpty { - print("⚠️ No questions found.") - return - } - - self.sortQuestions() - self.askAllQuestions() - } - } - - - - private func askAllQuestions() { - print("🟢 Asking all sorted questions...") - var displayedAny = false - - for question in questions { - if shouldAskQuestionAgain(for: question) { - print("🆗 Displaying question: \(question.text)") - displayQuestion(question) - displayedAny = true - break // Ask one question at a time - } - } - - if !displayedAny { - print("⚠️ No questions to display!") - } + fetchQuestionsFromFirestore() } - +// - // MARK: - Sort Questions - private func sortQuestions() { - print("📌 Sorting questions before asking.") - - self.questions.sort { (q1, q2) -> Bool in - if !q1.isAnswered && q2.isAnswered { return true } - if q1.isAnswered && !q2.isAnswered { return false } - return q1.priority < q2.priority - } - - for question in self.questions { - print("➡️ Question in sorted order: \(question.text), isAnswered: \(question.isAnswered)") - } - } - - -// private func moveToNextQuestion() { -// // Assuming `questions` is a list of all fetched questions -// guard !questions.isEmpty else { return } +// +// +// override func viewDidLoad() { +// super.viewDidLoad() +// setupBackground() // Use this method instead of direct assignment +// setupNavigationBar() +// setupUI() +// fetchQuestionsFromFirestore() +// } +// +// private func setupBackground() { +// // Remove any existing gradient layers first +// view.layer.sublayers?.filter { $0 is CAGradientLayer }.forEach { $0.removeFromSuperlayer() } // -// // Logic to find the next question that needs to be asked -// // For example, you might have a flag `isAnswered` or `askInterval` to determine which question to show -// if let nextQuestion = questions.first(where: { !$0.isAnswered }) { -// displayQuestion(nextQuestion) -// } else { -// print("No unanswered questions available.") +// // For all devices, use gradient background +// let gradientLayer = AppColors.createAppBackgroundGradientLayer() +// gradientLayer.frame = view.bounds +// view.layer.insertSublayer(gradientLayer, at: 0) +// } +// +// override func viewDidLayoutSubviews() { +// super.viewDidLayoutSubviews() +// // Update gradient layer frame when view size changes +// if let gradientLayer = view.layer.sublayers?.first(where: { $0 is CAGradientLayer }) as? CAGradientLayer { +// gradientLayer.frame = view.bounds // } // } - - - - private func shouldAskQuestionAgain(for question: Question) -> Bool { - if !question.isAnswered { - return true // Question hasn't been answered yet, ask it - } - - // Check if the current time is greater than the next ask time - if let lastAsked = question.lastAsked { - let nextAskTime = lastAsked.addingTimeInterval(TimeInterval(question.askInterval)) - if Date() >= nextAskTime { - return true // Time to ask again - } - } - - return false +// + private func setupNavigationBar() { + navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationController?.navigationBar.shadowImage = UIImage() + navigationController?.navigationBar.tintColor = AppColors.iconColor + navigationController?.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: AppColors.secondaryTextColor, + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .semibold) + ] + title = "Daily Questions" } - - - - // MARK: - Generate Random Questions (for testing) - private func generateRandomQuestions() -> [Question] { - var randomQuestions: [Question] = [] - - // Generate 5 random questions for testing - for i in 1...5 { - let question = Question( - text: "Sample Question #\(i)", - category: .immediateMemory, - subcategory: .general, - tag: nil, - answerOptions: ["Option A", "Option B", "Option C", "Option D"], - answers: [], - correctAnswers: ["Option A"], - image: nil, - isAnswered: false, - askInterval: 0, - timeFrame: TimeFrame(from: "00:00", to: "00:00"), - priority: 1, - audio: nil, - isActive: true, - hint: nil, - confidence: nil, - hardness: 1, - questionType: .multipleChoice - ) - randomQuestions.append(question) - } + + private func setupUI() { + view.addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(questionCountLabel) + contentView.addSubview(progressView) + contentView.addSubview(questionCard) + questionCard.addSubview(questionLabel) + questionCard.addSubview(questionImageView) - return randomQuestions - } - - - // MARK: - Display Current Question - private func displayCurrentQuestion() { - guard currentQuestionIndex < questions.count else { - print("No more questions") - return - } + // Setup the two-column grid for options + questionCard.addSubview(optionsGridContainer) + optionsGridContainer.addSubview(leftColumnStackView) + optionsGridContainer.addSubview(rightColumnStackView) + optionsGridContainer.addSubview(fullWidthOptionContainer) - let currentQuestion = questions[currentQuestionIndex] + contentView.addSubview(submitButton) + contentView.addSubview(footerLabel) - // Only display the question if it's time to ask it again or it hasn't been answered yet - if shouldAskQuestionAgain(for: currentQuestion) { - questionLabel.text = currentQuestion.text - - // Set image if available - if let imageString = currentQuestion.image, let imageData = Data(base64Encoded: imageString) { - questionImageView.image = UIImage(data: imageData) - } - - // Create option buttons dynamically based on the number of answer options - optionsContainer.arrangedSubviews.forEach { $0.removeFromSuperview() } - for option in currentQuestion.answerOptions { - let button = createOptionButton(with: option) - button.addTarget(self, action: #selector(optionSelected(_:)), for: .touchUpInside) - optionsContainer.addArrangedSubview(button) - } - } else { - moveToNextQuestion() // If not time to ask, skip to next question - } - } - - - // MARK: - Setup UI - private func setupUI() { - view.addSubview(questionLabel) - view.addSubview(questionImageView) - view.addSubview(optionsContainer) - view.addSubview(submitButton) - view.addSubview(footerLabel) - - submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside) - + submitButton.addTarget(self, action: #selector(submitAnswer), for: .touchUpInside) setupConstraints() } - + private func setupConstraints() { + let safeArea = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ - questionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), - questionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - questionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - + // ScrollView + scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + // ContentView + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + // Question Count Label + questionCountLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + questionCountLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), + + // Progress View + progressView.topAnchor.constraint(equalTo: questionCountLabel.bottomAnchor, constant: 8), + progressView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), + progressView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), + progressView.heightAnchor.constraint(equalToConstant: 8), + + // Question Card + questionCard.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 24), + questionCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + questionCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + // Question Label inside Card + questionLabel.topAnchor.constraint(equalTo: questionCard.topAnchor, constant: 24), + questionLabel.leadingAnchor.constraint(equalTo: questionCard.leadingAnchor, constant: 20), + questionLabel.trailingAnchor.constraint(equalTo: questionCard.trailingAnchor, constant: -20), + + // Question Image inside Card questionImageView.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20), - questionImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + questionImageView.centerXAnchor.constraint(equalTo: questionCard.centerXAnchor), questionImageView.widthAnchor.constraint(equalToConstant: 150), questionImageView.heightAnchor.constraint(equalToConstant: 150), - - optionsContainer.topAnchor.constraint(equalTo: questionImageView.bottomAnchor, constant: 20), - optionsContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - optionsContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - - submitButton.topAnchor.constraint(equalTo: optionsContainer.bottomAnchor, constant: 30), - submitButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - submitButton.widthAnchor.constraint(equalToConstant: 120), - submitButton.heightAnchor.constraint(equalToConstant: 50), - + + // Options Grid Container + optionsGridContainer.topAnchor.constraint(equalTo: questionImageView.bottomAnchor, constant: 24), + optionsGridContainer.leadingAnchor.constraint(equalTo: questionCard.leadingAnchor, constant: 20), + optionsGridContainer.trailingAnchor.constraint(equalTo: questionCard.trailingAnchor, constant: -20), + optionsGridContainer.bottomAnchor.constraint(equalTo: questionCard.bottomAnchor, constant: -24), + + // Left Column Stack View + leftColumnStackView.topAnchor.constraint(equalTo: optionsGridContainer.topAnchor), + leftColumnStackView.leadingAnchor.constraint(equalTo: optionsGridContainer.leadingAnchor), + leftColumnStackView.widthAnchor.constraint(equalTo: optionsGridContainer.widthAnchor, multiplier: 0.48), + + // Right Column Stack View + rightColumnStackView.topAnchor.constraint(equalTo: optionsGridContainer.topAnchor), + rightColumnStackView.trailingAnchor.constraint(equalTo: optionsGridContainer.trailingAnchor), + rightColumnStackView.widthAnchor.constraint(equalTo: optionsGridContainer.widthAnchor, multiplier: 0.48), + + // Full Width Option Container (for last option when needed) + fullWidthOptionContainer.leadingAnchor.constraint(equalTo: optionsGridContainer.leadingAnchor), + fullWidthOptionContainer.trailingAnchor.constraint(equalTo: optionsGridContainer.trailingAnchor), + + // Submit Button + submitButton.topAnchor.constraint(equalTo: questionCard.bottomAnchor, constant: 32), + submitButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + submitButton.widthAnchor.constraint(equalToConstant: 180), + submitButton.heightAnchor.constraint(equalToConstant: 54), + + // Footer Label footerLabel.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 20), - footerLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - footerLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16) + footerLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), + footerLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), + footerLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) ]) + + // Create constraints that will be activated/deactivated as needed + fullWidthOptionTopConstraint = fullWidthOptionContainer.topAnchor.constraint(equalTo: leftColumnStackView.bottomAnchor, constant: 12) + fullWidthOptionHeightConstraint = fullWidthOptionContainer.heightAnchor.constraint(equalToConstant: 0) + + // Initially deactivate these constraints + fullWidthOptionTopConstraint?.isActive = false + fullWidthOptionHeightConstraint?.isActive = true } - - // MARK: - Create Option Button - private func createOptionButton(with title: String) -> UIButton { + +// private func createOptionButton(with title: String, isFullWidth: Bool = false) -> UIButton { +// let button = UIButton(type: .system) +// button.setTitle(title, for: .normal) +// button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium) +// button.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) +// button.backgroundColor = .white +// button.layer.cornerRadius = 16 +// button.layer.borderWidth = 1.5 +// button.layer.borderColor = AppColors.highlightColor.cgColor + + + + private func createOptionButton(with title: String, isFullWidth: Bool = false) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 16) - button.setTitleColor(.black, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium) + button.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) button.backgroundColor = .white - button.layer.cornerRadius = 10 - button.layer.borderWidth = 1 - button.layer.borderColor = UIColor.lightGray.cgColor + button.layer.cornerRadius = 16 + button.layer.borderWidth = 1.5 + button.layer.borderColor = AppColors.highlightColor.cgColor + + // Store original appearance + originalButtonAppearance[button] = ButtonAppearance( + backgroundColor: .white, + borderColor: AppColors.highlightColor.cgColor, + textColor: AppColors.primaryButtonTextColor + ) + + + // Center align text for better appearance in grid layout + button.contentHorizontalAlignment = .center + button.titleLabel?.textAlignment = .center + button.titleLabel?.numberOfLines = 0 // Allow multiple lines for longer options + button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + button.translatesAutoresizingMaskIntoConstraints = false - button.heightAnchor.constraint(equalToConstant: 50).isActive = true + // Adjust height for grid layout + button.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true + + // Add subtle shadow for depth +// button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.06 + button.layer.shadowOffset = CGSize(width: 0, height: 2) + button.layer.shadowRadius = 4 + return button } + + private struct ButtonAppearance { + let backgroundColor: UIColor + let borderColor: CGColor + let textColor: UIColor + } - // MARK: - Option Selection + private var originalButtonAppearance = [UIButton: ButtonAppearance]() + + @objc private func optionSelected(_ sender: UIButton) { - selectedOptionButton?.layer.borderColor = UIColor.lightGray.cgColor - selectedOptionButton?.backgroundColor = .white + // Reset previous selection to original appearance + if let previousButton = selectedOptionButton, + let originalAppearance = originalButtonAppearance[previousButton] { + previousButton.layer.borderColor = originalAppearance.borderColor + previousButton.backgroundColor = originalAppearance.backgroundColor + previousButton.setTitleColor(originalAppearance.textColor, for: .normal) + } + // Set new selection selectedOptionButton = sender - sender.layer.borderColor = UIColor.systemBlue.cgColor - sender.backgroundColor = UIColor(red: 0.9, green: 0.95, blue: 1.0, alpha: 1.0) - } - - // MARK: - Submit Button Action - @objc private func submitButtonTapped() { - guard let selectedAnswer = selectedOptionButton?.title(for: .normal) else { return } - - // Mark question as answered - var currentQuestion = questions[currentQuestionIndex] - currentQuestion.isAnswered = true - currentQuestion.answers.append(selectedAnswer) + sender.layer.borderColor = AppColors.primaryButtonColor.cgColor + sender.backgroundColor = AppColors.primaryButtonColor.withAlphaComponent(0.1) + sender.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) - // Update Firestore with the answer - let questionRef = db.collection("users").document(verifiedUserDocID).collection("questions").document(currentQuestion.id!) - questionRef.updateData([ - "isAnswered": true, - "answers": currentQuestion.answers // Allow multiple answers here - ]) { [weak self] error in - if let error = error { - print("Error updating question: \(error.localizedDescription)") - } else { - self?.showNextQuestionConfirmation() // Go to the next available question after submitting the answer - } + // Enable submit button with animation + UIView.animate(withDuration: 0.3) { + self.submitButton.alpha = 1.0 + self.submitButton.transform = CGAffineTransform.identity } } + @objc private func submitAnswer() { + guard let selectedAnswer = selectedOptionButton?.title(for: .normal) else { + // Show a subtle animation to indicate no selection + UIView.animate(withDuration: 0.3, animations: { + self.submitButton.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + }) { _ in + UIView.animate(withDuration: 0.3) { + self.submitButton.transform = CGAffineTransform.identity + } + } + return + } - - - private func submitAnswer(_ answer: String) { var currentQuestion = questions[currentQuestionIndex] - guard let questionID = currentQuestion.id else { - print("❌ Error: Question ID is missing.") + print("❌❌ Error: Question ID is missing.") return } - // Mark as answered and add the answer currentQuestion.isAnswered = true - currentQuestion.answers.append(answer) + currentQuestion.answers.append(selectedAnswer) - // Update Firestore document + // Show loading state on button + submitButton.setTitle("Submitting...", for: .normal) + submitButton.isEnabled = false + let questionRef = db.collection("users").document(verifiedUserDocID).collection("questions").document(questionID) questionRef.updateData([ "isAnswered": true, "answers": currentQuestion.answers, "lastAsked": Date() ]) { [weak self] error in + guard let self = self else { return } + + // Reset button state + self.submitButton.setTitle("Submit", for: .normal) + self.submitButton.isEnabled = true + if let error = error { - print("❌ Error updating question: \(error.localizedDescription)") + print("❌❌ Error updating question: \(error.localizedDescription)") + // Show error briefly + let originalColor = self.submitButton.backgroundColor + UIView.animate(withDuration: 0.3, animations: { + self.submitButton.backgroundColor = UIColor.systemRed + }) { _ in + UIView.animate(withDuration: 0.3) { + self.submitButton.backgroundColor = originalColor + } + } } else { - self?.showNextQuestionConfirmation() + self.updateStreakAndAnalytics() + self.showNextQuestionCard() } } } - - - private func showNextQuestionConfirmation() { - let alertController = UIAlertController( - title: "Question Submitted", - message: "Do you want to go to the next question or exit?", - preferredStyle: .alert - ) + private func showNextQuestionCard() { + // Create blur effect + let blurEffect = UIBlurEffect(style: .light) + blurEffectView = UIVisualEffectView(effect: blurEffect) + blurEffectView?.frame = view.bounds + blurEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + blurEffectView?.alpha = 0 + + // Create card view + nextQuestionCardView = NextQuestionCardView() + nextQuestionCardView?.delegate = self + nextQuestionCardView?.frame = CGRect(x: 0, y: 0, width: 300, height: 400) + nextQuestionCardView?.center = view.center + nextQuestionCardView?.alpha = 0 + nextQuestionCardView?.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) - // "Next Question" action - let nextQuestionAction = UIAlertAction(title: "Next Question", style: .default) { [weak self] _ in - self?.moveToNextQuestion() + if let blurView = blurEffectView, let cardView = nextQuestionCardView { + view.addSubview(blurView) + view.addSubview(cardView) + + // Animate in + UIView.animate(withDuration: 0.3) { + blurView.alpha = 1 + cardView.alpha = 1 + cardView.transform = .identity + } } + } + + private func dismissCard() { + UIView.animate(withDuration: 0.3, animations: { + self.blurEffectView?.alpha = 0 + self.nextQuestionCardView?.alpha = 0 + self.nextQuestionCardView?.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + }) { _ in + self.blurEffectView?.removeFromSuperview() + self.nextQuestionCardView?.removeFromSuperview() + } + } + + func showNoQuestionsReadyAlert() { + let successVC = SuccessQuestionsViewController() + successVC.modalPresentationStyle = .overFullScreen + successVC.modalTransitionStyle = .crossDissolve - // "Exit" action - let exitAction = UIAlertAction(title: "Exit", style: .cancel) { [weak self] _ in + // Set the exit action + successVC.onExitTapped = { [weak self] in self?.exitQuestionFlow() } - // Add actions to alert - alertController.addAction(nextQuestionAction) - alertController.addAction(exitAction) - - // Present the alert - present(alertController, animated: true, completion: nil) + present(successVC, animated: true, completion: nil) } - - - - // MARK: - Move to Next Question - private func moveToNextQuestion() { + // MARK: - Exit the Question Flow + private func exitQuestionFlow() { + navigationController?.popViewController(animated: true) + } + + func moveToNextQuestion() { currentQuestionIndex += 1 - while currentQuestionIndex < questions.count { - let nextQuestion = questions[currentQuestionIndex] - if shouldAskQuestionAgain(for: nextQuestion) { - displayQuestion(nextQuestion) - return - } - currentQuestionIndex += 1 + if currentQuestionIndex < questions.count { + displayQuestion(questions[currentQuestionIndex]) + updateProgressBar() + } else { + showNoQuestionsReadyAlert() } - - print("🚫 No more questions available.") - exitQuestionFlow() } - - - - private func displayQuestion(_ question: Question) { - questionLabel.text = question.text - - // Set image if available - if let imageString = question.image, let imageData = Data(base64Encoded: imageString) { - questionImageView.image = UIImage(data: imageData) - } + + func updateProgressBar() { + guard !questions.isEmpty else { return } + let progress = Float(currentQuestionIndex + 1) / Float(questions.count) - // Create option buttons - optionsContainer.arrangedSubviews.forEach { $0.removeFromSuperview() } - for option in question.answerOptions { - let button = createOptionButton(with: option) - button.addTarget(self, action: #selector(optionSelected(_:)), for: .touchUpInside) - optionsContainer.addArrangedSubview(button) + // Animate progress update + UIView.animate(withDuration: 0.5) { + self.progressView.setProgress(progress, animated: true) } - // Update question as being asked - updateQuestionLastAsked(question) - } - - - private func updateQuestionLastAsked(_ question: Question) { - let questionRef = db.collection("users").document(verifiedUserDocID).collection("questions").document(question.id!) - questionRef.updateData([ - "lastAsked": Date() - ]) + // Update question count label + questionCountLabel.text = "Question \(currentQuestionIndex + 1) of \(questions.count)" } - - private func showNoQuestionsReadyAlert() { - let alertController = UIAlertController( - title: "No Questions Ready", - message: "All questions are either answered or not due for asking yet.", - preferredStyle: .alert - ) + // Updated displayQuestion function to handle 2-column layout with the last option spanning both columns if needed + func displayQuestion(_ question: Question) { + // Clear existing options + self.leftColumnStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + self.rightColumnStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + self.fullWidthOptionContainer.subviews.forEach { $0.removeFromSuperview() } - let exitAction = UIAlertAction(title: "Exit", style: .cancel) { [weak self] _ in - self?.exitQuestionFlow() - } + // Deactivate constraints for full width option + fullWidthOptionTopConstraint?.isActive = false + fullWidthOptionHeightConstraint?.isActive = true - alertController.addAction(exitAction) - present(alertController, animated: true, completion: nil) - } - - - - // MARK: - Exit the Question Flow - private func exitQuestionFlow() { - // Optionally, you can handle any exit logic (e.g., navigate to another screen or stop the process) - print("Exiting question flow.") - // For example, pop the current view controller if you're using a navigation controller - navigationController?.popViewController(animated: true) + // Animation for question change + UIView.transition(with: questionCard, duration: 0.3, options: .transitionCrossDissolve, animations: { + self.questionLabel.text = question.text + + if let imageString = question.image, let imageData = Data(base64Encoded: imageString) { + self.questionImageView.image = UIImage(data: imageData) + self.questionImageView.isHidden = false + } else { + self.questionImageView.isHidden = true + } + + let options = question.answerOptions + let optionCount = options.count + + // Handle distribution of options + if optionCount % 2 == 1 && optionCount > 1 { + // Odd number of options - last one will span full width + let pairsCount = (optionCount - 1) / 2 + + // Add pairs to left and right columns + for i in 0.. UIButton { +// let button = UIButton(type: .system) +// button.setTitle(title, for: .normal) +// button.titleLabel?.font = UIFont.systemFont(ofSize: 16) +// button.setTitleColor(.black, for: .normal) +// button.backgroundColor = .white +// button.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius +// button.layer.borderWidth = 1 +// button.layer.borderColor = UIColor.lightGray.cgColor +// button.translatesAutoresizingMaskIntoConstraints = false +// button.heightAnchor.constraint(equalToConstant: 50).isActive = true +// return button +// } +// +// // MARK: - Option Selection +// @objc private func optionSelected(_ sender: UIButton) { +// // Reset the previously selected button's appearance +// selectedOptionButton?.layer.borderColor = UIColor.lightGray.cgColor +// selectedOptionButton?.backgroundColor = .white +// +// // Update the selected button's appearance +// selectedOptionButton = sender +// sender.layer.borderColor = UIColor.systemBlue.cgColor +// sender.backgroundColor = UIColor(red: 0.9, green: 0.95, blue: 1.0, alpha: 1.0) +// } +// +// @objc private func submitButtonTapped() { +// +// guard let selectedOptionButton = selectedOptionButton, var question = question else { return } +// +// let selectedAnswer = selectedOptionButton.title(for: .normal) ?? "" +// +// // Mark the question as answered +// question.isAnswered = true +// +// // Update button state +// submitButton.backgroundColor = .systemGreen +// submitButton.setTitle("Submitted", for: .normal) +// +// // Notify delegate that the answer was submitted +// delegate?.didSubmitAnswer(for: question) +// +// // Update Firestore (Original CODE1 Firestore Update) +// db.collection("questions").document(question.id ?? "<#default value#>").updateData([ +// "isAnswered": true +// ]) { (error) in +// if let error = error { +// print("Error updating question: \(error.localizedDescription)") +// } else { +// print("Question successfully updated") +// } +// } +// +// let questionsFetcher = QuestionsManager(verifiedUserDocID: verifiedUserDocID) +// +// let userQuestionRef = db.collection("users") +// .document(questionsFetcher.verifiedUserDocID) +// .collection("questions") +// .document(question.id ?? "") +// +// userQuestionRef.setData([ +// "correctAnswers": [selectedAnswer], +// "isAnswered": true // Update the isAnswered field in Firestore +// ], merge: true) { error in +// if let error = error { +// print("Error updating correct answer: \(error.localizedDescription)") +// } else { +// print("Correct answer successfully saved in Firestore.") +// } +// } +// } +// +//} +//#Preview{ +// QuestionDetailViewController(verifiedUserDocID: "E4McfMAfgATYMSvzx43wm7r1WQ23") +//} + import UIKit import FirebaseFirestore @@ -6,7 +267,6 @@ protocol QuestionDetailDelegate: AnyObject { } class QuestionDetailViewController: UIViewController { - weak var delegate: QuestionDetailDelegate? var question: Question? var selectedOptionButton: UIButton? @@ -16,214 +276,318 @@ class QuestionDetailViewController: UIViewController { self.verifiedUserDocID = verifiedUserDocID super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // Firestore reference private let db = Firestore.firestore() - + // UI Components + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.showsVerticalScrollIndicator = false + scrollView.translatesAutoresizingMaskIntoConstraints = false + return scrollView + }() + + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let questionCard: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 24 + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.08 + view.layer.shadowOffset = CGSize(width: 0, height: 6) + view.layer.shadowRadius = 12 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + private let questionLabel: UILabel = { let label = UILabel() - label.font = UIFont.boldSystemFont(ofSize: 22) - label.textColor = .black + label.font = UIFont.systemFont(ofSize: 22, weight: .bold) + label.textColor = AppColors.iconColor label.numberOfLines = 0 label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label }() - + private let questionImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true + imageView.layer.cornerRadius = 16 imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() - - private let optionsContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 16 - stackView.translatesAutoresizingMaskIntoConstraints = false - return stackView + + private let optionsContainer: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view }() - + private let submitButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Submit", for: .normal) - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18) + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont button.setTitleColor(.white, for: .normal) - button.backgroundColor = UIColor.systemBlue - button.layer.cornerRadius = 10 + button.backgroundColor = AppColors.iconColor + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius button.translatesAutoresizingMaskIntoConstraints = false return button }() - + private let footerLabel: UILabel = { let label = UILabel() - label.text = "Keep going — each one sharpens your mind and warms hearts!" - label.font = UIFont.italicSystemFont(ofSize: 14) - label.textColor = .gray + label.text = "Keep going — each one helps your loved ones" + label.font = UIFont.italicSystemFont(ofSize: 16) + label.textColor = AppColors.iconColor.withAlphaComponent(0.7) label.numberOfLines = 0 label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label }() - + + private let questionCountLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14, weight: .medium) + label.textColor = AppColors.iconColor.withAlphaComponent(0.7) + label.textAlignment = .right + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // Store original button appearance + private struct ButtonAppearance { + let backgroundColor: UIColor + let borderColor: CGColor + let textColor: UIColor + } + + private var originalButtonAppearance = [UIButton: ButtonAppearance]() + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor(white: 0.95, alpha: 1.0) + setupBackground() + setupNavigationBar() setupUI() - fetchQuestionFromFirestore() // Fetch question from Firestore } - - // MARK: - Fetch Question from Firestore - private func fetchQuestionFromFirestore() { - guard let questionId = question?.id else { return } - - db.collection("questions").document(questionId).getDocument { [weak self] (document, error) in - if let error = error { - print("Error fetching question: \(error.localizedDescription)") - } else if let document = document, document.exists, let data = document.data() { - // Initialize the question with the data dictionary - do { - self?.question = try Question(from: data as! Decoder) // Ensure the initializer for Question exists - self?.setupUI() // Update UI after fetching the question - } catch { - print("Error initializing question: \(error.localizedDescription)") - } - } - } + + private func setupBackground() { + view.backgroundColor = AppColors.cardBackgroundColor } - - // MARK: - UI Setup + + private func setupNavigationBar() { + navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationController?.navigationBar.shadowImage = UIImage() + navigationController?.navigationBar.tintColor = AppColors.iconColor + navigationController?.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: AppColors.secondaryTextColor, + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .semibold) + ] + title = "Daily Questions" + } + private func setupUI() { guard let question = question else { return } - + // Add subviews - view.addSubview(questionLabel) + view.addSubview(scrollView) + scrollView.addSubview(contentView) + contentView.addSubview(questionCountLabel) + contentView.addSubview(questionCard) + questionCard.addSubview(questionLabel) + questionCard.addSubview(questionImageView) + questionCard.addSubview(optionsContainer) + contentView.addSubview(submitButton) + contentView.addSubview(footerLabel) - // Safely unwrap and handle the image + // Set question text and image + questionLabel.text = question.text if let imageString = question.image, let imageData = Data(base64Encoded: imageString) { questionImageView.image = UIImage(data: imageData) - view.addSubview(questionImageView) - } - - view.addSubview(optionsContainer) - view.addSubview(submitButton) - view.addSubview(footerLabel) - - // Set question text - questionLabel.text = question.text - - // Create option buttons - optionsContainer.arrangedSubviews.forEach { $0.removeFromSuperview() } // Clear old options - for (index, option) in question.answerOptions.enumerated() { - if index % 2 == 0 { - let horizontalStack = UIStackView() - horizontalStack.axis = .horizontal - horizontalStack.spacing = 16 - horizontalStack.distribution = .fillEqually - horizontalStack.translatesAutoresizingMaskIntoConstraints = false - optionsContainer.addArrangedSubview(horizontalStack) - } - let button = createOptionButton(with: option) - button.addTarget(self, action: #selector(optionSelected(_:)), for: .touchUpInside) - (optionsContainer.arrangedSubviews.last as? UIStackView)?.addArrangedSubview(button) + questionImageView.isHidden = false + } else { + questionImageView.isHidden = true } - + + // Clear existing options + optionsContainer.subviews.forEach { $0.removeFromSuperview() } + + // Setup options in a 2x2 grid layout + setupOptionsGrid(with: question.answerOptions) + // Setup constraints setupConstraints() - + // Add submit button action submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside) + + // Initial submit button state + submitButton.alpha = 0.7 + submitButton.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) } - - // MARK: - Setup Constraints - private func setupConstraints() { - NSLayoutConstraint.activate([ - questionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), - questionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - questionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16) - ]) - - if question?.image != nil { - NSLayoutConstraint.activate([ - questionImageView.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20), - questionImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - questionImageView.widthAnchor.constraint(equalToConstant: 150), - questionImageView.heightAnchor.constraint(equalToConstant: 150), - - optionsContainer.topAnchor.constraint(equalTo: questionImageView.bottomAnchor, constant: 20) - ]) - } else { + + private func setupOptionsGrid(with options: [String]) { + let columns = 2 + let spacing: CGFloat = 12 + let additionalRightPadding: CGFloat = 10 // Add extra padding on the right side + + for (index, option) in options.enumerated() { + let row = index / columns + let column = index % columns + + let button = createOptionButton(with: option) + button.addTarget(self, action: #selector(optionSelected(_:)), for: .touchUpInside) + optionsContainer.addSubview(button) + + // Calculate position in grid + button.translatesAutoresizingMaskIntoConstraints = false + + // Adjust button width to leave more space on the right + let totalHorizontalPadding = 40 + additionalRightPadding // 20 (left) + 20 (right) + additionalRightPadding + let buttonWidth = (UIScreen.main.bounds.width - totalHorizontalPadding - spacing) / 2 + NSLayoutConstraint.activate([ - optionsContainer.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20) + button.widthAnchor.constraint(equalToConstant: buttonWidth), + button.heightAnchor.constraint(equalToConstant: 60), + button.topAnchor.constraint(equalTo: optionsContainer.topAnchor, constant: CGFloat(row) * (60 + spacing)), + button.leadingAnchor.constraint(equalTo: optionsContainer.leadingAnchor, constant: CGFloat(column) * (buttonWidth + spacing)) ]) } - + + // Set height of optionsContainer based on number of rows + let rows = (options.count + columns - 1) / columns + let containerHeight = CGFloat(rows) * 60 + CGFloat(rows - 1) * spacing + optionsContainer.heightAnchor.constraint(equalToConstant: containerHeight).isActive = true + } + + private func setupConstraints() { + let safeArea = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ - optionsContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - optionsContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - - submitButton.topAnchor.constraint(equalTo: optionsContainer.bottomAnchor, constant: 30), - submitButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - submitButton.widthAnchor.constraint(equalToConstant: 120), - submitButton.heightAnchor.constraint(equalToConstant: 50), - + // ScrollView + scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + // ContentView + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + // Question Count Label + questionCountLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + questionCountLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), + + // Question Card + questionCard.topAnchor.constraint(equalTo: questionCountLabel.bottomAnchor, constant: 24), + questionCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + questionCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + // Question Label + questionLabel.topAnchor.constraint(equalTo: questionCard.topAnchor, constant: 24), + questionLabel.leadingAnchor.constraint(equalTo: questionCard.leadingAnchor, constant: 20), + questionLabel.trailingAnchor.constraint(equalTo: questionCard.trailingAnchor, constant: -20), + + // Question Image + questionImageView.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 20), + questionImageView.centerXAnchor.constraint(equalTo: questionCard.centerXAnchor), + questionImageView.widthAnchor.constraint(equalToConstant: 150), + questionImageView.heightAnchor.constraint(equalToConstant: 150), + + // Options Container + optionsContainer.topAnchor.constraint(equalTo: questionImageView.bottomAnchor, constant: 24), + optionsContainer.leadingAnchor.constraint(equalTo: questionCard.leadingAnchor, constant: 20), + optionsContainer.trailingAnchor.constraint(equalTo: questionCard.trailingAnchor, constant: -20), // Ensure it stays within card + optionsContainer.bottomAnchor.constraint(equalTo: questionCard.bottomAnchor, constant: -24), + + // Submit Button + submitButton.topAnchor.constraint(equalTo: questionCard.bottomAnchor, constant: 32), + submitButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + submitButton.widthAnchor.constraint(equalToConstant: 180), + submitButton.heightAnchor.constraint(equalToConstant: 54), + + // Footer Label footerLabel.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 20), - footerLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - footerLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16) + footerLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), + footerLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), + footerLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) ]) } - - // MARK: - Create Option Button + private func createOptionButton(with title: String) -> UIButton { let button = UIButton(type: .system) button.setTitle(title, for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 16) - button.setTitleColor(.black, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .medium) + button.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) button.backgroundColor = .white - button.layer.cornerRadius = 10 - button.layer.borderWidth = 1 - button.layer.borderColor = UIColor.lightGray.cgColor + button.layer.cornerRadius = 16 + button.layer.borderWidth = 1.5 + button.layer.borderColor = AppColors.highlightColor.cgColor + + originalButtonAppearance[button] = ButtonAppearance( + backgroundColor: .white, + borderColor: AppColors.highlightColor.cgColor, + textColor: AppColors.primaryButtonTextColor + ) + + button.contentHorizontalAlignment = .center + button.titleLabel?.textAlignment = .center + button.titleLabel?.numberOfLines = 0 + button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) button.translatesAutoresizingMaskIntoConstraints = false - button.heightAnchor.constraint(equalToConstant: 50).isActive = true + + button.layer.shadowColor = UIColor.black.cgColor + button.layer.shadowOpacity = 0.06 + button.layer.shadowOffset = CGSize(width: 0, height: 2) + button.layer.shadowRadius = 4 + return button } - - // MARK: - Option Selection + @objc private func optionSelected(_ sender: UIButton) { - // Reset the previously selected button's appearance - selectedOptionButton?.layer.borderColor = UIColor.lightGray.cgColor - selectedOptionButton?.backgroundColor = .white - - // Update the selected button's appearance + if let previousButton = selectedOptionButton, + let originalAppearance = originalButtonAppearance[previousButton] { + previousButton.layer.borderColor = originalAppearance.borderColor + previousButton.backgroundColor = originalAppearance.backgroundColor + previousButton.setTitleColor(originalAppearance.textColor, for: .normal) + } + selectedOptionButton = sender - sender.layer.borderColor = UIColor.systemBlue.cgColor - sender.backgroundColor = UIColor(red: 0.9, green: 0.95, blue: 1.0, alpha: 1.0) + sender.layer.borderColor = AppColors.primaryButtonColor.cgColor + sender.backgroundColor = AppColors.primaryButtonColor.withAlphaComponent(0.1) + sender.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) + + UIView.animate(withDuration: 0.3) { + self.submitButton.alpha = 1.0 + self.submitButton.transform = CGAffineTransform.identity + } } - + @objc private func submitButtonTapped() { - guard let selectedOptionButton = selectedOptionButton, var question = question else { return } - + let selectedAnswer = selectedOptionButton.title(for: .normal) ?? "" - - // Mark the question as answered question.isAnswered = true - - // Update button state - submitButton.backgroundColor = .systemGreen + + submitButton.backgroundColor = AppColors.iconColor submitButton.setTitle("Submitted", for: .normal) - - // Notify delegate that the answer was submitted + delegate?.didSubmitAnswer(for: question) - - // Update Firestore (Original CODE1 Firestore Update) + db.collection("questions").document(question.id ?? "<#default value#>").updateData([ "isAnswered": true ]) { (error) in @@ -233,17 +597,16 @@ class QuestionDetailViewController: UIViewController { print("Question successfully updated") } } - + let questionsFetcher = QuestionsManager(verifiedUserDocID: verifiedUserDocID) - let userQuestionRef = db.collection("users") .document(questionsFetcher.verifiedUserDocID) .collection("questions") .document(question.id ?? "") - + userQuestionRef.setData([ "correctAnswers": [selectedAnswer], - "isAnswered": true // Update the isAnswered field in Firestore + "isAnswered": true ], merge: true) { error in if let error = error { print("Error updating correct answer: \(error.localizedDescription)") @@ -252,5 +615,8 @@ class QuestionDetailViewController: UIViewController { } } } - } + +//#Preview { +// QuestionDetailViewController(verifiedUserDocID: "E4McfMAfgATYMSvzx43wm7r1WQ23") +//} diff --git a/recap/Views/FamilyHomeCards/DailyQuestionCardView.swift b/recap/Views/FamilyHomeCards/DailyQuestionCardView.swift old mode 100644 new mode 100755 index ca8288b..9e264c7 --- a/recap/Views/FamilyHomeCards/DailyQuestionCardView.swift +++ b/recap/Views/FamilyHomeCards/DailyQuestionCardView.swift @@ -1,80 +1,88 @@ // -// DailyQuestionView.swift +// DailyQuestionCardView.swift // Recap // -// Created by admin70 on 15/01/25. +// Created by khushi on 15/01/25. // import UIKit class DailyQuestionCardView: UIView { - private let iconImageView = UIImageView() - private let titleLabel = UILabel() - private let descriptionLabel = UILabel() - private let separatorView = UIView() - private let arrowImageView = UIImageView() - - // Use a closure to handle navigation action + private let iconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "old man")) + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "Daily Questions" + label.textColor = AppColors.primaryTextColor + label.font = Constants.FontandColors.titleFont + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.text = "A small moment of your time can spark precious memories of your loved ones." + label.font = .systemFont(ofSize: 16) + label.textColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let separatorView: UIView = { + let view = UIView() + view.backgroundColor = .systemGray4 + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(systemName: "chevron.right")) + imageView.tintColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + var navigateToDetail: (() -> Void)? override init(frame: CGRect) { super.init(frame: frame) setupUI() - setupTapGesture() } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() - setupTapGesture() } private func setupUI() { - backgroundColor = .white - layer.cornerRadius = 16 + backgroundColor = AppColors.cardBackgroundColor + layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius layer.shadowColor = UIColor.black.cgColor layer.shadowOpacity = 0.1 layer.shadowOffset = CGSize(width: 0, height: 2) layer.shadowRadius = 4 - translatesAutoresizingMaskIntoConstraints = false - - // Icon Image - iconImageView.image = UIImage(named: "old man") // Replace with your image asset name - iconImageView.contentMode = .scaleAspectFit - iconImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(iconImageView) - - // Title Label - titleLabel.text = "Daily Questions" - titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .semibold) - titleLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) - - // Arrow Image - arrowImageView.image = UIImage(systemName: "chevron.right") // Use SF Symbols - arrowImageView.tintColor = .gray - arrowImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(arrowImageView) - - // Separator View - separatorView.backgroundColor = .systemGray4 - separatorView.translatesAutoresizingMaskIntoConstraints = false - addSubview(separatorView) - - // Description Label - descriptionLabel.text = "A little effort each day keeps the memory strong – do it for the love you share with your family." - descriptionLabel.font = UIFont.systemFont(ofSize: 16, weight: .regular) - descriptionLabel.textColor = .gray - descriptionLabel.numberOfLines = 0 - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(descriptionLabel) - - // Constraints + + [iconImageView, titleLabel, arrowImageView, separatorView, descriptionLabel].forEach(addSubview) + + setupConstraints() + + // Add tap gesture directly in setupUI (removed separate method) + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) + addGestureRecognizer(tapGesture) + } + + private func setupConstraints() { NSLayoutConstraint.activate([ - iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), - iconImageView.widthAnchor.constraint(equalToConstant: 60), - iconImageView.heightAnchor.constraint(equalToConstant: 60), + iconImageView.widthAnchor.constraint(equalToConstant: 100), + iconImageView.heightAnchor.constraint(equalToConstant: 100), titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24), titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), @@ -86,29 +94,21 @@ class DailyQuestionCardView: UIView { arrowImageView.heightAnchor.constraint(equalToConstant: 22), separatorView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), - separatorView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 24), + separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), separatorView.heightAnchor.constraint(equalToConstant: 1), descriptionLabel.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 12), - descriptionLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 24), - descriptionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + descriptionLabel.leadingAnchor.constraint(equalTo: separatorView.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: separatorView.trailingAnchor), descriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) ]) } - private func setupTapGesture() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) - addGestureRecognizer(tapGesture) - } - @objc private func didTapCard() { - navigateToDetail?() // Trigger the navigation action + navigateToDetail?() } } - - - -#Preview(){ +#Preview{ DailyQuestionCardView() } diff --git a/recap/Views/FamilyHomeCards/StreaksCardView.swift b/recap/Views/FamilyHomeCards/StreaksCardView.swift old mode 100644 new mode 100755 index 23df255..a504513 --- a/recap/Views/FamilyHomeCards/StreaksCardView.swift +++ b/recap/Views/FamilyHomeCards/StreaksCardView.swift @@ -4,6 +4,7 @@ // // Created by admin70 on 15/01/25. // + import UIKit class StreakCardView: UIView { @@ -12,16 +13,17 @@ class StreakCardView: UIView { private let streaksLabel: UILabel = { let label = UILabel() - label.text = "Streaks 🔥" - label.font = UIFont.systemFont(ofSize: 24, weight: .semibold) + label.text = "Daily Checker 🧠" + label.font = Constants.FontandColors.titleFont + label.textColor = AppColors.primaryTextColor label.translatesAutoresizingMaskIntoConstraints = false return label }() private let arrowImageView: UIImageView = { let imageView = UIImageView() - imageView.image = UIImage(systemName: "chevron.right") - imageView.tintColor = .black + imageView.image = UIImage(systemName: Constants.FontandColors.chevronName) + imageView.tintColor = Constants.FontandColors.chevronColor imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() @@ -42,26 +44,40 @@ class StreakCardView: UIView { return stackView }() + // Labels to update dynamically + private var maxStreakLabel: UILabel! + private var currentStreakLabel: UILabel! + private var activeDaysLabel: UILabel! + init() { super.init(frame: .zero) setupUI() addTapGesture() + fetchStreakData() // Fetch saved streak data on initialization } required init?(coder: NSCoder) { super.init(coder: coder) setupUI() addTapGesture() + fetchStreakData() // Fetch saved streak data on initialization } + override func didMoveToWindow() { + super.didMoveToWindow() + fetchStreakData() + } + private func setupUI() { - backgroundColor = .white - layer.cornerRadius = 12 + backgroundColor = AppColors.cardBackgroundColor + layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius layer.shadowColor = UIColor.systemOrange.cgColor - layer.shadowOpacity = 0.1 - layer.shadowOffset = CGSize(width: 0, height: 2) - layer.shadowRadius = 4 - + layer.shadowOpacity = Float( + Constants.FontandColors.defaultshadowOpacity + ) + layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + addSubview(streaksLabel) addSubview(arrowImageView) addSubview(separatorView) @@ -71,9 +87,14 @@ class StreakCardView: UIView { let currentStreakView = createStatView(title: "Current Streak", value: "0") let activeDaysView = createStatView(title: "Active Days", value: "0") - statsStackView.addArrangedSubview(maxStreakView) - statsStackView.addArrangedSubview(currentStreakView) - statsStackView.addArrangedSubview(activeDaysView) + // Store references to value labels + self.maxStreakLabel = maxStreakView.1 + self.currentStreakLabel = currentStreakView.1 + self.activeDaysLabel = activeDaysView.1 + + statsStackView.addArrangedSubview(maxStreakView.0) + statsStackView.addArrangedSubview(currentStreakView.0) + statsStackView.addArrangedSubview(activeDaysView.0) NSLayoutConstraint.activate([ streaksLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), @@ -98,18 +119,19 @@ class StreakCardView: UIView { ]) } - private func createStatView(title: String, value: String) -> UIView { + private func createStatView(title: String, value: String) -> (UIView, UILabel) { let statView = UIView() + + let valueLabel = UILabel() + valueLabel.text = value + valueLabel.font = UIFont.systemFont(ofSize: 30, weight: .bold) + valueLabel.textColor = AppColors.iconColor + let titleLabel = UILabel() titleLabel.text = title titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold) titleLabel.textColor = .black - let valueLabel = UILabel() - valueLabel.text = value - valueLabel.font = UIFont.systemFont(ofSize: 25, weight: .bold) // Increased font size - valueLabel.textColor = .systemOrange - let stackView = UIStackView(arrangedSubviews: [valueLabel, titleLabel]) stackView.axis = .vertical stackView.alignment = .center @@ -123,7 +145,35 @@ class StreakCardView: UIView { stackView.centerYAnchor.constraint(equalTo: statView.centerYAnchor) ]) - return statView + return (statView, valueLabel) + } + + // Fetch stored streak stats from UserDefaults + private func fetchStreakData() { + let maxStreak = UserDefaults.standard.integer(forKey: "maxStreak") + let currentStreak = UserDefaults.standard.integer(forKey: "currentStreak") + let activeDays = UserDefaults.standard.integer(forKey: "activeDays") + + print("Fetched Streak Data:") + print("Max Streak: \(maxStreak)") + print("Current Streak: \(currentStreak)") + print("Active Days: \(activeDays)") + + updateStreakStats(maxStreak: maxStreak, currentStreak: currentStreak, activeDays: activeDays) + } + + // Update streak stats with dynamic values + func updateStreakStats(maxStreak: Int, currentStreak: Int, activeDays: Int) { + DispatchQueue.main.async { + print("Updating UI with Streak Data:") + print("Max Streak: \(maxStreak)") + print("Current Streak: \(currentStreak)") + print("Active Days: \(activeDays)") + + self.maxStreakLabel.text = "\(maxStreak)" + self.currentStreakLabel.text = "\(currentStreak)" + self.activeDaysLabel.text = "\(activeDays)" + } } private func addTapGesture() { @@ -136,7 +186,6 @@ class StreakCardView: UIView { onTap?() } } - -#Preview(){ +#Preview { StreakCardView() } diff --git a/recap/Views/FamilyHomeCards/TrendsCardView.swift b/recap/Views/FamilyHomeCards/TrendsCardView.swift old mode 100644 new mode 100755 index da89c5d..a989e7a --- a/recap/Views/FamilyHomeCards/TrendsCardView.swift +++ b/recap/Views/FamilyHomeCards/TrendsCardView.swift @@ -13,156 +13,218 @@ class TrendsCardView: UIView { private let recentGraphView = UIView() private let remoteGraphView = UIView() private let segmentedControl = UISegmentedControl(items: ["Immediate", "Recent", "Remote"]) - + private let immediateInsightsButton = UIButton() private let recentInsightsButton = UIButton() private let remoteInsightsButton = UIButton() - + + private var immediateMemoryData: [ImmediateMemoryData] = [] + private var recentMemoryData: [RecentMemoryData] = [] + private var remoteMemoryData: [RemoteMemoryData] = [] + + private var currentMonth: String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM" + return formatter.string(from: Date()) + } + var onGraphTap: ((Int) -> Void)? var onInsightsTap: ((Int) -> Void)? - - override init(frame: CGRect) { + + private var verifiedUserDocID: String + + init(frame: CGRect, verifiedUserDocID: String) { + self.verifiedUserDocID = verifiedUserDocID super.init(frame: frame) setupUI() setupTapGestures() + fetchAllMemoryData() } - + required init?(coder: NSCoder) { - super.init(coder: coder) + fatalError("init(coder:) has not been implemented") setupUI() + setupTapGestures() } - + private func setupUI() { - self.backgroundColor = .white + self.backgroundColor = AppColors.cardBackgroundColor self.layer.cornerRadius = 12 self.layer.shadowColor = UIColor.black.cgColor self.layer.shadowOpacity = 0.1 self.layer.shadowOffset = CGSize(width: 0, height: 2) self.layer.shadowRadius = 4 self.translatesAutoresizingMaskIntoConstraints = false - + let titleLabel = UILabel() titleLabel.text = "Trends" - titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .semibold) + titleLabel.textColor = AppColors.primaryTextColor + titleLabel.font = Constants.FontandColors.titleFont titleLabel.translatesAutoresizingMaskIntoConstraints = false self.addSubview(titleLabel) - + segmentedControl.selectedSegmentIndex = 0 segmentedControl.addTarget(self, action: #selector(segmentedControlChanged), for: .valueChanged) segmentedControl.translatesAutoresizingMaskIntoConstraints = false self.addSubview(segmentedControl) - - setupGraph(immediateGraphView, correctAnswers: 8, incorrectAnswers: 2, chartType: "donut") - setupGraph(recentGraphView, correctAnswers: 0, incorrectAnswers: 0, chartType: "bar", data: recentMemoryData) - setupGraph(remoteGraphView, correctAnswers: 0, incorrectAnswers: 0, chartType: "line", data: novemberReports) - + setupInsightsButton(immediateInsightsButton, title: "View Immediate Insights", tag: 1) setupInsightsButton(recentInsightsButton, title: "View Recent Insights", tag: 2) setupInsightsButton(remoteInsightsButton, title: "View Remote Insights", tag: 3) - + self.addSubview(immediateGraphView) self.addSubview(recentGraphView) self.addSubview(remoteGraphView) self.addSubview(immediateInsightsButton) self.addSubview(recentInsightsButton) self.addSubview(remoteInsightsButton) - + immediateGraphView.isHidden = false recentGraphView.isHidden = true remoteGraphView.isHidden = true immediateInsightsButton.isHidden = false recentInsightsButton.isHidden = true remoteInsightsButton.isHidden = true - + NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 16), titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - + segmentedControl.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 18), segmentedControl.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), segmentedControl.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - + immediateGraphView.topAnchor.constraint(equalTo: self.topAnchor, constant: 110), - immediateGraphView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - immediateGraphView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - immediateGraphView.heightAnchor.constraint(equalToConstant: 300), - immediateGraphView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -100), - - // Immediate Insights Button - immediateInsightsButton.topAnchor.constraint(equalTo: immediateGraphView.bottomAnchor, constant: 8), - immediateInsightsButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - immediateInsightsButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - immediateInsightsButton.heightAnchor.constraint(equalToConstant: 44), - - // Recent Graph View (Updated to match Immediate Graph View) - recentGraphView.topAnchor.constraint(equalTo: self.topAnchor, constant: 110), - recentGraphView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - recentGraphView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - recentGraphView.heightAnchor.constraint(equalToConstant: 300), - recentGraphView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -100), - - // Recent Insights Button - recentInsightsButton.topAnchor.constraint(equalTo: recentGraphView.bottomAnchor, constant: 8), - recentInsightsButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - recentInsightsButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - recentInsightsButton.heightAnchor.constraint(equalTo: immediateInsightsButton.heightAnchor), - - // Remote Graph View (Updated to match Immediate Graph View) - remoteGraphView.topAnchor.constraint(equalTo: self.topAnchor, constant: 110), - remoteGraphView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - remoteGraphView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - remoteGraphView.heightAnchor.constraint(equalToConstant: 300), - remoteGraphView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -100), - - // Remote Insights Button - remoteInsightsButton.topAnchor.constraint(equalTo: remoteGraphView.bottomAnchor, constant: 8), - remoteInsightsButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), - remoteInsightsButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), - remoteInsightsButton.heightAnchor.constraint(equalTo: immediateInsightsButton.heightAnchor) - ]) + immediateGraphView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + immediateGraphView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + immediateGraphView.heightAnchor.constraint(equalToConstant: 300), + immediateGraphView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -100), + + immediateInsightsButton.topAnchor.constraint(equalTo: immediateGraphView.bottomAnchor, constant: 8), + immediateInsightsButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + immediateInsightsButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + immediateInsightsButton.heightAnchor.constraint(equalToConstant: 44), + + recentGraphView.topAnchor.constraint(equalTo: self.topAnchor, constant: 110), + recentGraphView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + recentGraphView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + recentGraphView.heightAnchor.constraint(equalToConstant: 300), + recentGraphView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -100), + + recentInsightsButton.topAnchor.constraint(equalTo: recentGraphView.bottomAnchor, constant: 8), + recentInsightsButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + recentInsightsButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + recentInsightsButton.heightAnchor.constraint(equalTo: immediateInsightsButton.heightAnchor), + + remoteGraphView.topAnchor.constraint(equalTo: self.topAnchor, constant: 110), + remoteGraphView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + remoteGraphView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + remoteGraphView.heightAnchor.constraint(equalToConstant: 300), + remoteGraphView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -100), + + remoteInsightsButton.topAnchor.constraint(equalTo: remoteGraphView.bottomAnchor, constant: 8), + remoteInsightsButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + remoteInsightsButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), + remoteInsightsButton.heightAnchor.constraint(equalTo: immediateInsightsButton.heightAnchor) + ]) } - + private func setupInsightsButton(_ button: UIButton, title: String, tag: Int) { button.setTitle(title, for: .normal) - button.backgroundColor = UIColor.systemBlue - button.setTitleColor(.white, for: .normal) - button.layer.cornerRadius = 8 + button.backgroundColor = AppColors.primaryButtonColor + button.setTitleColor(AppColors.primaryButtonTextColor, for: .normal) + button.layer.cornerRadius = 12 + button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) button.translatesAutoresizingMaskIntoConstraints = false button.tag = tag button.addTarget(self, action: #selector(handleInsightsTap(_:)), for: .touchUpInside) } - - private func setupGraph(_ containerView: UIView, correctAnswers: Int, incorrectAnswers: Int, chartType: String, data: Any? = nil) { + + private func fetchAllMemoryData() { + fetchImmediateMemoryData(for: verifiedUserDocID) { data in + DispatchQueue.main.async { + self.immediateMemoryData = data + self.setupGraph(self.immediateGraphView, chartType: "donut", data: data) + } + } + + fetchRecentMemoryData(for: verifiedUserDocID, selectedMonth: "March") { data in + DispatchQueue.main.async { + self.recentMemoryData = data + self.setupGraph(self.recentGraphView, chartType: "bar", data: data) + } + } + + fetchRemoteMemoryData(for: verifiedUserDocID, month: currentMonth) { data in + DispatchQueue.main.async { + self.remoteMemoryData = data + self.setupGraph(self.remoteGraphView, chartType: "line", data: data) + } + } + } + + private func setupGraph(_ containerView: UIView, chartType: String, data: Any?) { containerView.backgroundColor = UIColor.systemGray6 containerView.layer.cornerRadius = 8 containerView.translatesAutoresizingMaskIntoConstraints = false - + containerView.subviews.forEach { $0.removeFromSuperview() } + var hostingController: UIHostingController? - + switch chartType { case "donut": - let donutChart = DonutChartView(correctAnswers: correctAnswers, incorrectAnswers: incorrectAnswers) - hostingController = UIHostingController(rootView: AnyView(donutChart)) + if let immediateData = data as? [ImmediateMemoryData] { + // Always create a data point for today, even if we need to use 0 values + let today = Date() + + // Try to get today's data if it exists + let todayString = { () -> String in + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter.string(from: today) + }() + + let todayData = immediateData.first(where: { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter.string(from: $0.date) == todayString + }) + + // Use today's data if available, otherwise create empty data for today + let dataToDisplay = todayData ?? ImmediateMemoryData( + date: today, + correctAnswers: 0, + incorrectAnswers: 0, + status: .processing + ) + + let donutChart = ModernDonutChartView( + correctAnswers: dataToDisplay.correctAnswers, + incorrectAnswers: dataToDisplay.incorrectAnswers + ) + hostingController = UIHostingController(rootView: AnyView(donutChart)) + } case "bar": - if let recentMemoryData = data as? [RecentMemoryData] { - let barChart = BarChartView(data: recentMemoryData) + // Rest of your code remains unchanged + if let recentData = data as? [RecentMemoryData] { + let barChart = BarChartView(data: recentData) hostingController = UIHostingController(rootView: AnyView(barChart)) } case "line": - if let monthlyReportData = data as? [MonthlyReport] { - let lineChart = LineChartView(data: monthlyReportData, threshold: 7) + if let remoteData = data as? [RemoteMemoryData] { + let lineChart = LineChartView(data: remoteData) hostingController = UIHostingController(rootView: AnyView(lineChart)) } default: return } - + guard let hostView = hostingController?.view else { return } hostView.translatesAutoresizingMaskIntoConstraints = false hostView.backgroundColor = .clear containerView.addSubview(hostView) - + NSLayoutConstraint.activate([ hostView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8), hostView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8), @@ -170,7 +232,7 @@ class TrendsCardView: UIView { hostView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), ]) } - + private func setupTapGestures() { [immediateGraphView, recentGraphView, remoteGraphView].enumerated().forEach { index, view in let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) @@ -178,28 +240,34 @@ class TrendsCardView: UIView { view.addGestureRecognizer(tapGesture) } } - + @objc private func handleTap(_ sender: UITapGestureRecognizer) { if let tag = sender.view?.tag { onGraphTap?(tag) } } - + @objc private func handleInsightsTap(_ sender: UIButton) { onInsightsTap?(sender.tag) } - + @objc private func segmentedControlChanged(_ sender: UISegmentedControl) { immediateGraphView.isHidden = sender.selectedSegmentIndex != 0 recentGraphView.isHidden = sender.selectedSegmentIndex != 1 remoteGraphView.isHidden = sender.selectedSegmentIndex != 2 - + immediateInsightsButton.isHidden = sender.selectedSegmentIndex != 0 recentInsightsButton.isHidden = sender.selectedSegmentIndex != 1 remoteInsightsButton.isHidden = sender.selectedSegmentIndex != 2 } -} - -#Preview { - TrendsCardView() + + private func fetchRecentMemoryData(for verifiedUserDocID: String, selectedMonth: String, completion: @escaping ([RecentMemoryData]) -> Void) { + let allDaysOfWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + var weeklyData: [RecentMemoryData] = allDaysOfWeek.map { RecentMemoryData(day: $0, correctAnswers: 0, incorrectAnswers: 0) } + + // Simulate fetching data (replace with actual Firestore call) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + completion(weeklyData) + } + } } diff --git a/recap/Views/FamilyMemberCell.swift b/recap/Views/FamilyMemberCell.swift old mode 100644 new mode 100755 index db354f7..eb84074 --- a/recap/Views/FamilyMemberCell.swift +++ b/recap/Views/FamilyMemberCell.swift @@ -1,3 +1,176 @@ +// +// FamilyMemberCell.swift +// recap +// +// Created by Diptayan Jash on 05/11/24. +// +// +//import SDWebImage +//import UIKit +// +//class FamilyMemberCell: UICollectionViewCell { +// static let identifier = "FamilyMemberCell" +// +// // MARK: - UI Components +// +// private let containerView: UIView = { +// let view = UIView() +// view.backgroundColor = .white +// view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius +// view.clipsToBounds = true +// return view +// }() +// +// private let profileImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.contentMode = .scaleAspectFill +// imageView.layer.cornerRadius = 50 // Make it perfectly circular +// imageView.clipsToBounds = true +// imageView.layer.borderWidth = 3 +// imageView.layer.borderColor = UIColor.systemBlue.withAlphaComponent(0.2).cgColor +// return imageView +// }() +// +// private let nameLabel: UILabel = { +// let label = UILabel() +// label.font = Constants.FontandColors.titleFont +// label.textColor = Constants.FontandColors.titleColor +// label.textAlignment = .center +// return label +// }() +// +// private let relationshipLabel: UILabel = { +// let label = UILabel() +// label.font = Constants.FontandColors.descriptionFont +// label.textColor = .systemBlue +// label.textAlignment = .center +// return label +// }() +// +// private let phoneLabel: UILabel = { +// let label = UILabel() +// label.font = Constants.FontandColors.descriptionFont +// label.textColor = Constants.FontandColors.descriptionColor +// label.textAlignment = .center +// return label +// }() +// +// // MARK: - Initialization +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// setupUI() +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// // MARK: - UI Setup +// +// private func setupUI() { +// // Add shadow to the cell +// layer.shadowColor = UIColor.black.cgColor +// layer.shadowOffset = CGSize(width: 0, height: 4) +// layer.shadowRadius = 8 +// layer.shadowOpacity = 0.1 +// layer.masksToBounds = false +// +// // Add container view +// contentView.addSubview(containerView) +// containerView.translatesAutoresizingMaskIntoConstraints = false +// +// // Add subviews +// [profileImageView, nameLabel, relationshipLabel, phoneLabel].forEach { +// containerView.addSubview($0) +// $0.translatesAutoresizingMaskIntoConstraints = false +// } +// +// NSLayoutConstraint.activate([ +// // Container view constraints +// containerView.topAnchor.constraint(equalTo: contentView.topAnchor), +// containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// +// // Profile image constraints +// profileImageView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16), +// profileImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), +// profileImageView.widthAnchor.constraint(equalToConstant: 100), +// profileImageView.heightAnchor.constraint(equalToConstant: 100), +// +// // Name label constraints +// nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 12), +// nameLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8), +// nameLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), +// +// // Relationship label constraints +// relationshipLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4), +// relationshipLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8), +// relationshipLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), +// +// // Phone label constraints +// phoneLabel.topAnchor.constraint(equalTo: relationshipLabel.bottomAnchor, constant: 4), +// phoneLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8), +// phoneLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), +// ]) +// } +// +// // MARK: - Configuration +// +// func configure(with member: FamilyMember) { +// nameLabel.text = member.name +// relationshipLabel.text = member.relationship +// phoneLabel.text = member.phone +// +// profileImageView.sd_setImage( +// with: URL(string: member.imageURL), +// placeholderImage: UIImage(systemName: "person.circle.fill"), +// options: .refreshCached +// ) +// } +// +// // MARK: - Selection Animation +// +// override var isHighlighted: Bool { +// didSet { +// animateSelection(isHighlighted: isHighlighted) +// } +// } +// +// private func animateSelection(isHighlighted: Bool) { +// let transform: CGAffineTransform = isHighlighted ? .init(scaleX: 0.95, y: 0.95) : .identity +// let shadowOpacity: Float = isHighlighted ? 0.2 : 0.1 +// +// UIView.animate( +// withDuration: 0.3, +// delay: 0, +// usingSpringWithDamping: 1.0, +// initialSpringVelocity: 0.5, +// options: [.allowUserInteraction, .beginFromCurrentState], +// animations: { +// self.transform = transform +// self.layer.shadowOpacity = shadowOpacity +// self.containerView.backgroundColor = isHighlighted ? .systemGray6 : .white +// } +// ) +// } +// +// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { +// super.traitCollectionDidChange(previousTraitCollection) +// +// // Update shadow for dark mode +// if traitCollection.userInterfaceStyle == .dark { +// layer.shadowColor = UIColor.white.cgColor +// layer.shadowOpacity = 0.05 +// } else { +// layer.shadowColor = UIColor.black.cgColor +// layer.shadowOpacity = 0.1 +// } +// } +//} + + // // FamilyMemberCell.swift // recap @@ -10,115 +183,168 @@ import UIKit class FamilyMemberCell: UICollectionViewCell { static let identifier = "FamilyMemberCell" - + + // MARK: - UI Components + + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = .white + view.layer.cornerRadius = 16 + view.clipsToBounds = true + return view + }() + private let profileImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 8 imageView.clipsToBounds = true + // Remove circular shape to make it more gallery-like return imageView }() - private let nameLabel: UILabel = { - let label = UILabel() - label.font = .boldSystemFont(ofSize: 16) - label.textColor = .black - return label + private let overlayGradient: CAGradientLayer = { + let gradient = CAGradientLayer() + gradient.colors = [ + UIColor.black.withAlphaComponent(0.0).cgColor, + UIColor.black.withAlphaComponent(0.6).cgColor + ] + gradient.locations = [0.5, 1.0] + return gradient }() - private let relationshipLabel: UILabel = { + private let nameLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 14) - label.textColor = .gray + label.font = .systemFont(ofSize: 18, weight: .semibold) + label.textColor = .white + label.textAlignment = .left return label }() - private let phoneLabel: UILabel = { + private let relationshipLabel: UILabel = { let label = UILabel() - label.font = .systemFont(ofSize: 14) - label.textColor = .gray + label.font = .systemFont(ofSize: 14, weight: .medium) + label.textColor = .white + label.textAlignment = .left + label.alpha = 0.9 return label }() - private let accessoryIcon: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImage(systemName: "chevron.right") - imageView.tintColor = .gray - imageView.contentMode = .scaleAspectFit - return imageView - }() - - private let dividerLine: UIView = { - let view = UIView() - view.backgroundColor = .lightGray.withAlphaComponent(0.3) - return view - }() - + // MARK: - Initialization + override init(frame: CGRect) { super.init(frame: frame) - - // Set cell size - contentView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - contentView.widthAnchor.constraint(equalToConstant: 170), - contentView.heightAnchor.constraint(equalToConstant: 226), - ]) - - // Cell styling - contentView.layer.cornerRadius = 15 - contentView.layer.shadowColor = UIColor.black.cgColor - contentView.layer.shadowOpacity = 0.1 - contentView.layer.shadowOffset = CGSize(width: 0, height: 2) - contentView.layer.shadowRadius = 4 - contentView.backgroundColor = .white - - // Add subviews - contentView.addSubview(profileImageView) - contentView.addSubview(nameLabel) - contentView.addSubview(relationshipLabel) - contentView.addSubview(phoneLabel) - contentView.addSubview(accessoryIcon) - contentView.addSubview(dividerLine) - - // Layout the subviews with the specified dimensions - profileImageView.frame = CGRect(x: (contentView.frame.size.width - 90) / 2, y: 10, width: 100, height: 100) - - dividerLine.frame = CGRect(x: 10, y: profileImageView.frame.maxY + 10, width: contentView.frame.size.width - 20, height: 1) - - nameLabel.frame = CGRect(x: 10, y: dividerLine.frame.maxY + 8, width: contentView.frame.size.width - 40, height: 20) - - accessoryIcon.frame = CGRect(x: contentView.frame.size.width - 25, y: dividerLine.frame.maxY + 8, width: 15, height: 15) - - relationshipLabel.frame = CGRect(x: 10, y: nameLabel.frame.maxY + 5, width: contentView.frame.size.width - 20, height: 18) - - phoneLabel.frame = CGRect(x: 10, y: relationshipLabel.frame.maxY + 5, width: contentView.frame.size.width - 20, height: 18) + setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MARK: - UI Setup + + private func setupUI() { + // Add shadow to the cell + layer.shadowColor = UIColor.black.cgColor + layer.shadowOffset = CGSize(width: 0, height: 8) + layer.shadowRadius = 16 + layer.shadowOpacity = 0.15 + layer.masksToBounds = false + + // Add container view + contentView.addSubview(containerView) + containerView.translatesAutoresizingMaskIntoConstraints = false + + // Add image view + containerView.addSubview(profileImageView) + profileImageView.translatesAutoresizingMaskIntoConstraints = false + + // Add gradient layer + containerView.layer.addSublayer(overlayGradient) + + // Add text labels + [nameLabel, relationshipLabel].forEach { + containerView.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + NSLayoutConstraint.activate([ + // Container view constraints - fill the entire cell + containerView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + + // Profile image constraints - fill the container + profileImageView.topAnchor.constraint(equalTo: containerView.topAnchor), + profileImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + profileImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + profileImageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + + // Text labels positioned at the bottom with padding + nameLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + nameLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + nameLabel.bottomAnchor.constraint(equalTo: relationshipLabel.topAnchor, constant: -4), + + relationshipLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + relationshipLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + relationshipLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16), + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + // Update gradient frame + overlayGradient.frame = bounds + } -// func configure(with familyMember: FamilyMember) { - //// imageView.image = UIImage(named: familyMember.imageName) -// imageView.sd_setImage(with: URL(string: familyMember.imageURL), placeholderImage: UIImage(named: "placeholder")) -// nameLabel.text = familyMember.name -// relationshipLabel.text = familyMember.relationship -// phoneLabel.text = familyMember.phone -// } - - + // MARK: - Configuration + func configure(with member: FamilyMember) { nameLabel.text = member.name relationshipLabel.text = member.relationship - - if let savedImage = UserDefaultsStorageFamilyMember.shared.getFamilyMemberImage(for: member.id) { - profileImageView.image = savedImage + + profileImageView.sd_setImage( + with: URL(string: member.imageURL), + placeholderImage: UIImage(systemName: "person.fill"), + options: .refreshCached + ) + } + + // MARK: - Selection Animation + + override var isHighlighted: Bool { + didSet { + animateSelection(isHighlighted: isHighlighted) + } + } + + private func animateSelection(isHighlighted: Bool) { + let transform: CGAffineTransform = isHighlighted ? .init(scaleX: 0.98, y: 0.98) : .identity + let shadowOpacity: Float = isHighlighted ? 0.2 : 0.15 + + UIView.animate( + withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 1.0, + initialSpringVelocity: 0.5, + options: [.allowUserInteraction, .beginFromCurrentState], + animations: { + self.transform = transform + self.layer.shadowOpacity = shadowOpacity + } + ) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + // Update shadow for dark mode + if traitCollection.userInterfaceStyle == .dark { + layer.shadowColor = UIColor.white.cgColor + layer.shadowOpacity = 0.08 } else { - profileImageView.image = UIImage(systemName: "person.circle.fill") + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.15 } } } - -#Preview() { - FamilyMemberCell() -} diff --git a/recap/Views/FamilyMemberDetailViewController.swift b/recap/Views/FamilyMemberDetailViewController.swift old mode 100644 new mode 100755 index 836964a..2cf1e01 --- a/recap/Views/FamilyMemberDetailViewController.swift +++ b/recap/Views/FamilyMemberDetailViewController.swift @@ -1,16 +1,12 @@ -// -// FamilyMemberDetailViewController.swift -// recap -// -// Created by Diptayan Jash on 05/11/24. -// - import SDWebImage import UIKit class FamilyMemberDetailViewController: UIViewController { var member: FamilyMember private var stackView: UIStackView! + private var imageView: UIImageView! + private var scrollView: UIScrollView! + private var contentView: UIView! init(member: FamilyMember) { self.member = member @@ -37,35 +33,67 @@ class FamilyMemberDetailViewController: UIViewController { private func setLayout() { view.backgroundColor = .systemBackground navigationItem.title = member.name - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(dismissView)) + let closeButton = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(dismissView)) + closeButton.tintColor = AppColors.iconColor // Change this to your desired color (e.g., .red, .green, or a custom UIColor) + navigationItem.rightBarButtonItem = closeButton + + // Create scroll view for better accessibility + scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + + // Setup constraints for scroll view + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + ]) - let imageView = createProfileImageView() + // Create a much larger image view + imageView = createEnhancedProfileImageView() + + // Add tap gesture to image for zooming feature + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleImageTap)) + imageView.isUserInteractionEnabled = true + imageView.addGestureRecognizer(tapGesture) let nameLabel = createNameLabel() let relationshipLabel = createRelationshipLabel() - let phoneStack = createInfoStack(withIcon: "phone.fill", text: member.phone, color: .green) - let emailStack = createInfoStack(withIcon: "envelope.fill", text: "\(member.name.lowercased().replacingOccurrences(of: " ", with: ""))@example.com", color: .blue) + let phoneStack = createInfoStack(withIcon: "phone.fill", text: member.phone, color: AppColors.iconColor) + let emailStack = createInfoStack(withIcon: "envelope.fill", text: "\(member.email)", color:AppColors.iconColor) let callButton = createCallButton() - callButton.widthAnchor.constraint(equalToConstant: 200).isActive = true + callButton.widthAnchor.constraint(equalToConstant: 250).isActive = true let detailsStack = UIStackView(arrangedSubviews: [nameLabel, relationshipLabel, phoneStack, emailStack, callButton]) detailsStack.axis = .vertical - detailsStack.spacing = 10 + detailsStack.spacing = 16 detailsStack.alignment = .center stackView = UIStackView(arrangedSubviews: [imageView, detailsStack]) stackView.axis = .vertical // Start in portrait mode - stackView.spacing = 20 + stackView.spacing = 24 stackView.alignment = .center - - view.addSubview(stackView) stackView.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(stackView) NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24), + stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) ]) updateLayout(for: view.bounds.size) @@ -76,39 +104,68 @@ class FamilyMemberDetailViewController: UIViewController { // Landscape: Horizontal layout stackView.axis = .horizontal stackView.alignment = .center + + // In landscape, make image smaller to fit side by side with details + imageView.heightAnchor.constraint(equalToConstant: size.height * 0.7).isActive = true + imageView.widthAnchor.constraint(equalToConstant: size.height * 0.7).isActive = true } else { // Portrait: Vertical layout stackView.axis = .vertical stackView.alignment = .center + + // In portrait, make image larger + let imageSize = min(size.width * 0.85, 350) + imageView.heightAnchor.constraint(equalToConstant: imageSize).isActive = true + imageView.widthAnchor.constraint(equalToConstant: imageSize).isActive = true } } - private func createProfileImageView() -> UIImageView { + private func createEnhancedProfileImageView() -> UIImageView { let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + + // Make image much larger - size will be set in updateLayout + imageView.layer.cornerRadius = 16 // Less rounded corners for larger image + + // Add a subtle border to make image stand out + imageView.layer.borderWidth = 3 + imageView.layer.borderColor = AppColors.primaryButtonColor.cgColor + + // Add subtle shadow effect + imageView.layer.shadowColor = UIColor.black.cgColor + imageView.layer.shadowOffset = CGSize(width: 0, height: 2) + imageView.layer.shadowRadius = 6 + imageView.layer.shadowOpacity = 0.2 + + // Load image from UserDefaults if available, otherwise fetch from URL using SDWebImage if let savedImage = UserDefaultsStorageFamilyMember.shared.getFamilyMemberImage(for: member.id) { imageView.image = savedImage } else { - imageView.image = UIImage(systemName: "person.circle.fill") + imageView.sd_setImage( + with: URL(string: member.imageURL), + placeholderImage: UIImage(systemName: "person.circle.fill"), + options: [.highPriority, .retryFailed], + completed: { [weak self] (image, error, cacheType, url) in + if let error = error { + print("Error loading image: \(error.localizedDescription)") + } + // Add high-contrast mode for better visibility if needed + if UIAccessibility.isInvertColorsEnabled { + imageView.layer.borderWidth = 5 + imageView.layer.borderColor = UIColor.white.cgColor + } + } + ) } -// imageView.image = UIImage(named: member.imageName) -// imageView -// .sd_setImage( -// with: URL(string: member.imageURL), -// placeholderImage: UIImage(named: "placeholder") -// ) - imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 10 - imageView.clipsToBounds = true - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.widthAnchor.constraint(equalToConstant: 150).isActive = true - imageView.heightAnchor.constraint(equalToConstant: 150).isActive = true return imageView } private func createNameLabel() -> UILabel { let label = UILabel() label.text = member.name - label.font = UIFont.systemFont(ofSize: 24, weight: .bold) + label.font = UIFont.systemFont(ofSize: 28, weight: .bold) // Larger font label.textAlignment = .center return label } @@ -116,8 +173,8 @@ class FamilyMemberDetailViewController: UIViewController { private func createRelationshipLabel() -> UILabel { let label = UILabel() label.text = member.relationship - label.font = UIFont.systemFont(ofSize: 18, weight: .regular) - label.textColor = .gray + label.font = UIFont.systemFont(ofSize: 22, weight: .medium) // Larger font + label.textColor = AppColors.iconColor // More vibrant color label.textAlignment = .center return label } @@ -126,30 +183,40 @@ class FamilyMemberDetailViewController: UIViewController { let iconImageView = UIImageView(image: UIImage(systemName: iconName)) iconImageView.tintColor = color iconImageView.translatesAutoresizingMaskIntoConstraints = false - iconImageView.widthAnchor.constraint(equalToConstant: 20).isActive = true - iconImageView.heightAnchor.constraint(equalToConstant: 20).isActive = true + iconImageView.widthAnchor.constraint(equalToConstant: 24).isActive = true + iconImageView.heightAnchor.constraint(equalToConstant: 24).isActive = true let label = UILabel() label.text = text - label.font = UIFont.systemFont(ofSize: 16) + label.font = UIFont.systemFont(ofSize: 20) // Larger font let stackView = UIStackView(arrangedSubviews: [iconImageView, label]) stackView.axis = .horizontal - stackView.spacing = 8 + stackView.spacing = 12 return stackView } private func createCallButton() -> UIButton { let button = UIButton(type: .system) button.setTitle("Call \(member.name)", for: .normal) - button.setTitleColor(.systemBlue, for: .normal) - button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.2) - button.layer.cornerRadius = 10 - button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) - button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20) + button + .setTitleColor( + AppColors.iconColor, + for: .normal + ) + button.backgroundColor = AppColors.primaryButtonColor + button.layer.cornerRadius = Constants.ButtonStyle.DefaultButtonCornerRadius + button.titleLabel?.font = Constants.ButtonStyle.DefaultButtonFont + button.contentEdgeInsets = UIEdgeInsets(top: 16, left: 32, bottom: 16, right: 32) button.addTarget(self, action: #selector(callPhoneNumber), for: .touchUpInside) return button } + + @objc private func handleImageTap() { + // Create a full-screen image view controller + let fullScreenVC = FullScreenImageViewController(image: imageView.image, name: member.name) + present(fullScreenVC, animated: true) + } @objc private func dismissView() { dismiss(animated: true, completion: nil) @@ -159,9 +226,123 @@ class FamilyMemberDetailViewController: UIViewController { let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() guard let url = URL(string: "tel://\(member.phone)"), UIApplication.shared.canOpenURL(url) else { - print("This device does not support phone calls.") + // Show a more accessible alert for failure + let alert = UIAlertController( + title: "Cannot Make Call", + message: "This device doesn't support phone calls or the number is invalid.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) return } UIApplication.shared.open(url) } } + +// Full screen image view controller for tapping on images +class FullScreenImageViewController: UIViewController { + private var imageView: UIImageView! + private var scrollView: UIScrollView! + private var nameLabel: UILabel! + private var personName: String + + init(image: UIImage?, name: String) { + self.personName = name + super.init(nibName: nil, bundle: nil) + setupFullScreenView(with: image) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupFullScreenView(with image: UIImage?) { + view.backgroundColor = .black + + // Add close button + let closeButton = UIButton(type: .system) + closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) + closeButton.tintColor = .white + closeButton.translatesAutoresizingMaskIntoConstraints = false + closeButton.addTarget(self, action: #selector(dismissFullScreen), for: .touchUpInside) + + // Create scroll view for zooming + scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.minimumZoomScale = 1.0 + scrollView.maximumZoomScale = 4.0 + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + scrollView.delegate = self + + // Create image view + imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + + // Create name label + nameLabel = UILabel() + nameLabel.text = personName + nameLabel.textColor = .white + nameLabel.font = UIFont.systemFont(ofSize: 32, weight: .bold) + nameLabel.textAlignment = .center + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.layer.shadowColor = UIColor.black.cgColor + nameLabel.layer.shadowOffset = CGSize(width: 0, height: 1) + nameLabel.layer.shadowRadius = 3 + nameLabel.layer.shadowOpacity = 0.7 + + // Add views to hierarchy + view.addSubview(scrollView) + scrollView.addSubview(imageView) + view.addSubview(nameLabel) + view.addSubview(closeButton) + + // Setup constraints + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + imageView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor), + imageView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + imageView.heightAnchor.constraint(equalTo: scrollView.heightAnchor), + + nameLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + + closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + closeButton.widthAnchor.constraint(equalToConstant: 44), + closeButton.heightAnchor.constraint(equalToConstant: 44) + ]) + + // Add double tap gesture for zooming + let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:))) + doubleTapGesture.numberOfTapsRequired = 2 + scrollView.addGestureRecognizer(doubleTapGesture) + } + + @objc private func dismissFullScreen() { + dismiss(animated: true) + } + + @objc private func handleDoubleTap(_ gesture: UITapGestureRecognizer) { + if scrollView.zoomScale > 1.0 { + scrollView.setZoomScale(1.0, animated: true) + } else { + scrollView.setZoomScale(2.0, animated: true) + } + } +} + +// MARK: - UIScrollViewDelegate +extension FullScreenImageViewController: UIScrollViewDelegate { + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return imageView + } +} diff --git a/recap/Views/Games/CategoryStackView.swift b/recap/Views/Games/CategoryStackView.swift old mode 100644 new mode 100755 index eb679dd..8b1b760 --- a/recap/Views/Games/CategoryStackView.swift +++ b/recap/Views/Games/CategoryStackView.swift @@ -1,81 +1,177 @@ import UIKit protocol CategoryStackViewDelegate: AnyObject { - func categoryStackView(_ stackView: CategoryStackView, didReceiveDropWith word: String) + func categoryStackView(_ stackView: CategoryStackView, didReceiveDropWith word: String, category: String) } class CategoryStackView: UIStackView { weak var delegate: CategoryStackViewDelegate? let category: String - + let containerView: UIView = { let view = UIView() - view.backgroundColor = .systemGray6 - view.layer.cornerRadius = 12 + view.backgroundColor = .systemBackground + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + view.layer.borderWidth = 2 + view.layer.borderColor = AppColors.primaryButtonColor.cgColor + view.layer.shadowColor = AppColors.inverseTextColor.cgColor + view.layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) + view.layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + view.layer.shadowRadius = Constants.FontandColors.defaultshadowRadius return view }() - - private let titleLabel: UILabel = { + + let titleLabel: UILabel = { let label = UILabel() - label.font = .boldSystemFont(ofSize: 18) + label.font = .systemFont(ofSize: 18, weight: .bold) label.textAlignment = .center + label.textColor = AppColors.primaryButtonTextColor return label }() - private let wordsStack: UIStackView = { + // Add scrollView to handle overflow + let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.showsVerticalScrollIndicator = true + scrollView.alwaysBounceVertical = true + return scrollView + }() + + let wordsStack: UIStackView = { let stack = UIStackView() stack.axis = .vertical stack.spacing = 8 stack.alignment = .center + stack.distribution = .equalSpacing return stack }() - + + let emptyStateLabel: UILabel = { + let label = UILabel() + label.text = "Drop items" + label.font = Constants.FontandColors.descriptionFont + label.textColor = Constants.FontandColors.descriptionColor + label.textAlignment = .center + return label + }() + init(title: String) { - self.category = title + category = title super.init(frame: .zero) - + titleLabel.text = title setupUI() + setupDropInteraction() } - + required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupUI() { axis = .vertical spacing = 8 alignment = .fill + + // Add scrollView to containerView + containerView.addSubview(scrollView) + scrollView.addSubview(wordsStack) + containerView.addSubview(emptyStateLabel) - containerView.addSubview(wordsStack) + scrollView.translatesAutoresizingMaskIntoConstraints = false wordsStack.translatesAutoresizingMaskIntoConstraints = false - + emptyStateLabel.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ - containerView.heightAnchor.constraint(equalToConstant: 150), + containerView.heightAnchor.constraint(equalToConstant: 180), - wordsStack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8), - wordsStack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8), - wordsStack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), - wordsStack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8) + // ScrollView constraints + scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12), + scrollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 12), + scrollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -12), + scrollView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -12), + + // WordsStack constraints - note the width matches the scrollView width + wordsStack.topAnchor.constraint(equalTo: scrollView.topAnchor), + wordsStack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + wordsStack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + wordsStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + // This ensures the width of the stack matches the scrollView + wordsStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + emptyStateLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8), + emptyStateLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), + emptyStateLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + emptyStateLabel.heightAnchor.constraint(equalToConstant: 40), ]) - + addArrangedSubview(titleLabel) addArrangedSubview(containerView) } - + + private func setupDropInteraction() { + let dropInteraction = UIDropInteraction(delegate: self) + containerView.addInteraction(dropInteraction) + } + func addWord(_ word: String) { + emptyStateLabel.isHidden = true + + let label = createWordLabel(word) + wordsStack.addArrangedSubview(label) + + label.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [], animations: { + label.transform = .identity + label.alpha = 1 + }) + + // Scroll to bottom when a new word is added + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let bottomOffset = CGPoint(x: 0, y: self.scrollView.contentSize.height - self.scrollView.bounds.height) + self.scrollView.setContentOffset(bottomOffset, animated: true) + } + } + + private func createWordLabel(_ word: String) -> UILabel { let label = UILabel() label.text = word label.font = .systemFont(ofSize: 15, weight: .medium) label.textAlignment = .center - label.backgroundColor = .systemGreen.withAlphaComponent(0.2) + label.backgroundColor = getRandomColor().withAlphaComponent(0.2) label.layer.cornerRadius = 8 label.layer.masksToBounds = true - label.layoutMargins = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) - - wordsStack.addArrangedSubview(label) + label.alpha = 0.9 + label.translatesAutoresizingMaskIntoConstraints = false + label.heightAnchor.constraint(equalToConstant: 36).isActive = true + label.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true + + return label + } + + private func getRandomColor() -> UIColor { + let colors: [UIColor] = [.systemRed, .systemBlue, .systemGreen, .systemOrange, .systemPurple] + return colors.randomElement() ?? .systemBlue + } +} + +extension CategoryStackView: UIDropInteractionDelegate { + func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { + return session.canLoadObjects(ofClass: NSString.self) + } + + func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { + return UIDropProposal(operation: .copy) + } + + func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { + session.loadObjects(ofClass: NSString.self) { items in + guard let word = items.first as? String else { return } + self.delegate?.categoryStackView(self, didReceiveDropWith: word, category: self.category) + } } } -#Preview(){ - CategoryStackView(title: "George") + +#Preview() { + CategoryStackView(title: "Fruits") } diff --git a/recap/Views/Games/GamesCell.swift b/recap/Views/Games/GamesCell.swift new file mode 100755 index 0000000..9ca6eb9 --- /dev/null +++ b/recap/Views/Games/GamesCell.swift @@ -0,0 +1,241 @@ +//import UIKit +// +//class GamesTableViewCell: UITableViewCell { +// static let identifier = "GamesTableViewCell" +// +// private let containerView: UIView = { +// let view = UIView() +// view.backgroundColor = .white +// view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius +// view.layer.shadowColor = Constants.FontandColors.defaultshadowColor +// view.layer.shadowOpacity = Float( +// Constants.FontandColors.defaultshadowOpacity +// ) +// view.layer.shadowOffset = Constants.FontandColors.defaultshadowOffset +// view.layer.shadowRadius = Constants.FontandColors.defaultshadowRadius +// return view +// }() +// +// private let gameImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.contentMode = .scaleAspectFit +// imageView.clipsToBounds = true +// imageView.translatesAutoresizingMaskIntoConstraints = false +// return imageView +// }() +// +// private let nameLabel: UILabel = { +// let label = UILabel() +// label.font = Constants.FontandColors.titleFont +// label.textColor = Constants.FontandColors.titleColor +// label.translatesAutoresizingMaskIntoConstraints = false +// return label +// }() +// +// private let accessoryIcon: UIImageView = { +// let imageView = UIImageView() +// imageView.image = UIImage(systemName: Constants.FontandColors.chevronName) +// imageView.tintColor = Constants.FontandColors.chevronColor +// imageView.contentMode = .scaleAspectFit +// imageView.translatesAutoresizingMaskIntoConstraints = false +// return imageView +// }() +// +// private let dividerLine: UIView = { +// let view = UIView() +// view.backgroundColor = .systemGray4 +// view.translatesAutoresizingMaskIntoConstraints = false +// return view +// }() +// +// private let descriptionLabel: UILabel = { +// let label = UILabel() +// label.font = Constants.FontandColors.descriptionFont +// label.textColor = Constants.FontandColors.descriptionColor +// label.numberOfLines = 0 +// label.translatesAutoresizingMaskIntoConstraints = false +// return label +// }() +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// +// selectionStyle = .none +// backgroundColor = .clear +// contentView.backgroundColor = .clear +// +// // Add subviews to cell +// contentView.addSubview(containerView) +// containerView.addSubview(gameImageView) +// containerView.addSubview(nameLabel) +// containerView.addSubview(accessoryIcon) +// containerView.addSubview(dividerLine) +// containerView.addSubview(descriptionLabel) +// +// // Enable Auto Layout +// containerView.translatesAutoresizingMaskIntoConstraints = false +// +// // Set up constraints +// NSLayoutConstraint.activate([ +// // Container view constraints +// containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.paddingKeys.DefaultPaddingTop), +// containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.paddingKeys.DefaultPaddingLeft), +// containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingRight), +// containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: Constants.paddingKeys.DefaultPaddingBottom), +// +// // Image view constraints +// gameImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), +// gameImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), +// gameImageView.widthAnchor.constraint(equalToConstant: 60), +// gameImageView.heightAnchor.constraint(equalToConstant: 60), +// +// // Name label constraints +// nameLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 24), +// nameLabel.leadingAnchor.constraint(equalTo: gameImageView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingLeft), +// nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: accessoryIcon.leadingAnchor, constant: Constants.paddingKeys.DefaultPaddingRight), +// +// // Accessory icon constraints +// accessoryIcon.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor), +// accessoryIcon.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -24), +// accessoryIcon.widthAnchor.constraint(equalToConstant: 14), +// accessoryIcon.heightAnchor.constraint(equalToConstant: 22), +// +// // Divider line constraints +// dividerLine.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 12), +// dividerLine.leadingAnchor.constraint(equalTo: gameImageView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingLeft), +// dividerLine.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingRight), +// dividerLine.heightAnchor.constraint(equalToConstant: 1), +// +// // Description label constraints +// descriptionLabel.topAnchor.constraint(equalTo: dividerLine.bottomAnchor, constant: 12), +// descriptionLabel.leadingAnchor.constraint(equalTo: gameImageView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingLeft), +// descriptionLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8), +// descriptionLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -24) +// ]) +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// override func prepareForReuse() { +// super.prepareForReuse() +// gameImageView.image = nil +// nameLabel.text = nil +// descriptionLabel.text = nil +// } +// +// func configure(with game: Games) { +// gameImageView.image = UIImage(named: game.imageName) +// nameLabel.text = game.name +// descriptionLabel.text = game.description +// } +//} +//#Preview{ +// GamesTableViewCell() +//} +import UIKit + +class GamesTableViewCell: UITableViewCell { + static let identifier = "GamesTableViewCell" + + private let containerView: UIView = { + let view = UIView() + view.backgroundColor = AppColors.cardBackgroundColor + view.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + view.layer.shadowColor = Constants.FontandColors.defaultshadowColor + view.layer.shadowOpacity = Float( + Constants.FontandColors.defaultshadowOpacity + ) + view.layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + view.layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + return view + }() + + private let gameImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private let nameLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 24, weight: .bold) + label.textColor = Constants.FontandColors.titleColor + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let accessoryIcon: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: Constants.FontandColors.chevronName) + imageView.tintColor = Constants.FontandColors.chevronColor + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + selectionStyle = .none + backgroundColor = .clear + contentView.backgroundColor = .clear + + // Add subviews to cell + contentView.addSubview(containerView) + containerView.addSubview(gameImageView) + containerView.addSubview(nameLabel) + containerView.addSubview(accessoryIcon) + + // Enable Auto Layout + containerView.translatesAutoresizingMaskIntoConstraints = false + + // Set up constraints + NSLayoutConstraint.activate([ + // Container view constraints + containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Constants.paddingKeys.DefaultPaddingTop), + containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constants.paddingKeys.DefaultPaddingLeft), + containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingRight), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: Constants.paddingKeys.DefaultPaddingBottom), + + // Image view constraints + gameImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + gameImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + gameImageView.widthAnchor.constraint(equalToConstant: 120), + gameImageView.heightAnchor.constraint(equalToConstant: 120), + + // Name label constraints + nameLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + nameLabel.leadingAnchor.constraint(equalTo: gameImageView.trailingAnchor, constant: Constants.paddingKeys.DefaultPaddingLeft), + nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: accessoryIcon.leadingAnchor, constant: Constants.paddingKeys.DefaultPaddingRight), + + // Accessory icon constraints + accessoryIcon.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + accessoryIcon.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -24), + accessoryIcon.widthAnchor.constraint(equalToConstant: 14), + accessoryIcon.heightAnchor.constraint(equalToConstant: 22) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + gameImageView.image = nil + nameLabel.text = nil + } + + func configure(with game: Games) { + gameImageView.image = UIImage(named: game.imageName) + nameLabel.text = game.name + } +} + +#Preview{ + GamesTableViewCell() +} diff --git a/recap/Views/GamesCell.swift b/recap/Views/GamesCell.swift deleted file mode 100644 index a895bb8..0000000 --- a/recap/Views/GamesCell.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// GamesCell.swift -// recap -// -// Created by Diptayan Jash on 09/11/24. -// - -import UIKit - -class GamesCell: UICollectionViewCell { - static let identifier = "GameCell" - - private let imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = 8 - imageView.clipsToBounds = true - return imageView - }() - - private let nameLabel: UILabel = { - let label = UILabel() - label.font = .boldSystemFont(ofSize: 18) - label.textColor = .black - return label - }() - - private let accessoryIcon: UIImageView = { - let imageView = UIImageView() - imageView.image = UIImage(systemName: "chevron.right") - imageView.tintColor = .gray - imageView.contentMode = .scaleAspectFit - return imageView - }() - - private let dividerLine: UIView = { - let view = UIView() - view.backgroundColor = .lightGray.withAlphaComponent(0.3) - return view - }() - - private let descriptionLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 15) - label.textColor = .gray - label.numberOfLines = 0 - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - // Set cell size - contentView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - contentView.widthAnchor.constraint(equalToConstant: 170), - contentView.heightAnchor.constraint(equalToConstant: 226), - ]) - - // Cell styling - contentView.layer.cornerRadius = 15 - contentView.layer.shadowColor = UIColor.black.cgColor - contentView.layer.shadowOpacity = 0.1 - contentView.layer.shadowOffset = CGSize(width: 0, height: 2) - contentView.layer.shadowRadius = 4 - contentView.backgroundColor = .white - - // Add subviews - contentView.addSubview(imageView) - contentView.addSubview(nameLabel) - contentView.addSubview(accessoryIcon) - contentView.addSubview(dividerLine) - contentView.addSubview(descriptionLabel) - - // Layout the subviews with the specified dimensions - imageView.frame = CGRect(x: (contentView.frame.size.width - 90) / 2, y: 10, width: 100, height: 100) - - dividerLine.frame = CGRect(x: 10, y: imageView.frame.maxY + 10, width: contentView.frame.size.width - 20, height: 1) - - nameLabel.frame = CGRect(x: 10, y: dividerLine.frame.maxY + 8, width: contentView.frame.size.width - 40, height: 20) - - accessoryIcon.frame = CGRect(x: contentView.frame.size.width - 25, y: dividerLine.frame.maxY + 8, width: 15, height: 15) - - descriptionLabel.frame = CGRect(x: 10, y: nameLabel.frame.maxY + 5, width: contentView.frame.size.width - 20, height: 20) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(with gamesDemo: Games) { - imageView.image = UIImage(named: gamesDemo.imageName) - nameLabel.text = gamesDemo.name - descriptionLabel.text = gamesDemo.description - } -} diff --git a/recap/Views/PatientHomeCards/LetsReadCardView.swift b/recap/Views/PatientHomeCards/LetsReadCardView.swift new file mode 100755 index 0000000..4f3b91d --- /dev/null +++ b/recap/Views/PatientHomeCards/LetsReadCardView.swift @@ -0,0 +1,221 @@ +//// +//// LetsReadCardView.swift +//// recap +//// +//// Created by admin70 on 11/02/25. +//// +//import UIKit +// +//class LetsReadCardView: UIView { +// private let iconImageView = UIImageView() +// private let titleLabel = UILabel() +// private let descriptionLabel = UILabel() +// private let separatorView = UIView() +// private let arrowImageView = UIImageView() +// +// var navigateToDetail: (() -> Void)? +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// setupUI() +// setupTapGesture() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// setupUI() +// setupTapGesture() +// } +// +// private func setupUI() { +// backgroundColor = AppColors.cardBackgroundColor +// layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius +// layer.shadowColor = Constants.FontandColors.defaultshadowColor +// layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) +// layer.shadowOffset = Constants.FontandColors.defaultshadowOffset +// layer.shadowRadius = Constants.FontandColors.defaultshadowRadius +// translatesAutoresizingMaskIntoConstraints = false +// +// // Icon Image +// iconImageView.image = UIImage(named: "BigShoesTorso") +// iconImageView.contentMode = .scaleAspectFit +// iconImageView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(iconImageView) +// +// // Title Label +// titleLabel.text = "Let's Read" +// titleLabel.textColor = AppColors.primaryTextColor +// titleLabel.font = Constants.FontandColors.titleFont +// titleLabel.translatesAutoresizingMaskIntoConstraints = false +// addSubview(titleLabel) +// +// // Arrow Image +// arrowImageView.image = UIImage(systemName: Constants.FontandColors.chevronName) +// arrowImageView.tintColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) +// arrowImageView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(arrowImageView) +// +// // Separator View +// separatorView.backgroundColor = .systemGray +// separatorView.alpha = 0.5 +// separatorView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(separatorView) +// +// // Description Label +// descriptionLabel.text = "Each word you read strengthens your journey. Keep exploring!" +// descriptionLabel.font = UIFont.systemFont(ofSize: 16) +// descriptionLabel.textColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) +// descriptionLabel.numberOfLines = 2 +// descriptionLabel.translatesAutoresizingMaskIntoConstraints = false +// addSubview(descriptionLabel) +// +// // Constraints +// NSLayoutConstraint.activate([ +// iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), +// iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), +// iconImageView.widthAnchor.constraint(equalToConstant: 100), +// iconImageView.heightAnchor.constraint(equalToConstant: 100), +// +// titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24), +// titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), +// titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: arrowImageView.leadingAnchor, constant: -8), +// +// arrowImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), +// arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), +// arrowImageView.widthAnchor.constraint(equalToConstant: 14), +// arrowImageView.heightAnchor.constraint(equalToConstant: 22), +// +// separatorView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), +// separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), +// separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), +// separatorView.heightAnchor.constraint(equalToConstant: 1), +// +// descriptionLabel.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 10), +// descriptionLabel.leadingAnchor.constraint(equalTo: separatorView.leadingAnchor), +// descriptionLabel.trailingAnchor.constraint(equalTo: separatorView.trailingAnchor), +// descriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) +// ]) +// } +// +// private func setupTapGesture() { +// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) +// addGestureRecognizer(tapGesture) +// } +// +// @objc private func didTapCard() { +// navigateToDetail?() +// } +//} +// +//#Preview { +// LetsReadCardView() +//} + + + + +import UIKit + +class LetsReadCardView: UIView { + private let iconImageView = UIImageView() + private let titleLabel = UILabel() + private let descriptionLabel = UILabel() + private let separatorView = UIView() + private let arrowImageView = UIImageView() + + var navigateToDetail: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + setupTapGesture() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + setupTapGesture() + } + + private func setupUI() { + backgroundColor = AppColors.cardBackgroundColor + layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + layer.shadowColor = Constants.FontandColors.defaultshadowColor + layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) + layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + translatesAutoresizingMaskIntoConstraints = false + + // Icon Image + iconImageView.image = UIImage(named: "BigShoesTorso") + iconImageView.contentMode = .scaleAspectFit + iconImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(iconImageView) + + // Title Label + titleLabel.text = "Let's Read" + titleLabel.textColor = AppColors.primaryTextColor + titleLabel.font = Constants.FontandColors.titleFont + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + + // Arrow Image + arrowImageView.image = UIImage(systemName: Constants.FontandColors.chevronName) + arrowImageView.tintColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(arrowImageView) + + // Separator View + separatorView.backgroundColor = .systemGray.withAlphaComponent(0.5) + separatorView.translatesAutoresizingMaskIntoConstraints = false + addSubview(separatorView) + + // Description Label + descriptionLabel.text = "Each word you read strengthens your journey. Keep exploring!" + descriptionLabel.font = UIFont.systemFont(ofSize: 16) + descriptionLabel.textColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) + descriptionLabel.numberOfLines = 2 + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(descriptionLabel) + + // Constraints + NSLayoutConstraint.activate([ + iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), + iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: 100), + iconImageView.heightAnchor.constraint(equalToConstant: 100), + + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24), + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: arrowImageView.leadingAnchor, constant: -8), + + arrowImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + arrowImageView.widthAnchor.constraint(equalToConstant: 14), + arrowImageView.heightAnchor.constraint(equalToConstant: 22), + + separatorView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), + separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + separatorView.heightAnchor.constraint(equalToConstant: 1), + + descriptionLabel.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 12), + descriptionLabel.leadingAnchor.constraint(equalTo: separatorView.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: separatorView.trailingAnchor), + descriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) + ]) + } + + private func setupTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) + addGestureRecognizer(tapGesture) + } + + @objc private func didTapCard() { + navigateToDetail?() + } +} + +#Preview { + LetsReadCardView() +} diff --git a/recap/Views/PatientHomeCards/QuestionsCardView.swift b/recap/Views/PatientHomeCards/QuestionsCardView.swift new file mode 100755 index 0000000..9f4347a --- /dev/null +++ b/recap/Views/PatientHomeCards/QuestionsCardView.swift @@ -0,0 +1,216 @@ +//// +//// QuestionsCardView.swift.swift +//// recap +//// +//// Created by admin70 on 11/02/25. +//// +//import UIKit +// +//class QuestionsCardView: UIView { +// private let iconImageView = UIImageView() +// private let titleLabel = UILabel() +// private let descriptionLabel = UILabel() +// private let separatorView = UIView() +// private let arrowImageView = UIImageView() +// +// var navigateToDetail: (() -> Void)? +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// setupUI() +// setupTapGesture() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// setupUI() +// setupTapGesture() +// } +// +// private func setupUI() { +// backgroundColor = AppColors.cardBackgroundColor +// layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius +// layer.shadowColor = Constants.FontandColors.defaultshadowColor +// layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) +// layer.shadowOffset = Constants.FontandColors.defaultshadowOffset +// layer.shadowRadius = Constants.FontandColors.defaultshadowRadius +// translatesAutoresizingMaskIntoConstraints = false +// +// iconImageView.image = UIImage(named: "oldMan") +// iconImageView.contentMode = .scaleAspectFit +// iconImageView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(iconImageView) +// +// titleLabel.text = "Daily Questions" +// titleLabel.textColor = AppColors.primaryTextColor +// titleLabel.font = Constants.FontandColors.titleFont +// titleLabel.translatesAutoresizingMaskIntoConstraints = false +// addSubview(titleLabel) +// +// arrowImageView.image = UIImage( +// systemName: Constants.FontandColors.chevronName +// ) +// arrowImageView.tintColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) +// arrowImageView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(arrowImageView) +// +// separatorView.backgroundColor = .systemGray4 +// separatorView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(separatorView) +// +// descriptionLabel.text = "A little effort each day keeps the memory strong – do it for your family." +// descriptionLabel.font = Constants.FontandColors.descriptionFont +// descriptionLabel.textColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) +// descriptionLabel.numberOfLines = 0 +// descriptionLabel.translatesAutoresizingMaskIntoConstraints = false +// addSubview(descriptionLabel) +// +// NSLayoutConstraint.activate( +//[ +// iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), +// iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), +// iconImageView.widthAnchor.constraint(equalToConstant: 100), +// iconImageView.heightAnchor.constraint(equalToConstant: 100), +// +// titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24), +// titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), +// titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: arrowImageView.leadingAnchor, constant: -8), +// +// arrowImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), +// arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), +// arrowImageView.widthAnchor.constraint(equalToConstant: 14), +// arrowImageView.heightAnchor.constraint(equalToConstant: 22), +// +// separatorView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), +// separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), +// separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), +// separatorView.heightAnchor.constraint(equalToConstant: 1), +// +// descriptionLabel.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 12), +// descriptionLabel.leadingAnchor.constraint(equalTo: separatorView.leadingAnchor), +// descriptionLabel.trailingAnchor.constraint(equalTo: separatorView.trailingAnchor), +// descriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) +// ] +//) +// } +// +// private func setupTapGesture() { +// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) +// addGestureRecognizer(tapGesture) +// } +// +// @objc private func didTapCard() { +// navigateToDetail?() +// } +//} +// +//#Preview { +// QuestionsCardView() +//} + +// +// QuestionsCardView.swift +// recap +// +// Created by admin70 on 11/02/25. +// + +import UIKit + +class QuestionsCardView: UIView { + private let iconImageView = UIImageView() + private let titleLabel = UILabel() + private let descriptionLabel = UILabel() + private let separatorView = UIView() + private let arrowImageView = UIImageView() + + var navigateToDetail: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + setupTapGesture() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + setupTapGesture() + } + + private func setupUI() { + backgroundColor = AppColors.cardBackgroundColor + layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + layer.shadowColor = Constants.FontandColors.defaultshadowColor + layer.shadowOpacity = Float(Constants.FontandColors.defaultshadowOpacity) + layer.shadowOffset = Constants.FontandColors.defaultshadowOffset + layer.shadowRadius = Constants.FontandColors.defaultshadowRadius + translatesAutoresizingMaskIntoConstraints = false + + iconImageView.image = UIImage(named: "oldMan") + iconImageView.contentMode = .scaleAspectFit + iconImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(iconImageView) + + titleLabel.text = "Daily Questions" + titleLabel.textColor = AppColors.primaryTextColor + titleLabel.font = Constants.FontandColors.titleFont + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + + arrowImageView.image = UIImage(systemName: Constants.FontandColors.chevronName) + arrowImageView.tintColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(arrowImageView) + + separatorView.backgroundColor = .systemGray.withAlphaComponent(0.5) + separatorView.translatesAutoresizingMaskIntoConstraints = false + addSubview(separatorView) + + descriptionLabel.text = "A little effort each day keeps the memory strong – do it for your family." + descriptionLabel.font = UIFont.systemFont(ofSize: 16) + descriptionLabel.textColor = AppColors.secondaryTextColor.withAlphaComponent(0.6) + descriptionLabel.numberOfLines = 0 + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(descriptionLabel) + + NSLayoutConstraint.activate([ + iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10), + iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: 100), + iconImageView.heightAnchor.constraint(equalToConstant: 100), + + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24), + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: arrowImageView.leadingAnchor, constant: -8), + + arrowImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + arrowImageView.widthAnchor.constraint(equalToConstant: 14), + arrowImageView.heightAnchor.constraint(equalToConstant: 22), + + separatorView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), + separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + separatorView.heightAnchor.constraint(equalToConstant: 1), + + descriptionLabel.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 12), + descriptionLabel.leadingAnchor.constraint(equalTo: separatorView.leadingAnchor), + descriptionLabel.trailingAnchor.constraint(equalTo: separatorView.trailingAnchor), + descriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) + ]) + } + + private func setupTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) + addGestureRecognizer(tapGesture) + } + + @objc private func didTapCard() { + navigateToDetail?() + } +} + +#Preview { + QuestionsCardView() +} diff --git a/recap/Views/PatientHomeCards/StreaksCard.swift b/recap/Views/PatientHomeCards/StreaksCard.swift new file mode 100755 index 0000000..26dd303 --- /dev/null +++ b/recap/Views/PatientHomeCards/StreaksCard.swift @@ -0,0 +1,116 @@ +// +// StreaksCard.swift +// recap +// +// Created by khushi on 11/02/25. +// +import UIKit + +class StreaksCard: UIView { + private let iconImageView = UIImageView() + private let titleLabel = UILabel() + private let descriptionLabel = UILabel() + private let separatorView = UIView() + private let arrowImageView = UIImageView() + private let progressBar = UIProgressView(progressViewStyle: .default) + + var navigateToDetail: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + setupTapGesture() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + setupTapGesture() + } + + private func setupUI() { + backgroundColor = AppColors.cardBackgroundColor + layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.1 + layer.shadowOffset = CGSize(width: 0, height: 2) + layer.shadowRadius = 4 + translatesAutoresizingMaskIntoConstraints = false + + iconImageView.image = UIImage(named: "cosmonaut") + iconImageView.contentMode = .scaleAspectFit + iconImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(iconImageView) + + titleLabel.text = "Streaks" + titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .semibold) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + + arrowImageView.image = UIImage(systemName: "chevron.right") + arrowImageView.tintColor = .gray + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(arrowImageView) + + separatorView.backgroundColor = .systemGray4 + separatorView.translatesAutoresizingMaskIntoConstraints = false + addSubview(separatorView) + + descriptionLabel.text = "See how active you are." + descriptionLabel.font = UIFont.systemFont(ofSize: 16, weight: .regular) + descriptionLabel.textColor = .gray + descriptionLabel.numberOfLines = 0 + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(descriptionLabel) + + progressBar.progress = 0.7 // Example progress + progressBar.trackTintColor = .systemGray4 + progressBar.progressTintColor = .systemPurple + progressBar.translatesAutoresizingMaskIntoConstraints = false + addSubview(progressBar) + + NSLayoutConstraint.activate([ + iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + iconImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: 60), + iconImageView.heightAnchor.constraint(equalToConstant: 60), + + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24), + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: arrowImageView.leadingAnchor, constant: -8), + + arrowImageView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + arrowImageView.widthAnchor.constraint(equalToConstant: 14), + arrowImageView.heightAnchor.constraint(equalToConstant: 22), + + separatorView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), + separatorView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 24), + separatorView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + separatorView.heightAnchor.constraint(equalToConstant: 1), + + descriptionLabel.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 12), + descriptionLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 24), + descriptionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + + progressBar.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 12), + progressBar.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 24), + progressBar.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + progressBar.heightAnchor.constraint(equalToConstant: 10), // Increase height + progressBar.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -24) + ]) + } + + private func setupTapGesture() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapCard)) + addGestureRecognizer(tapGesture) + } + + @objc private func didTapCard() { + navigateToDetail?() + } +} + +#Preview(){ + StreaksCard() +} diff --git a/recap/Views/Streaks/StreaksViewController+CollectionView.swift b/recap/Views/Streaks/StreaksViewController+CollectionView.swift old mode 100644 new mode 100755 index 78e3b0b..4c0da92 --- a/recap/Views/Streaks/StreaksViewController+CollectionView.swift +++ b/recap/Views/Streaks/StreaksViewController+CollectionView.swift @@ -1,3 +1,62 @@ +//// +//// StreaksViewController+CollectionView.swift +//// recap +//// +//// Created by user@47 on 16/01/25. +//// +// +//import UIKit +// +//extension StreaksViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +// +// func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { +// return daysInMonth(year: currentYear, month: currentMonth) +// } +// +// func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { +// guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CalendarCell", for: indexPath) as? CalendarCell else { +// return UICollectionViewCell() +// } +// +// let day = indexPath.item + 1 +// let currentDate = Date() +// let calendar = Calendar.current +// let components = DateComponents(year: currentYear, month: currentMonth, day: day) +// let cellDate = calendar.date(from: components)! +// +// // Convert day to formatted string for matching the streakDates keys +// let dateFormatter = DateFormatter() +// dateFormatter.dateFormat = "yyyy-MM-dd" +// let formattedDate = dateFormatter.string(from: cellDate) +// +// // Check if the day exists in the streakDates dictionary +// if let isStreakDay = streakDates[formattedDate] { +// if isStreakDay { +// cell.contentView.backgroundColor = AppColors.primaryButtonColor +// } else { +// cell.contentView.backgroundColor = .white +// } +// } else if cellDate > currentDate { +// cell.contentView.backgroundColor = .clear +// } else { +// cell.contentView.backgroundColor = .white +// } +// +// // Set the day label +// cell.dayLabel.text = "\(day)" +// cell.contentView.layer.cornerRadius = cell.contentView.frame.width / 2 +// return cell +// } +// +// // Get Number of Days in a Month +// func daysInMonth(year: Int, month: Int) -> Int { +// let calendar = Calendar.current +// let dateComponents = DateComponents(year: year, month: month) +// return calendar.range(of: .day, in: .month, for: calendar.date(from: dateComponents)!)?.count ?? 30 +// } +//} + + // // StreaksViewController+CollectionView.swift // recap @@ -8,10 +67,12 @@ import UIKit extension StreaksViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - - // MARK: - UICollectionViewDataSource Methods + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return daysInMonth(year: currentYear, month: currentMonth) // Get the number of days in the current month + // Total cells = days in month + leading empty cells for weekday offset, capped at 42 for a 6-row grid + let days = daysInMonth(year: currentYear, month: currentMonth) + let firstDayOffset = firstDayWeekdayOffset(year: currentYear, month: currentMonth) + return min(days + firstDayOffset, 42) // Ensure no extra cells beyond a 6-row grid } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -19,40 +80,97 @@ extension StreaksViewController: UICollectionViewDataSource, UICollectionViewDel return UICollectionViewCell() } - let day = indexPath.item + 1 - let currentDate = Date() + let firstDayOffset = firstDayWeekdayOffset(year: currentYear, month: currentMonth) + let dayIndex = indexPath.item - firstDayOffset + 1 + let daysInCurrentMonth = daysInMonth(year: currentYear, month: currentMonth) + + // Handle empty cells (before 1st of month or after last day) + if indexPath.item < firstDayOffset || dayIndex > daysInCurrentMonth { + cell.dayLabel.text = "" + cell.contentView.backgroundColor = .clear + cell.contentView.layer.borderWidth = 0 + cell.contentView.layer.cornerRadius = 0 + return cell + } + + // Configure cell for valid day + let day = dayIndex let calendar = Calendar.current let components = DateComponents(year: currentYear, month: currentMonth, day: day) let cellDate = calendar.date(from: components)! - // Convert day to formatted string for matching the streakDates keys + // Convert day to formatted string for matching streakDates keys let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" let formattedDate = dateFormatter.string(from: cellDate) // Check if the day exists in the streakDates dictionary + let currentDate = Date() if let isStreakDay = streakDates[formattedDate] { if isStreakDay { - cell.contentView.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.5) + cell.contentView.backgroundColor = AppColors.primaryButtonColor } else { - cell.contentView.backgroundColor = .white + cell.contentView.backgroundColor = Constants.BGs.GreyBG } } else if cellDate > currentDate { cell.contentView.backgroundColor = .clear } else { - cell.contentView.backgroundColor = .white + cell.contentView.backgroundColor = Constants.BGs.GreyBG } - // Set the day label + // Set the day label with consistent font cell.dayLabel.text = "\(day)" + cell.dayLabel.font = Constants.FontandColors.descriptionFont + cell.dayLabel.textColor = AppColors.primaryTextColor cell.contentView.layer.cornerRadius = cell.contentView.frame.width / 2 + cell.contentView.layer.masksToBounds = true + return cell } - // MARK: - Helper Method to Get Number of Days in a Month + // Get Number of Days in a Month func daysInMonth(year: Int, month: Int) -> Int { - let calendar = Calendar.current + var calendar = Calendar.current + calendar.firstWeekday = 1 // Set Sunday as the first day of the week (1 = Sunday) let dateComponents = DateComponents(year: year, month: month) return calendar.range(of: .day, in: .month, for: calendar.date(from: dateComponents)!)?.count ?? 30 } + + // Calculate the weekday offset for the 1st of the month + func firstDayWeekdayOffset(year: Int, month: Int) -> Int { + var calendar = Calendar.current + calendar.firstWeekday = 1 // Set Sunday as the first day of the week (1 = Sunday) + let dateComponents = DateComponents(year: year, month: month, day: 1) + guard let firstDay = calendar.date(from: dateComponents) else { return 0 } + let weekday = calendar.component(.weekday, from: firstDay) + // Adjust weekday to 0-based index (Sunday = 0, Monday = 1, ..., Saturday = 6) + return (weekday - 1) + } + + // Define layout for cells (adjust size for 7-column grid) + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let totalWidth = collectionView.bounds.width - (Constants.paddingKeys.DefaultPaddingLeft * 2) + let cellWidth = totalWidth / 7 // 7 columns for days of the week + let cellHeight = cellWidth // Square cells + return CGSize(width: cellWidth, height: cellHeight) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return 0 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 0 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets( + top: CGFloat(Constants.paddingKeys.DefaultPaddingTop), + left: CGFloat(Constants.paddingKeys.DefaultPaddingLeft), + bottom: CGFloat(Constants.paddingKeys.DefaultPaddingTop), + right: CGFloat(Constants.paddingKeys.DefaultPaddingLeft) + ) + } } + + diff --git a/recap/Views/Streaks/StreaksViewController+Helpers.swift b/recap/Views/Streaks/StreaksViewController+Helpers.swift old mode 100644 new mode 100755 index f880652..a4c8d75 --- a/recap/Views/Streaks/StreaksViewController+Helpers.swift +++ b/recap/Views/Streaks/StreaksViewController+Helpers.swift @@ -33,7 +33,7 @@ extension StreaksViewController { valueLabel.text = value valueLabel.font = UIFont.boldSystemFont(ofSize: 24) // Increased font size valueLabel.textAlignment = .center - valueLabel.textColor = .systemOrange // Orange color for the number + valueLabel.textColor = AppColors.primaryButtonColor// Orange color for the number valueLabel.translatesAutoresizingMaskIntoConstraints = false container.addSubview(valueLabel) diff --git a/recap/Views/Streaks/StreaksViewController+Setup.swift b/recap/Views/Streaks/StreaksViewController+Setup.swift old mode 100644 new mode 100755 index f257bd5..9f5e397 --- a/recap/Views/Streaks/StreaksViewController+Setup.swift +++ b/recap/Views/Streaks/StreaksViewController+Setup.swift @@ -3,12 +3,12 @@ // recap // // Created by user@47 on 16/01/25. -// import UIKit extension StreaksViewController { - // MARK: - Setup Gradient Background + + // Gradient Background func setupGradientBackground() { let gradientLayer = CAGradientLayer() gradientLayer.colors = [ @@ -40,13 +40,28 @@ extension StreaksViewController { // MARK: - Setup Navigation Bar func setupNavBar() { - title = "Streaks" + title = "Daily Checker" } - // MARK: - Setup Streak Stats View + // Method to update the streak stats on the UI + func updateStreakStats(maxStreak: Int, currentStreak: Int, activeDays: Int) { + DispatchQueue.main.async { + self.maxStreakLabel.text = "\(maxStreak)" + self.currentStreakLabel.text = "\(currentStreak)" + self.activeDaysLabel.text = "\(activeDays)" + + // Store values persistently + UserDefaults.standard.set(maxStreak, forKey: "maxStreak") + UserDefaults.standard.set(currentStreak, forKey: "currentStreak") + UserDefaults.standard.set(activeDays, forKey: "activeDays") + UserDefaults.standard.synchronize() + } + } + + // Method to set up the streak stats view func setupStreakStatsView() { - streakStatsView.backgroundColor = .white - streakStatsView.layer.cornerRadius = 12 + streakStatsView.backgroundColor = AppColors.primaryButtonColor + streakStatsView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius streakStatsView.layer.shadowColor = UIColor.black.cgColor streakStatsView.layer.shadowOpacity = 0.1 streakStatsView.layer.shadowOffset = CGSize(width: 0, height: 2) @@ -54,15 +69,34 @@ extension StreaksViewController { streakStatsView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(streakStatsView) + // Create the info button with an icon + let infoButton = UIButton(type: .system) + infoButton.setImage(UIImage(systemName: "info.circle.fill"), for: .normal) + infoButton.tintColor = AppColors.iconColor + infoButton.backgroundColor = .clear // Remove background for icon-only button + infoButton.layer.cornerRadius = 12 + infoButton.translatesAutoresizingMaskIntoConstraints = false + infoButton.isUserInteractionEnabled = true + streakStatsView.addSubview(infoButton) + + // Add a gesture recognizer for infoButton + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(infoButtonTapped)) + infoButton.addGestureRecognizer(tapGesture) + let streakStatsStackView = UIStackView() streakStatsStackView.axis = .horizontal streakStatsStackView.distribution = .fillEqually streakStatsStackView.alignment = .center + streakStatsStackView.spacing = 10 streakStatsStackView.translatesAutoresizingMaskIntoConstraints = false - let maxStreakView = createStatView(title: "Max Streak", value: "0") - let currentStreakView = createStatView(title: "Current Streak", value: "0") - let activeDaysView = createStatView(title: "Active Days", value: "0") + let (maxStreakView, maxStreakLabel) = createStatView(title: "Max Streak", value: "0") + let (currentStreakView, currentStreakLabel) = createStatView(title: "Current Streak", value: "0") + let (activeDaysView, activeDaysLabel) = createStatView(title: "Active Days", value: "0") + + self.maxStreakLabel = maxStreakLabel + self.currentStreakLabel = currentStreakLabel + self.activeDaysLabel = activeDaysLabel streakStatsStackView.addArrangedSubview(maxStreakView) streakStatsStackView.addArrangedSubview(currentStreakView) @@ -71,139 +105,305 @@ extension StreaksViewController { streakStatsView.addSubview(streakStatsStackView) NSLayoutConstraint.activate([ - streakStatsView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + streakStatsView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), streakStatsView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), streakStatsView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - streakStatsView.heightAnchor.constraint(equalToConstant: 80), // Decreased height + streakStatsView.heightAnchor.constraint(equalToConstant: 80), streakStatsStackView.leadingAnchor.constraint(equalTo: streakStatsView.leadingAnchor, constant: 16), streakStatsStackView.trailingAnchor.constraint(equalTo: streakStatsView.trailingAnchor, constant: -16), - streakStatsStackView.topAnchor.constraint(equalTo: streakStatsView.topAnchor, constant: 16), - streakStatsStackView.bottomAnchor.constraint(equalTo: streakStatsView.bottomAnchor, constant: -16) + streakStatsStackView.topAnchor.constraint(equalTo: streakStatsView.topAnchor, constant: 12), + streakStatsStackView.bottomAnchor.constraint(equalTo: streakStatsView.bottomAnchor, constant: -12), + + // Info button constraints + infoButton.topAnchor.constraint(equalTo: streakStatsView.topAnchor, constant: 8), + infoButton.trailingAnchor.constraint(equalTo: streakStatsView.trailingAnchor, constant: -8), + infoButton.widthAnchor.constraint(equalToConstant: 24), + infoButton.heightAnchor.constraint(equalToConstant: 24) + ]) + } + + // Action for info button tap + @objc func infoButtonTapped() { + showInfoCard() + } + + // Method to show the info card + private func showInfoCard() { + let infoCard = UIView() + infoCard.backgroundColor = AppColors.primaryButtonColor + infoCard.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius + infoCard.layer.shadowColor = UIColor.black.cgColor + infoCard.layer.shadowOpacity = 0.1 + infoCard.layer.shadowOffset = CGSize(width: 0, height: 4) + infoCard.layer.shadowRadius = 8 + infoCard.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(infoCard) + + // Add a close button + let closeButton = UIButton(type: .system) + closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) + closeButton.tintColor = .gray + closeButton.translatesAutoresizingMaskIntoConstraints = false + closeButton.addTarget(self, action: #selector(dismissInfoCard), for: .touchUpInside) + infoCard.addSubview(closeButton) + + // Add a title label + let titleLabel = UILabel() + titleLabel.text = "Streak Information" + titleLabel.font = UIFont.boldSystemFont(ofSize: 20) + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + infoCard.addSubview(titleLabel) + + // Add a label with the explanation text + let infoLabel = UILabel() + infoLabel.numberOfLines = 0 + infoLabel.text = """ + • Max Streak: The longest streak you've ever achieved without a break. + + • Current Streak: The consecutive days your current streak is going on. + + • Active Days: The number of days you answered a question since you downloaded the app. + """ + infoLabel.font = UIFont.systemFont(ofSize: 16) + infoLabel.textColor = AppColors.primaryTextColor + infoLabel.translatesAutoresizingMaskIntoConstraints = false + infoCard.addSubview(infoLabel) + + // Set up the info card's constraints + NSLayoutConstraint.activate([ + infoCard.centerXAnchor.constraint(equalTo: view.centerXAnchor), + infoCard.centerYAnchor.constraint(equalTo: view.centerYAnchor), + infoCard.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.85), + infoCard.heightAnchor.constraint(equalToConstant: 280), + + closeButton.topAnchor.constraint(equalTo: infoCard.topAnchor, constant: 16), + closeButton.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -16), + closeButton.widthAnchor.constraint(equalToConstant: 30), + closeButton.heightAnchor.constraint(equalToConstant: 30), + + titleLabel.topAnchor.constraint(equalTo: infoCard.topAnchor, constant: 24), + titleLabel.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -16), + + infoLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), + infoLabel.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 24), + infoLabel.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -24), + infoLabel.bottomAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: -24) + ]) + + // Add a blurred background + let blurEffect = UIBlurEffect(style: .dark) + let blurredView = UIVisualEffectView(effect: blurEffect) + blurredView.frame = view.bounds + blurredView.alpha = 0.5 + view.insertSubview(blurredView, belowSubview: infoCard) + + // Animate the appearance of the info card + infoCard.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + infoCard.alpha = 0 + + UIView.animate(withDuration: 0.3) { + infoCard.transform = .identity + infoCard.alpha = 1 + } + } + + // Method to dismiss the info card + @objc func dismissInfoCard() { + if let infoCard = view.subviews.last(where: { $0 is UIView && $0.layer.cornerRadius == Constants.CardSize.DefaultCardCornerRadius }) { + UIView.animate(withDuration: 0.3, animations: { + infoCard.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + infoCard.alpha = 0 + if let blurredView = self.view.subviews.first(where: { $0 is UIVisualEffectView }) { + blurredView.alpha = 0 + } + }) { _ in + infoCard.removeFromSuperview() + if let blurredView = self.view.subviews.first(where: { $0 is UIVisualEffectView }) { + blurredView.removeFromSuperview() + } + } + } + } + + // Method to create a custom stat view with a title and value label + func createStatView(title: String, value: String) -> (UIView, UILabel) { + let statView = UIView() + statView.translatesAutoresizingMaskIntoConstraints = false + + // Create and configure the value label (NUMBER IN MIDDLE) + let valueLabel = UILabel() + valueLabel.text = value + valueLabel.font = UIFont.boldSystemFont(ofSize: 28) // Large size for emphasis + valueLabel.textColor = AppColors.iconColor + valueLabel.textAlignment = .center + valueLabel.translatesAutoresizingMaskIntoConstraints = false + + // Create and configure the title label (TEXT BELOW NUMBER) + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.font = UIFont.systemFont(ofSize: 12) + titleLabel.textColor = .gray + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + // Add the labels to the statView + statView.addSubview(valueLabel) + statView.addSubview(titleLabel) + + // Set up constraints for the labels + NSLayoutConstraint.activate([ + valueLabel.centerXAnchor.constraint(equalTo: statView.centerXAnchor), + valueLabel.topAnchor.constraint(equalTo: statView.topAnchor), + + titleLabel.centerXAnchor.constraint(equalTo: statView.centerXAnchor), + titleLabel.topAnchor.constraint(equalTo: valueLabel.bottomAnchor, constant: 4), + titleLabel.bottomAnchor.constraint(equalTo: statView.bottomAnchor) ]) + + return (statView, valueLabel) } + // Method to set up the header view with the "This Week" heading and current dates func setupHeaderView() { headerView.backgroundColor = .white - headerView.layer.cornerRadius = 12 + headerView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius headerView.layer.shadowColor = UIColor.black.cgColor headerView.layer.shadowOpacity = 0.1 headerView.layer.shadowOffset = CGSize(width: 0, height: 2) headerView.layer.shadowRadius = 4 headerView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(headerView) - + + let titleLabel = UILabel() + titleLabel.text = "" + titleLabel.font = UIFont.boldSystemFont(ofSize: 20) + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + headerView.addSubview(titleLabel) + let largeFlameImageView = UIImageView(image: UIImage(systemName: "flame.fill")) - largeFlameImageView.tintColor = .systemOrange + largeFlameImageView.tintColor = AppColors.iconColor largeFlameImageView.contentMode = .scaleAspectFit largeFlameImageView.translatesAutoresizingMaskIntoConstraints = false - + // Increase image size and make it more prominent largeFlameImageView.heightAnchor.constraint(equalToConstant: 40).isActive = true largeFlameImageView.widthAnchor.constraint(equalToConstant: 40).isActive = true - + let daysStackView = UIStackView() daysStackView.axis = .horizontal daysStackView.distribution = .equalSpacing daysStackView.alignment = .center daysStackView.spacing = 16 // Increased spacing for more separation daysStackView.translatesAutoresizingMaskIntoConstraints = false - + // Get the current year and month (yyyy-MM format) let currentYearMonth = getCurrentYearMonth() - + // Fetch streaks for the current year and month streakService.getStreaksForUser(yearMonth: currentYearMonth) { [weak self] streak in guard let self = self else { return } - + // Update the streak dates UI if let streak = streak { - self.updateStreakDatesWithStreaks(streak.streakDates) + self.streakDates = streak.streakDates } else { print("⚠️ No streak data found for \(currentYearMonth), but not uploading default data.") } - + // Ensure the calendar reloads DispatchQueue.main.async { self.calendarCollectionView.reloadData() } } - + // Get the current week (Sunday to Saturday) let currentWeekDates = getCurrentWeekDates() - + // Fetch streakDates dynamically from StreakService streakService.getStreaksForUser(yearMonth: currentYearMonth) { [weak self] streak in guard let self = self else { return } - + // Assuming `streakDates` is a dictionary of dates and their corresponding streak status var streakDates: [String: Bool] = [:] - + if let streak = streak { streakDates = streak.streakDates // Example data for streak dates } - - print("Current Week Dates: \(currentWeekDates)") // Debug statement to check the current week dates - print("Streak Dates: \(streakDates)") // Debug statement to check the streak dates - + for day in currentWeekDates { let dayContainer = UIStackView() dayContainer.axis = .vertical dayContainer.alignment = .center - dayContainer.spacing = 8 // Increase space between the flame icon and day label + dayContainer.spacing = 8 // Adjusted space for better layout dayContainer.translatesAutoresizingMaskIntoConstraints = false - + let flameImage = UIImageView(image: UIImage(systemName: "flame.fill")) - - // Debugging date format comparison - print("Comparing day: \(day) with streakDates: \(streakDates)") - + // Check if the date exists in streakDates dictionary and its value is true if let isStreak = streakDates[day], isStreak { - flameImage.tintColor = UIColor.systemOrange // Orange color if it's a streak day + flameImage.tintColor = AppColors.iconColor } else { - flameImage.tintColor = .systemGray // Default gray for unmarked days + flameImage.tintColor = AppColors.primaryButtonColor // Default gray for unmarked days } - + flameImage.contentMode = .scaleAspectFit flameImage.translatesAutoresizingMaskIntoConstraints = false - + // Increase flame icon size flameImage.heightAnchor.constraint(equalToConstant: 24).isActive = true flameImage.widthAnchor.constraint(equalToConstant: 24).isActive = true - - let dayLabel = UILabel() - dayLabel.text = self.getShortWeekdayFormat(from: day) // Convert the date to the short weekday format - dayLabel.font = UIFont.boldSystemFont(ofSize: 18) // Increased font size - dayLabel.textAlignment = .center - dayLabel.translatesAutoresizingMaskIntoConstraints = false - + + // Date Label above the flame icon + let dateLabel = UILabel() + dateLabel.text = self.getShortDateFormat(from: day) // Convert the date to just the day (e.g., "13") + dateLabel.font = UIFont.boldSystemFont(ofSize: 18) // Adjusted font size for date + dateLabel.textAlignment = .center + dateLabel.translatesAutoresizingMaskIntoConstraints = false + + // Short Weekday Label below the flame icon + let shortWeekdayLabel = UILabel() + shortWeekdayLabel.text = self.getShortWeekdayFormat(from: day) // Short weekday format (e.g., "Mon") + shortWeekdayLabel.font = UIFont.boldSystemFont(ofSize: 16) // Adjusted font size for short weekday + shortWeekdayLabel.textAlignment = .center + shortWeekdayLabel.translatesAutoresizingMaskIntoConstraints = false + + // Add the date above the flame icon + dayContainer.addArrangedSubview(dateLabel) + + // Add the flame icon in the center dayContainer.addArrangedSubview(flameImage) - dayContainer.addArrangedSubview(dayLabel) + + // Add the short weekday name below the flame icon + dayContainer.addArrangedSubview(shortWeekdayLabel) + daysStackView.addArrangedSubview(dayContainer) - - NSLayoutConstraint.activate([ - flameImage.heightAnchor.constraint(equalToConstant: 24), - flameImage.widthAnchor.constraint(equalToConstant: 24), - ]) } DispatchQueue.main.async { self.headerView.addSubview(daysStackView) NSLayoutConstraint.activate([ - self.headerView.topAnchor.constraint(equalTo: self.streakStatsView.bottomAnchor, constant: 32), // Increased spacing from the streakStatsView + self.headerView.topAnchor.constraint(equalTo: self.streakStatsView.bottomAnchor, constant: 12), self.headerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), self.headerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -16), - self.headerView.heightAnchor.constraint(equalToConstant: 70), // Decreased height - + self.headerView.heightAnchor.constraint(equalToConstant: 140), // Increased height to accommodate bigger container + + // Title label constraints + titleLabel.topAnchor.constraint(equalTo: self.headerView.topAnchor, constant: 16), + titleLabel.centerXAnchor.constraint(equalTo: self.headerView.centerXAnchor), + daysStackView.leadingAnchor.constraint(equalTo: self.headerView.leadingAnchor, constant: 16), daysStackView.trailingAnchor.constraint(equalTo: self.headerView.trailingAnchor, constant: -16), - daysStackView.centerYAnchor.constraint(equalTo: self.headerView.centerYAnchor) + daysStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + daysStackView.bottomAnchor.constraint(equalTo: self.headerView.bottomAnchor, constant: -16) ]) } } } - + // Helper function to get the current year's month (yyyy-MM format) func getCurrentYearMonth() -> String { let calendar = Calendar.current @@ -213,7 +413,7 @@ extension StreaksViewController { return yearMonthFormatter.string(from: currentDate) } - + // Helper function to get the current week's dates (Sunday to Saturday) func getCurrentWeekDates() -> [String] { let calendar = Calendar.current @@ -240,7 +440,7 @@ extension StreaksViewController { return currentWeekDays } - + // Helper function to get the short weekday format (e.g., "Mon", "Tue") func getShortWeekdayFormat(from date: String) -> String { let dateFormatter = DateFormatter() @@ -254,35 +454,47 @@ extension StreaksViewController { } return "" } - - + + func getShortDateFormat(from dateString: String) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" // The format your dates are currently in + + if let date = dateFormatter.date(from: dateString) { + dateFormatter.dateFormat = "d" // "d" gives you the day of the month (e.g., "12") + return dateFormatter.string(from: date) + } + + return "" // Return an empty string if date conversion fails + } + // MARK: - Setup Calendar View func setupCalendarView() { - // Calendar View setup + // Calendar View setup with modern styling calendarView.backgroundColor = .white - calendarView.layer.cornerRadius = 12 + calendarView.layer.cornerRadius = Constants.CardSize.DefaultCardCornerRadius calendarView.layer.shadowColor = UIColor.black.cgColor - calendarView.layer.shadowOpacity = 0.1 - calendarView.layer.shadowOffset = CGSize(width: 0, height: 2) - calendarView.layer.shadowRadius = 4 + calendarView.layer.shadowOpacity = 0.15 + calendarView.layer.shadowOffset = CGSize(width: 0, height: 3) + calendarView.layer.shadowRadius = 8 calendarView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(calendarView) - // Previous Month Button setup - previousMonthButton.setTitle("<", for: .normal) - previousMonthButton.setTitleColor(.systemOrange, for: .normal) + // Previous Month Button setup with modern icon + previousMonthButton.setImage(UIImage(systemName: "chevron.left.circle.fill"), for: .normal) + previousMonthButton.tintColor = AppColors.iconColor previousMonthButton.addTarget(self, action: #selector(handlePreviousMonth), for: .touchUpInside) previousMonthButton.translatesAutoresizingMaskIntoConstraints = false - // Next Month Button setup - nextMonthButton.setTitle(">", for: .normal) - nextMonthButton.setTitleColor(.systemOrange, for: .normal) + // Next Month Button setup with modern icon + nextMonthButton.setImage(UIImage(systemName: "chevron.right.circle.fill"), for: .normal) + nextMonthButton.tintColor = AppColors.iconColor nextMonthButton.addTarget(self, action: #selector(handleNextMonth), for: .touchUpInside) nextMonthButton.translatesAutoresizingMaskIntoConstraints = false - // Month-Year Label setup + // Month-Year Label setup with modern typography monthYearLabel.text = formattedMonthYear() - monthYearLabel.font = UIFont.boldSystemFont(ofSize: 22) + monthYearLabel.font = UIFont.systemFont(ofSize: 22, weight: .bold) + monthYearLabel.textColor = AppColors.primaryTextColor monthYearLabel.textAlignment = .center monthYearLabel.translatesAutoresizingMaskIntoConstraints = false @@ -294,6 +506,34 @@ extension StreaksViewController { calendarHeaderStack.translatesAutoresizingMaskIntoConstraints = false calendarView.addSubview(calendarHeaderStack) + // Add weekday labels + let weekdayStack = UIStackView() + weekdayStack.axis = .horizontal + weekdayStack.distribution = .fillEqually + weekdayStack.spacing = 0 + weekdayStack.translatesAutoresizingMaskIntoConstraints = false + calendarView.addSubview(weekdayStack) + + let weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + for weekday in weekdays { + let label = UILabel() + label.text = weekday + label.font = UIFont.systemFont(ofSize: 12, weight: .medium) + label.textColor = AppColors.secondaryTextColor + label.textAlignment = .center + weekdayStack.addArrangedSubview(label) + } + + // Modify the collection view layout to ensure 7 days per row + let layout = UICollectionViewFlowLayout() + // Calculate item width to fit 7 days per row (with some padding) + let availableWidth = UIScreen.main.bounds.width - 32 - 32 // Screen width minus leading and trailing constraints + let itemWidth = (availableWidth - 6 * 4) / 7 // 6 spaces between 7 items, each 4 points wide + layout.itemSize = CGSize(width: itemWidth, height: itemWidth) + layout.minimumInteritemSpacing = 4 + layout.minimumLineSpacing = 8 + calendarCollectionView.collectionViewLayout = layout + // Calendar Collection View setup calendarCollectionView.register(CalendarCell.self, forCellWithReuseIdentifier: "CalendarCell") calendarCollectionView.dataSource = self @@ -308,18 +548,27 @@ extension StreaksViewController { calendarView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 16), calendarView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), calendarView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), - calendarView.heightAnchor.constraint(equalToConstant: 370), // Decreased height + calendarView.heightAnchor.constraint(equalToConstant: 390), // Made slightly taller to accommodate weekday labels // Calendar Header Stack constraints calendarHeaderStack.topAnchor.constraint(equalTo: calendarView.topAnchor, constant: 16), calendarHeaderStack.leadingAnchor.constraint(equalTo: calendarView.leadingAnchor, constant: 16), calendarHeaderStack.trailingAnchor.constraint(equalTo: calendarView.trailingAnchor, constant: -16), + // Weekday Stack constraints + weekdayStack.topAnchor.constraint(equalTo: calendarHeaderStack.bottomAnchor, constant: 16), + weekdayStack.leadingAnchor.constraint(equalTo: calendarView.leadingAnchor, constant: 16), + weekdayStack.trailingAnchor.constraint(equalTo: calendarView.trailingAnchor, constant: -16), + weekdayStack.heightAnchor.constraint(equalToConstant: 20), + // Calendar Collection View constraints - calendarCollectionView.topAnchor.constraint(equalTo: calendarHeaderStack.bottomAnchor, constant: 16), + calendarCollectionView.topAnchor.constraint(equalTo: weekdayStack.bottomAnchor, constant: 8), calendarCollectionView.leadingAnchor.constraint(equalTo: calendarView.leadingAnchor, constant: 16), calendarCollectionView.trailingAnchor.constraint(equalTo: calendarView.trailingAnchor, constant: -16), calendarCollectionView.bottomAnchor.constraint(equalTo: calendarView.bottomAnchor, constant: -16) ]) } } +#Preview{ + StreaksViewController(verifiedUserDocID: "E4McfMAfgATYMSvzx43wm7r1WQ23") +}