[ADD] add auth pages
This commit is contained in:
3
app.json
3
app.json
@@ -34,7 +34,8 @@
|
|||||||
"imageWidth": 76
|
"imageWidth": 76
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-secure-store"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true,
|
"typedRoutes": true,
|
||||||
|
|||||||
266
package-lock.json
generated
266
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@react-navigation/bottom-tabs": "^7.15.5",
|
"@react-navigation/bottom-tabs": "^7.15.5",
|
||||||
"@react-navigation/elements": "^2.9.10",
|
"@react-navigation/elements": "^2.9.10",
|
||||||
"@react-navigation/native": "^7.1.33",
|
"@react-navigation/native": "^7.1.33",
|
||||||
|
"axios": "^1.16.1",
|
||||||
"expo": "~55.0.24",
|
"expo": "~55.0.24",
|
||||||
"expo-constants": "~55.0.16",
|
"expo-constants": "~55.0.16",
|
||||||
"expo-device": "~55.0.17",
|
"expo-device": "~55.0.17",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"expo-image": "~55.0.10",
|
"expo-image": "~55.0.10",
|
||||||
"expo-linking": "~55.0.15",
|
"expo-linking": "~55.0.15",
|
||||||
"expo-router": "~55.0.14",
|
"expo-router": "~55.0.14",
|
||||||
|
"expo-secure-store": "~55.0.14",
|
||||||
"expo-splash-screen": "~55.0.21",
|
"expo-splash-screen": "~55.0.21",
|
||||||
"expo-status-bar": "~55.0.6",
|
"expo-status-bar": "~55.0.6",
|
||||||
"expo-symbols": "~55.0.8",
|
"expo-symbols": "~55.0.8",
|
||||||
@@ -3098,6 +3100,45 @@
|
|||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
|
||||||
|
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.16.0",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||||
@@ -3466,6 +3507,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||||
@@ -3631,6 +3684,17 @@
|
|||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
@@ -3823,6 +3887,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -3858,6 +3930,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.4.tgz",
|
||||||
"integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA=="
|
"integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -3889,6 +3974,14 @@
|
|||||||
"stackframe": "^1.3.4"
|
"stackframe": "^1.3.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-errors": {
|
"node_modules/es-errors": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
@@ -3897,6 +3990,31 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -4229,6 +4347,14 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-secure-store": {
|
||||||
|
"version": "55.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-55.0.14.tgz",
|
||||||
|
"integrity": "sha512-OKp9pDiTa4kgChop8+pTRJGBPhkJUcAxP5c6JbivNr4bmx3I+gKmAj1ov4KOXkY95TpWdHO+GQ4+0BgSY2P3JQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-server": {
|
"node_modules/expo-server": {
|
||||||
"version": "55.0.9",
|
"version": "55.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.9.tgz",
|
||||||
@@ -4657,11 +4783,64 @@
|
|||||||
"resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
|
||||||
"integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="
|
"integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fontfaceobserver": {
|
"node_modules/fontfaceobserver": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz",
|
||||||
"integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="
|
"integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -4712,6 +4891,29 @@
|
|||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-nonce": {
|
"node_modules/get-nonce": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
@@ -4728,6 +4930,18 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/getenv": {
|
"node_modules/getenv": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz",
|
||||||
@@ -4752,6 +4966,17 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -4765,6 +4990,31 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||||
@@ -5649,6 +5899,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
|
||||||
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="
|
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@@ -6572,6 +6830,14 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/query-string": {
|
"node_modules/query-string": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@react-navigation/bottom-tabs": "^7.15.5",
|
"@react-navigation/bottom-tabs": "^7.15.5",
|
||||||
"@react-navigation/elements": "^2.9.10",
|
"@react-navigation/elements": "^2.9.10",
|
||||||
"@react-navigation/native": "^7.1.33",
|
"@react-navigation/native": "^7.1.33",
|
||||||
|
"axios": "^1.16.1",
|
||||||
"expo": "~55.0.24",
|
"expo": "~55.0.24",
|
||||||
"expo-constants": "~55.0.16",
|
"expo-constants": "~55.0.16",
|
||||||
"expo-device": "~55.0.17",
|
"expo-device": "~55.0.17",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"expo-image": "~55.0.10",
|
"expo-image": "~55.0.10",
|
||||||
"expo-linking": "~55.0.15",
|
"expo-linking": "~55.0.15",
|
||||||
"expo-router": "~55.0.14",
|
"expo-router": "~55.0.14",
|
||||||
|
"expo-secure-store": "~55.0.14",
|
||||||
"expo-splash-screen": "~55.0.21",
|
"expo-splash-screen": "~55.0.21",
|
||||||
"expo-status-bar": "~55.0.6",
|
"expo-status-bar": "~55.0.6",
|
||||||
"expo-symbols": "~55.0.8",
|
"expo-symbols": "~55.0.8",
|
||||||
@@ -31,11 +33,11 @@
|
|||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-native": "0.83.6",
|
"react-native": "0.83.6",
|
||||||
"react-native-gesture-handler": "~2.30.0",
|
"react-native-gesture-handler": "~2.30.0",
|
||||||
"react-native-worklets": "0.7.4",
|
|
||||||
"react-native-reanimated": "4.2.1",
|
"react-native-reanimated": "4.2.1",
|
||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.23.0",
|
"react-native-screens": "~4.23.0",
|
||||||
"react-native-web": "~0.21.0"
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-worklets": "0.7.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
|||||||
229
src/app/(auth)/OTPLogin.tsx
Normal file
229
src/app/(auth)/OTPLogin.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { useAuth } from "@/constants/AuthContext";
|
||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { authStyles } from "@/styles/auth.styles";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import apiClient from "../../services/api";
|
||||||
|
|
||||||
|
export default function OTPLogin() {
|
||||||
|
const { phoneNumber } = useLocalSearchParams<{ phoneNumber: string }>();
|
||||||
|
const [otp, setOtp] = useState("");
|
||||||
|
const [focusedField, setFocusedField] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [timeLeft, setTimeLeft] = useState(120);
|
||||||
|
const [canResend, setCanResend] = useState(false);
|
||||||
|
const { signIn } = useAuth();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeLeft <= 0) {
|
||||||
|
setCanResend(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTimeLeft((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [timeLeft]);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOTP = async () => {
|
||||||
|
if (!otp || otp.length < 4) {
|
||||||
|
Alert.alert("خطا", "لطفا کد تایید را کامل وارد کنید");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post("/api/auth/verify_login/", {
|
||||||
|
phone_number: phoneNumber,
|
||||||
|
code: otp,
|
||||||
|
});
|
||||||
|
const { access } = response.data;
|
||||||
|
await signIn(access);
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert(
|
||||||
|
"خطا",
|
||||||
|
error.response?.data?.message || "کد تایید اشتباه است",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResendCode = async () => {
|
||||||
|
if (!phoneNumber) return;
|
||||||
|
if (!canResend) {
|
||||||
|
Alert.alert("صبر کنید", `لطفاً ${timeLeft} ثانیه دیگر تلاش کنید`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await apiClient.post("/api/auth/login/", { phone_number: phoneNumber });
|
||||||
|
Alert.alert("کد مجدداً ارسال شد");
|
||||||
|
setTimeLeft(120);
|
||||||
|
setCanResend(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert("خطا", "مشکل در ارسال مجدد کد");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollViewRef.current?.scrollTo({ y: 120, animated: true });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = useMemo(
|
||||||
|
() => authStyles(theme, focusedField),
|
||||||
|
[theme, focusedField],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
<View style={{ marginBottom: 12 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleBack}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: theme.backgroundElement,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="arrow-left"
|
||||||
|
size={16}
|
||||||
|
color={theme.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.phoneCard}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<View style={styles.iconCircle}>
|
||||||
|
<Feather name="key" size={18} color={theme.accent} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.headerTextContainer}>
|
||||||
|
<Text style={styles.headerTitle}>تایید کد</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
کد تایید برای شماره {phoneNumber} ارسال شد
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={styles.fieldLabel}>کد تایید</Text>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather
|
||||||
|
name="key"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
focusedField === "otp" ? theme.accent : theme.textSecondary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="کد ۶ رقمی"
|
||||||
|
placeholderTextColor={theme.inputPlaceholder}
|
||||||
|
value={otp}
|
||||||
|
onChangeText={setOtp}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocusedField("otp");
|
||||||
|
handleFocus();
|
||||||
|
}}
|
||||||
|
onBlur={() => setFocusedField(null)}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
maxLength={6}
|
||||||
|
autoFocus
|
||||||
|
textAlign="center"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={isLoading}
|
||||||
|
style={styles.continueButton}
|
||||||
|
onPress={handleVerifyOTP}
|
||||||
|
>
|
||||||
|
{!isLoading && (
|
||||||
|
<Feather name="arrow-left" size={14} color="#fff" />
|
||||||
|
)}
|
||||||
|
<Text style={styles.continueText}>
|
||||||
|
{isLoading ? "در حال تایید..." : "تایید کد"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={isLoading || !canResend}
|
||||||
|
style={[
|
||||||
|
styles.secondaryButton,
|
||||||
|
(!canResend || isLoading) && styles.secondaryButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleResendCode}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="refresh-cw"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
!canResend || isLoading ? theme.textSecondary : theme.accent
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.secondaryButtonText,
|
||||||
|
(!canResend || isLoading) &&
|
||||||
|
styles.secondaryButtonTextDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{!canResend
|
||||||
|
? `ارسال مجدد کد (${formatTime(timeLeft)})`
|
||||||
|
: "ارسال مجدد کد"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
229
src/app/(auth)/OTPRegister.tsx
Normal file
229
src/app/(auth)/OTPRegister.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { useAuth } from "@/constants/AuthContext";
|
||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { authStyles } from "@/styles/auth.styles";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import apiClient from "../../services/api";
|
||||||
|
|
||||||
|
export default function OTPRegister() {
|
||||||
|
const { phoneNumber } = useLocalSearchParams<{ phoneNumber: string }>();
|
||||||
|
const [otp, setOtp] = useState("");
|
||||||
|
const [focusedField, setFocusedField] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [timeLeft, setTimeLeft] = useState(120);
|
||||||
|
const [canResend, setCanResend] = useState(false);
|
||||||
|
const { signIn } = useAuth();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeLeft <= 0) {
|
||||||
|
setCanResend(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTimeLeft((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [timeLeft]);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOTP = async () => {
|
||||||
|
if (!otp || otp.length < 4) {
|
||||||
|
Alert.alert("خطا", "لطفا کد تایید را کامل وارد کنید");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post("/api/auth/verify_register/", {
|
||||||
|
phone_number: phoneNumber,
|
||||||
|
code: otp,
|
||||||
|
});
|
||||||
|
const { access } = response.data;
|
||||||
|
await signIn(access);
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert(
|
||||||
|
"خطا",
|
||||||
|
error.response?.data?.message || "کد تایید اشتباه است",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResendCode = async () => {
|
||||||
|
if (!phoneNumber) return;
|
||||||
|
if (!canResend) {
|
||||||
|
Alert.alert("صبر کنید", `لطفاً ${timeLeft} ثانیه دیگر تلاش کنید`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await apiClient.post("/api/auth/login/", { phone_number: phoneNumber });
|
||||||
|
Alert.alert("کد مجدداً ارسال شد");
|
||||||
|
setTimeLeft(120);
|
||||||
|
setCanResend(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert("خطا", "مشکل در ارسال مجدد کد");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollViewRef.current?.scrollTo({ y: 120, animated: true });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = useMemo(
|
||||||
|
() => authStyles(theme, focusedField),
|
||||||
|
[theme, focusedField],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
<View style={{ marginBottom: 12 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleBack}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: theme.backgroundElement,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="arrow-left"
|
||||||
|
size={16}
|
||||||
|
color={theme.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.phoneCard}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<View style={styles.iconCircle}>
|
||||||
|
<Feather name="key" size={18} color={theme.accent} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.headerTextContainer}>
|
||||||
|
<Text style={styles.headerTitle}>تایید کد</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
کد تایید برای شماره {phoneNumber} ارسال شد
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={styles.fieldLabel}>کد تایید</Text>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather
|
||||||
|
name="key"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
focusedField === "otp" ? theme.accent : theme.textSecondary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="کد ۶ رقمی"
|
||||||
|
placeholderTextColor={theme.inputPlaceholder}
|
||||||
|
value={otp}
|
||||||
|
onChangeText={setOtp}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocusedField("otp");
|
||||||
|
handleFocus();
|
||||||
|
}}
|
||||||
|
onBlur={() => setFocusedField(null)}
|
||||||
|
keyboardType="number-pad"
|
||||||
|
maxLength={6}
|
||||||
|
autoFocus
|
||||||
|
textAlign="center"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={isLoading}
|
||||||
|
style={styles.continueButton}
|
||||||
|
onPress={handleVerifyOTP}
|
||||||
|
>
|
||||||
|
{!isLoading && (
|
||||||
|
<Feather name="arrow-left" size={14} color="#fff" />
|
||||||
|
)}
|
||||||
|
<Text style={styles.continueText}>
|
||||||
|
{isLoading ? "در حال تایید..." : "تایید کد"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={isLoading || !canResend}
|
||||||
|
style={[
|
||||||
|
styles.secondaryButton,
|
||||||
|
(!canResend || isLoading) && styles.secondaryButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleResendCode}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="refresh-cw"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
!canResend || isLoading ? theme.textSecondary : theme.accent
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.secondaryButtonText,
|
||||||
|
(!canResend || isLoading) &&
|
||||||
|
styles.secondaryButtonTextDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{!canResend
|
||||||
|
? `ارسال مجدد کد (${formatTime(timeLeft)})`
|
||||||
|
: "ارسال مجدد کد"}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
244
src/app/(auth)/SignUp.tsx
Normal file
244
src/app/(auth)/SignUp.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import apiClient from "@/services/api";
|
||||||
|
import { authStyles } from "@/styles/auth.styles";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import React, { useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
const SignUpScreen = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [focusedField, setFocusedField] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
const isPhoneValid = phoneNumber.trim().length >= 10;
|
||||||
|
const isUsernameValid = username.trim().length >= 3;
|
||||||
|
const isEmailValid = email.includes("@") && email.includes(".");
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignUp = async () => {
|
||||||
|
if (!isPhoneValid) {
|
||||||
|
Alert.alert("خطا", "شماره موبایل معتبر وارد کنید (حداقل ۱۰ رقم)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isUsernameValid) {
|
||||||
|
Alert.alert("خطا", "نام کاربری باید حداقل ۳ کاراکتر باشد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isEmailValid) {
|
||||||
|
Alert.alert("خطا", "ایمیل معتبر وارد کنید");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await apiClient.post("/api/auth/register/", {
|
||||||
|
phone_number: phoneNumber,
|
||||||
|
username: username,
|
||||||
|
email: email,
|
||||||
|
});
|
||||||
|
Alert.alert(
|
||||||
|
"ثبتنامه موفق",
|
||||||
|
"حساب کاربری شما ایجاد شد. لطفاً وارد شوید.",
|
||||||
|
);
|
||||||
|
router.push("/(auth)/login");
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert(
|
||||||
|
"خطا",
|
||||||
|
error.response?.data?.message || "مشکلی در ثبتنامه پیش آمد",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
router.push("/(auth)/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (fieldName: string) => {
|
||||||
|
setFocusedField(fieldName);
|
||||||
|
let yOffset = 0;
|
||||||
|
switch (fieldName) {
|
||||||
|
case "phone":
|
||||||
|
yOffset = 200;
|
||||||
|
break;
|
||||||
|
case "username":
|
||||||
|
yOffset = 280;
|
||||||
|
break;
|
||||||
|
case "email":
|
||||||
|
yOffset = 360;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollViewRef.current?.scrollTo({ y: yOffset, animated: true });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = useMemo(
|
||||||
|
() => authStyles(theme, focusedField),
|
||||||
|
[theme, focusedField],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
<View style={{ marginBottom: 12 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleBack}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: theme.backgroundElement,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="arrow-left"
|
||||||
|
size={16}
|
||||||
|
color={theme.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* کارت ثبتنام */}
|
||||||
|
<View style={styles.phoneCard}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<View style={styles.iconCircle}>
|
||||||
|
<Feather name="user-plus" size={18} color={theme.accent} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.headerTextContainer}>
|
||||||
|
<Text style={styles.headerTitle}>ایجاد حساب کاربری</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* شماره موبایل */}
|
||||||
|
<Text style={styles.fieldLabel}>شماره موبایل</Text>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather
|
||||||
|
name="phone"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
focusedField === "phone" ? theme.accent : theme.textSecondary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="09123456789"
|
||||||
|
placeholderTextColor={theme.inputPlaceholder}
|
||||||
|
value={phoneNumber}
|
||||||
|
onChangeText={setPhoneNumber}
|
||||||
|
onFocus={() => handleFocus("phone")}
|
||||||
|
onBlur={() => setFocusedField(null)}
|
||||||
|
keyboardType="phone-pad"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textAlign="right"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* نام کاربری */}
|
||||||
|
<Text style={styles.fieldLabel}>نام کاربری</Text>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather
|
||||||
|
name="user"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
focusedField === "username"
|
||||||
|
? theme.accent
|
||||||
|
: theme.textSecondary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="example_user"
|
||||||
|
placeholderTextColor={theme.inputPlaceholder}
|
||||||
|
value={username}
|
||||||
|
onChangeText={setUsername}
|
||||||
|
onFocus={() => handleFocus("username")}
|
||||||
|
onBlur={() => setFocusedField(null)}
|
||||||
|
autoCapitalize="none"
|
||||||
|
textAlign="right"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* ایمیل */}
|
||||||
|
<Text style={styles.fieldLabel}>ایمیل سازمانی</Text>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather
|
||||||
|
name="mail"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
focusedField === "email" ? theme.accent : theme.textSecondary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="example@company.com"
|
||||||
|
placeholderTextColor={theme.inputPlaceholder}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
onFocus={() => handleFocus("email")}
|
||||||
|
onBlur={() => setFocusedField(null)}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textAlign="right"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.continueButton}
|
||||||
|
onPress={handleSignUp}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Text style={styles.continueText}>
|
||||||
|
{isLoading ? "در حال ثبتنام..." : "ثبتنام"}
|
||||||
|
</Text>
|
||||||
|
<Feather name="arrow-left" size={14} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.switchContainer}>
|
||||||
|
<Text style={styles.switchText}>حساب کاربری دارید؟ </Text>
|
||||||
|
<TouchableOpacity onPress={handleLogin}>
|
||||||
|
<Text style={styles.switchLink}>وارد شوید</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignUpScreen;
|
||||||
13
src/app/(auth)/_layout.tsx
Normal file
13
src/app/(auth)/_layout.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Tabs, Stack } from "expo-router";
|
||||||
|
|
||||||
|
export default function AuthLayout() {
|
||||||
|
return (
|
||||||
|
<Tabs screenOptions={{ headerShown: false, tabBarStyle: { display: "none" } }}>
|
||||||
|
<Tabs.Screen name="homeLogin" options={{ href: null }} />
|
||||||
|
<Tabs.Screen name="login" options={{ href: null }} />
|
||||||
|
<Tabs.Screen name="OTPLogin" options={{ href: null }} />
|
||||||
|
<Tabs.Screen name="OTPRegister" options={{ href: null }} />
|
||||||
|
<Tabs.Screen name="SignUp" options={{ href: null }} />
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
src/app/(auth)/homeLogin.tsx
Normal file
190
src/app/(auth)/homeLogin.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { router } from "expo-router";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
ScrollView,
|
||||||
|
StatusBar,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
DimensionValue,
|
||||||
|
} from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { createWelcomeStyles } from "@/styles/auth.styles";
|
||||||
|
|
||||||
|
interface CardItem {
|
||||||
|
id: string;
|
||||||
|
dotColor: string;
|
||||||
|
line1WidthPercent: number;
|
||||||
|
line2WidthPercent: number;
|
||||||
|
tagText: string;
|
||||||
|
tagBg: string;
|
||||||
|
tagColor: string;
|
||||||
|
isAccent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WelcomeScreen = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.background === '#0D0D14';
|
||||||
|
|
||||||
|
const getCardColors = (accentType: 'primary' | 'green' | 'orange' | 'pink') => {
|
||||||
|
if (isDark) {
|
||||||
|
switch (accentType) {
|
||||||
|
case 'primary':
|
||||||
|
return { tagBg: '#1a1635', tagColor: '#8B7FFF', dotColor: theme.accent };
|
||||||
|
case 'green':
|
||||||
|
return { tagBg: '#0a1f18', tagColor: '#5DCAA5', dotColor: '#1D9E75' };
|
||||||
|
case 'orange':
|
||||||
|
return { tagBg: '#1e1500', tagColor: '#EF9F27', dotColor: '#EF9F27' };
|
||||||
|
case 'pink':
|
||||||
|
return { tagBg: '#1a0d12', tagColor: '#ED93B1', dotColor: '#D4537E' };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (accentType) {
|
||||||
|
case 'primary':
|
||||||
|
return { tagBg: '#EEEEFF', tagColor: '#6C63FF', dotColor: theme.accent };
|
||||||
|
case 'green':
|
||||||
|
return { tagBg: '#E6F7F0', tagColor: '#1D9E75', dotColor: '#1D9E75' };
|
||||||
|
case 'orange':
|
||||||
|
return { tagBg: '#FFF3E6', tagColor: '#EF9F27', dotColor: '#EF9F27' };
|
||||||
|
case 'pink':
|
||||||
|
return { tagBg: '#FFEEF2', tagColor: '#D4537E', dotColor: '#D4537E' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tagBg: theme.backgroundElement, tagColor: theme.textSecondary, dotColor: theme.textSecondary };
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardsData: CardItem[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
dotColor: getCardColors('primary').dotColor,
|
||||||
|
line1WidthPercent: 85,
|
||||||
|
line2WidthPercent: 60,
|
||||||
|
tagText: "طراحی",
|
||||||
|
tagBg: getCardColors('primary').tagBg,
|
||||||
|
tagColor: getCardColors('primary').tagColor,
|
||||||
|
isAccent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
dotColor: getCardColors('green').dotColor,
|
||||||
|
line1WidthPercent: 75,
|
||||||
|
line2WidthPercent: 45,
|
||||||
|
tagText: "انجام شد",
|
||||||
|
tagBg: getCardColors('green').tagBg,
|
||||||
|
tagColor: getCardColors('green').tagColor,
|
||||||
|
isAccent: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
dotColor: getCardColors('orange').dotColor,
|
||||||
|
line1WidthPercent: 65,
|
||||||
|
line2WidthPercent: 40,
|
||||||
|
tagText: "بررسی",
|
||||||
|
tagBg: getCardColors('orange').tagBg,
|
||||||
|
tagColor: getCardColors('orange').tagColor,
|
||||||
|
isAccent: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
dotColor: getCardColors('pink').dotColor,
|
||||||
|
line1WidthPercent: 80,
|
||||||
|
line2WidthPercent: 50,
|
||||||
|
tagText: "راهاندازی",
|
||||||
|
tagBg: getCardColors('pink').tagBg,
|
||||||
|
tagColor: getCardColors('pink').tagColor,
|
||||||
|
isAccent: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const percent = (value: number): DimensionValue => `${value}%` as DimensionValue;
|
||||||
|
|
||||||
|
const dynamicStyles = useMemo(
|
||||||
|
() => createWelcomeStyles(theme, isDark),
|
||||||
|
[theme, isDark]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderMiniCard = (item: CardItem) => {
|
||||||
|
const cardStyle = item.isAccent
|
||||||
|
? [dynamicStyles.miniCard, dynamicStyles.miniCardAccent]
|
||||||
|
: dynamicStyles.miniCard;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={item.id} style={cardStyle}>
|
||||||
|
<View style={[dynamicStyles.mcDot, { backgroundColor: item.dotColor }]} />
|
||||||
|
<View style={dynamicStyles.mcLines}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
dynamicStyles.mcLine,
|
||||||
|
{ width: percent(item.line1WidthPercent) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
dynamicStyles.mcLine,
|
||||||
|
{ width: percent(item.line2WidthPercent) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[dynamicStyles.mcTag, { backgroundColor: item.tagBg }]}>
|
||||||
|
<Text style={[dynamicStyles.mcTagText, { color: item.tagColor }]}>
|
||||||
|
{item.tagText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusBarStyle = isDark ? 'light-content' : 'dark-content';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={dynamicStyles.container}>
|
||||||
|
<StatusBar barStyle={statusBarStyle} backgroundColor={theme.background} />
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={dynamicStyles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={dynamicStyles.logoMark}>
|
||||||
|
<View style={dynamicStyles.logoBox}>
|
||||||
|
<Text style={dynamicStyles.logoIcon}>◧</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={dynamicStyles.logoWord}>Plania</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={dynamicStyles.heroGrid}>
|
||||||
|
{cardsData.map(renderMiniCard)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={dynamicStyles.heroCta}>
|
||||||
|
<View>
|
||||||
|
<Text style={dynamicStyles.tagline}>
|
||||||
|
کار را مدیریت کن،{"\n"}
|
||||||
|
<Text style={dynamicStyles.taglineAccent}>زیبا.</Text>
|
||||||
|
</Text>
|
||||||
|
<Text style={dynamicStyles.subText}>
|
||||||
|
با تیم خود در زمان واقعی برنامهریزی،{"\n"}پیگیری و همکاری کن.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={dynamicStyles.primaryButton}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
onPress={() => router.push("/(auth)/SignUp")}
|
||||||
|
>
|
||||||
|
<Text style={dynamicStyles.primaryButtonText}>شروع رایگان</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={dynamicStyles.outlineButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() => router.push("/(auth)/login")}
|
||||||
|
>
|
||||||
|
<Text style={dynamicStyles.outlineButtonText}>حساب کاربری دارم</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WelcomeScreen;
|
||||||
168
src/app/(auth)/login.tsx
Normal file
168
src/app/(auth)/login.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import apiClient from "@/services/api";
|
||||||
|
import { authStyles } from "@/styles/auth.styles";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import React, { useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
const LoginScreen = () => {
|
||||||
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
|
const [focusedField, setFocusedField] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendOTP = async () => {
|
||||||
|
if (!phoneNumber) {
|
||||||
|
Alert.alert("خطا", "لطفا شماره موبایل خود را وارد کنید");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await apiClient.post("/api/auth/login/", { phone_number: phoneNumber });
|
||||||
|
Alert.alert("کد تایید ارسال شد", "لطفا کد ارسال شده را وارد کنید");
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: "/OTPLogin",
|
||||||
|
params: { phoneNumber: phoneNumber },
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
Alert.alert(
|
||||||
|
"خطا",
|
||||||
|
error.response?.data?.message || "مشکلی در ارسال کد پیش آمد",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignUp = () => {
|
||||||
|
router.push("/(auth)/SignUp");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollViewRef.current?.scrollTo({ y: 120, animated: true });
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = useMemo(
|
||||||
|
() => authStyles(theme, focusedField),
|
||||||
|
[theme, focusedField],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
contentContainerStyle={styles.scrollContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
{/* دکمه بازگشت */}
|
||||||
|
<View style={{ marginBottom: 12 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleBack}
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: theme.backgroundElement,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="arrow-left"
|
||||||
|
size={16}
|
||||||
|
color={theme.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.phoneCard}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<View style={styles.iconCircle}>
|
||||||
|
<Feather name="log-in" size={18} color={theme.accent} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.headerTextContainer}>
|
||||||
|
<Text style={styles.headerTitle}>ورود به حساب</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.fieldLabel}>شماره موبایل</Text>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather
|
||||||
|
name="phone"
|
||||||
|
size={14}
|
||||||
|
color={
|
||||||
|
focusedField === "phone" ? theme.accent : theme.textSecondary
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="09123456789"
|
||||||
|
placeholderTextColor={theme.inputPlaceholder}
|
||||||
|
value={phoneNumber}
|
||||||
|
onChangeText={setPhoneNumber}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocusedField("phone");
|
||||||
|
handleFocus();
|
||||||
|
}}
|
||||||
|
onBlur={() => setFocusedField(null)}
|
||||||
|
keyboardType="phone-pad"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textAlign="right"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.continueButton}
|
||||||
|
onPress={handleSendOTP}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Text style={styles.continueText}>
|
||||||
|
{isLoading ? "در حال ارسال..." : "ورود"}
|
||||||
|
</Text>
|
||||||
|
<Feather name="arrow-left" size={14} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.switchContainer}>
|
||||||
|
<Text style={styles.switchText}>حساب کاربری ندارید؟ </Text>
|
||||||
|
<TouchableOpacity onPress={handleSignUp}>
|
||||||
|
<Text style={styles.switchLink}>ایجاد حساب رایگان</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginScreen;
|
||||||
5
src/app/(tabs)/_layout.tsx
Normal file
5
src/app/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import AppTabs from "@/components/app-tabs";
|
||||||
|
|
||||||
|
export default function TabsLayout() {
|
||||||
|
return <AppTabs />;
|
||||||
|
}
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from "expo-image";
|
||||||
import { SymbolView } from 'expo-symbols';
|
import { SymbolView } from "expo-symbols";
|
||||||
import React from 'react';
|
import React, { useLayoutEffect } from "react";
|
||||||
import { Platform, Pressable, ScrollView, StyleSheet } from 'react-native';
|
import { Platform, Pressable, ScrollView, StyleSheet } from "react-native";
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
import { ExternalLink } from '@/components/external-link';
|
import { ExternalLink } from "@/components/external-link";
|
||||||
import { ThemedText } from '@/components/themed-text';
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { ThemedView } from '@/components/themed-view';
|
import { ThemedView } from "@/components/themed-view";
|
||||||
import { Collapsible } from '@/components/ui/collapsible';
|
import { Collapsible } from "@/components/ui/collapsible";
|
||||||
import { WebBadge } from '@/components/web-badge';
|
import { WebBadge } from "@/components/web-badge";
|
||||||
import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
|
import { BottomTabInset, MaxContentWidth, Spacing } from "@/constants/theme";
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { useNavigation } from "expo-router";
|
||||||
|
|
||||||
export default function TabTwoScreen() {
|
export default function TabTwoScreen() {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
title: "explore",
|
||||||
|
tabBarIcon: ({ color, size }: { color: string; size: number }) => (
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/tabIcons/explore.png')}
|
||||||
|
style={{ width: size, height: size, tintColor: color }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
const safeAreaInsets = useSafeAreaInsets();
|
const safeAreaInsets = useSafeAreaInsets();
|
||||||
const insets = {
|
const insets = {
|
||||||
...safeAreaInsets,
|
...safeAreaInsets,
|
||||||
@@ -37,12 +50,13 @@ export default function TabTwoScreen() {
|
|||||||
<ScrollView
|
<ScrollView
|
||||||
style={[styles.scrollView, { backgroundColor: theme.background }]}
|
style={[styles.scrollView, { backgroundColor: theme.background }]}
|
||||||
contentInset={insets}
|
contentInset={insets}
|
||||||
contentContainerStyle={[styles.contentContainer, contentPlatformStyle]}>
|
contentContainerStyle={[styles.contentContainer, contentPlatformStyle]}
|
||||||
|
>
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<ThemedView style={styles.titleContainer}>
|
<ThemedView style={styles.titleContainer}>
|
||||||
<ThemedText type="subtitle">Explore</ThemedText>
|
<ThemedText type="subtitle">Explore</ThemedText>
|
||||||
<ThemedText style={styles.centerText} themeColor="textSecondary">
|
<ThemedText style={styles.centerText} themeColor="textSecondary">
|
||||||
This starter app includes example{'\n'}code to help you get started.
|
This starter app includes example{"\n"}code to help you get started.
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
|
|
||||||
<ExternalLink href="https://docs.expo.dev" asChild>
|
<ExternalLink href="https://docs.expo.dev" asChild>
|
||||||
@@ -51,7 +65,11 @@ export default function TabTwoScreen() {
|
|||||||
<ThemedText type="link">Expo documentation</ThemedText>
|
<ThemedText type="link">Expo documentation</ThemedText>
|
||||||
<SymbolView
|
<SymbolView
|
||||||
tintColor={theme.text}
|
tintColor={theme.text}
|
||||||
name={{ ios: 'arrow.up.right.square', android: 'link', web: 'link' }}
|
name={{
|
||||||
|
ios: "arrow.up.right.square",
|
||||||
|
android: "link",
|
||||||
|
web: "link",
|
||||||
|
}}
|
||||||
size={12}
|
size={12}
|
||||||
/>
|
/>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
@@ -62,11 +80,13 @@ export default function TabTwoScreen() {
|
|||||||
<ThemedView style={styles.sectionsWrapper}>
|
<ThemedView style={styles.sectionsWrapper}>
|
||||||
<Collapsible title="File-based routing">
|
<Collapsible title="File-based routing">
|
||||||
<ThemedText type="small">
|
<ThemedText type="small">
|
||||||
This app has two screens: <ThemedText type="code">src/app/index.tsx</ThemedText> and{' '}
|
This app has two screens:{" "}
|
||||||
|
<ThemedText type="code">src/app/index.tsx</ThemedText> and{" "}
|
||||||
<ThemedText type="code">src/app/explore.tsx</ThemedText>
|
<ThemedText type="code">src/app/explore.tsx</ThemedText>
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText type="small">
|
<ThemedText type="small">
|
||||||
The layout file in <ThemedText type="code">src/app/_layout.tsx</ThemedText> sets up
|
The layout file in{" "}
|
||||||
|
<ThemedText type="code">src/app/_layout.tsx</ThemedText> sets up
|
||||||
the tab navigator.
|
the tab navigator.
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
||||||
@@ -75,14 +95,18 @@ export default function TabTwoScreen() {
|
|||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<Collapsible title="Android, iOS, and web support">
|
<Collapsible title="Android, iOS, and web support">
|
||||||
<ThemedView type="backgroundElement" style={styles.collapsibleContent}>
|
<ThemedView
|
||||||
|
type="backgroundElement"
|
||||||
|
style={styles.collapsibleContent}
|
||||||
|
>
|
||||||
<ThemedText type="small">
|
<ThemedText type="small">
|
||||||
You can open this project on Android, iOS, and the web. To open the web version,
|
You can open this project on Android, iOS, and the web. To open
|
||||||
press <ThemedText type="smallBold">w</ThemedText> in the terminal running this
|
the web version, press{" "}
|
||||||
project.
|
<ThemedText type="smallBold">w</ThemedText> in the terminal
|
||||||
|
running this project.
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/tutorial-web.png')}
|
source={require("@/assets/images/tutorial-web.png")}
|
||||||
style={styles.imageTutorial}
|
style={styles.imageTutorial}
|
||||||
/>
|
/>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
@@ -90,11 +114,15 @@ export default function TabTwoScreen() {
|
|||||||
|
|
||||||
<Collapsible title="Images">
|
<Collapsible title="Images">
|
||||||
<ThemedText type="small">
|
<ThemedText type="small">
|
||||||
For static images, you can use the <ThemedText type="code">@2x</ThemedText> and{' '}
|
For static images, you can use the{" "}
|
||||||
<ThemedText type="code">@3x</ThemedText> suffixes to provide files for different
|
<ThemedText type="code">@2x</ThemedText> and{" "}
|
||||||
screen densities.
|
<ThemedText type="code">@3x</ThemedText> suffixes to provide files
|
||||||
|
for different screen densities.
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<Image source={require('@/assets/images/react-logo.png')} style={styles.imageReact} />
|
<Image
|
||||||
|
source={require("@/assets/images/react-logo.png")}
|
||||||
|
style={styles.imageReact}
|
||||||
|
/>
|
||||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
@@ -102,9 +130,10 @@ export default function TabTwoScreen() {
|
|||||||
|
|
||||||
<Collapsible title="Light and dark mode components">
|
<Collapsible title="Light and dark mode components">
|
||||||
<ThemedText type="small">
|
<ThemedText type="small">
|
||||||
This template has light and dark mode support. The{' '}
|
This template has light and dark mode support. The{" "}
|
||||||
<ThemedText type="code">useColorScheme()</ThemedText> hook lets you inspect what the
|
<ThemedText type="code">useColorScheme()</ThemedText> hook lets
|
||||||
user's current color scheme is, and so you can adjust UI colors accordingly.
|
you inspect what the user's current color scheme is, and so
|
||||||
|
you can adjust UI colors accordingly.
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
||||||
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
<ThemedText type="linkPrimary">Learn more</ThemedText>
|
||||||
@@ -113,14 +142,17 @@ export default function TabTwoScreen() {
|
|||||||
|
|
||||||
<Collapsible title="Animations">
|
<Collapsible title="Animations">
|
||||||
<ThemedText type="small">
|
<ThemedText type="small">
|
||||||
This template includes an example of an animated component. The{' '}
|
This template includes an example of an animated component. The{" "}
|
||||||
<ThemedText type="code">src/components/ui/collapsible.tsx</ThemedText> component uses
|
<ThemedText type="code">
|
||||||
the powerful <ThemedText type="code">react-native-reanimated</ThemedText> library to
|
src/components/ui/collapsible.tsx
|
||||||
animate opening this hint.
|
</ThemedText>{" "}
|
||||||
|
component uses the powerful{" "}
|
||||||
|
<ThemedText type="code">react-native-reanimated</ThemedText>{" "}
|
||||||
|
library to animate opening this hint.
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
{Platform.OS === 'web' && <WebBadge />}
|
{Platform.OS === "web" && <WebBadge />}
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
@@ -131,8 +163,8 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
maxWidth: MaxContentWidth,
|
maxWidth: MaxContentWidth,
|
||||||
@@ -140,24 +172,24 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
gap: Spacing.three,
|
gap: Spacing.three,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
paddingHorizontal: Spacing.four,
|
paddingHorizontal: Spacing.four,
|
||||||
paddingVertical: Spacing.six,
|
paddingVertical: Spacing.six,
|
||||||
},
|
},
|
||||||
centerText: {
|
centerText: {
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
pressed: {
|
pressed: {
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
linkButton: {
|
linkButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
paddingHorizontal: Spacing.four,
|
paddingHorizontal: Spacing.four,
|
||||||
paddingVertical: Spacing.two,
|
paddingVertical: Spacing.two,
|
||||||
borderRadius: Spacing.five,
|
borderRadius: Spacing.five,
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
gap: Spacing.one,
|
gap: Spacing.one,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
sectionsWrapper: {
|
sectionsWrapper: {
|
||||||
gap: Spacing.five,
|
gap: Spacing.five,
|
||||||
@@ -165,10 +197,10 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: Spacing.three,
|
paddingTop: Spacing.three,
|
||||||
},
|
},
|
||||||
collapsibleContent: {
|
collapsibleContent: {
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
imageTutorial: {
|
imageTutorial: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
aspectRatio: 296 / 171,
|
aspectRatio: 296 / 171,
|
||||||
borderRadius: Spacing.three,
|
borderRadius: Spacing.three,
|
||||||
marginTop: Spacing.two,
|
marginTop: Spacing.two,
|
||||||
@@ -176,6 +208,6 @@ const styles = StyleSheet.create({
|
|||||||
imageReact: {
|
imageReact: {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
alignSelf: 'center',
|
alignSelf: "center",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
143
src/app/(tabs)/index.tsx
Normal file
143
src/app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import * as Device from "expo-device";
|
||||||
|
import { Button, Platform, StyleSheet, ToastAndroid } from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
import { AnimatedIcon } from "@/components/animated-icon";
|
||||||
|
import { HintRow } from "@/components/hint-row";
|
||||||
|
import { ThemedText } from "@/components/themed-text";
|
||||||
|
import { ThemedView } from "@/components/themed-view";
|
||||||
|
import { WebBadge } from "@/components/web-badge";
|
||||||
|
import { BottomTabInset, MaxContentWidth, Spacing } from "@/constants/theme";
|
||||||
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
|
import apiClient from "@/services/api";
|
||||||
|
import { useAuth } from "@/constants/AuthContext";
|
||||||
|
import { Image } from "react-native";
|
||||||
|
import { useNavigation } from "expo-router";
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
bio: string;
|
||||||
|
phone_number: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
profile_picture: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDevMenuHint() {
|
||||||
|
if (Platform.OS === "web") {
|
||||||
|
return <ThemedText type="small">use browser devtools</ThemedText>;
|
||||||
|
}
|
||||||
|
if (Device.isDevice) {
|
||||||
|
return (
|
||||||
|
<ThemedText type="small">
|
||||||
|
shake device or press <ThemedText type="code">m</ThemedText> in terminal
|
||||||
|
</ThemedText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const shortcut = Platform.OS === "android" ? "cmd+m (or ctrl+m)" : "cmd+d";
|
||||||
|
return (
|
||||||
|
<ThemedText type="small">
|
||||||
|
press <ThemedText type="code">{shortcut}</ThemedText>
|
||||||
|
</ThemedText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomeScreen() {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
title: "خانه",
|
||||||
|
tabBarIcon: ({ color, size }: { color: string; size: number}) => (
|
||||||
|
<Image
|
||||||
|
source={require("@/assets/images/tabIcons/home.png")}
|
||||||
|
style={{ width: size, height: size, tintColor: color }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
const { signOut } = useAuth();
|
||||||
|
|
||||||
|
const [profile, setProfile] = useState<Profile>();
|
||||||
|
useEffect(() => {
|
||||||
|
apiClient
|
||||||
|
.get("api/auth/profile/")
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data) {
|
||||||
|
setProfile(res.data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {});
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<ThemedView style={styles.container}>
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<ThemedView style={styles.heroSection}>
|
||||||
|
<AnimatedIcon image={profile?.profile_picture || ""} />
|
||||||
|
<ThemedText type="title" style={styles.title}>
|
||||||
|
{profile?.name}
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={styles.title}>
|
||||||
|
phone : {profile?.phone_number}
|
||||||
|
</ThemedText>
|
||||||
|
</ThemedView>
|
||||||
|
|
||||||
|
<ThemedText type="code" style={styles.code}>
|
||||||
|
get started
|
||||||
|
</ThemedText>
|
||||||
|
|
||||||
|
<ThemedView type="backgroundElement" style={styles.stepContainer}>
|
||||||
|
<HintRow
|
||||||
|
title="Try editing"
|
||||||
|
hint={<ThemedText type="code">src/app/index.tsx</ThemedText>}
|
||||||
|
/>
|
||||||
|
<HintRow title="Dev tools" hint={getDevMenuHint()} />
|
||||||
|
<HintRow
|
||||||
|
title="Fresh start"
|
||||||
|
hint={<ThemedText type="code">npm run reset-project</ThemedText>}
|
||||||
|
/>
|
||||||
|
</ThemedView>
|
||||||
|
|
||||||
|
{Platform.OS === "web" && <WebBadge />}
|
||||||
|
<Button title="خروج" onPress={signOut} />
|
||||||
|
</SafeAreaView>
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
flexDirection: "row",
|
||||||
|
},
|
||||||
|
safeArea: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: Spacing.four,
|
||||||
|
alignItems: "center",
|
||||||
|
gap: Spacing.three,
|
||||||
|
paddingBottom: BottomTabInset + Spacing.three,
|
||||||
|
maxWidth: MaxContentWidth,
|
||||||
|
},
|
||||||
|
heroSection: {
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: Spacing.four,
|
||||||
|
gap: Spacing.four,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
textTransform: "uppercase",
|
||||||
|
},
|
||||||
|
stepContainer: {
|
||||||
|
gap: Spacing.three,
|
||||||
|
alignSelf: "stretch",
|
||||||
|
paddingHorizontal: Spacing.three,
|
||||||
|
paddingVertical: Spacing.four,
|
||||||
|
borderRadius: Spacing.four,
|
||||||
|
},
|
||||||
|
});
|
||||||
172
src/app/LoadingScreen.tsx
Normal file
172
src/app/LoadingScreen.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// LoadingScreen.tsx
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
ActivityIndicator,
|
||||||
|
Animated,
|
||||||
|
Easing,
|
||||||
|
Dimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
interface LoadingScreenProps {
|
||||||
|
message?: string;
|
||||||
|
spinnerColor?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
fullScreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnimatedDotProps {
|
||||||
|
delay: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingScreen: React.FC<LoadingScreenProps> = ({
|
||||||
|
message = 'در حال بارگذاری ...',
|
||||||
|
spinnerColor = '#6366F1',
|
||||||
|
backgroundColor = '#FFFFFF',
|
||||||
|
textColor = '#4B5563',
|
||||||
|
fullScreen = true,
|
||||||
|
}) => {
|
||||||
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
const scaleAnim = useRef(new Animated.Value(0.9)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(fadeAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 600,
|
||||||
|
easing: Easing.out(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.spring(scaleAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
friction: 8,
|
||||||
|
tension: 40,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
]).start();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, fullScreen && styles.fullScreen, { backgroundColor }]}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.content,
|
||||||
|
{
|
||||||
|
opacity: fadeAnim,
|
||||||
|
transform: [{ scale: scaleAnim }],
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={styles.spinnerWrapper}>
|
||||||
|
<ActivityIndicator size="large" color={spinnerColor} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{message ? <Text style={[styles.message, { color: textColor }]}>{message}</Text> : null}
|
||||||
|
|
||||||
|
<View style={styles.dotContainer}>
|
||||||
|
<AnimatedDot delay={0} color={spinnerColor} />
|
||||||
|
<AnimatedDot delay={200} color={spinnerColor} />
|
||||||
|
<AnimatedDot delay={400} color={spinnerColor} />
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AnimatedDot: React.FC<AnimatedDotProps> = ({ delay, color }) => {
|
||||||
|
const anim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.loop(
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(anim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 400,
|
||||||
|
delay,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(anim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 400,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
).start();
|
||||||
|
}, [delay]);
|
||||||
|
|
||||||
|
const translateY = anim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0, -8],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.dot,
|
||||||
|
{
|
||||||
|
backgroundColor: color,
|
||||||
|
transform: [{ translateY }],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
fullScreen: {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: 999,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
spinnerWrapper: {
|
||||||
|
marginBottom: 24,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 5,
|
||||||
|
borderRadius: 40,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
marginBottom: 16,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
dotContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
dot: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginHorizontal: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoadingScreen;
|
||||||
@@ -1,16 +1,81 @@
|
|||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
// app/_layout.tsx
|
||||||
import React from 'react';
|
import { Redirect, Stack, useRouter, useSegments } from "expo-router";
|
||||||
import { useColorScheme } from 'react-native';
|
import { AuthProvider, useAuth } from "@/constants/AuthContext";
|
||||||
|
import { AnimatedSplashOverlay } from "@/components/animated-icon";
|
||||||
|
import { Appearance, Button, useColorScheme } from "react-native";
|
||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
import LoadingScreen from "./LoadingScreen";
|
||||||
|
|
||||||
import { AnimatedSplashOverlay } from '@/components/animated-icon';
|
function RootLayoutNav() {
|
||||||
import AppTabs from '@/components/app-tabs';
|
const { isLogin, isLoading } = useAuth();
|
||||||
|
const segments = useSegments();
|
||||||
|
const router = useRouter();
|
||||||
|
const inAuthGroup = segments[0] === "(auth)";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
if (!isLogin && !inAuthGroup) {
|
||||||
|
router.replace("/(auth)/homeLogin");
|
||||||
|
} else if (isLogin && inAuthGroup) {
|
||||||
|
router.replace("/(tabs)");
|
||||||
|
}
|
||||||
|
}, [isLogin, isLoading, inAuthGroup]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingScreen message="در حال آمادهسازی..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialRoute = isLogin ? "(tabs)" : "(auth)";
|
||||||
|
|
||||||
export default function TabLayout() {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<Stack
|
||||||
<AnimatedSplashOverlay />
|
initialRouteName={initialRoute}
|
||||||
<AppTabs />
|
screenOptions={{ headerShown: false }}
|
||||||
</ThemeProvider>
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
const systemColorScheme = useColorScheme();
|
||||||
|
const [userSelectedTheme, setUserSelectedTheme] = useState<
|
||||||
|
"light" | "dark" | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTheme = async () => {
|
||||||
|
try {
|
||||||
|
const saved = await SecureStore.getItemAsync("userTheme");
|
||||||
|
if (saved === "light" || saved === "dark") {
|
||||||
|
setUserSelectedTheme(saved);
|
||||||
|
Appearance.setColorScheme(saved);
|
||||||
|
} else {
|
||||||
|
setUserSelectedTheme(null);
|
||||||
|
Appearance.setColorScheme("unspecified");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTheme();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = async () => {
|
||||||
|
const currentEffective = userSelectedTheme ?? systemColorScheme ?? "light";
|
||||||
|
const newTheme = currentEffective === "light" ? "dark" : "light";
|
||||||
|
|
||||||
|
setUserSelectedTheme(newTheme);
|
||||||
|
await SecureStore.setItemAsync("userTheme", newTheme);
|
||||||
|
Appearance.setColorScheme(newTheme);
|
||||||
|
};
|
||||||
|
const activeColorScheme = userSelectedTheme ?? systemColorScheme ?? "light";
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<RootLayoutNav />
|
||||||
|
{/* <Button title="تم" onPress={toggleTheme}/> */}
|
||||||
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import * as Device from 'expo-device';
|
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
import { AnimatedIcon } from '@/components/animated-icon';
|
|
||||||
import { HintRow } from '@/components/hint-row';
|
|
||||||
import { ThemedText } from '@/components/themed-text';
|
|
||||||
import { ThemedView } from '@/components/themed-view';
|
|
||||||
import { WebBadge } from '@/components/web-badge';
|
|
||||||
import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
|
|
||||||
|
|
||||||
function getDevMenuHint() {
|
|
||||||
if (Platform.OS === 'web') {
|
|
||||||
return <ThemedText type="small">use browser devtools</ThemedText>;
|
|
||||||
}
|
|
||||||
if (Device.isDevice) {
|
|
||||||
return (
|
|
||||||
<ThemedText type="small">
|
|
||||||
shake device or press <ThemedText type="code">m</ThemedText> in terminal
|
|
||||||
</ThemedText>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const shortcut = Platform.OS === 'android' ? 'cmd+m (or ctrl+m)' : 'cmd+d';
|
|
||||||
return (
|
|
||||||
<ThemedText type="small">
|
|
||||||
press <ThemedText type="code">{shortcut}</ThemedText>
|
|
||||||
</ThemedText>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
|
||||||
return (
|
|
||||||
<ThemedView style={styles.container}>
|
|
||||||
<SafeAreaView style={styles.safeArea}>
|
|
||||||
<ThemedView style={styles.heroSection}>
|
|
||||||
<AnimatedIcon />
|
|
||||||
<ThemedText type="title" style={styles.title}>
|
|
||||||
Welcome to Expo
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
|
|
||||||
<ThemedText type="code" style={styles.code}>
|
|
||||||
get started
|
|
||||||
</ThemedText>
|
|
||||||
|
|
||||||
<ThemedView type="backgroundElement" style={styles.stepContainer}>
|
|
||||||
<HintRow
|
|
||||||
title="Try editing"
|
|
||||||
hint={<ThemedText type="code">src/app/index.tsx</ThemedText>}
|
|
||||||
/>
|
|
||||||
<HintRow title="Dev tools" hint={getDevMenuHint()} />
|
|
||||||
<HintRow
|
|
||||||
title="Fresh start"
|
|
||||||
hint={<ThemedText type="code">npm run reset-project</ThemedText>}
|
|
||||||
/>
|
|
||||||
</ThemedView>
|
|
||||||
|
|
||||||
{Platform.OS === 'web' && <WebBadge />}
|
|
||||||
</SafeAreaView>
|
|
||||||
</ThemedView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
safeArea: {
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: Spacing.four,
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: Spacing.three,
|
|
||||||
paddingBottom: BottomTabInset + Spacing.three,
|
|
||||||
maxWidth: MaxContentWidth,
|
|
||||||
},
|
|
||||||
heroSection: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: Spacing.four,
|
|
||||||
gap: Spacing.four,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
stepContainer: {
|
|
||||||
gap: Spacing.three,
|
|
||||||
alignSelf: 'stretch',
|
|
||||||
paddingHorizontal: Spacing.three,
|
|
||||||
paddingVertical: Spacing.four,
|
|
||||||
borderRadius: Spacing.four,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Image } from 'expo-image';
|
import { Image } from "expo-image";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Dimensions, StyleSheet, View } from 'react-native';
|
import { Dimensions, StyleSheet, View } from "react-native";
|
||||||
import Animated, { Easing, Keyframe } from 'react-native-reanimated';
|
import Animated, { Easing, Keyframe } from "react-native-reanimated";
|
||||||
import { scheduleOnRN } from 'react-native-worklets';
|
import { scheduleOnRN } from "react-native-worklets";
|
||||||
|
|
||||||
const INITIAL_SCALE_FACTOR = Dimensions.get('screen').height / 90;
|
const INITIAL_SCALE_FACTOR = Dimensions.get("screen").height / 90;
|
||||||
const DURATION = 600;
|
const DURATION = 600;
|
||||||
|
|
||||||
export function AnimatedSplashOverlay() {
|
export function AnimatedSplashOverlay() {
|
||||||
@@ -31,102 +31,39 @@ export function AnimatedSplashOverlay() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return <></>;
|
||||||
<Animated.View
|
|
||||||
entering={splashKeyframe.duration(DURATION).withCallback((finished) => {
|
|
||||||
'worklet';
|
|
||||||
if (finished) {
|
|
||||||
scheduleOnRN(setVisible, false);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
style={styles.backgroundSolidColor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyframe = new Keyframe({
|
interface AnimatedIconProp {
|
||||||
0: {
|
image: string;
|
||||||
transform: [{ scale: INITIAL_SCALE_FACTOR }],
|
}
|
||||||
},
|
|
||||||
100: {
|
|
||||||
transform: [{ scale: 1 }],
|
|
||||||
easing: Easing.elastic(0.7),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const logoKeyframe = new Keyframe({
|
export function AnimatedIcon({ image }: AnimatedIconProp) {
|
||||||
0: {
|
|
||||||
transform: [{ scale: 1.3 }],
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
40: {
|
|
||||||
transform: [{ scale: 1.3 }],
|
|
||||||
opacity: 0,
|
|
||||||
easing: Easing.elastic(0.7),
|
|
||||||
},
|
|
||||||
100: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: [{ scale: 1 }],
|
|
||||||
easing: Easing.elastic(0.7),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const glowKeyframe = new Keyframe({
|
|
||||||
0: {
|
|
||||||
transform: [{ rotateZ: '0deg' }],
|
|
||||||
},
|
|
||||||
100: {
|
|
||||||
transform: [{ rotateZ: '7200deg' }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export function AnimatedIcon() {
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.iconContainer}>
|
<View style={styles.iconContainer}>
|
||||||
<Animated.View entering={glowKeyframe.duration(60 * 1000 * 4)} style={styles.glow}>
|
<Image style={styles.background} source={image} />
|
||||||
<Image style={styles.glow} source={require('@/assets/images/logo-glow.png')} />
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<Animated.View entering={keyframe.duration(DURATION)} style={styles.background} />
|
|
||||||
<Animated.View style={styles.imageContainer} entering={logoKeyframe.duration(DURATION)}>
|
|
||||||
<Image style={styles.image} source={require('@/assets/images/expo-logo.png')} />
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
imageContainer: {
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
glow: {
|
|
||||||
width: 201,
|
|
||||||
height: 201,
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
width: 128,
|
width: 128,
|
||||||
height: 128,
|
height: 128,
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
image: {
|
|
||||||
position: 'absolute',
|
|
||||||
width: 76,
|
|
||||||
height: 71,
|
|
||||||
},
|
|
||||||
background: {
|
background: {
|
||||||
borderRadius: 40,
|
borderRadius: 40,
|
||||||
experimental_backgroundImage: `linear-gradient(180deg, #3C9FFE, #0274DF)`,
|
experimental_backgroundImage: `linear-gradient(180deg, #3C9FFE, #0274DF)`,
|
||||||
width: 128,
|
width: 128,
|
||||||
height: 128,
|
height: 128,
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
},
|
},
|
||||||
backgroundSolidColor: {
|
backgroundSolidColor: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: '#208AEF',
|
backgroundColor: "#208AEF",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
104
src/constants/AuthContext.tsx
Normal file
104
src/constants/AuthContext.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// contexts/AuthContext.tsx
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import apiClient from "@/services/api"; // نمونه axios که بعداً میسازیم
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
isLogin: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
signIn: (token: string) => Promise<void>;
|
||||||
|
signOut: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType>({
|
||||||
|
isLogin: false,
|
||||||
|
isLoading: true,
|
||||||
|
signIn: async () => {},
|
||||||
|
signOut: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isLogin, setIsLogin] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const loadToken = () => {
|
||||||
|
axios
|
||||||
|
.get("https://api.plania.ir/api/auth/token/verify/")
|
||||||
|
.then((res) => {
|
||||||
|
setIsLogin(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
axios
|
||||||
|
.get(`https://api.plania.ir/api/auth/token/refresh/`)
|
||||||
|
.then(() => {
|
||||||
|
loadToken();
|
||||||
|
}).catch((err)=>{
|
||||||
|
if(err.response.status===401){
|
||||||
|
setIsLogin(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
loadToken();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {}, [isLogin]);
|
||||||
|
|
||||||
|
const signIn = async (token: string) => {
|
||||||
|
try {
|
||||||
|
setIsLogin(true);
|
||||||
|
router.push({
|
||||||
|
pathname: "/(tabs)",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save token", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
try {
|
||||||
|
apiClient.post("api/auth/logout/").then(async (res) => {
|
||||||
|
setIsLogin(false);
|
||||||
|
router.push({
|
||||||
|
pathname: "/(auth)/login",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove token", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
isLogin,
|
||||||
|
isLoading,
|
||||||
|
signIn,
|
||||||
|
signOut,
|
||||||
|
}),
|
||||||
|
[isLogin, isLoading, signIn, signOut],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ isLogin, isLoading, signIn, signOut }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
return useContext(AuthContext);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
|
* Below are the colors that are used in the app. The colors are defined for light and dark mode.
|
||||||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
* This theme matches the OTP verification screen design.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@/global.css';
|
import '@/global.css';
|
||||||
@@ -9,18 +9,26 @@ import { Platform } from 'react-native';
|
|||||||
|
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
light: {
|
light: {
|
||||||
text: '#000000',
|
text: '#1A1A1E', // تیرهتر و خواناتر از مشکی خالص
|
||||||
background: '#ffffff',
|
background: '#F8F9FC', // پسزمینهٔ بسیار روشن با مایل به آبی
|
||||||
backgroundElement: '#F0F0F3',
|
backgroundElement: '#FFFFFF', // کارتها و فیلدهای ورودی سفید
|
||||||
backgroundSelected: '#E0E1E6',
|
backgroundSelected: '#E8ECF4', // آیتم انتخابشده با رنگ ملایم
|
||||||
textSecondary: '#60646C',
|
textSecondary: '#6E6E7A', // خاکستری با کنتراست خوب
|
||||||
|
accent: '#6C63FF', // همان رنگ بنفش برند
|
||||||
|
border: '#DCDEE3', // حاشیهٔ کمرنگ و شیک
|
||||||
|
inputBackground: '#FFFFFF', // ورودیها سفید
|
||||||
|
inputPlaceholder: '#9A9AA6', // پلیسهولدر کمی تیرهتر برای خوانایی
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
text: '#ffffff',
|
text: '#FFFFFF',
|
||||||
background: '#000000',
|
background: '#0D0D14', // Main background
|
||||||
backgroundElement: '#212225',
|
backgroundElement: '#161622', // Input, button backgrounds
|
||||||
backgroundSelected: '#2E3135',
|
backgroundSelected: '#2E3135',
|
||||||
textSecondary: '#B0B4BA',
|
textSecondary: '#888888', // Subtitle, labels
|
||||||
|
accent: '#6C63FF', // Primary button, focus border
|
||||||
|
border: '#2A2A3E', // Borders, dividers
|
||||||
|
inputBackground: '#161622', // Same as backgroundElement
|
||||||
|
inputPlaceholder: '#5A5A6E',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -28,16 +36,12 @@ export type ThemeColor = keyof typeof Colors.light & keyof typeof Colors.dark;
|
|||||||
|
|
||||||
export const Fonts = Platform.select({
|
export const Fonts = Platform.select({
|
||||||
ios: {
|
ios: {
|
||||||
/** iOS `UIFontDescriptorSystemDesignDefault` */
|
|
||||||
sans: 'system-ui',
|
sans: 'system-ui',
|
||||||
/** iOS `UIFontDescriptorSystemDesignSerif` */
|
|
||||||
serif: 'ui-serif',
|
serif: 'ui-serif',
|
||||||
/** iOS `UIFontDescriptorSystemDesignRounded` */
|
|
||||||
rounded: 'ui-rounded',
|
rounded: 'ui-rounded',
|
||||||
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
|
|
||||||
mono: 'ui-monospace',
|
mono: 'ui-monospace',
|
||||||
},
|
},
|
||||||
default: {
|
android: {
|
||||||
sans: 'normal',
|
sans: 'normal',
|
||||||
serif: 'serif',
|
serif: 'serif',
|
||||||
rounded: 'normal',
|
rounded: 'normal',
|
||||||
@@ -49,17 +53,59 @@ export const Fonts = Platform.select({
|
|||||||
rounded: 'var(--font-rounded)',
|
rounded: 'var(--font-rounded)',
|
||||||
mono: 'var(--font-mono)',
|
mono: 'var(--font-mono)',
|
||||||
},
|
},
|
||||||
|
default: {
|
||||||
|
sans: 'normal',
|
||||||
|
serif: 'serif',
|
||||||
|
rounded: 'normal',
|
||||||
|
mono: 'monospace',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Spacing = {
|
export const Spacing = {
|
||||||
half: 2,
|
half: 2,
|
||||||
one: 4,
|
one: 4,
|
||||||
two: 8,
|
two: 8,
|
||||||
three: 16,
|
three: 12,
|
||||||
four: 24,
|
four: 16,
|
||||||
five: 32,
|
five: 20,
|
||||||
six: 64,
|
six: 24,
|
||||||
|
seven: 32,
|
||||||
|
eight: 48,
|
||||||
|
nine: 64,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const Typography = {
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '500',
|
||||||
|
lineHeight: 28,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '400',
|
||||||
|
lineHeight: 16,
|
||||||
|
},
|
||||||
|
fieldLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '500',
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '400',
|
||||||
|
},
|
||||||
|
caption: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: '400',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const BorderRadius = {
|
||||||
|
small: 8,
|
||||||
|
medium: 12,
|
||||||
|
large: 16,
|
||||||
|
xlarge: 24,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BottomTabInset = Platform.select({ ios: 50, android: 80 }) ?? 0;
|
export const BottomTabInset = Platform.select({ ios: 50, android: 80 }) ?? 0;
|
||||||
export const MaxContentWidth = 800;
|
export const MaxContentWidth = 800;
|
||||||
70
src/services/api.ts
Normal file
70
src/services/api.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
|
||||||
|
interface RetryableRequestConfig extends InternalAxiosRequestConfig {
|
||||||
|
_retry?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueItem {
|
||||||
|
resolve: (value?: any) => void;
|
||||||
|
reject: (reason?: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: "https://api.plania.ir",
|
||||||
|
timeout: 15000,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let isRefreshing = false;
|
||||||
|
let failedQueue: QueueItem[] = [];
|
||||||
|
|
||||||
|
const processQueue = (error: AxiosError | null, token: string | null = null) => {
|
||||||
|
failedQueue.forEach((prom) => {
|
||||||
|
if (error) {
|
||||||
|
prom.reject(error);
|
||||||
|
} else {
|
||||||
|
prom.resolve(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
failedQueue = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
const originalRequest = error.config as RetryableRequestConfig;
|
||||||
|
|
||||||
|
if (error.response?.status !== 401 || originalRequest._retry) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
try {
|
||||||
|
await apiClient.get("/api/auth/token/refresh/");
|
||||||
|
processQueue(null);
|
||||||
|
return apiClient(originalRequest);
|
||||||
|
} catch (refreshError) {
|
||||||
|
console.log("asdf")
|
||||||
|
processQueue(refreshError as AxiosError);
|
||||||
|
if (router && typeof router.push === "function") {
|
||||||
|
router.push("/(auth)/login");
|
||||||
|
}
|
||||||
|
return Promise.reject(refreshError);
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
failedQueue.push({ resolve, reject });
|
||||||
|
})
|
||||||
|
.then(() => apiClient(originalRequest))
|
||||||
|
.catch((err) => Promise.reject(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default apiClient;
|
||||||
276
src/styles/auth.styles.ts
Normal file
276
src/styles/auth.styles.ts
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { BorderRadius, Spacing, Typography } from "@/constants/theme";
|
||||||
|
import { Theme } from "@/types/theme";
|
||||||
|
import { DimensionValue, StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
export const createWelcomeStyles = (theme: Theme, isDark: boolean) => {
|
||||||
|
const percent = (value: number): DimensionValue =>
|
||||||
|
`${value}%` as DimensionValue;
|
||||||
|
|
||||||
|
return StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingHorizontal: Spacing.four,
|
||||||
|
paddingTop: Spacing.five,
|
||||||
|
paddingBottom: Spacing.seven,
|
||||||
|
},
|
||||||
|
logoMark: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: Spacing.seven,
|
||||||
|
},
|
||||||
|
logoBox: {
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
backgroundColor: theme.accent,
|
||||||
|
borderRadius: BorderRadius.small,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginHorizontal: Spacing.two,
|
||||||
|
},
|
||||||
|
logoIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
logoWord: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.text,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
},
|
||||||
|
heroGrid: {
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: Spacing.five,
|
||||||
|
},
|
||||||
|
miniCard: {
|
||||||
|
width: "48%",
|
||||||
|
backgroundColor: theme.backgroundElement,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
borderRadius: BorderRadius.medium,
|
||||||
|
padding: Spacing.two,
|
||||||
|
marginBottom: Spacing.three,
|
||||||
|
},
|
||||||
|
miniCardAccent: {
|
||||||
|
backgroundColor: isDark ? "#1a1635" : "#F0EFFF",
|
||||||
|
borderColor: isDark ? "#534AB7" : theme.accent + "40",
|
||||||
|
},
|
||||||
|
mcDot: {
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: 3.5,
|
||||||
|
marginBottom: Spacing.two,
|
||||||
|
},
|
||||||
|
mcLines: {
|
||||||
|
marginBottom: Spacing.one,
|
||||||
|
},
|
||||||
|
mcLine: {
|
||||||
|
height: 3.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: isDark ? "#2e2e46" : "#E0E0E6",
|
||||||
|
marginVertical: Spacing.half,
|
||||||
|
},
|
||||||
|
mcTag: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
borderRadius: BorderRadius.small / 2,
|
||||||
|
paddingVertical: Spacing.half,
|
||||||
|
paddingHorizontal: Spacing.one,
|
||||||
|
marginTop: Spacing.two,
|
||||||
|
},
|
||||||
|
mcTagText: {
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
heroCta: {
|
||||||
|
marginTop: Spacing.one,
|
||||||
|
},
|
||||||
|
tagline: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.text,
|
||||||
|
lineHeight: 24,
|
||||||
|
marginBottom: Spacing.two,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
taglineAccent: {
|
||||||
|
color: isDark ? "#AFA9EC" : theme.accent,
|
||||||
|
},
|
||||||
|
subText: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: theme.textSecondary,
|
||||||
|
lineHeight: 15,
|
||||||
|
marginBottom: Spacing.four,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
outlineButton: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
borderRadius: BorderRadius.large,
|
||||||
|
paddingVertical: Spacing.three,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
outlineButtonText: {
|
||||||
|
color: theme.textSecondary,
|
||||||
|
fontSize: Typography.body.fontSize,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
primaryButton: {
|
||||||
|
backgroundColor: theme.accent,
|
||||||
|
borderRadius: BorderRadius.large,
|
||||||
|
paddingVertical: Spacing.three,
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: Spacing.two,
|
||||||
|
},
|
||||||
|
primaryButtonText: {
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontSize: Typography.body.fontSize,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authStyles = (theme: Theme, focusedField: string | null = null) =>
|
||||||
|
StyleSheet.create({
|
||||||
|
safeArea: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
},
|
||||||
|
scrollContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: Spacing.six,
|
||||||
|
paddingHorizontal: Spacing.four,
|
||||||
|
},
|
||||||
|
phoneCard: {
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
borderRadius: 32,
|
||||||
|
paddingHorizontal: Spacing.four,
|
||||||
|
paddingVertical: Spacing.five,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
},
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: Spacing.three,
|
||||||
|
marginBottom: Spacing.five,
|
||||||
|
},
|
||||||
|
iconCircle: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: BorderRadius.large,
|
||||||
|
backgroundColor: theme.backgroundElement,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.accent + "80",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
headerTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: Typography.title.fontSize,
|
||||||
|
fontWeight: Typography.title.fontWeight,
|
||||||
|
color: theme.text,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
fieldLabel: {
|
||||||
|
fontSize: Typography.fieldLabel.fontSize,
|
||||||
|
fontWeight: Typography.fieldLabel.fontWeight as any,
|
||||||
|
color: theme.textSecondary,
|
||||||
|
letterSpacing: Typography.fieldLabel.letterSpacing,
|
||||||
|
marginBottom: Spacing.two,
|
||||||
|
marginTop: Spacing.one,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: theme.inputBackground,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: focusedField ? theme.accent : theme.border,
|
||||||
|
borderRadius: BorderRadius.medium,
|
||||||
|
paddingVertical: Spacing.two,
|
||||||
|
paddingHorizontal: Spacing.three,
|
||||||
|
marginBottom: Spacing.three,
|
||||||
|
gap: Spacing.two,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 12,
|
||||||
|
color: theme.text,
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
continueButton: {
|
||||||
|
backgroundColor: theme.accent,
|
||||||
|
borderRadius: BorderRadius.large,
|
||||||
|
paddingVertical: Spacing.three,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: Spacing.two,
|
||||||
|
marginTop: Spacing.two,
|
||||||
|
marginBottom: Spacing.four,
|
||||||
|
},
|
||||||
|
continueText: {
|
||||||
|
fontSize: Typography.body.fontSize,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
switchContainer: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: Spacing.two,
|
||||||
|
},
|
||||||
|
switchText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: theme.textSecondary,
|
||||||
|
},
|
||||||
|
switchLink: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.accent,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: Typography.subtitle.fontSize,
|
||||||
|
fontWeight: Typography.subtitle.fontWeight,
|
||||||
|
color: theme.textSecondary,
|
||||||
|
lineHeight: Typography.subtitle.lineHeight,
|
||||||
|
marginBottom: Spacing.four,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
|
||||||
|
secondaryButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: Spacing.two,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: theme.border,
|
||||||
|
borderRadius: BorderRadius.large,
|
||||||
|
paddingVertical: Spacing.three,
|
||||||
|
marginTop: Spacing.two,
|
||||||
|
},
|
||||||
|
secondaryButtonDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
secondaryButtonText: {
|
||||||
|
fontSize: Typography.body.fontSize,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: theme.accent,
|
||||||
|
},
|
||||||
|
secondaryButtonTextDisabled: {
|
||||||
|
color: theme.textSecondary,
|
||||||
|
},
|
||||||
|
});
|
||||||
11
src/types/theme.ts
Normal file
11
src/types/theme.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export type Theme = {
|
||||||
|
background: string;
|
||||||
|
backgroundElement: string;
|
||||||
|
backgroundSelected: string;
|
||||||
|
text: string;
|
||||||
|
textSecondary: string;
|
||||||
|
accent: string;
|
||||||
|
border: string;
|
||||||
|
inputBackground: string;
|
||||||
|
inputPlaceholder: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user