diff --git a/.env b/.env
new file mode 100644
index 0000000..588d5ab
--- /dev/null
+++ b/.env
@@ -0,0 +1,8 @@
+EXPO_PUBLIC_SUPABASE_URL=https://qyvnryhskgmvgjajqqru.supabase.co
+EXPO_PUBLIC_SUPABASE_ANON_KEY=sb_publishable_fazCCLmO7XjtryY28ePR-A_CS7aU6fF
+
+# GOOGLE MAPS (Fase 2)
+EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=CAIzaSyDBXsQiWnLehBpTCW7Xg--MNQ3wTfkexXA
+
+# SPOTIFY (Fase 3)
+EXPO_PUBLIC_SPOTIFY_CLIENT_ID=C7fa1e7acbf7e44f18bf28d74f14fb9cb
\ No newline at end of file
diff --git a/App.tsx b/App.tsx
index 0329d0c..84ad1a9 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,20 +1,19 @@
import { StatusBar } from 'expo-status-bar';
-import { StyleSheet, Text, View } from 'react-native';
+import React from 'react';
+import { NavigationContainer } from '@react-navigation/native';
+import { SafeAreaProvider } from 'react-native-safe-area-context';
+import AppNavigator from './src/navigation/AppNavigator';
+import { AuthProvider } from './src/contexts/AuthContext';
export default function App() {
return (
-
- Open up App.tsx to start working on your app!
-
-
+
+
+
+
+
+
+
+
);
}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- alignItems: 'center',
- justifyContent: 'center',
- },
-});
diff --git a/app.json b/app.json
index 79b7223..fa01599 100644
--- a/app.json
+++ b/app.json
@@ -25,6 +25,9 @@
},
"web": {
"favicon": "./assets/favicon.png"
- }
+ },
+ "plugins": [
+ "expo-web-browser"
+ ]
}
}
diff --git a/package-lock.json b/package-lock.json
index b33beec..599e01d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,10 +8,25 @@
"name": "roadtrip-dj",
"version": "1.0.0",
"dependencies": {
+ "@react-native-async-storage/async-storage": "2.2.0",
+ "@react-navigation/bottom-tabs": "^7.15.12",
+ "@react-navigation/material-top-tabs": "^7.4.25",
+ "@react-navigation/native": "^7.2.3",
+ "@react-navigation/native-stack": "^7.14.13",
+ "@supabase/supabase-js": "^2.105.4",
"expo": "~54.0.33",
+ "expo-auth-session": "~7.0.11",
+ "expo-crypto": "~15.0.9",
"expo-status-bar": "~3.0.9",
+ "expo-web-browser": "~15.0.11",
+ "lucide-react-native": "^1.14.0",
"react": "19.1.0",
- "react-native": "0.81.5"
+ "react-native": "0.81.5",
+ "react-native-pager-view": "6.9.1",
+ "react-native-safe-area-context": "~5.6.0",
+ "react-native-screens": "~4.16.0",
+ "react-native-svg": "15.12.1",
+ "react-native-url-polyfill": "^3.0.0"
},
"devDependencies": {
"@types/react": "~19.1.0",
@@ -2704,6 +2719,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
+ "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
+ "license": "MIT",
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.65 <1.0"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@@ -2964,6 +2991,159 @@
"integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==",
"license": "MIT"
},
+ "node_modules/@react-navigation/bottom-tabs": {
+ "version": "7.15.12",
+ "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.12.tgz",
+ "integrity": "sha512-Kp7oUEWgUB3NLBbgPkE8DGPtHU6jfhqPQGhFlUYYJ+PeoFcRX++Y1GMn90yYanCKpob8I7l6/YbzhN39owO06Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.9.16",
+ "color": "^4.2.3",
+ "sf-symbols-typescript": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.2.3",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/core": {
+ "version": "7.17.3",
+ "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.3.tgz",
+ "integrity": "sha512-cFOzT4d6oOjdAWwk69onVQXhEN1CHmGau5zCP5DO9mLeO/N1Db0a/ZXP57fn0t/6lf7OPX8vl6tPcv3lBR4F/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/routers": "^7.5.4",
+ "escape-string-regexp": "^4.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^3.3.11",
+ "query-string": "^7.1.3",
+ "react-is": "^19.1.0",
+ "use-latest-callback": "^0.2.4",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0"
+ }
+ },
+ "node_modules/@react-navigation/core/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@react-navigation/core/node_modules/react-is": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz",
+ "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-navigation/elements": {
+ "version": "2.9.16",
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.16.tgz",
+ "integrity": "sha512-uScoLXOvQwdj7w9hn69kyubNYm7EZMAX9fAqbrTIA8mYUAv+9qfhJxOcO8VXcoT0Vm8EKNDXqg5n5WNxcdN0Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "color": "^4.2.3",
+ "use-latest-callback": "^0.2.4",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@react-native-masked-view/masked-view": ">= 0.2.0",
+ "@react-navigation/native": "^7.2.3",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-masked-view/masked-view": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-navigation/material-top-tabs": {
+ "version": "7.4.25",
+ "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-7.4.25.tgz",
+ "integrity": "sha512-ifQIQbSWcfe9435kCzhgPzGoTZHoZplf5lEUnSfBVo6wYdM+R2oo92KMrqc/ZYQKfxrmqJYtnWOphT4IkEuJdA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.9.16",
+ "color": "^4.2.3",
+ "react-native-tab-view": "^4.3.0"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.2.3",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-pager-view": ">= 6.0.0",
+ "react-native-safe-area-context": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/native": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.3.tgz",
+ "integrity": "sha512-Q6vENZJnrRUmNzPa8m/SINzV0IQ2ndEQvVHQaJ0M1TvtyB8OWO/3hCl3ukWvnRUakroFNgwYokBXUaRhVvqU6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/core": "^7.17.3",
+ "escape-string-regexp": "^4.0.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^3.3.11",
+ "use-latest-callback": "^0.2.4"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0",
+ "react-native": "*"
+ }
+ },
+ "node_modules/@react-navigation/native-stack": {
+ "version": "7.14.13",
+ "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.13.tgz",
+ "integrity": "sha512-o6hNgvwUiKZFIFQI+27YndmtSRxgJXFAJDwkBhmNeD8EEdJUxom2NDKzqFPjwsDYQIRYXJmIHR3Qz2cRsGwSYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/elements": "^2.9.16",
+ "color": "^4.2.3",
+ "sf-symbols-typescript": "^2.1.0",
+ "warn-once": "^0.1.1"
+ },
+ "peerDependencies": {
+ "@react-navigation/native": "^7.2.3",
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-safe-area-context": ">= 4.0.0",
+ "react-native-screens": ">= 4.0.0"
+ }
+ },
+ "node_modules/@react-navigation/native/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@react-navigation/routers": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.4.tgz",
+ "integrity": "sha512-5ONLNA3hKwAo3n95ENaZvWHkLeC8+7dgy8U/D+mO0Tvrih21nfxGNRqizI+qN2gxryWvYRk/pq5NsnTw6TtZbg==",
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
@@ -2988,6 +3168,90 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.4.tgz",
+ "integrity": "sha512-Ejfa37M5xoIwoxVebxRahnwubPo8g22qkXQ4p50+N9MIvU9UZoN+A8dwVPtczzGf8oV/YXN80ZPxK4aWXuSN/A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.4.tgz",
+ "integrity": "sha512-JVNKbBft3Qkja+WlGaE026AJ2AH9K0UTsxsfvEIHgd4zFrBor4BYRCrYFrv9IDsvVqkF72wKDsODJl5GY/C4tA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/phoenix": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz",
+ "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.4.tgz",
+ "integrity": "sha512-SppIyLo/kTwIlz1qpv2HN1EQqBg0GVktrDDFsXygYROha3MgVn4rT7p5EjFHFqXQm2rdRGb/BI7bc+jr10m91w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.4.tgz",
+ "integrity": "sha512-6ov6c59+8D9h7q4M4Gy/uDJlC0Akxl9/714Y+6vJ+Sijuc16TS/p5DwhfRCLNcIhNiej1gEt+CQUwsjiPt4PxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/phoenix": "^0.4.2",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.4.tgz",
+ "integrity": "sha512-Jx+pzMP1Whjof2PWHoVBUA75/p7PQE9CqKBzn1oXVyJDOggMLSH2OzVWwsXYaxEpdC1K/KltwmOX44nL3LHl9g==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.4.tgz",
+ "integrity": "sha512-cEnx+k49knU+qdIP7rXwR6fqEXPHZs+74xFK1R0S8MgQ7v9tbePVdGxvO03n3bPympMdJWVLadARBfU4TgNHCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.105.4",
+ "@supabase/functions-js": "2.105.4",
+ "@supabase/postgrest-js": "2.105.4",
+ "@supabase/realtime-js": "2.105.4",
+ "@supabase/storage-js": "2.105.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -3625,6 +3889,12 @@
"node": ">=0.6"
}
},
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
+ },
"node_modules/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -3915,6 +4185,19 @@
"node": ">=0.8"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3930,6 +4213,34 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/color/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -4062,6 +4373,56 @@
"node": ">= 8"
}
},
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -4086,6 +4447,15 @@
}
}
},
+ "node_modules/decode-uri-component": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -4153,6 +4523,61 @@
"node": ">=8"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -4207,6 +4632,18 @@
"node": ">= 0.8"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-editor": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -4341,6 +4778,87 @@
}
}
},
+ "node_modules/expo-auth-session": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-7.0.11.tgz",
+ "integrity": "sha512-AhWtt/m9rb1Po77X/VBFbeE6UTgbm2vXP2iCblUSRsHCw2qD6lO0ulKUB8Xyxy9FtoI9yrNQ1iwCNgIIgo8VYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-application": "~7.0.8",
+ "expo-constants": "~18.0.13",
+ "expo-crypto": "~15.0.9",
+ "expo-linking": "~8.0.12",
+ "expo-web-browser": "~15.0.11",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-auth-session/node_modules/expo-application": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
+ "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-auth-session/node_modules/expo-constants": {
+ "version": "18.0.13",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
+ "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config": "~12.0.13",
+ "@expo/env": "~2.0.8"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-crypto": {
+ "version": "15.0.9",
+ "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.9.tgz",
+ "integrity": "sha512-SNWKa2fXx7v9gkp1h/7nqXY5XN7qgNDn3yRc2aO0gWGbeMbvob/haMxxsPFe9f51aqH5NjNCqHf2kvLhvAd8KQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-linking": {
+ "version": "8.0.12",
+ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz",
+ "integrity": "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-constants": "~18.0.13",
+ "invariant": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-linking/node_modules/expo-constants": {
+ "version": "18.0.13",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz",
+ "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config": "~12.0.13",
+ "@expo/env": "~2.0.8"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-modules-autolinking": {
"version": "3.0.25",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz",
@@ -4462,6 +4980,16 @@
"react-native": "*"
}
},
+ "node_modules/expo-web-browser": {
+ "version": "15.0.11",
+ "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.11.tgz",
+ "integrity": "sha512-r2LS4Ro6DgUPZkcaEfgt8mp9eJuoA93x11Jh7S6utFe0FEzvUNn2yFhxg8XVwESaaHGt2k5V8LuK36rsp0BeIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo/node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -4907,6 +5435,12 @@
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0"
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -4934,6 +5468,15 @@
"node": ">=8"
}
},
+ "node_modules/filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -5194,6 +5737,15 @@
"node": ">= 14"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5279,6 +5831,12 @@
"loose-envify": "^1.0.0"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
"node_modules/is-core-module": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
@@ -5327,6 +5885,15 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -6216,6 +6783,17 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react-native": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-1.14.0.tgz",
+ "integrity": "sha512-FkOjd4JHIB8SplakHQjeo4RR/5peXtl+PbL+TghQqWzcQ+AKbJ/PFl5xEnerLtSQ4EIq6B9uXblY7QSyKg05WQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-native": "*",
+ "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
+ }
+ },
"node_modules/makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -6231,12 +6809,30 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0"
},
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
+ "node_modules/merge-options": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
+ "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -6803,6 +7399,18 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -7238,6 +7846,24 @@
"qrcode-terminal": "bin/qrcode-terminal.js"
}
},
+ "node_modules/query-string": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
+ "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
+ "license": "MIT",
+ "dependencies": {
+ "decode-uri-component": "^0.2.2",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -7290,6 +7916,18 @@
"ws": "^7"
}
},
+ "node_modules/react-freeze": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
+ "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -7363,6 +8001,82 @@
"react-native": "*"
}
},
+ "node_modules/react-native-pager-view": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.9.1.tgz",
+ "integrity": "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-safe-area-context": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
+ "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-screens": {
+ "version": "4.16.0",
+ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
+ "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-freeze": "^1.0.0",
+ "react-native-is-edge-to-edge": "^1.2.1",
+ "warn-once": "^0.1.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-svg": {
+ "version": "15.12.1",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
+ "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "css-tree": "^1.1.3",
+ "warn-once": "0.1.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-tab-view": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-4.3.0.tgz",
+ "integrity": "sha512-qPMF75uz/7+MuVG2g+YETdGMzlWZnhC6iI4h/7EBbwIBwNBIBi2z4OA6KhY3IOOBwGHXEIz5IyA6doDqifYBHg==",
+ "license": "MIT",
+ "dependencies": {
+ "use-latest-callback": "^0.2.4"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0",
+ "react-native": "*",
+ "react-native-pager-view": ">= 6.0.0"
+ }
+ },
+ "node_modules/react-native-url-polyfill": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-3.0.0.tgz",
+ "integrity": "sha512-aA5CiuUCUb/lbrliVCJ6lZ17/RpNJzvTO/C7gC/YmDQhTUoRD5q5HlJfwLWcxz4VgAhHwXKzhxH+wUN24tAdqg==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url-without-unicode": "8.0.0-3"
+ },
+ "peerDependencies": {
+ "react-native": "*"
+ }
+ },
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
@@ -7839,6 +8553,15 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/sf-symbols-typescript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
+ "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7889,6 +8612,15 @@
"plist": "^3.0.5"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -7950,6 +8682,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -8013,6 +8754,15 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -8376,6 +9126,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"license": "Apache-2.0"
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@@ -8502,6 +9258,24 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/use-latest-callback": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
+ "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -8554,6 +9328,12 @@
"makeerror": "1.0.12"
}
},
+ "node_modules/warn-once": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
+ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
+ "license": "MIT"
+ },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
diff --git a/package.json b/package.json
index 521d049..d28a654 100644
--- a/package.json
+++ b/package.json
@@ -9,10 +9,25 @@
"web": "expo start --web"
},
"dependencies": {
+ "@react-native-async-storage/async-storage": "2.2.0",
+ "@react-navigation/bottom-tabs": "^7.15.12",
+ "@react-navigation/material-top-tabs": "^7.4.25",
+ "@react-navigation/native": "^7.2.3",
+ "@react-navigation/native-stack": "^7.14.13",
+ "@supabase/supabase-js": "^2.105.4",
"expo": "~54.0.33",
+ "expo-auth-session": "~7.0.11",
+ "expo-crypto": "~15.0.9",
"expo-status-bar": "~3.0.9",
+ "expo-web-browser": "~15.0.11",
+ "lucide-react-native": "^1.14.0",
"react": "19.1.0",
- "react-native": "0.81.5"
+ "react-native": "0.81.5",
+ "react-native-pager-view": "6.9.1",
+ "react-native-safe-area-context": "~5.6.0",
+ "react-native-screens": "~4.16.0",
+ "react-native-svg": "15.12.1",
+ "react-native-url-polyfill": "^3.0.0"
},
"devDependencies": {
"@types/react": "~19.1.0",
diff --git a/src/components/TimelineItem.tsx b/src/components/TimelineItem.tsx
new file mode 100644
index 0000000..7f1bca2
--- /dev/null
+++ b/src/components/TimelineItem.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { colors } from '../utils/colors';
+
+interface TimelineItemProps {
+ title: string;
+ subtitle: string;
+ type: 'start' | 'stop' | 'end';
+ isLast?: boolean;
+}
+
+export default function TimelineItem({ title, subtitle, type, isLast = false }: TimelineItemProps) {
+
+ const getDotStyle = () => {
+ switch (type) {
+ case 'start': return styles.dotStart;
+ case 'stop': return styles.dotStop;
+ case 'end': return styles.dotEnd;
+ }
+ };
+
+ return (
+
+ {/* Left Timeline Visual */}
+
+
+ {!isLast && }
+
+
+ {/* Right Content */}
+
+ {title}
+ {subtitle}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ },
+ timelineVisual: {
+ alignItems: 'center',
+ width: 20,
+ marginRight: 16,
+ },
+ dot: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ marginTop: 4,
+ },
+ dotStart: {
+ backgroundColor: colors.primary,
+ },
+ dotStop: {
+ backgroundColor: '#3B82F6', // Blue for stops
+ },
+ dotEnd: {
+ backgroundColor: '#111827', // Black for end
+ },
+ line: {
+ flex: 1,
+ width: 2,
+ backgroundColor: colors.inputBorder,
+ marginVertical: 4,
+ minHeight: 40,
+ },
+ content: {
+ flex: 1,
+ paddingBottom: 24,
+ },
+ contentStop: {
+ backgroundColor: '#F0F5FF', // Light blue background for stop
+ padding: 12,
+ borderRadius: 12,
+ marginBottom: 24,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 4,
+ },
+ subtitle: {
+ fontSize: 13,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ subtitleStop: {
+ color: '#2563EB', // Blue text for stop subtitle
+ },
+});
diff --git a/src/components/TrackItem.tsx b/src/components/TrackItem.tsx
new file mode 100644
index 0000000..e939fcb
--- /dev/null
+++ b/src/components/TrackItem.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+import { colors } from '../utils/colors';
+
+interface TrackItemProps {
+ index: number;
+ title: string;
+ artist: string;
+ duration: string;
+}
+
+export default function TrackItem({ index, title, artist, duration }: TrackItemProps) {
+ return (
+
+ {index}
+
+
+ {title}
+ {artist}
+
+
+ {duration}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 12,
+ },
+ indexText: {
+ width: 30,
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#9CA3AF', // Lighter gray for index
+ textAlign: 'center',
+ marginRight: 12,
+ },
+ infoContainer: {
+ flex: 1,
+ },
+ titleText: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 4,
+ },
+ artistText: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ durationText: {
+ fontSize: 14,
+ color: '#9CA3AF',
+ fontWeight: '500',
+ },
+});
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..d81cf1e
--- /dev/null
+++ b/src/contexts/AuthContext.tsx
@@ -0,0 +1,45 @@
+import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
+import { Session, User } from '@supabase/supabase-js';
+import { supabase } from '../services/supabase';
+
+interface AuthContextType {
+ user: User | null;
+ session: Session | null;
+ loading: boolean;
+}
+
+const AuthContext = createContext({
+ user: null,
+ session: null,
+ loading: true,
+});
+
+export const useAuth = () => useContext(AuthContext);
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [user, setUser] = useState(null);
+ const [session, setSession] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ setSession(session);
+ setUser(session?.user ?? null);
+ setLoading(false);
+ });
+
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
+ setSession(session);
+ setUser(session?.user ?? null);
+ setLoading(false);
+ });
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx
new file mode 100644
index 0000000..f48b7f5
--- /dev/null
+++ b/src/navigation/AppNavigator.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import { ActivityIndicator, View } from 'react-native';
+import LoginScreen from '../screens/auth/LoginScreen';
+import RegisterScreen from '../screens/auth/RegisterScreen';
+import TabNavigator from './TabNavigator';
+import NewTripScreen from '../screens/trip/NewTripScreen';
+import TripDetailsScreen from '../screens/trip/TripDetailsScreen';
+import { useAuth } from '../contexts/AuthContext';
+import { colors } from '../utils/colors';
+
+const Stack = createNativeStackNavigator();
+
+export default function AppNavigator() {
+ const { user, loading } = useAuth();
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {!user ? (
+ // Auth Flow
+ <>
+
+
+ >
+ ) : (
+ // Main Flow
+ <>
+
+
+
+ {/* Modals */}
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/navigation/TabNavigator.tsx b/src/navigation/TabNavigator.tsx
new file mode 100644
index 0000000..9457ba0
--- /dev/null
+++ b/src/navigation/TabNavigator.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { View, StyleSheet, TouchableOpacity } from 'react-native';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { Home, User, Plus } from 'lucide-react-native';
+import { colors } from '../utils/colors';
+
+import HomeScreen from '../screens/main/HomeScreen';
+import ProfileScreen from '../screens/main/ProfileScreen';
+import NewTripScreen from '../screens/trip/NewTripScreen';
+
+const Tab = createBottomTabNavigator();
+
+const CustomTabBarButton = ({ children, onPress }: any) => (
+
+
+ {children}
+
+
+);
+
+export default function TabNavigator() {
+ return (
+
+ ,
+ }}
+ />
+
+ ({
+ tabPress: (e) => {
+ e.preventDefault();
+ navigation.navigate('NewTripModal');
+ },
+ })}
+ options={{
+ tabBarIcon: () => ,
+ tabBarButton: (props) => ,
+ }}
+ />
+
+ ,
+ }}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tabBar: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ elevation: 0,
+ backgroundColor: colors.white,
+ height: 90,
+ paddingBottom: 20, // SafeArea spacing
+ borderTopWidth: 1,
+ borderTopColor: colors.inputBorder,
+ },
+ customButtonContainer: {
+ top: -20,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ customButton: {
+ width: 60,
+ height: 60,
+ borderRadius: 30,
+ backgroundColor: colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ shadowColor: colors.primary,
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+ shadowOpacity: 0.3,
+ shadowRadius: 4.65,
+ elevation: 8,
+ },
+});
diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx
new file mode 100644
index 0000000..9149ba2
--- /dev/null
+++ b/src/screens/auth/LoginScreen.tsx
@@ -0,0 +1,250 @@
+import React, { useState } from 'react';
+import { View, Text, TextInput, TouchableOpacity, StyleSheet, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert, ActivityIndicator } from 'react-native';
+import { Car, Music } from 'lucide-react-native';
+import { colors } from '../../utils/colors';
+import { supabase } from '../../services/supabase';
+import { makeRedirectUri } from 'expo-auth-session';
+import * as WebBrowser from 'expo-web-browser';
+
+WebBrowser.maybeCompleteAuthSession();
+// @ts-ignore
+export default function LoginScreen({ navigation }) {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleLogin = async () => {
+ if (!email || !password) {
+ Alert.alert('Erro', 'Por favor preenche todos os campos.');
+ return;
+ }
+
+ setLoading(true);
+ const { error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error) {
+ Alert.alert('Erro no login', error.message);
+ }
+ setLoading(false);
+ };
+
+ const handleSpotifyAuth = async () => {
+ Alert.alert('Teste', 'O botão do Spotify foi clicado!');
+ try {
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider: 'spotify',
+ options: {
+ redirectTo: makeRedirectUri()
+ }
+ });
+
+ if (error) {
+ Alert.alert('Erro no login Spotify', error.message);
+ return;
+ }
+
+ if (data?.url) {
+ await WebBrowser.openAuthSessionAsync(data.url, makeRedirectUri());
+ }
+ } catch (err: any) {
+ Alert.alert('Erro', err?.message || 'Ocorreu um erro no Spotify Auth.');
+ }
+ };
+
+ return (
+
+
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+
+
+ Roadtrip DJ
+ O teu guia de carros e música
+
+
+ {/* Form Card */}
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Entrar
+ )}
+
+
+
+ {/* Note: In a real app we would use an actual Spotify SVG logo. Using Music icon for now as a placeholder for the Spotify logo. */}
+
+ Entrar com Spotify
+
+
+
+ Não tens conta?
+ navigation.navigate('Register')}>
+ Criar conta
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.primary,
+ },
+ keyboardView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingTop: 60,
+ paddingBottom: 40,
+ },
+ headerContainer: {
+ alignItems: 'center',
+ marginBottom: 40,
+ },
+ iconWrapper: {
+ position: 'relative',
+ marginBottom: 20,
+ width: 100,
+ height: 100,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ carIconContainer: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ musicIconContainer: {
+ position: 'absolute',
+ bottom: 5,
+ right: 5,
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: '#000000',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: '800',
+ color: colors.white,
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: colors.white,
+ fontWeight: '500',
+ },
+ card: {
+ backgroundColor: colors.white,
+ width: '100%',
+ borderRadius: 24,
+ padding: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 12,
+ elevation: 5,
+ },
+ input: {
+ backgroundColor: colors.inputBackground,
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ fontSize: 16,
+ marginBottom: 16,
+ color: colors.textMain,
+ },
+ primaryButton: {
+ backgroundColor: colors.primary,
+ borderRadius: 12,
+ paddingVertical: 16,
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ primaryButtonText: {
+ color: colors.white,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ spotifyButton: {
+ backgroundColor: colors.spotify,
+ borderRadius: 12,
+ paddingVertical: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 24,
+ },
+ spotifyIcon: {
+ marginRight: 8,
+ },
+ spotifyButtonText: {
+ color: colors.white,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ footerContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ footerText: {
+ color: colors.textSecondary,
+ fontSize: 14,
+ },
+ footerLink: {
+ color: colors.textMain,
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
+});
diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx
new file mode 100644
index 0000000..7bf8cfe
--- /dev/null
+++ b/src/screens/auth/RegisterScreen.tsx
@@ -0,0 +1,277 @@
+import React, { useState } from 'react';
+import { View, Text, TextInput, TouchableOpacity, StyleSheet, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert, ActivityIndicator } from 'react-native';
+import { Car, Music } from 'lucide-react-native';
+import { colors } from '../../utils/colors';
+import { supabase } from '../../services/supabase';
+import { makeRedirectUri } from 'expo-auth-session';
+import * as WebBrowser from 'expo-web-browser';
+
+WebBrowser.maybeCompleteAuthSession();
+// @ts-ignore
+export default function RegisterScreen({ navigation }) {
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleRegister = async () => {
+ if (!name || !email || !password || !confirmPassword) {
+ Alert.alert('Erro', 'Por favor preenche todos os campos.');
+ return;
+ }
+ if (password !== confirmPassword) {
+ Alert.alert('Erro', 'As passwords não coincidem.');
+ return;
+ }
+
+ setLoading(true);
+ const { error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ data: {
+ name: name,
+ }
+ }
+ });
+
+ if (error) {
+ Alert.alert('Erro no registo', error.message);
+ }
+ setLoading(false);
+ };
+
+ const handleSpotifyAuth = async () => {
+ Alert.alert('Teste', 'O botão do Spotify foi clicado!');
+ try {
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider: 'spotify',
+ options: {
+ redirectTo: makeRedirectUri()
+ }
+ });
+
+ if (error) {
+ Alert.alert('Erro no login Spotify', error.message);
+ return;
+ }
+
+ if (data?.url) {
+ await WebBrowser.openAuthSessionAsync(data.url, makeRedirectUri());
+ }
+ } catch (err: any) {
+ Alert.alert('Erro', err?.message || 'Ocorreu um erro no Spotify Auth.');
+ }
+ };
+
+ return (
+
+
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+
+
+ Roadtrip DJ
+ Cria a tua conta
+
+
+ {/* Form Card */}
+
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Criar Conta
+ )}
+
+
+
+ {/* Note: Placeholder Spotify logo */}
+
+ Registar com Spotify
+
+
+
+ Já tens conta?
+ navigation.navigate('Login')}>
+ Entrar
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.primary,
+ },
+ keyboardView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingTop: 40,
+ paddingBottom: 40,
+ },
+ headerContainer: {
+ alignItems: 'center',
+ marginBottom: 30,
+ },
+ iconWrapper: {
+ position: 'relative',
+ marginBottom: 20,
+ width: 100,
+ height: 100,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ carIconContainer: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ musicIconContainer: {
+ position: 'absolute',
+ bottom: 5,
+ right: 5,
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: '#000000',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ fontSize: 32,
+ fontWeight: '800',
+ color: colors.white,
+ marginBottom: 8,
+ },
+ subtitle: {
+ fontSize: 16,
+ color: colors.white,
+ fontWeight: '500',
+ },
+ card: {
+ backgroundColor: colors.white,
+ width: '100%',
+ borderRadius: 24,
+ padding: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 12,
+ elevation: 5,
+ },
+ input: {
+ backgroundColor: colors.inputBackground,
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ fontSize: 16,
+ marginBottom: 16,
+ color: colors.textMain,
+ },
+ primaryButton: {
+ backgroundColor: colors.primary,
+ borderRadius: 12,
+ paddingVertical: 16,
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ primaryButtonText: {
+ color: colors.white,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ spotifyButton: {
+ backgroundColor: colors.spotify,
+ borderRadius: 12,
+ paddingVertical: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 24,
+ },
+ spotifyIcon: {
+ marginRight: 8,
+ },
+ spotifyButtonText: {
+ color: colors.white,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ footerContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ footerText: {
+ color: colors.textSecondary,
+ fontSize: 14,
+ },
+ footerLink: {
+ color: colors.textMain,
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
+});
diff --git a/src/screens/main/HomeScreen.tsx b/src/screens/main/HomeScreen.tsx
new file mode 100644
index 0000000..c106016
--- /dev/null
+++ b/src/screens/main/HomeScreen.tsx
@@ -0,0 +1,211 @@
+import React from 'react';
+import { View, Text, StyleSheet, ScrollView, TouchableOpacity, SafeAreaView, ImageBackground } from 'react-native';
+import { NavigationProp } from '@react-navigation/native';
+import { Navigation } from 'lucide-react-native'; // Closest to MapPin with outline
+import { Clock } from 'lucide-react-native';
+import { colors } from '../../utils/colors';
+import { useAuth } from '../../contexts/AuthContext';
+
+interface Props {
+ navigation: NavigationProp;
+}
+
+export default function HomeScreen({ navigation }: Props) {
+ const { user } = useAuth();
+ const userName = user?.user_metadata?.name || 'Viajante';
+ const initial = userName.charAt(0).toUpperCase();
+
+ return (
+
+
+
+ {/* Header Section */}
+
+ As Tuas Viagens
+
+ {initial}
+
+
+
+ {/* Main Trip Card (Removed hardcoded data - empty state below) */}
+ {/* Prompt Card */}
+
+ Pronto para a próxima?
+
+ Descobre a melhor rota e a banda sonora{'\n'}perfeita para o caminho.
+
+ navigation.navigate('NewTripModal')}
+ >
+ Criar Nova Viagem
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ scrollContent: {
+ paddingHorizontal: 20,
+ paddingTop: 10,
+ paddingBottom: 120, // Extra space for bottom tab bar and floating button
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 24,
+ },
+ title: {
+ fontSize: 26,
+ fontWeight: '800',
+ color: colors.textMain,
+ },
+ avatar: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ backgroundColor: '#FFE5D4', // Light orange
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ avatarText: {
+ color: colors.primaryDark,
+ fontWeight: 'bold',
+ fontSize: 16,
+ },
+ mainTripCard: {
+ backgroundColor: colors.white,
+ borderRadius: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 8 },
+ shadowOpacity: 0.05,
+ shadowRadius: 12,
+ elevation: 4,
+ marginBottom: 24,
+ },
+ tripImage: {
+ height: 180,
+ justifyContent: 'flex-end',
+ },
+ imageOverlay: {
+ padding: 20,
+ backgroundColor: 'rgba(0,0,0,0.3)',
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ },
+ tripTitle: {
+ color: colors.white,
+ fontSize: 22,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ },
+ tripDate: {
+ color: colors.white,
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ tripContent: {
+ padding: 20,
+ },
+ statsRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 20,
+ },
+ statItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ statText: {
+ fontSize: 15,
+ fontWeight: 'bold',
+ color: colors.textSecondary,
+ },
+ statDot: {
+ width: 4,
+ height: 4,
+ borderRadius: 2,
+ backgroundColor: colors.inputBorder,
+ marginHorizontal: 12,
+ },
+ itinerarySnippet: {
+ flexDirection: 'row',
+ backgroundColor: colors.background,
+ padding: 16,
+ borderRadius: 16,
+ },
+ timeline: {
+ alignItems: 'center',
+ marginRight: 12,
+ paddingVertical: 4,
+ },
+ dot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: colors.primary,
+ },
+ dotEmpty: {
+ backgroundColor: 'transparent',
+ borderWidth: 2,
+ borderColor: colors.primary,
+ },
+ line: {
+ width: 2,
+ height: 20,
+ backgroundColor: colors.inputBorder,
+ marginVertical: 2,
+ },
+ locations: {
+ justifyContent: 'space-between',
+ },
+ locationText: {
+ fontSize: 15,
+ fontWeight: '600',
+ color: colors.textSecondary,
+ },
+ promptCard: {
+ backgroundColor: '#FFF5EB', // Very light orange
+ borderRadius: 24,
+ padding: 24,
+ borderWidth: 2,
+ borderColor: '#FFD4B8', // Dashed border color approx
+ borderStyle: 'dashed',
+ alignItems: 'center',
+ },
+ promptTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: '#8C3800', // Dark brownish orange
+ marginBottom: 8,
+ },
+ promptSubtitle: {
+ fontSize: 14,
+ color: '#B34700',
+ textAlign: 'center',
+ marginBottom: 20,
+ lineHeight: 20,
+ },
+ promptButton: {
+ backgroundColor: colors.primary,
+ paddingHorizontal: 24,
+ paddingVertical: 14,
+ borderRadius: 20,
+ width: '100%',
+ alignItems: 'center',
+ },
+ promptButtonText: {
+ color: colors.white,
+ fontWeight: 'bold',
+ fontSize: 16,
+ },
+});
diff --git a/src/screens/main/ProfileScreen.tsx b/src/screens/main/ProfileScreen.tsx
new file mode 100644
index 0000000..23694e5
--- /dev/null
+++ b/src/screens/main/ProfileScreen.tsx
@@ -0,0 +1,281 @@
+import React from 'react';
+import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, Image, ScrollView } from 'react-native';
+import { Settings, Music, Map as MapIcon, Heart, LogOut } from 'lucide-react-native';
+import { colors } from '../../utils/colors';
+import { supabase } from '../../services/supabase';
+import { useAuth } from '../../contexts/AuthContext';
+
+// @ts-ignore
+export default function ProfileScreen({ navigation }) {
+ const { user } = useAuth();
+
+ const handleLogout = async () => {
+ const { error } = await supabase.auth.signOut();
+ if (error) {
+ console.error('Error signing out:', error);
+ }
+ };
+
+ const userName = user?.user_metadata?.name || user?.email?.split('@')[0] || 'Viajante';
+ const userEmail = user?.email || '';
+
+ return (
+
+
+
+ {/* Header */}
+
+ Perfil
+
+
+
+
+
+
+
+ {/* Profile Info */}
+
+
+
+ {userName.charAt(0).toUpperCase()}
+
+
+
+
+
+
+ {userName}
+ {userEmail}
+
+
+ {/* Stats Row */}
+
+
+ 0
+ VIAGENS
+
+
+ 0
+ SEGUIDORES
+
+
+ 0
+ A SEGUIR
+
+
+
+
+
+ {/* Preferences Section */}
+
+ ESTATÍSTICAS E PREFERÊNCIAS
+
+
+
+ {/* Preference Item 1 */}
+
+
+
+
+
+ Distância Total
+ 0 km conduzidos
+
+
+
+ {/* Preference Item 2 */}
+
+
+
+
+
+ Género Favorito
+ Ainda não definido
+
+
+
+
+
+
+ {/* Logout Button */}
+
+
+ Terminar Sessão
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ scrollContent: {
+ paddingHorizontal: 20,
+ paddingTop: 10,
+ paddingBottom: 120, // Space for bottom tabs
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 10,
+ },
+ headerTitle: {
+ fontSize: 28,
+ fontWeight: '800',
+ color: colors.textMain,
+ },
+ settingsButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: colors.inputBackground,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ headerDivider: {
+ height: 1,
+ backgroundColor: colors.inputBorder,
+ marginTop: 16,
+ marginBottom: 30,
+ marginHorizontal: -20, // Extend to screen edges
+ },
+ profileSection: {
+ alignItems: 'center',
+ marginBottom: 30,
+ },
+ avatarContainer: {
+ position: 'relative',
+ marginBottom: 16,
+ },
+ avatarImagePlaceholder: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ backgroundColor: '#FFE5D4',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ avatarImageText: {
+ color: colors.primaryDark,
+ fontWeight: 'bold',
+ fontSize: 40,
+ },
+ spotifyBadge: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: colors.spotify,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 2,
+ borderColor: colors.background,
+ },
+ userName: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 4,
+ },
+ userHandle: {
+ fontSize: 16,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ statsRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingHorizontal: 10,
+ marginBottom: 24,
+ },
+ statCol: {
+ alignItems: 'center',
+ flex: 1,
+ },
+ statNumber: {
+ fontSize: 22,
+ fontWeight: '800',
+ color: colors.textMain,
+ marginBottom: 4,
+ },
+ statLabel: {
+ fontSize: 11,
+ fontWeight: 'bold',
+ color: colors.textSecondary,
+ letterSpacing: 1,
+ },
+ sectionDivider: {
+ height: 1,
+ backgroundColor: colors.inputBorder,
+ marginBottom: 30,
+ },
+ preferencesSection: {
+ marginBottom: 30,
+ },
+ sectionTitle: {
+ fontSize: 12,
+ fontWeight: 'bold',
+ color: colors.textSecondary,
+ letterSpacing: 1,
+ marginBottom: 16,
+ },
+ preferencesCard: {
+ backgroundColor: colors.white,
+ borderRadius: 24,
+ padding: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 3,
+ },
+ prefItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ prefIconContainer: {
+ width: 48,
+ height: 48,
+ borderRadius: 16,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 16,
+ },
+ prefTextContainer: {
+ flex: 1,
+ },
+ prefTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 4,
+ },
+ prefSubtitle: {
+ fontSize: 14,
+ color: colors.textSecondary,
+ },
+ logoutButton: {
+ backgroundColor: '#FEF2F2', // Light red
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 16,
+ borderRadius: 16,
+ marginBottom: 20,
+ },
+ logoutIcon: {
+ marginRight: 8,
+ },
+ logoutText: {
+ color: colors.error,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+});
diff --git a/src/screens/trip/NewTripScreen.tsx b/src/screens/trip/NewTripScreen.tsx
new file mode 100644
index 0000000..5b06b06
--- /dev/null
+++ b/src/screens/trip/NewTripScreen.tsx
@@ -0,0 +1,285 @@
+import React from 'react';
+import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, TextInput, ImageBackground, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
+import { X, MapPin, ArrowRight } from 'lucide-react-native';
+import { colors } from '../../utils/colors';
+
+// @ts-ignore
+export default function NewTripScreen({ navigation }) {
+
+ const handleCreateRoute = () => {
+ // In a real app, this would trigger the orchestration flow (Ollama -> Spotify -> Firebase)
+ // For now, we mock success and navigate to the Trip Details
+ navigation.replace('TripDetails');
+ };
+
+ return (
+
+
+ {/* Header */}
+
+ Nova Viagem
+ navigation.goBack()}
+ >
+
+
+
+
+
+ {/* Map Area Placeholder */}
+
+ {/* Using a solid light gray color instead of a complex map image to keep it clean */}
+
+
+
+
+
+
+
+
+
+ {/* Form Card */}
+
+
+
+ NOME DA VIAGEM
+
+
+
+
+ {/* Visual timeline on the left */}
+
+
+
+
+
+
+
+
+ PARTIDA
+
+
+
+
+ DESTINO
+
+
+
+
+
+
+
+ {/* Bottom Actions */}
+
+
+ Criar Rota & Playlist
+
+
+
+
+ A IA vai analisar o trajeto, pontos de interesse, clima{'\n'}e duração para criar a banda sonora perfeita.
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: colors.white,
+ },
+ container: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 20,
+ paddingTop: 16,
+ paddingBottom: 16,
+ backgroundColor: colors.white,
+ zIndex: 10,
+ },
+ title: {
+ fontSize: 22,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ },
+ closeButton: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: colors.inputBackground,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ mapArea: {
+ height: 180,
+ backgroundColor: '#F0F2F5', // Light map-like gray
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ mockRouteVisual: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 40,
+ width: '100%',
+ },
+ routeDotLarge: {
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: colors.primary,
+ borderWidth: 4,
+ borderColor: colors.white,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ zIndex: 2,
+ },
+ routeLineDashed: {
+ flex: 1,
+ height: 4,
+ borderWidth: 2,
+ borderColor: colors.primary,
+ borderStyle: 'dashed',
+ marginHorizontal: -4, // Overlap slightly
+ zIndex: 1,
+ },
+ routePinLarge: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: '#000000',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 2,
+ borderColor: colors.white,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ zIndex: 2,
+ },
+ formCard: {
+ backgroundColor: colors.white,
+ borderTopLeftRadius: 32,
+ borderTopRightRadius: 32,
+ padding: 24,
+ marginTop: -32, // Overlap the map
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: -4 },
+ shadowOpacity: 0.05,
+ shadowRadius: 12,
+ elevation: 10,
+ },
+ inputGroup: {
+ marginBottom: 20,
+ },
+ inputLabel: {
+ fontSize: 12,
+ fontWeight: 'bold',
+ color: colors.textSecondary,
+ marginBottom: 8,
+ letterSpacing: 0.5,
+ },
+ textInput: {
+ backgroundColor: colors.inputBackground,
+ borderRadius: 16,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ fontSize: 16,
+ color: colors.textMain,
+ fontWeight: '500',
+ },
+ routeInputContainer: {
+ flexDirection: 'row',
+ marginTop: 10,
+ },
+ routeTimeline: {
+ alignItems: 'center',
+ width: 30,
+ marginTop: 38, // Align with inputs
+ marginRight: 8,
+ },
+ timelineDot: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ backgroundColor: colors.primary,
+ },
+ timelineLine: {
+ width: 1,
+ height: 60,
+ backgroundColor: colors.inputBorder,
+ marginVertical: 4,
+ },
+ timelinePin: {
+ marginTop: 4,
+ },
+ routeInputs: {
+ flex: 1,
+ },
+ routeTextInput: {
+ fontWeight: 'bold',
+ },
+ bottomActions: {
+ paddingHorizontal: 24,
+ paddingBottom: 40,
+ backgroundColor: colors.white,
+ },
+ primaryButton: {
+ backgroundColor: colors.primary,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: 18,
+ borderRadius: 16,
+ marginBottom: 16,
+ shadowColor: colors.primary,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 6,
+ },
+ primaryButtonText: {
+ color: colors.white,
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginRight: 8,
+ },
+ disclaimerText: {
+ textAlign: 'center',
+ color: colors.textSecondary,
+ fontSize: 12,
+ lineHeight: 18,
+ fontWeight: '500',
+ },
+});
diff --git a/src/screens/trip/TripDetailsScreen.tsx b/src/screens/trip/TripDetailsScreen.tsx
new file mode 100644
index 0000000..e05027f
--- /dev/null
+++ b/src/screens/trip/TripDetailsScreen.tsx
@@ -0,0 +1,452 @@
+import React, { useState } from 'react';
+import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ImageBackground, SafeAreaView, FlatList } from 'react-native';
+import { ArrowLeft, Share2, MoreVertical, Navigation, Clock, Music, Compass, MapPin, Play } from 'lucide-react-native';
+import { colors } from '../../utils/colors';
+import TimelineItem from '../../components/TimelineItem';
+import TrackItem from '../../components/TrackItem';
+
+const MOCK_TRACKS = [
+ { id: '1', title: 'Born to Run', artist: 'Bruce Springsteen', duration: '4:30' },
+ { id: '2', title: 'Hotel California', artist: 'Eagles', duration: '6:30' },
+ { id: '3', title: 'Holocene', artist: 'Bon Iver', duration: '5:37' },
+ { id: '4', title: 'Ganges', artist: 'The National', duration: '4:12' },
+];
+
+// @ts-ignore
+export default function TripDetailsScreen({ navigation }) {
+ const [activeTab, setActiveTab] = useState<'rota' | 'playlist'>('rota');
+
+ const renderRotaTab = () => (
+
+ {/* Stats Cards */}
+
+
+ {/* @ts-ignore */}
+
+ 314 km
+ Distância
+
+
+
+
+ 3h 15m
+ Tempo
+
+
+
+ {/* DJ Guide Card */}
+
+
+
+
+
+ O Teu DJ Guide:
+
+
+ "Na primeira hora da viagem, ouve Rock clássico para acordar. Quando passares pela zona de Leiria, ouve Indie Folk. Ah, e faz uma paragem na área de serviço de Pombal porque o café lá tem ótimas reviews."
+
+
+
+ {/* Itinerary */}
+
+
+
+ Itinerário
+
+
+
+
+
+
+
+
+
+ {/* Maps Action */}
+
+
+ Abrir no Google Maps
+
+
+ );
+
+ const renderPlaylistTab = () => (
+
+ {/* Generated Playlist Card */}
+
+
+ {/* Mocking large spotify logo with music icon for now */}
+
+
+ Playlist Gerada
+ 45 músicas • 3h 20m de viagem
+
+
+
+ Ouvir no Spotify
+
+
+
+ {/* Preview List */}
+ Pré-visualização (Músicas Iniciais)
+
+ {MOCK_TRACKS.map((track, index) => (
+
+ ))}
+
+
+ );
+
+ return (
+
+
+ {/* Header Image */}
+
+
+
+ navigation.goBack()}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ROADTRIP
+
+ Lisbon to Porto Coastline
+ May 10, 2026
+
+
+
+
+ {/* Content Area overlapping image slightly */}
+
+
+ {/* Custom Tabs */}
+
+ setActiveTab('rota')}
+ >
+
+ Rota & DJ
+
+
+
+ setActiveTab('playlist')}
+ >
+
+ Playlist Spotify
+
+
+
+
+ {/* Render Tab Content */}
+ {activeTab === 'rota' ? renderRotaTab() : renderPlaylistTab()}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ headerImage: {
+ width: '100%',
+ height: 320,
+ justifyContent: 'flex-start',
+ },
+ headerSafeArea: {
+ flex: 1,
+ backgroundColor: 'rgba(0,0,0,0.3)', // Overlay
+ justifyContent: 'space-between',
+ },
+ headerNav: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingHorizontal: 20,
+ paddingTop: 16,
+ },
+ navIconButton: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: 'rgba(255,255,255,0.2)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ navRightActions: {
+ flexDirection: 'row',
+ },
+ headerTitles: {
+ paddingHorizontal: 20,
+ paddingBottom: 40, // Room for overlap
+ },
+ tag: {
+ backgroundColor: colors.primary,
+ alignSelf: 'flex-start',
+ paddingHorizontal: 12,
+ paddingVertical: 4,
+ borderRadius: 8,
+ marginBottom: 8,
+ },
+ tagText: {
+ color: colors.white,
+ fontSize: 12,
+ fontWeight: 'bold',
+ letterSpacing: 1,
+ },
+ mainTitle: {
+ fontSize: 32,
+ fontWeight: '800',
+ color: colors.white,
+ marginBottom: 4,
+ lineHeight: 38,
+ },
+ subTitle: {
+ fontSize: 16,
+ color: colors.white,
+ fontWeight: '500',
+ },
+ contentWrapper: {
+ flex: 1,
+ backgroundColor: colors.background,
+ borderTopLeftRadius: 24,
+ borderTopRightRadius: 24,
+ marginTop: -24,
+ minHeight: 500,
+ },
+ tabContainer: {
+ flexDirection: 'row',
+ borderBottomWidth: 1,
+ borderBottomColor: colors.inputBorder,
+ paddingHorizontal: 20,
+ marginTop: 10,
+ },
+ tabButton: {
+ flex: 1,
+ paddingVertical: 16,
+ alignItems: 'center',
+ borderBottomWidth: 2,
+ borderBottomColor: 'transparent',
+ },
+ tabButtonActive: {
+ borderBottomColor: colors.primary,
+ },
+ tabText: {
+ fontSize: 15,
+ fontWeight: 'bold',
+ color: colors.textSecondary,
+ },
+ tabTextActive: {
+ color: colors.primary,
+ },
+ tabContent: {
+ padding: 20,
+ },
+ statsContainer: {
+ flexDirection: 'row',
+ backgroundColor: colors.white,
+ borderRadius: 20,
+ padding: 20,
+ marginBottom: 24,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 3,
+ },
+ statCard: {
+ flex: 1,
+ alignItems: 'center',
+ },
+ dividerVertical: {
+ width: 1,
+ backgroundColor: colors.inputBorder,
+ marginHorizontal: 10,
+ },
+ statValue: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 4,
+ },
+ statLabel: {
+ fontSize: 13,
+ color: colors.textSecondary,
+ fontWeight: '500',
+ },
+ djCard: {
+ backgroundColor: '#FFF5EB',
+ borderRadius: 20,
+ padding: 20,
+ marginBottom: 30,
+ borderWidth: 1,
+ borderColor: '#FFE0C2',
+ },
+ djHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ djIconContainer: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ backgroundColor: colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: 10,
+ },
+ djTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: '#8C3800',
+ },
+ djText: {
+ fontSize: 15,
+ lineHeight: 24,
+ color: '#A34200',
+ fontWeight: '500',
+ },
+ itinerarySection: {
+ marginBottom: 30,
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 20,
+ },
+ sectionTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ },
+ timelineContainer: {
+ backgroundColor: colors.white,
+ borderRadius: 20,
+ padding: 20,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 3,
+ },
+ mapsButton: {
+ backgroundColor: '#111827', // Almost black
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 18,
+ borderRadius: 30, // Highly rounded
+ marginBottom: 40,
+ },
+ mapsButtonText: {
+ color: colors.white,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+
+ // Playlist Tab Styles
+ playlistGeneratedCard: {
+ backgroundColor: '#E8F5E9', // Light green
+ borderRadius: 24,
+ padding: 30,
+ alignItems: 'center',
+ marginBottom: 30,
+ borderWidth: 1,
+ borderColor: '#C8E6C9',
+ },
+ spotifyLogoLarge: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: '#000',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 16,
+ },
+ playlistTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 8,
+ },
+ playlistSubtitle: {
+ fontSize: 15,
+ color: colors.textSecondary,
+ marginBottom: 24,
+ },
+ spotifyActionBtn: {
+ backgroundColor: colors.spotify,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ paddingVertical: 14,
+ borderRadius: 30,
+ width: '100%',
+ justifyContent: 'center',
+ shadowColor: colors.spotify,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 5,
+ },
+ spotifyActionText: {
+ color: colors.white,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ previewTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: colors.textMain,
+ marginBottom: 16,
+ },
+ previewListCard: {
+ backgroundColor: colors.white,
+ borderRadius: 20,
+ padding: 20,
+ marginBottom: 40,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.05,
+ shadowRadius: 8,
+ elevation: 3,
+ },
+});
diff --git a/src/services/googleMaps.ts b/src/services/googleMaps.ts
new file mode 100644
index 0000000..d49b380
--- /dev/null
+++ b/src/services/googleMaps.ts
@@ -0,0 +1,6 @@
+// Placeholder for Google Maps logic
+export const GOOGLE_MAPS_API_KEY = "PLACEHOLDER_API_KEY";
+
+export const getDirections = async (origin: string, destination: string) => {
+ // Logic to fetch directions
+};
diff --git a/src/services/ollama.ts b/src/services/ollama.ts
new file mode 100644
index 0000000..d6e8e77
--- /dev/null
+++ b/src/services/ollama.ts
@@ -0,0 +1,6 @@
+// Placeholder for Ollama API logic
+export const OLLAMA_API_URL = "https://apichat.epvc.pt/";
+
+export const generateTripGuide = async (origin: string, destination: string, waypoints: string[], duration: string) => {
+ // Logic to call Ollama
+};
diff --git a/src/services/spotify.ts b/src/services/spotify.ts
new file mode 100644
index 0000000..148936d
--- /dev/null
+++ b/src/services/spotify.ts
@@ -0,0 +1,10 @@
+// Placeholder for Spotify logic
+export const SPOTIFY_CLIENT_ID = "PLACEHOLDER_CLIENT_ID";
+
+export const searchSpotifyTracks = async (query: string) => {
+ // Logic to search tracks
+};
+
+export const createPlaylist = async (userId: string, tracks: string[]) => {
+ // Logic to create playlist
+};
diff --git a/src/services/supabase.ts b/src/services/supabase.ts
new file mode 100644
index 0000000..c539eb8
--- /dev/null
+++ b/src/services/supabase.ts
@@ -0,0 +1,15 @@
+import 'react-native-url-polyfill/auto';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { createClient } from '@supabase/supabase-js';
+
+const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL as string;
+const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string;
+
+export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
+ auth: {
+ storage: AsyncStorage,
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: false,
+ },
+});
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..331a95e
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,34 @@
+export interface User {
+ uid: string;
+ email: string;
+ name?: string;
+ spotifyToken?: string;
+}
+
+export interface Trip {
+ id: string;
+ userId: string;
+ origin: string;
+ destination: string;
+ waypoints?: string[];
+ date: string;
+ durationMinutes: number;
+ distanceKm: number;
+ djMessage?: string;
+ playlistId?: string;
+}
+
+export interface Route {
+ origin: string;
+ destination: string;
+ duration: string;
+ distance: string;
+ polyline: string;
+}
+
+export interface PlaylistTrack {
+ trackTitle: string;
+ artist: string;
+ reason: string;
+ uri?: string;
+}
diff --git a/src/utils/colors.ts b/src/utils/colors.ts
new file mode 100644
index 0000000..0feef72
--- /dev/null
+++ b/src/utils/colors.ts
@@ -0,0 +1,14 @@
+export const colors = {
+ primary: '#FF7518',
+ primaryDark: '#E6630B', // Slightly darker orange for pressed states
+ spotify: '#1DB954',
+ spotifyDark: '#179C45',
+ background: '#F9FAFB', // Light gray/white background
+ cardBackground: '#FFFFFF',
+ textMain: '#111827',
+ textSecondary: '#6B7280',
+ inputBackground: '#F3F4F6',
+ inputBorder: '#E5E7EB',
+ error: '#EF4444',
+ white: '#FFFFFF',
+};