Implementação do RoadtripDJ - Funcionalidades e UI

This commit is contained in:
Eduardo Silva
2026-05-15 12:26:05 +01:00
parent 846525d2e3
commit 56c2f20c11
22 changed files with 3013 additions and 17 deletions

8
.env Normal file
View File

@@ -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

27
App.tsx
View File

@@ -1,20 +1,19 @@
import { StatusBar } from 'expo-status-bar'; 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() { export default function App() {
return ( return (
<View style={styles.container}> <SafeAreaProvider>
<Text>Open up App.tsx to start working on your app!</Text> <AuthProvider>
<StatusBar style="auto" /> <NavigationContainer>
</View> <AppNavigator />
</NavigationContainer>
<StatusBar style="auto" hidden={false} />
</AuthProvider>
</SafeAreaProvider>
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@@ -25,6 +25,9 @@
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
} },
"plugins": [
"expo-web-browser"
]
} }
} }

782
package-lock.json generated
View File

@@ -8,10 +8,25 @@
"name": "roadtrip-dj", "name": "roadtrip-dj",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "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": "~54.0.33",
"expo-auth-session": "~7.0.11",
"expo-crypto": "~15.0.9",
"expo-status-bar": "~3.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": "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": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
@@ -2704,6 +2719,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@react-native/assets-registry": {
"version": "0.81.5", "version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", "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==", "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==",
"license": "MIT" "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": { "node_modules/@sinclair/typebox": {
"version": "0.27.10", "version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
@@ -2988,6 +3168,90 @@
"@sinonjs/commons": "^3.0.0" "@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": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -3625,6 +3889,12 @@
"node": ">=0.6" "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": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -3915,6 +4185,19 @@
"node": ">=0.8" "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": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3930,6 +4213,34 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT" "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": { "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",
@@ -4062,6 +4373,56 @@
"node": ">= 8" "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": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "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": { "node_modules/deep-extend": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -4153,6 +4523,61 @@
"node": ">=8" "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": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -4207,6 +4632,18 @@
"node": ">= 0.8" "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": { "node_modules/env-editor": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", "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": { "node_modules/expo-modules-autolinking": {
"version": "3.0.25", "version": "3.0.25",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz",
@@ -4462,6 +4980,16 @@
"react-native": "*" "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": { "node_modules/expo/node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "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==", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0" "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": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -4934,6 +5468,15 @@
"node": ">=8" "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": { "node_modules/finalhandler": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -5194,6 +5737,15 @@
"node": ">= 14" "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": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5279,6 +5831,12 @@
"loose-envify": "^1.0.0" "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": { "node_modules/is-core-module": {
"version": "2.16.2", "version": "2.16.2",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
@@ -5327,6 +5885,15 @@
"node": ">=0.12.0" "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": { "node_modules/is-wsl": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -6216,6 +6783,17 @@
"yallist": "^3.0.2" "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": { "node_modules/makeerror": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -6231,12 +6809,30 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0" "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": { "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",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT" "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": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "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": "^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": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -7238,6 +7846,24 @@
"qrcode-terminal": "bin/qrcode-terminal.js" "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": { "node_modules/queue": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -7290,6 +7916,18 @@
"ws": "^7" "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": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -7363,6 +8001,82 @@
"react-native": "*" "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": { "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5", "version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
@@ -7839,6 +8553,15 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7889,6 +8612,15 @@
"plist": "^3.0.5" "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": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -7950,6 +8682,15 @@
"node": ">=0.10.0" "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": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -8013,6 +8754,15 @@
"node": ">= 0.10.0" "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": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -8376,6 +9126,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"license": "Apache-2.0" "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": { "node_modules/type-detect": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@@ -8502,6 +9258,24 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -8554,6 +9328,12 @@
"makeerror": "1.0.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": { "node_modules/wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -9,10 +9,25 @@
"web": "expo start --web" "web": "expo start --web"
}, },
"dependencies": { "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": "~54.0.33",
"expo-auth-session": "~7.0.11",
"expo-crypto": "~15.0.9",
"expo-status-bar": "~3.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": "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": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",

View File

@@ -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 (
<View style={styles.container}>
{/* Left Timeline Visual */}
<View style={styles.timelineVisual}>
<View style={[styles.dot, getDotStyle()]} />
{!isLast && <View style={styles.line} />}
</View>
{/* Right Content */}
<View style={[styles.content, type === 'stop' && styles.contentStop]}>
<Text style={styles.title}>{title}</Text>
<Text style={[styles.subtitle, type === 'stop' && styles.subtitleStop]}>{subtitle}</Text>
</View>
</View>
);
}
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
},
});

View File

@@ -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 (
<View style={styles.container}>
<Text style={styles.indexText}>{index}</Text>
<View style={styles.infoContainer}>
<Text style={styles.titleText}>{title}</Text>
<Text style={styles.artistText}>{artist}</Text>
</View>
<Text style={styles.durationText}>{duration}</Text>
</View>
);
}
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',
},
});

View File

@@ -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<AuthContextType>({
user: null,
session: null,
loading: true,
});
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(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 (
<AuthContext.Provider value={{ user, session, loading }}>
{children}
</AuthContext.Provider>
);
};

View File

@@ -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 (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: colors.background }}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{!user ? (
// Auth Flow
<>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</>
) : (
// Main Flow
<>
<Stack.Screen name="Main" component={TabNavigator} />
<Stack.Screen name="TripDetails" component={TripDetailsScreen} />
{/* Modals */}
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen name="NewTripModal" component={NewTripScreen} />
</Stack.Group>
</>
)}
</Stack.Navigator>
);
}

View File

@@ -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) => (
<TouchableOpacity
style={styles.customButtonContainer}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.customButton}>
{children}
</View>
</TouchableOpacity>
);
export default function TabNavigator() {
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
tabBarShowLabel: false,
tabBarStyle: styles.tabBar,
tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: colors.textSecondary,
}}
>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarIcon: ({ color }) => <Home color={color} size={24} />,
}}
/>
<Tab.Screen
name="NewTrip"
component={NewTripScreen}
listeners={({ navigation }) => ({
tabPress: (e) => {
e.preventDefault();
navigation.navigate('NewTripModal');
},
})}
options={{
tabBarIcon: () => <Plus color={colors.white} size={28} />,
tabBarButton: (props) => <CustomTabBarButton {...props} />,
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarIcon: ({ color }) => <User color={color} size={24} />,
}}
/>
</Tab.Navigator>
);
}
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,
},
});

View File

@@ -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 (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* Header Section */}
<View style={styles.headerContainer}>
<View style={styles.iconWrapper}>
<View style={styles.carIconContainer}>
<Car color={colors.white} size={32} />
</View>
<View style={styles.musicIconContainer}>
<Music color={colors.primary} size={16} />
</View>
</View>
<Text style={styles.title}>Roadtrip DJ</Text>
<Text style={styles.subtitle}>O teu guia de carros e música</Text>
</View>
{/* Form Card */}
<View style={styles.card}>
<TextInput
style={styles.input}
placeholder="Email"
placeholderTextColor={colors.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
value={email}
onChangeText={setEmail}
/>
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor={colors.textSecondary}
secureTextEntry={true}
value={password}
onChangeText={setPassword}
/>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleLogin}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.primaryButtonText}>Entrar</Text>
)}
</TouchableOpacity>
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyAuth}>
{/* 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. */}
<Music color={colors.white} size={20} style={styles.spotifyIcon} />
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
</TouchableOpacity>
<View style={styles.footerContainer}>
<Text style={styles.footerText}>Não tens conta? </Text>
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
<Text style={styles.footerLink}>Criar conta</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
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',
},
});

View File

@@ -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 (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView}
>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* Header Section */}
<View style={styles.headerContainer}>
<View style={styles.iconWrapper}>
<View style={styles.carIconContainer}>
<Car color={colors.white} size={32} />
</View>
<View style={styles.musicIconContainer}>
<Music color={colors.primary} size={16} />
</View>
</View>
<Text style={styles.title}>Roadtrip DJ</Text>
<Text style={styles.subtitle}>Cria a tua conta</Text>
</View>
{/* Form Card */}
<View style={styles.card}>
<TextInput
style={styles.input}
placeholder="Nome"
placeholderTextColor={colors.textSecondary}
autoCapitalize="words"
value={name}
onChangeText={setName}
/>
<TextInput
style={styles.input}
placeholder="Email"
placeholderTextColor={colors.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
value={email}
onChangeText={setEmail}
/>
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor={colors.textSecondary}
secureTextEntry={true}
value={password}
onChangeText={setPassword}
/>
<TextInput
style={styles.input}
placeholder="Confirmar Password"
placeholderTextColor={colors.textSecondary}
secureTextEntry={true}
value={confirmPassword}
onChangeText={setConfirmPassword}
/>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleRegister}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.primaryButtonText}>Criar Conta</Text>
)}
</TouchableOpacity>
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyAuth}>
{/* Note: Placeholder Spotify logo */}
<Music color={colors.white} size={20} style={styles.spotifyIcon} />
<Text style={styles.spotifyButtonText}>Registar com Spotify</Text>
</TouchableOpacity>
<View style={styles.footerContainer}>
<Text style={styles.footerText}> tens conta? </Text>
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
<Text style={styles.footerLink}>Entrar</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
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',
},
});

View File

@@ -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<any>;
}
export default function HomeScreen({ navigation }: Props) {
const { user } = useAuth();
const userName = user?.user_metadata?.name || 'Viajante';
const initial = userName.charAt(0).toUpperCase();
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* Header Section */}
<View style={styles.header}>
<Text style={styles.title}>As Tuas Viagens</Text>
<View style={styles.avatar}>
<Text style={styles.avatarText}>{initial}</Text>
</View>
</View>
{/* Main Trip Card (Removed hardcoded data - empty state below) */}
{/* Prompt Card */}
<View style={styles.promptCard}>
<Text style={styles.promptTitle}>Pronto para a próxima?</Text>
<Text style={styles.promptSubtitle}>
Descobre a melhor rota e a banda sonora{'\n'}perfeita para o caminho.
</Text>
<TouchableOpacity
style={styles.promptButton}
onPress={() => navigation.navigate('NewTripModal')}
>
<Text style={styles.promptButtonText}>Criar Nova Viagem</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
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,
},
});

View File

@@ -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 (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Perfil</Text>
<TouchableOpacity style={styles.settingsButton}>
<Settings color={colors.textMain} size={20} />
</TouchableOpacity>
</View>
<View style={styles.headerDivider} />
{/* Profile Info */}
<View style={styles.profileSection}>
<View style={styles.avatarContainer}>
<View style={styles.avatarImagePlaceholder}>
<Text style={styles.avatarImageText}>{userName.charAt(0).toUpperCase()}</Text>
</View>
<View style={styles.spotifyBadge}>
<Music color={colors.white} size={12} />
</View>
</View>
<Text style={styles.userName}>{userName}</Text>
<Text style={styles.userHandle}>{userEmail}</Text>
</View>
{/* Stats Row */}
<View style={styles.statsRow}>
<View style={styles.statCol}>
<Text style={styles.statNumber}>0</Text>
<Text style={styles.statLabel}>VIAGENS</Text>
</View>
<View style={styles.statCol}>
<Text style={styles.statNumber}>0</Text>
<Text style={styles.statLabel}>SEGUIDORES</Text>
</View>
<View style={styles.statCol}>
<Text style={styles.statNumber}>0</Text>
<Text style={styles.statLabel}>A SEGUIR</Text>
</View>
</View>
<View style={styles.sectionDivider} />
{/* Preferences Section */}
<View style={styles.preferencesSection}>
<Text style={styles.sectionTitle}>ESTATÍSTICAS E PREFERÊNCIAS</Text>
<View style={styles.preferencesCard}>
{/* Preference Item 1 */}
<View style={styles.prefItem}>
<View style={[styles.prefIconContainer, { backgroundColor: '#FFF0E5' }]}>
<MapIcon color={colors.primary} size={20} />
</View>
<View style={styles.prefTextContainer}>
<Text style={styles.prefTitle}>Distância Total</Text>
<Text style={styles.prefSubtitle}>0 km conduzidos</Text>
</View>
</View>
{/* Preference Item 2 */}
<View style={[styles.prefItem, { marginBottom: 0, marginTop: 24 }]}>
<View style={[styles.prefIconContainer, { backgroundColor: '#E8F5E9' }]}>
<Heart color={colors.spotify} size={20} />
</View>
<View style={styles.prefTextContainer}>
<Text style={styles.prefTitle}>Género Favorito</Text>
<Text style={styles.prefSubtitle}>Ainda não definido</Text>
</View>
</View>
</View>
</View>
{/* Logout Button */}
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<LogOut color={colors.error} size={20} style={styles.logoutIcon} />
<Text style={styles.logoutText}>Terminar Sessão</Text>
</TouchableOpacity>
</ScrollView>
</SafeAreaView>
);
}
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',
},
});

View File

@@ -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 (
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Nova Viagem</Text>
<TouchableOpacity
style={styles.closeButton}
onPress={() => navigation.goBack()}
>
<X color={colors.textMain} size={20} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.scrollContent}>
{/* Map Area Placeholder */}
<View style={styles.mapArea}>
{/* Using a solid light gray color instead of a complex map image to keep it clean */}
<View style={styles.mockRouteVisual}>
<View style={styles.routeDotLarge} />
<View style={styles.routeLineDashed} />
<View style={styles.routePinLarge}>
<MapPin color={colors.white} size={14} />
</View>
</View>
</View>
{/* Form Card */}
<View style={styles.formCard}>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>NOME DA VIAGEM</Text>
<TextInput
style={styles.textInput}
placeholder="Ex: Fim de semana no Algarve"
placeholderTextColor={colors.textSecondary}
/>
</View>
<View style={styles.routeInputContainer}>
{/* Visual timeline on the left */}
<View style={styles.routeTimeline}>
<View style={styles.timelineDot} />
<View style={styles.timelineLine} />
<MapPin color={colors.textSecondary} size={16} style={styles.timelinePin} />
</View>
<View style={styles.routeInputs}>
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>PARTIDA</Text>
<TextInput
style={[styles.textInput, styles.routeTextInput]}
value="Lisboa, Portugal"
placeholderTextColor={colors.textMain}
/>
</View>
<View style={[styles.inputGroup, { marginBottom: 0 }]}>
<Text style={styles.inputLabel}>DESTINO</Text>
<TextInput
style={[styles.textInput, styles.routeTextInput]}
value="Porto, Portugal"
placeholderTextColor={colors.textMain}
/>
</View>
</View>
</View>
</View>
{/* Bottom Actions */}
<View style={styles.bottomActions}>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleCreateRoute}
>
<Text style={styles.primaryButtonText}>Criar Rota & Playlist</Text>
<ArrowRight color={colors.white} size={20} />
</TouchableOpacity>
<Text style={styles.disclaimerText}>
A IA vai analisar o trajeto, pontos de interesse, clima{'\n'}e duração para criar a banda sonora perfeita.
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
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',
},
});

View File

@@ -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 = () => (
<View style={styles.tabContent}>
{/* Stats Cards */}
<View style={styles.statsContainer}>
<View style={styles.statCard}>
{/* @ts-ignore */}
<Navigation color={colors.primary} size={24} style={{ marginBottom: 8 }} />
<Text style={styles.statValue}>314 km</Text>
<Text style={styles.statLabel}>Distância</Text>
</View>
<View style={styles.dividerVertical} />
<View style={styles.statCard}>
<Clock color={colors.primary} size={24} style={{ marginBottom: 8 }} />
<Text style={styles.statValue}>3h 15m</Text>
<Text style={styles.statLabel}>Tempo</Text>
</View>
</View>
{/* DJ Guide Card */}
<View style={styles.djCard}>
<View style={styles.djHeader}>
<View style={styles.djIconContainer}>
<Music color={colors.white} size={16} />
</View>
<Text style={styles.djTitle}>O Teu DJ Guide:</Text>
</View>
<Text style={styles.djText}>
"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."
</Text>
</View>
{/* Itinerary */}
<View style={styles.itinerarySection}>
<View style={styles.sectionHeader}>
<Compass color={colors.textMain} size={20} style={{ marginRight: 8 }} />
<Text style={styles.sectionTitle}>Itinerário</Text>
</View>
<View style={styles.timelineContainer}>
<TimelineItem
title="Lisbon, Portugal"
subtitle="Partida • 0 km"
type="start"
/>
<TimelineItem
title="Área de Serviço de Pombal"
subtitle="Paragem sugerida para café. + 145 km"
type="stop"
/>
<TimelineItem
title="Porto, Portugal"
subtitle="Destino • 314 km"
type="end"
isLast
/>
</View>
</View>
{/* Maps Action */}
<TouchableOpacity style={styles.mapsButton}>
<MapPin color={colors.white} size={20} style={{ marginRight: 8 }} />
<Text style={styles.mapsButtonText}>Abrir no Google Maps</Text>
</TouchableOpacity>
</View>
);
const renderPlaylistTab = () => (
<View style={styles.tabContent}>
{/* Generated Playlist Card */}
<View style={styles.playlistGeneratedCard}>
<View style={styles.spotifyLogoLarge}>
{/* Mocking large spotify logo with music icon for now */}
<Music color={colors.spotify} size={40} />
</View>
<Text style={styles.playlistTitle}>Playlist Gerada</Text>
<Text style={styles.playlistSubtitle}>45 músicas 3h 20m de viagem</Text>
<TouchableOpacity style={styles.spotifyActionBtn}>
<Play fill={colors.white} color={colors.white} size={18} style={{ marginRight: 8 }} />
<Text style={styles.spotifyActionText}>Ouvir no Spotify</Text>
</TouchableOpacity>
</View>
{/* Preview List */}
<Text style={styles.previewTitle}>Pré-visualização (Músicas Iniciais)</Text>
<View style={styles.previewListCard}>
{MOCK_TRACKS.map((track, index) => (
<TrackItem
key={track.id}
index={index + 1}
title={track.title}
artist={track.artist}
duration={track.duration}
/>
))}
</View>
</View>
);
return (
<View style={styles.container}>
<ScrollView>
{/* Header Image */}
<ImageBackground
source={{ uri: 'https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?q=80&w=2021&auto=format&fit=crop' }}
style={styles.headerImage}
>
<SafeAreaView style={styles.headerSafeArea}>
<View style={styles.headerNav}>
<TouchableOpacity style={styles.navIconButton} onPress={() => navigation.goBack()}>
<ArrowLeft color={colors.white} size={24} />
</TouchableOpacity>
<View style={styles.navRightActions}>
<TouchableOpacity style={styles.navIconButton}>
<Share2 color={colors.white} size={20} />
</TouchableOpacity>
<TouchableOpacity style={[styles.navIconButton, { marginLeft: 12 }]}>
<MoreVertical color={colors.white} size={20} />
</TouchableOpacity>
</View>
</View>
<View style={styles.headerTitles}>
<View style={styles.tag}>
<Text style={styles.tagText}>ROADTRIP</Text>
</View>
<Text style={styles.mainTitle}>Lisbon to Porto Coastline</Text>
<Text style={styles.subTitle}>May 10, 2026</Text>
</View>
</SafeAreaView>
</ImageBackground>
{/* Content Area overlapping image slightly */}
<View style={styles.contentWrapper}>
{/* Custom Tabs */}
<View style={styles.tabContainer}>
<TouchableOpacity
style={[styles.tabButton, activeTab === 'rota' && styles.tabButtonActive]}
onPress={() => setActiveTab('rota')}
>
<Text style={[styles.tabText, activeTab === 'rota' && styles.tabTextActive]}>
Rota & DJ
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tabButton, activeTab === 'playlist' && styles.tabButtonActive]}
onPress={() => setActiveTab('playlist')}
>
<Text style={[styles.tabText, activeTab === 'playlist' && styles.tabTextActive]}>
Playlist Spotify
</Text>
</TouchableOpacity>
</View>
{/* Render Tab Content */}
{activeTab === 'rota' ? renderRotaTab() : renderPlaylistTab()}
</View>
</ScrollView>
</View>
);
}
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,
},
});

View File

@@ -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
};

6
src/services/ollama.ts Normal file
View File

@@ -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
};

10
src/services/spotify.ts Normal file
View File

@@ -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
};

15
src/services/supabase.ts Normal file
View File

@@ -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,
},
});

34
src/types/index.ts Normal file
View File

@@ -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;
}

14
src/utils/colors.ts Normal file
View File

@@ -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',
};