Login and Home
BIN
logo/icon_pix.png
Normal file
|
After Width: | Height: | Size: 835 B |
BIN
logo/pedifoods.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
logo/pedifoods_dark.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 897 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -1,122 +1,116 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images": [
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-20@2x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "20x20",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "20x20"
|
"filename": "icon_iphone_20pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-20@3x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "20x20",
|
||||||
"scale": "3x",
|
"scale": "3x",
|
||||||
"size" : "20x20"
|
"filename": "icon_iphone_20pt@3x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-29.png",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"scale" : "1x",
|
|
||||||
"size" : "29x29"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "AppIcon-29@2x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "29x29",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "29x29"
|
"filename": "icon_iphone_29pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-29@3x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "29x29",
|
||||||
"scale": "3x",
|
"scale": "3x",
|
||||||
"size" : "29x29"
|
"filename": "icon_iphone_29pt@3x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-40@2x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "40x40",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "40x40"
|
"filename": "icon_iphone_40pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-40@3x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "40x40",
|
||||||
"scale": "3x",
|
"scale": "3x",
|
||||||
"size" : "40x40"
|
"filename": "icon_iphone_40pt@3x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon@2x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "60x60",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "60x60"
|
"filename": "icon_iphone_60pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon@3x.png",
|
|
||||||
"idiom": "iphone",
|
"idiom": "iphone",
|
||||||
|
"size": "60x60",
|
||||||
"scale": "3x",
|
"scale": "3x",
|
||||||
"size" : "60x60"
|
"filename": "icon_iphone_60pt@3x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-20~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "20x20",
|
||||||
"scale": "1x",
|
"scale": "1x",
|
||||||
"size" : "20x20"
|
"filename": "icon_ipad_20pt@1x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-20@2x~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "20x20",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "20x20"
|
"filename": "icon_ipad_20pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-29~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "29x29",
|
||||||
"scale": "1x",
|
"scale": "1x",
|
||||||
"size" : "29x29"
|
"filename": "icon_ipad_29pt@1x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-29@2x~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "29x29",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "29x29"
|
"filename": "icon_ipad_29pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-40~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "40x40",
|
||||||
"scale": "1x",
|
"scale": "1x",
|
||||||
"size" : "40x40"
|
"filename": "icon_ipad_40pt@1x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-40@2x~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "40x40",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "40x40"
|
"filename": "icon_ipad_40pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "76x76",
|
||||||
"scale": "1x",
|
"scale": "1x",
|
||||||
"size" : "76x76"
|
"filename": "icon_ipad_76pt@1x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon@2x~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "76x76",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "76x76"
|
"filename": "icon_ipad_76pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon-83.5@2x~ipad.png",
|
|
||||||
"idiom": "ipad",
|
"idiom": "ipad",
|
||||||
|
"size": "83.5x83.5",
|
||||||
"scale": "2x",
|
"scale": "2x",
|
||||||
"size" : "83.5x83.5"
|
"filename": "icon_ipad_83.5pt@2x.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "AppIcon~ios-marketing.png",
|
|
||||||
"idiom": "ios-marketing",
|
"idiom": "ios-marketing",
|
||||||
|
"size": "1024x1024",
|
||||||
"scale": "1x",
|
"scale": "1x",
|
||||||
"size" : "1024x1024"
|
"filename": "icon_ios-marketing_1024pt@1x.png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info": {
|
||||||
"author" : "xcode",
|
"version": 1,
|
||||||
"version" : 1
|
"author": "xcode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
52
pedi-foods/Darwin/Assets.xcassets/login_bg.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "login_light.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "login_dark.png",
|
||||||
|
"scale": "1x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
pedi-foods/Darwin/Assets.xcassets/login_bg.imageset/login_dark.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
pedi-foods/Darwin/Assets.xcassets/login_bg.imageset/login_light.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
52
pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "pedifoods.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "pedifoods_dark.png",
|
||||||
|
"scale": "1x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
pedi-foods/Darwin/Assets.xcassets/pedifoods.imageset/pedifoods_dark.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
52
pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "pin_image_app.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "pin_image_app_dark.png",
|
||||||
|
"scale": "1x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x",
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
pedi-foods/Darwin/Assets.xcassets/pin_image_app.imageset/pin_image_app_dark.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
@@ -183,7 +183,7 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = "/bin/sh -e";
|
shellPath = "/bin/sh -e";
|
||||||
shellScript = "if [ \"${SKIP_ZERO}\" != \"\" ]; then\n echo \"note: skipping skip due to SKIP_ZERO\"\n exit 0\nelif [ \"${ENABLE_PREVIEWS}\" = \"YES\" ]; then\n echo \"note: skipping skip due to ENABLE_PREVIEWS\"\n exit 0\nelif [ \"${ACTION}\" = \"install\" ]; then\n echo \"note: skipping skip due to archive install\"\n exit 0\nelif [ \"${SKIP_ACTION}\" = \"none\" ]; then\n echo \"note: skipping skip due to SKIP_ACTION none\"\n exit 0\nelse\n SKIP_ACTION=\"${SKIP_ACTION:-launch}\"\nfi\nPATH=${BUILD_ROOT}/Release:${BUILD_ROOT}/Debug:${BUILD_ROOT}/../../SourcePackages/artifacts/skip/skip/skip.artifactbundle/macos:${PATH}:${HOMEBREW_PREFIX:-/opt/homebrew}/bin\necho \"note: running gradle build with: $(which skip) gradle -p ${PWD}/../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\"\nskip gradle -p ../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\n";
|
shellScript = "if [ \"${SKIP_ZERO}\" != \"\" ]; then\n echo \"note: skipping skip due to SKIP_ZERO\"\n exit 0\nelif [ \"${ENABLE_PREVIEWS}\" = \"YES\" ]; then\n echo \"note: skipping skip due to ENABLE_PREVIEWS\"\n exit 0\nelif [ \"${ACTION}\" = \"install\" ]; then\n echo \"note: skipping skip due to archive install\"\n exit 0\nelif [ \"${PLATFORM_NAME}\" = \"iphonesimulator\" ]; then\n echo \"note: skipping skip gradle for iOS Simulator\"\n exit 0\nelif [ \"${SKIP_ACTION}\" = \"none\" ]; then\n echo \"note: skipping skip due to SKIP_ACTION none\"\n exit 0\nelse\n SKIP_ACTION=\"${SKIP_ACTION:-launch}\"\nfi\nPATH=${BUILD_ROOT}/Release:${BUILD_ROOT}/Debug:${BUILD_ROOT}/../../SourcePackages/artifacts/skip/skip/skip.artifactbundle/macos:${PATH}:${HOMEBREW_PREFIX:-/opt/homebrew}/bin\necho \"note: running gradle build with: $(which skip) gradle -p ${PWD}/../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\"\nskip gradle -p ../Android ${SKIP_ACTION:-launch}${CONFIGURATION:-Debug}\n";
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
@@ -203,7 +203,9 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
|
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
DEVELOPMENT_TEAM = K4E5BZMM4V;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
};
|
};
|
||||||
@@ -213,7 +215,9 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
|
baseConfigurationReference = 496EB72F2A6AE4DE00C1253B /* PediFoods.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
DEVELOPMENT_TEAM = K4E5BZMM4V;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Pedi Foods";
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
};
|
};
|
||||||
@@ -244,7 +248,7 @@
|
|||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym;
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
|||||||
44
pedi-foods/Sources/PediFoods/Components/Buttons.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PrimaryButton: View {
|
||||||
|
let title: String
|
||||||
|
var fullWidth: Bool = true
|
||||||
|
let action: @MainActor @Sendable () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: { action() }) {
|
||||||
|
Text(title)
|
||||||
|
.font(AppTypography.button)
|
||||||
|
.tracking(AppTypography.buttonLetterSpacing)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
.frame(maxWidth: fullWidth ? .infinity : nil)
|
||||||
|
.frame(height: 56)
|
||||||
|
.background(AppColors.primary)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SecondaryButton: View {
|
||||||
|
let title: String
|
||||||
|
var fullWidth: Bool = true
|
||||||
|
let action: @MainActor @Sendable () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: { action() }) {
|
||||||
|
Text(title)
|
||||||
|
.font(AppTypography.button)
|
||||||
|
.tracking(AppTypography.buttonLetterSpacing)
|
||||||
|
.foregroundStyle(AppColors.primary)
|
||||||
|
.frame(maxWidth: fullWidth ? .infinity : nil)
|
||||||
|
.frame(height: 56)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)
|
||||||
|
.stroke(AppColors.primary.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
pedi-foods/Sources/PediFoods/Components/Inputs.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SearchField: View {
|
||||||
|
var placeholder: String
|
||||||
|
@Binding var text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.foregroundStyle(AppColors.secondary)
|
||||||
|
TextField(placeholder, text: $text)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.frame(height: 52)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)
|
||||||
|
.stroke(AppColors.secondary.opacity(0.15), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PillButton: View {
|
||||||
|
let title: String
|
||||||
|
let isActive: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(title)
|
||||||
|
.font(AppTypography.caption)
|
||||||
|
.tracking(AppTypography.captionLetterSpacing)
|
||||||
|
.foregroundStyle(isActive ? AppColors.textInverse : AppColors.secondary)
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(isActive ? AppColors.primary : AppColors.backgroundLight)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
.overlay(
|
||||||
|
Capsule()
|
||||||
|
.stroke(AppColors.secondary.opacity(0.15), lineWidth: isActive ? 0 : 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
131
pedi-foods/Sources/PediFoods/Components/StoreCard.swift
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FeaturedStoreCard: View {
|
||||||
|
let store: FeaturedStoreCardModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)
|
||||||
|
.fill(AppColors.brandSoft)
|
||||||
|
.frame(height: 120)
|
||||||
|
.overlay(
|
||||||
|
Circle()
|
||||||
|
.fill(AppColors.surface)
|
||||||
|
.frame(width: 64, height: 64)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: store.iconName)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(AppColors.primary)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if let promo = store.promoText {
|
||||||
|
Text(promo)
|
||||||
|
.font(AppTypography.caption)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(Color.red)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: {}) {
|
||||||
|
Image(systemName: store.isFavorite ? "heart.fill" : "heart")
|
||||||
|
.foregroundStyle(store.isFavorite ? Color.red : AppColors.textMuted)
|
||||||
|
.padding(8)
|
||||||
|
.background(AppColors.surface)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.shadow(color: AppShadow.soft.color, radius: AppShadow.soft.radius, y: AppShadow.soft.y)
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(store.name)
|
||||||
|
.font(AppTypography.heading3)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color(hex: "#F5B335"))
|
||||||
|
Text(String(format: "%.1f", store.rating))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
Text("(\(store.reviews))")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
Text("·")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
Text(store.distance)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(store.category)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(AppColors.surface)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeaturedStoreCardModel: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let name: String
|
||||||
|
let rating: Double
|
||||||
|
let reviews: String
|
||||||
|
let distance: String
|
||||||
|
let category: String
|
||||||
|
let promoText: String?
|
||||||
|
let isFavorite: Bool
|
||||||
|
let iconName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpecialOfferCard: View {
|
||||||
|
let model: SpecialOfferCardModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous)
|
||||||
|
.fill(
|
||||||
|
LinearGradient(
|
||||||
|
colors: model.colors,
|
||||||
|
startPoint: .leading,
|
||||||
|
endPoint: .trailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white.opacity(0.18))
|
||||||
|
.frame(width: 120, height: 120)
|
||||||
|
.offset(x: 140, y: 10)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(model.title)
|
||||||
|
.font(AppTypography.heading2)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
Text(model.subtitle)
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(AppColors.textInverse.opacity(0.85))
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
}
|
||||||
|
.shadow(color: AppShadow.soft.color, radius: AppShadow.soft.radius, y: AppShadow.soft.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpecialOfferCardModel: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let colors: [Color]
|
||||||
|
}
|
||||||
@@ -1,177 +1,16 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum ContentTab: String, Hashable {
|
|
||||||
case welcome, home, settings
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@AppStorage("tab") var tab = ContentTab.welcome
|
@State var root: RootFlow = DefaultTokenStore().jwt == nil ? .auth : .main
|
||||||
@AppStorage("name") var welcomeName = "Skipper"
|
@State var selectedTab: MainTab = .home
|
||||||
@AppStorage("appearance") var appearance = ""
|
private let tokenStore: TokenStore = DefaultTokenStore()
|
||||||
@State var viewModel = ViewModel()
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView(selection: $tab) {
|
switch root {
|
||||||
NavigationStack {
|
case .auth:
|
||||||
WelcomeView(welcomeName: $welcomeName)
|
AuthFlowView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
|
||||||
}
|
case .main:
|
||||||
.tabItem { Label("Welcome", systemImage: "heart.fill") }
|
MainTabView(selectedTab: $selectedTab, root: $root, tokenStore: tokenStore)
|
||||||
.tag(ContentTab.welcome)
|
|
||||||
|
|
||||||
NavigationStack {
|
|
||||||
ItemListView()
|
|
||||||
.navigationTitle(Text("\(viewModel.items.count) Items"))
|
|
||||||
}
|
|
||||||
.tabItem { Label("Home", systemImage: "house.fill") }
|
|
||||||
.tag(ContentTab.home)
|
|
||||||
|
|
||||||
NavigationStack {
|
|
||||||
SettingsView(appearance: $appearance, welcomeName: $welcomeName)
|
|
||||||
.navigationTitle("Settings")
|
|
||||||
}
|
|
||||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
|
||||||
.tag(ContentTab.settings)
|
|
||||||
}
|
|
||||||
.environment(viewModel)
|
|
||||||
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WelcomeView : View {
|
|
||||||
@State var heartBeating = false
|
|
||||||
@Binding var welcomeName: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
Text("Hello [\(welcomeName)](https://skip.tools)!")
|
|
||||||
.padding()
|
|
||||||
Image(systemName: "heart.fill")
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.scaleEffect(heartBeating ? 1.5 : 1.0)
|
|
||||||
.animation(.easeInOut(duration: 1).repeatForever(), value: heartBeating)
|
|
||||||
.task { heartBeating = true }
|
|
||||||
}
|
|
||||||
.font(.largeTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ItemListView : View {
|
|
||||||
@Environment(ViewModel.self) var viewModel: ViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List {
|
|
||||||
ForEach(viewModel.items) { item in
|
|
||||||
NavigationLink(value: item) {
|
|
||||||
Label {
|
|
||||||
Text(item.itemTitle)
|
|
||||||
} icon: {
|
|
||||||
if item.favorite {
|
|
||||||
Image(systemName: "star.fill")
|
|
||||||
.foregroundStyle(.yellow)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onDelete { offsets in
|
|
||||||
viewModel.items.remove(atOffsets: offsets)
|
|
||||||
}
|
|
||||||
.onMove { fromOffsets, toOffset in
|
|
||||||
viewModel.items.move(fromOffsets: fromOffsets, toOffset: toOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationDestination(for: Item.self) { item in
|
|
||||||
ItemView(item: item)
|
|
||||||
.navigationTitle(item.itemTitle)
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItemGroup {
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
viewModel.items.insert(Item(), at: 0)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Label("Add", systemImage: "plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ItemView : View {
|
|
||||||
@State var item: Item
|
|
||||||
@Environment(ViewModel.self) var viewModel: ViewModel
|
|
||||||
@Environment(\.dismiss) var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
TextField("Title", text: $item.title)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
Toggle("Favorite", isOn: $item.favorite)
|
|
||||||
DatePicker("Date", selection: $item.date)
|
|
||||||
Text("Notes").font(.title3)
|
|
||||||
TextEditor(text: $item.notes)
|
|
||||||
.border(Color.secondary, width: 1.0)
|
|
||||||
}
|
|
||||||
.navigationBarBackButtonHidden()
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToolbarItem(placement: .confirmationAction) {
|
|
||||||
Button("Save") {
|
|
||||||
viewModel.save(item: item)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.disabled(!viewModel.isUpdated(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SettingsView : View {
|
|
||||||
@Binding var appearance: String
|
|
||||||
@Binding var welcomeName: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
TextField("Name", text: $welcomeName)
|
|
||||||
Picker("Appearance", selection: $appearance) {
|
|
||||||
Text("System").tag("")
|
|
||||||
Text("Light").tag("light")
|
|
||||||
Text("Dark").tag("dark")
|
|
||||||
}
|
|
||||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
|
||||||
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
|
||||||
Text("Version \(version) (\(buildNumber))")
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
PlatformHeartView()
|
|
||||||
Text("Powered by [Skip](https://skip.tools)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A view that shows a blue heart on iOS and a green heart on Android.
|
|
||||||
struct PlatformHeartView : View {
|
|
||||||
var body: some View {
|
|
||||||
#if os(Android)
|
|
||||||
ComposeView {
|
|
||||||
HeartComposer()
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
Text(verbatim: "💙")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SKIP
|
|
||||||
/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin.
|
|
||||||
struct HeartComposer : ContentComposer {
|
|
||||||
@Composable func Compose(context: ComposeContext) {
|
|
||||||
androidx.compose.material3.Text("💚", modifier: context.modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
enum RootFlow: Hashable {
|
||||||
|
case auth
|
||||||
|
case main
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MainTab: Hashable {
|
||||||
|
case home
|
||||||
|
case cart
|
||||||
|
case profile
|
||||||
|
}
|
||||||
101
pedi-foods/Sources/PediFoods/DesignSystem/DesignSystem.swift
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Colors
|
||||||
|
|
||||||
|
enum AppColors {
|
||||||
|
static let primary = Color(hex: "#1E6B43")
|
||||||
|
static let secondary = Color(hex: "#2F7C4B")
|
||||||
|
static let tertiary = Color(hex: "#8CFF2E")
|
||||||
|
|
||||||
|
static let brandDark = Color(hex: "#1B5C3A")
|
||||||
|
static let brandSoft = Color(hex: "#E8F2EC")
|
||||||
|
|
||||||
|
static let backgroundLight = Color(hex: "#F3F5F7")
|
||||||
|
static let backgroundDark = Color(hex: "#18230F")
|
||||||
|
static let surface = Color(hex: "#FFFFFF")
|
||||||
|
static let textPrimary = Color(hex: "#1C1F23")
|
||||||
|
static let textInverse = Color(hex: "#FFFFFF")
|
||||||
|
static let textMuted = Color(hex: "#7B8794")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppDarkColors {
|
||||||
|
static let background = Color(hex: "#18230F")
|
||||||
|
static let surface = Color(hex: "#FFFFFF", alpha: 0.05)
|
||||||
|
static let textPrimary = Color(hex: "#FFFFFF")
|
||||||
|
static let textSecondary = Color(hex: "#A3A3A3")
|
||||||
|
static let primary = AppColors.primary
|
||||||
|
static let secondary = AppColors.secondary
|
||||||
|
static let tertiary = AppColors.tertiary
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Typography
|
||||||
|
|
||||||
|
enum AppTypography {
|
||||||
|
static let fontFamily = "Plus Jakarta Sans"
|
||||||
|
|
||||||
|
static let heading1 = Font.custom(fontFamily, size: 28).weight(.heavy)
|
||||||
|
static let heading2 = Font.custom(fontFamily, size: 20).weight(.heavy)
|
||||||
|
static let heading3 = Font.custom(fontFamily, size: 16).weight(.bold)
|
||||||
|
static let body = Font.custom(fontFamily, size: 16).weight(.regular)
|
||||||
|
static let button = Font.custom(fontFamily, size: 14).weight(.heavy)
|
||||||
|
static let caption = Font.custom(fontFamily, size: 10).weight(.bold)
|
||||||
|
static let overline = Font.custom(fontFamily, size: 11).weight(.bold)
|
||||||
|
|
||||||
|
static let bodyLineHeight: CGFloat = 1.6
|
||||||
|
static let buttonLetterSpacing: CGFloat = 0.08
|
||||||
|
static let captionLetterSpacing: CGFloat = 0.12
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
|
||||||
|
enum AppLayout {
|
||||||
|
static let radiusMD: CGFloat = 12
|
||||||
|
static let radiusLG: CGFloat = 16
|
||||||
|
static let radiusXL: CGFloat = 24
|
||||||
|
static let radiusFull: CGFloat = 9999
|
||||||
|
|
||||||
|
static let spacing: [CGFloat] = [4, 8, 12, 16, 24, 32]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Shadow
|
||||||
|
|
||||||
|
struct ShadowSpec {
|
||||||
|
let color: Color
|
||||||
|
let radius: CGFloat
|
||||||
|
let y: CGFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppShadow {
|
||||||
|
static let soft = ShadowSpec(color: Color.black.opacity(0.08), radius: 20, y: 6)
|
||||||
|
static let glow = ShadowSpec(color: AppColors.tertiary.opacity(0.2), radius: 24, y: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
init(hex: String, alpha: Double = 1.0) {
|
||||||
|
let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||||
|
var int: UInt64 = 0
|
||||||
|
Scanner(string: cleaned).scanHexInt64(&int)
|
||||||
|
|
||||||
|
let r, g, b: UInt64
|
||||||
|
switch cleaned.count {
|
||||||
|
case 6: // RRGGBB
|
||||||
|
r = (int >> 16) & 0xFF
|
||||||
|
g = (int >> 8) & 0xFF
|
||||||
|
b = int & 0xFF
|
||||||
|
case 3: // RGB
|
||||||
|
r = ((int >> 8) & 0xF) * 17
|
||||||
|
g = ((int >> 4) & 0xF) * 17
|
||||||
|
b = (int & 0xF) * 17
|
||||||
|
default:
|
||||||
|
r = 0; g = 0; b = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(.sRGB,
|
||||||
|
red: Double(r) / 255,
|
||||||
|
green: Double(g) / 255,
|
||||||
|
blue: Double(b) / 255,
|
||||||
|
opacity: alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
|
"·" : {
|
||||||
|
"comment" : "A period character used to separate different pieces of information in a list.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"(%@)" : {
|
||||||
|
"comment" : "A text element showing the number of reviews for a store. The content inside the parentheses is the number of reviews.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lld Items" : {
|
"%lld Items" : {
|
||||||
"comment" : "Header title for a list that contains the number of items",
|
"comment" : "Header title for a list that contains the number of items",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -32,6 +41,7 @@
|
|||||||
},
|
},
|
||||||
"Add" : {
|
"Add" : {
|
||||||
"comment" : "Button in items list that will cause a new item to be added",
|
"comment" : "Button in items list that will cause a new item to be added",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -59,8 +69,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Ajuda" : {
|
||||||
|
"comment" : "A link to the help section of the app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Appearance" : {
|
"Appearance" : {
|
||||||
"comment" : "Settings select label for the interface style of the controls (light, dark, or default)",
|
"comment" : "Settings select label for the interface style of the controls (light, dark, or default)",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -88,8 +103,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Cadastro" : {
|
||||||
|
"comment" : "A label displayed above the registration form.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Cancel" : {
|
"Cancel" : {
|
||||||
"comment" : "Button title indicating that the operation should be cancelled",
|
"comment" : "Button title indicating that the operation should be cancelled",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -117,8 +137,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Canggu, Kuta Utara, Bali" : {
|
||||||
|
"comment" : "The address of the user's delivery location.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Carrinho" : {
|
||||||
|
"comment" : "A label for the cart section of the app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Categories" : {
|
||||||
|
"comment" : "A section header that lists available food categories.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Criar conta" : {
|
||||||
|
"comment" : "A link that navigates to the registration screen.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Daniel Loverde" : {
|
||||||
|
"comment" : "The name of the user in the profile view header.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"daniel@pedifoods.com.br" : {
|
||||||
|
"comment" : "An email address displayed in the user's profile.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Dark" : {
|
"Dark" : {
|
||||||
"comment" : "Menu item indicating that the appearance should be in dark mode",
|
"comment" : "Menu item indicating that the appearance should be in dark mode",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -148,6 +193,7 @@
|
|||||||
},
|
},
|
||||||
"Date" : {
|
"Date" : {
|
||||||
"comment" : "Item editor form label for the Date field",
|
"comment" : "Item editor form label for the Date field",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -175,8 +221,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"DELIVERY LOCATION" : {
|
||||||
|
"comment" : "A label describing the delivery location section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Descubra lorem ipsum ba bla" : {
|
||||||
|
"comment" : "A welcome message displayed on the login screen.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"DL" : {
|
||||||
|
"comment" : "An abbreviation for \"Delivery Lady\" used in the user's profile picture circle.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Email" : {
|
||||||
|
"comment" : "A text field for entering the user's email address.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Enderecos" : {
|
||||||
|
"comment" : "A link to the user's address list.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Enviado para %@" : {
|
||||||
|
"comment" : "A label below the \"OTP\" title that indicates the email to which the OTP was sent. The argument is the email address to which the OTP was sent.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Favorite" : {
|
"Favorite" : {
|
||||||
"comment" : "Item editor title label for marking the item as a favorite",
|
"comment" : "Item editor title label for marking the item as a favorite",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -206,6 +277,7 @@
|
|||||||
},
|
},
|
||||||
"Hello [%@](https://skip.tools)!" : {
|
"Hello [%@](https://skip.tools)!" : {
|
||||||
"comment" : "Welcome tab contents",
|
"comment" : "Welcome tab contents",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -233,6 +305,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Historico vazio" : {
|
||||||
|
"comment" : "A description displayed when a user has no orders in their history.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Home" : {
|
"Home" : {
|
||||||
"comment" : "Tab bar item title for the Home tab",
|
"comment" : "Tab bar item title for the Home tab",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -264,6 +340,7 @@
|
|||||||
},
|
},
|
||||||
"Light" : {
|
"Light" : {
|
||||||
"comment" : "Menu item indicating that the appearance should be in light mode",
|
"comment" : "Menu item indicating that the appearance should be in light mode",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -293,6 +370,7 @@
|
|||||||
},
|
},
|
||||||
"Name" : {
|
"Name" : {
|
||||||
"comment" : "Placeholder title for the Name field in a form",
|
"comment" : "Placeholder title for the Name field in a form",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -320,8 +398,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Não tem conta ainda?" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Nome" : {
|
||||||
|
"comment" : "A label for the user to input their name.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Notes" : {
|
"Notes" : {
|
||||||
"comment" : "Item editor form label for the Notes field",
|
"comment" : "Item editor form label for the Notes field",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -349,8 +435,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"O que vai querer pedir hoje?" : {
|
||||||
|
"comment" : "A heading displayed above a search bar in the home view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"OTP" : {
|
||||||
|
"comment" : "The title of a view that asks the user to enter an OTP code.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Pedidos" : {
|
||||||
|
"comment" : "Navigation links for different sections of the user's profile.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"PediFoods" : {
|
||||||
|
"comment" : "The name of the app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Perfil" : {
|
||||||
|
"comment" : "A label for the \"Profile\" tab in the main tab view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Powered by [Skip](https://skip.tools)" : {
|
"Powered by [Skip](https://skip.tools)" : {
|
||||||
"comment" : "Link markdown text for the Powered by… label",
|
"comment" : "Link markdown text for the Powered by… label",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -380,6 +487,7 @@
|
|||||||
},
|
},
|
||||||
"Save" : {
|
"Save" : {
|
||||||
"comment" : "Button title indicating that the current contents should be saved",
|
"comment" : "Button title indicating that the current contents should be saved",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -409,6 +517,7 @@
|
|||||||
},
|
},
|
||||||
"Settings" : {
|
"Settings" : {
|
||||||
"comment" : "Tab bar item title for the Settings tab",
|
"comment" : "Tab bar item title for the Settings tab",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -436,8 +545,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Seu carrinho esta vazio" : {
|
||||||
|
"comment" : "A description displayed when a user's cart is empty.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"System" : {
|
"System" : {
|
||||||
"comment" : "Menu item indicating that the appearance should be in the default system mode",
|
"comment" : "Menu item indicating that the appearance should be in the default system mode",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -465,8 +579,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Telefone" : {
|
||||||
|
"comment" : "A label for a text field where a user can enter their phone number.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Title" : {
|
"Title" : {
|
||||||
"comment" : "Label for the item editor form indicating the title of the item",
|
"comment" : "Label for the item editor form indicating the title of the item",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -496,6 +615,7 @@
|
|||||||
},
|
},
|
||||||
"Version %@ (%@)" : {
|
"Version %@ (%@)" : {
|
||||||
"comment" : "Settings label showing the current version of the app",
|
"comment" : "Settings label showing the current version of the app",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -531,6 +651,7 @@
|
|||||||
},
|
},
|
||||||
"Welcome" : {
|
"Welcome" : {
|
||||||
"comment" : "Tab bar item title for the Welcome tab",
|
"comment" : "Tab bar item title for the Welcome tab",
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"es" : {
|
"es" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -559,5 +680,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.1"
|
||||||
}
|
}
|
||||||
BIN
pedi-foods/Sources/PediFoods/Resources/login_dark.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
pedi-foods/Sources/PediFoods/Resources/login_light.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"fill-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : {
|
||||||
|
"automatic-gradient" : "srgb:0.99020,0.98039,1.00000,1.00000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : {
|
||||||
|
"solid" : "srgb:0.00000,0.00000,0.00000,1.00000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"blend-mode-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : "normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : "normal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fill-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearance" : "dark",
|
||||||
|
"value" : {
|
||||||
|
"solid" : "srgb:0.00000,0.50000,0.04260,1.00000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"glass" : false,
|
||||||
|
"hidden" : false,
|
||||||
|
"image-name" : "pedifoods_big.png",
|
||||||
|
"name" : "pedifoods_big",
|
||||||
|
"position-specializations" : [
|
||||||
|
{
|
||||||
|
"value" : {
|
||||||
|
"scale" : 1,
|
||||||
|
"translation-in-points" : [
|
||||||
|
137.390625,
|
||||||
|
11.984375
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "square",
|
||||||
|
"value" : {
|
||||||
|
"scale" : 0.35,
|
||||||
|
"translation-in-points" : [
|
||||||
|
8,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"squares" : [
|
||||||
|
"iOS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
pedi-foods/Sources/PediFoods/Resources/pedifoods.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
pedi-foods/Sources/PediFoods/Resources/pedifoods_dark.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
pedi-foods/Sources/PediFoods/Resources/pin_image_app.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
pedi-foods/Sources/PediFoods/Resources/pin_image_app_dark.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
104
pedi-foods/Sources/PediFoods/Services/ApiClient.swift
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import Foundation
|
||||||
|
#if canImport(FoundationNetworking)
|
||||||
|
import FoundationNetworking
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum NetworkError: Error, LocalizedError {
|
||||||
|
case invalidURL
|
||||||
|
case invalidResponse
|
||||||
|
case httpError(Int, String?)
|
||||||
|
case decodeError
|
||||||
|
case rateLimited
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .invalidURL: return "URL invalida"
|
||||||
|
case .invalidResponse: return "Resposta invalida do servidor"
|
||||||
|
case .httpError(let code, let message):
|
||||||
|
return message ?? "Erro HTTP (\(code))"
|
||||||
|
case .decodeError: return "Erro ao interpretar dados"
|
||||||
|
case .rateLimited: return "Muitas requisicoes. Tente novamente."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ApiRequest {
|
||||||
|
let path: String
|
||||||
|
let method: String
|
||||||
|
let module: ApiModule
|
||||||
|
let requiresAuth: Bool
|
||||||
|
let queryItems: [URLQueryItem]
|
||||||
|
let body: Data?
|
||||||
|
|
||||||
|
init(path: String,
|
||||||
|
method: String = "GET",
|
||||||
|
module: ApiModule = .none,
|
||||||
|
requiresAuth: Bool = true,
|
||||||
|
queryItems: [URLQueryItem] = [],
|
||||||
|
body: Data? = nil) {
|
||||||
|
self.path = path
|
||||||
|
self.method = method
|
||||||
|
self.module = module
|
||||||
|
self.requiresAuth = requiresAuth
|
||||||
|
self.queryItems = queryItems
|
||||||
|
self.body = body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ApiClient {
|
||||||
|
private let session: URLSession
|
||||||
|
private let tokenStore: TokenStore
|
||||||
|
|
||||||
|
init(session: URLSession = .shared, tokenStore: TokenStore = DefaultTokenStore()) {
|
||||||
|
self.session = session
|
||||||
|
self.tokenStore = tokenStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func send<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||||
|
let url = try buildURL(path: request.path, query: request.queryItems)
|
||||||
|
var urlRequest = URLRequest(url: url)
|
||||||
|
urlRequest.httpMethod = request.method
|
||||||
|
urlRequest.httpBody = request.body
|
||||||
|
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
||||||
|
if let token = ApiConfig.token(for: request.module) {
|
||||||
|
urlRequest.setValue(token, forHTTPHeaderField: "Atomenta-Token")
|
||||||
|
}
|
||||||
|
if request.requiresAuth, let jwt = tokenStore.jwt {
|
||||||
|
urlRequest.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
let (data, response) = try await session.data(for: urlRequest)
|
||||||
|
guard let http = response as? HTTPURLResponse else {
|
||||||
|
throw NetworkError.invalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
if http.statusCode == 429 {
|
||||||
|
throw NetworkError.rateLimited
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(200...299).contains(http.statusCode) {
|
||||||
|
let message = String(data: data, encoding: .utf8)
|
||||||
|
throw NetworkError.httpError(http.statusCode, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
return try JSONDecoder().decode(T.self, from: data)
|
||||||
|
} catch {
|
||||||
|
throw NetworkError.decodeError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
|
||||||
|
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
|
||||||
|
throw NetworkError.invalidURL
|
||||||
|
}
|
||||||
|
components.path = components.path.appending(path)
|
||||||
|
if !query.isEmpty {
|
||||||
|
components.queryItems = query
|
||||||
|
}
|
||||||
|
guard let url = components.url else { throw NetworkError.invalidURL }
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
30
pedi-foods/Sources/PediFoods/Services/ApiConfig.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ApiModule {
|
||||||
|
case app
|
||||||
|
case customer
|
||||||
|
case store
|
||||||
|
case resource
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ApiConfig {
|
||||||
|
static var baseURL: URL {
|
||||||
|
let raw = ProcessInfo.processInfo.environment["ATOMENTA_API_URL"] ?? "http://127.0.0.1:3001"
|
||||||
|
return URL(string: raw) ?? URL(string: "http://127.0.0.1:3001")!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokens provided by backend modules
|
||||||
|
static let storeToken = "550e8400-e29b-41d4-a716-446655440008"
|
||||||
|
static let customerToken = "550e8400-e29b-41d4-a716-44665544000a"
|
||||||
|
static let resourceToken = "550e8400-e29b-41d4-a716-446655440009"
|
||||||
|
|
||||||
|
static func token(for module: ApiModule) -> String? {
|
||||||
|
switch module {
|
||||||
|
case .store: return storeToken
|
||||||
|
case .customer: return customerToken
|
||||||
|
case .resource: return resourceToken
|
||||||
|
case .app, .none: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
pedi-foods/Sources/PediFoods/Services/ApiService.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ApiEnvelope<T: Decodable>: Decodable {
|
||||||
|
let error: Bool
|
||||||
|
let code: String?
|
||||||
|
let message: String?
|
||||||
|
let result: T?
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ApiService {
|
||||||
|
private let client: ApiClient
|
||||||
|
private var tokenStore: TokenStore
|
||||||
|
|
||||||
|
init(client: ApiClient = ApiClient(), tokenStore: TokenStore = DefaultTokenStore()) {
|
||||||
|
self.client = client
|
||||||
|
self.tokenStore = tokenStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Auth
|
||||||
|
|
||||||
|
func requestOtp(email: String) async throws -> ApiEnvelope<EmptyResult> {
|
||||||
|
let body = try JSONEncoder().encode(["email": email])
|
||||||
|
let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body)
|
||||||
|
return try await client.send(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOtp(email: String, otp: String) async throws -> ApiEnvelope<LoginResult> {
|
||||||
|
let body = try JSONEncoder().encode(["email": email, "otp": otp])
|
||||||
|
let req = ApiRequest(path: "/api/customer/login", method: "POST", module: .customer, requiresAuth: false, body: body)
|
||||||
|
let response: ApiEnvelope<LoginResult> = try await client.send(req)
|
||||||
|
if let token = response.result?.token {
|
||||||
|
tokenStore.jwt = token
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func profile() async throws -> ApiEnvelope<CustomerProfile> {
|
||||||
|
let req = ApiRequest(path: "/api/customer/profile", method: "GET", module: .customer, requiresAuth: true)
|
||||||
|
return try await client.send(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Stores
|
||||||
|
|
||||||
|
func listStores(lat: Double, lng: Double, category: String? = nil, search: String? = nil) async throws -> ApiEnvelope<[StoreSummary]> {
|
||||||
|
var items = [
|
||||||
|
URLQueryItem(name: "lat", value: String(lat)),
|
||||||
|
URLQueryItem(name: "lng", value: String(lng))
|
||||||
|
]
|
||||||
|
if let category {
|
||||||
|
items.append(URLQueryItem(name: "category", value: category))
|
||||||
|
}
|
||||||
|
if let search {
|
||||||
|
items.append(URLQueryItem(name: "search", value: search))
|
||||||
|
}
|
||||||
|
let req = ApiRequest(path: "/api/app/stores", method: "GET", module: .app, requiresAuth: true, queryItems: items)
|
||||||
|
return try await client.send(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DTOs
|
||||||
|
|
||||||
|
struct EmptyResult: Decodable {}
|
||||||
|
|
||||||
|
struct LoginResult: Decodable {
|
||||||
|
let token: String
|
||||||
|
let customer: CustomerProfile?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomerProfile: Decodable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let email: String
|
||||||
|
let phoneNumber: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StoreSummary: Decodable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let logo: String?
|
||||||
|
let cover: String?
|
||||||
|
let category: String?
|
||||||
|
let rating: Double?
|
||||||
|
let deliveryTime: String?
|
||||||
|
let deliveryFee: Double?
|
||||||
|
let distance: Double?
|
||||||
|
let isOpen: Bool?
|
||||||
|
let statusLabel: String?
|
||||||
|
}
|
||||||
20
pedi-foods/Sources/PediFoods/Services/TokenStore.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol TokenStore: AnyObject {
|
||||||
|
var jwt: String? { get set }
|
||||||
|
func clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DefaultTokenStore: TokenStore {
|
||||||
|
private let key = "auth_jwt"
|
||||||
|
private let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
var jwt: String? {
|
||||||
|
get { defaults.string(forKey: key) }
|
||||||
|
set { defaults.set(newValue, forKey: key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func clear() {
|
||||||
|
defaults.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
149
pedi-foods/Sources/PediFoods/Views/Auth/AuthFlowView.swift
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AuthFlowView: View {
|
||||||
|
@Binding var root: RootFlow
|
||||||
|
@Binding var selectedTab: MainTab
|
||||||
|
let tokenStore: TokenStore
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
LoginView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoginView: View {
|
||||||
|
@Binding var root: RootFlow
|
||||||
|
@Binding var selectedTab: MainTab
|
||||||
|
let tokenStore: TokenStore
|
||||||
|
@State var email = ""
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
@ViewBuilder private var logoImage: some View {
|
||||||
|
#if os(Android)
|
||||||
|
Image(colorScheme == .dark ? "pedifoods_dark" : "pedifoods", bundle: .module)
|
||||||
|
.resizable()
|
||||||
|
#else
|
||||||
|
SwiftUI.Image("pedifoods")
|
||||||
|
.resizable()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var pinHeroImage: some View {
|
||||||
|
#if os(Android)
|
||||||
|
Image(colorScheme == .dark ? "pin_image_app_dark" : "pin_image_app", bundle: .module)
|
||||||
|
.resizable()
|
||||||
|
#else
|
||||||
|
SwiftUI.Image("pin_image_app")
|
||||||
|
.resizable()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack(alignment: .top) {
|
||||||
|
(colorScheme == .dark ? AppColors.backgroundDark : AppColors.backgroundLight)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
pinHeroImage
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(height: 520)
|
||||||
|
.offset(y: -90)
|
||||||
|
.mask(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [.black, .black, .black.opacity(0.0)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Spacer().frame(height: 280)
|
||||||
|
logoImage
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(height: 200)
|
||||||
|
|
||||||
|
Text("Descubra lorem ipsum ba bla")
|
||||||
|
.font(AppTypography.heading1)
|
||||||
|
.foregroundStyle(colorScheme == .dark ? AppColors.textInverse : AppColors.textPrimary)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 28)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
PrimaryButton(title: "ENTRAR") {
|
||||||
|
// TODO: integrar com ApiService e OTP
|
||||||
|
tokenStore.jwt = "demo-token"
|
||||||
|
selectedTab = .home
|
||||||
|
root = .main
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 28)
|
||||||
|
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("Não tem conta ainda?")
|
||||||
|
.foregroundStyle(colorScheme == .dark ? AppColors.textInverse.opacity(0.9) : AppColors.textPrimary)
|
||||||
|
NavigationLink("Criar conta") {
|
||||||
|
RegistrationView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
|
||||||
|
}
|
||||||
|
.foregroundStyle(AppColors.primary)
|
||||||
|
}
|
||||||
|
.font(AppTypography.body)
|
||||||
|
|
||||||
|
Spacer().frame(height: 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegistrationView: View {
|
||||||
|
@Binding var root: RootFlow
|
||||||
|
@Binding var selectedTab: MainTab
|
||||||
|
let tokenStore: TokenStore
|
||||||
|
@State var name = ""
|
||||||
|
@State var email = ""
|
||||||
|
@State var phone = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Text("Cadastro")
|
||||||
|
.font(AppTypography.heading1)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
TextField("Nome", text: $name)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
TextField("Email", text: $email)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
TextField("Telefone", text: $phone)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
|
PrimaryButton(title: "Enviar OTP") {
|
||||||
|
// TODO: solicitar OTP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(24)
|
||||||
|
}
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OtpView: View {
|
||||||
|
let email: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Text("OTP")
|
||||||
|
.font(AppTypography.heading1)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
|
||||||
|
Text("Enviado para \(email)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.secondary)
|
||||||
|
}
|
||||||
|
.padding(24)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
pedi-foods/Sources/PediFoods/Views/Main/CartView.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CartView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Text("Carrinho")
|
||||||
|
.font(AppTypography.heading1)
|
||||||
|
|
||||||
|
Text("Seu carrinho esta vazio")
|
||||||
|
.foregroundStyle(AppColors.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
}
|
||||||
|
}
|
||||||
399
pedi-foods/Sources/PediFoods/Views/Main/HomeView.swift
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
import SwiftUI
|
||||||
|
#if os(iOS)
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct HomeView: View {
|
||||||
|
@State var searchText = ""
|
||||||
|
@State var selectedCategory = "Stores"
|
||||||
|
@State var scrollOffset: CGFloat = 0
|
||||||
|
|
||||||
|
private let categories: [CategoryModel] = [
|
||||||
|
.init(title: "Stores", systemIcon: "storefront"),
|
||||||
|
.init(title: "Asian", systemIcon: "fork.knife"),
|
||||||
|
.init(title: "Breakfast", systemIcon: "sun.max"),
|
||||||
|
.init(title: "Pizza", systemIcon: "takeoutbag.and.cup.and.straw"),
|
||||||
|
.init(title: "Dessert", systemIcon: "cup.and.saucer")
|
||||||
|
]
|
||||||
|
|
||||||
|
private let featuredStores: [FeaturedStoreCardModel] = [
|
||||||
|
.init(
|
||||||
|
name: "Burger Kingdom",
|
||||||
|
rating: 4.9,
|
||||||
|
reviews: "1.2k",
|
||||||
|
distance: "1.2 km",
|
||||||
|
category: "Fast Food",
|
||||||
|
promoText: "10% OFF",
|
||||||
|
isFavorite: false,
|
||||||
|
iconName: "takeoutbag.and.cup.and.straw"
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
name: "Sushi House",
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: "840",
|
||||||
|
distance: "2.2 km",
|
||||||
|
category: "Japanese",
|
||||||
|
promoText: nil,
|
||||||
|
isFavorite: true,
|
||||||
|
iconName: "fork.knife"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
private let nearbyStores: [FeaturedStoreCardModel] = [
|
||||||
|
.init(
|
||||||
|
name: "Pizza Prime",
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: "510",
|
||||||
|
distance: "1.8 km",
|
||||||
|
category: "Pizza",
|
||||||
|
promoText: nil,
|
||||||
|
isFavorite: false,
|
||||||
|
iconName: "takeoutbag.and.cup.and.straw"
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
name: "Aoyama",
|
||||||
|
rating: 4.9,
|
||||||
|
reviews: "2.4k",
|
||||||
|
distance: "3.1 km",
|
||||||
|
category: "Japanese",
|
||||||
|
promoText: nil,
|
||||||
|
isFavorite: true,
|
||||||
|
iconName: "fork.knife"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
private let specials: [SpecialOfferCardModel] = [
|
||||||
|
.init(title: "Get 50% OFF", subtitle: "For your first order", colors: [Color(hex: "#1E6B43"), Color(hex: "#58A56C")]),
|
||||||
|
.init(title: "Enjoy spicy day", subtitle: "Delivery in 20 min", colors: [Color(hex: "#F04B3E"), Color(hex: "#FF8C42")])
|
||||||
|
]
|
||||||
|
|
||||||
|
private let headerExpandedHeight: CGFloat = 260
|
||||||
|
private let headerCollapsedHeight: CGFloat = 120
|
||||||
|
private let contentTopPadding: CGFloat = 296
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let collapseProgress = clamp(value: -scrollOffset / (headerExpandedHeight - headerCollapsedHeight), lower: 0, upper: 1)
|
||||||
|
let headerHeight = headerExpandedHeight - (headerExpandedHeight - headerCollapsedHeight) * collapseProgress
|
||||||
|
let headerPadding = headerExpandedHeight - (headerExpandedHeight - headerCollapsedHeight) * collapseProgress
|
||||||
|
|
||||||
|
return ZStack(alignment: .top) {
|
||||||
|
#if os(iOS)
|
||||||
|
TrackableScrollView(onOffsetChange: { value in
|
||||||
|
scrollOffset = min(0, -value)
|
||||||
|
}) {
|
||||||
|
contentStack
|
||||||
|
.padding(.top, headerPadding + 16)
|
||||||
|
.padding(.bottom, 24)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
#else
|
||||||
|
ScrollView {
|
||||||
|
contentStack
|
||||||
|
.padding(.top, headerPadding + 16)
|
||||||
|
.padding(.bottom, 24)
|
||||||
|
}
|
||||||
|
.coordinateSpace(name: "scroll")
|
||||||
|
.onPreferenceChange(ScrollOffsetKey.self) { value in
|
||||||
|
scrollOffset = min(0, value)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
header(collapseProgress: collapseProgress, height: headerHeight)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .top)
|
||||||
|
}
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
.ignoresSafeArea(edges: .top)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var contentStack: some View {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
scrollOffsetMarker
|
||||||
|
|
||||||
|
categoriesSection
|
||||||
|
|
||||||
|
section(title: "Featured") {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
ForEach(featuredStores) { store in
|
||||||
|
FeaturedStoreCard(store: store)
|
||||||
|
.frame(width: 190)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section(title: "#SpecialForYou") {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
ForEach(specials) { item in
|
||||||
|
SpecialOfferCard(model: item)
|
||||||
|
.frame(width: 260, height: 120)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section(title: "Near you") {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
ForEach(nearbyStores) { store in
|
||||||
|
FeaturedStoreCard(store: store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func header(collapseProgress: CGFloat, height: CGFloat) -> some View {
|
||||||
|
let titleOpacity = 1 - clamp(value: collapseProgress * 1.2, lower: 0, upper: 1)
|
||||||
|
let topRowOpacity = 1 - clamp(value: collapseProgress * 1.4, lower: 0, upper: 1)
|
||||||
|
|
||||||
|
return ZStack(alignment: .top) {
|
||||||
|
RoundedRectangle(cornerRadius: 32, style: .continuous)
|
||||||
|
.fill(AppColors.brandDark)
|
||||||
|
.frame(height: height)
|
||||||
|
.overlay(headerRings.opacity(1 - collapseProgress), alignment: .topTrailing)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Spacer().frame(height: 20)
|
||||||
|
HStack(alignment: .center, spacing: 12) {
|
||||||
|
Circle()
|
||||||
|
.fill(AppColors.brandSoft)
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: "person.fill")
|
||||||
|
.foregroundStyle(AppColors.brandDark)
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("DELIVERY LOCATION")
|
||||||
|
.font(AppTypography.overline)
|
||||||
|
.tracking(AppTypography.captionLetterSpacing)
|
||||||
|
.foregroundStyle(AppColors.brandSoft)
|
||||||
|
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("Canggu, Kuta Utara, Bali")
|
||||||
|
.font(AppTypography.heading3)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.brandSoft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white.opacity(0.18))
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: "bell")
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.opacity(topRowOpacity)
|
||||||
|
.offset(y: collapseProgress * -12)
|
||||||
|
|
||||||
|
Text("O que vai querer pedir hoje?")
|
||||||
|
.font(AppTypography.heading1)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.opacity(titleOpacity)
|
||||||
|
.offset(y: collapseProgress * -20)
|
||||||
|
|
||||||
|
SearchBar(placeholder: "Search menu, restaurant or craving", text: $searchText)
|
||||||
|
.offset(y: collapseProgress * -140)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.top, 18)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var scrollOffsetMarker: some View {
|
||||||
|
#if os(iOS)
|
||||||
|
EmptyView()
|
||||||
|
#else
|
||||||
|
ScrollOffsetReader()
|
||||||
|
.offset(y: -contentTopPadding)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func section<Content: View>(title: String, @ViewBuilder content: () -> Content) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text(title)
|
||||||
|
.font(AppTypography.heading2)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var categoriesSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Categories")
|
||||||
|
.font(AppTypography.heading2)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ForEach(categories) { category in
|
||||||
|
CategoryChip(
|
||||||
|
title: category.title,
|
||||||
|
systemIcon: category.systemIcon,
|
||||||
|
isActive: category.title == selectedCategory
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
selectedCategory = category.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headerRings: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
||||||
|
.frame(width: 180, height: 180)
|
||||||
|
.offset(x: 40, y: -10)
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
||||||
|
.frame(width: 130, height: 130)
|
||||||
|
.offset(x: 70, y: 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CategoryModel: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let title: String
|
||||||
|
let systemIcon: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CategoryChip: View {
|
||||||
|
let title: String
|
||||||
|
let systemIcon: String
|
||||||
|
let isActive: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: systemIcon)
|
||||||
|
.font(.caption)
|
||||||
|
Text(title)
|
||||||
|
.font(AppTypography.heading3)
|
||||||
|
}
|
||||||
|
.foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(isActive ? AppColors.brandDark : AppColors.surface)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchBar: View {
|
||||||
|
let placeholder: String
|
||||||
|
@Binding var text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
TextField(placeholder, text: $text)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "slider.horizontal.3")
|
||||||
|
.foregroundStyle(AppColors.textMuted)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.frame(height: 52)
|
||||||
|
.background(AppColors.surface)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScrollOffsetKey: PreferenceKey {
|
||||||
|
static let defaultValue: CGFloat = 0
|
||||||
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
|
value = nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScrollOffsetReader: View {
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear
|
||||||
|
.preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .named("scroll")).minY)
|
||||||
|
}
|
||||||
|
.frame(height: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(value: CGFloat, lower: CGFloat, upper: CGFloat) -> CGFloat {
|
||||||
|
min(max(value, lower), upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@MainActor
|
||||||
|
struct TrackableScrollView<Content: View>: UIViewRepresentable {
|
||||||
|
let onOffsetChange: (CGFloat) -> Void
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(onOffsetChange: @escaping (CGFloat) -> Void, @ViewBuilder content: () -> Content) {
|
||||||
|
self.onOffsetChange = onOffsetChange
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(onOffsetChange: onOffsetChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIScrollView {
|
||||||
|
let scrollView = UIScrollView()
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.alwaysBounceVertical = true
|
||||||
|
|
||||||
|
let host = UIHostingController(rootView: content)
|
||||||
|
host.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
host.view.backgroundColor = .clear
|
||||||
|
scrollView.addSubview(host.view)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
host.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
||||||
|
host.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
||||||
|
host.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
||||||
|
host.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
||||||
|
host.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
context.coordinator.hostingController = host
|
||||||
|
scrollView.delegate = context.coordinator
|
||||||
|
return scrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIScrollView, context: Context) {
|
||||||
|
context.coordinator.hostingController?.rootView = content
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, UIScrollViewDelegate {
|
||||||
|
var hostingController: UIHostingController<Content>?
|
||||||
|
let onOffsetChange: (CGFloat) -> Void
|
||||||
|
|
||||||
|
init(onOffsetChange: @escaping (CGFloat) -> Void) {
|
||||||
|
self.onOffsetChange = onOffsetChange
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
|
onOffsetChange(scrollView.contentOffset.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
30
pedi-foods/Sources/PediFoods/Views/Main/MainTabView.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MainTabView: View {
|
||||||
|
@Binding var selectedTab: MainTab
|
||||||
|
@Binding var root: RootFlow
|
||||||
|
let tokenStore: TokenStore
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TabView(selection: $selectedTab) {
|
||||||
|
NavigationStack {
|
||||||
|
HomeView()
|
||||||
|
}
|
||||||
|
.tabItem { Label("Home", systemImage: "house.fill") }
|
||||||
|
.tag(MainTab.home)
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
CartView()
|
||||||
|
}
|
||||||
|
.tabItem { Label("Carrinho", systemImage: "cart.fill") }
|
||||||
|
.tag(MainTab.cart)
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
ProfileView(root: $root, selectedTab: $selectedTab, tokenStore: tokenStore)
|
||||||
|
}
|
||||||
|
.tabItem { Label("Perfil", systemImage: "person.fill") }
|
||||||
|
.tag(MainTab.profile)
|
||||||
|
}
|
||||||
|
.tint(AppColors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
74
pedi-foods/Sources/PediFoods/Views/Main/ProfileView.swift
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ProfileView: View {
|
||||||
|
@Binding var root: RootFlow
|
||||||
|
@Binding var selectedTab: MainTab
|
||||||
|
let tokenStore: TokenStore
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
header
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
NavigationLink("Pedidos") {
|
||||||
|
OrdersView()
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink("Enderecos") {
|
||||||
|
Text("Enderecos")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink("Ajuda") {
|
||||||
|
Text("Ajuda")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(AppTypography.body)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
SecondaryButton(title: "Sair") {
|
||||||
|
tokenStore.clear()
|
||||||
|
root = .auth
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
}
|
||||||
|
.padding(.top, 24)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(AppColors.primary)
|
||||||
|
.frame(width: 72, height: 72)
|
||||||
|
.overlay(
|
||||||
|
Text("DL")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundStyle(AppColors.textInverse)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text("Daniel Loverde")
|
||||||
|
.font(AppTypography.heading2)
|
||||||
|
.foregroundStyle(AppColors.textPrimary)
|
||||||
|
|
||||||
|
Text("daniel@pedifoods.com.br")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(AppColors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OrdersView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Text("Pedidos")
|
||||||
|
.font(AppTypography.heading1)
|
||||||
|
Text("Historico vazio")
|
||||||
|
.foregroundStyle(AppColors.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(AppColors.backgroundLight)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,12 +14,11 @@ Este arquivo deve ser usado como **prompt base canônico** para qualquer IA, des
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
colors: {
|
colors: {
|
||||||
primary: "#8CFF2E", // ação principal, CTAs, destaque
|
primary: "#388E3C", // ação principal, CTAs, destaque
|
||||||
secondary: "#388E3C", // ações secundárias, títulos
|
|
||||||
backgroundLight: "#F7F8F5",
|
backgroundLight: "#F7F8F5",
|
||||||
backgroundDark: "#18230F",
|
backgroundDark: "#18230F",
|
||||||
textPrimary: "#333333",
|
textPrimary: "#FFFFFF",
|
||||||
textInverse: "#FFFFFF"
|
textInverse: "#333333"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,21 +30,6 @@ Uso:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.2 🌙 Dark Theme
|
|
||||||
|
|
||||||
Dark mode nativo, não invertido.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
dark: {
|
|
||||||
background: "#18230F",
|
|
||||||
surface: "rgba(255,255,255,0.05)",
|
|
||||||
textPrimary: "#FFFFFF",
|
|
||||||
textSecondary: "#A3A3A3",
|
|
||||||
primary: "#8CFF2E",
|
|
||||||
secondary: "#388E3C"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.3 🔤 Tipografia
|
### 1.3 🔤 Tipografia
|
||||||
@@ -70,7 +54,7 @@ typography: {
|
|||||||
body: {
|
body: {
|
||||||
size: "16px",
|
size: "16px",
|
||||||
weight: 400,
|
weight: 400,
|
||||||
lineHeight: "1.6"
|
lineHeight: "0"
|
||||||
},
|
},
|
||||||
|
|
||||||
button: {
|
button: {
|
||||||
@@ -195,30 +179,3 @@ Icon {
|
|||||||
active?: boolean
|
active?: boolean
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. IMPLEMENTAÇÃO (TAILWIND-FIRST)
|
|
||||||
|
|
||||||
### 3.1 Tokens no Tailwind
|
|
||||||
|
|
||||||
```ts
|
|
||||||
theme.extend.colors = {
|
|
||||||
primary: "#8CFF2E",
|
|
||||||
secondary: "#388E3C",
|
|
||||||
backgroundLight: "#F7F8F5",
|
|
||||||
backgroundDark: "#18230F",
|
|
||||||
typographyDark: "#333333"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. PRINCÍPIOS DO SISTEMA
|
|
||||||
|
|
||||||
- Mobile-first
|
|
||||||
- Tailwind-first
|
|
||||||
- Dark mode por classe
|
|
||||||
- Tokens semânticos
|
|
||||||
- UI energética e saudável
|
|
||||||
- Feedback visual imediato
|
|
||||||
|
|||||||
BIN
reference_img/login_dark.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
reference_img/login_light.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
reference_img/pin_image_app.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
reference_img/pin_image_app_dark.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |