Implementação do RoadtripDJ - Funcionalidades e UI
This commit is contained in:
8
.env
Normal file
8
.env
Normal 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
27
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 (
|
||||
<View style={styles.container}>
|
||||
<Text>Open up App.tsx to start working on your app!</Text>
|
||||
<StatusBar style="auto" />
|
||||
</View>
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<NavigationContainer>
|
||||
<AppNavigator />
|
||||
</NavigationContainer>
|
||||
<StatusBar style="auto" hidden={false} />
|
||||
</AuthProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
5
app.json
5
app.json
@@ -25,6 +25,9 @@
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"expo-web-browser"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
782
package-lock.json
generated
782
package-lock.json
generated
@@ -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",
|
||||
|
||||
17
package.json
17
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",
|
||||
|
||||
94
src/components/TimelineItem.tsx
Normal file
94
src/components/TimelineItem.tsx
Normal 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
|
||||
},
|
||||
});
|
||||
60
src/components/TrackItem.tsx
Normal file
60
src/components/TrackItem.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
45
src/contexts/AuthContext.tsx
Normal file
45
src/contexts/AuthContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
47
src/navigation/AppNavigator.tsx
Normal file
47
src/navigation/AppNavigator.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
104
src/navigation/TabNavigator.tsx
Normal file
104
src/navigation/TabNavigator.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
250
src/screens/auth/LoginScreen.tsx
Normal file
250
src/screens/auth/LoginScreen.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
277
src/screens/auth/RegisterScreen.tsx
Normal file
277
src/screens/auth/RegisterScreen.tsx
Normal 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}>Já 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',
|
||||
},
|
||||
});
|
||||
211
src/screens/main/HomeScreen.tsx
Normal file
211
src/screens/main/HomeScreen.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
281
src/screens/main/ProfileScreen.tsx
Normal file
281
src/screens/main/ProfileScreen.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
285
src/screens/trip/NewTripScreen.tsx
Normal file
285
src/screens/trip/NewTripScreen.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
452
src/screens/trip/TripDetailsScreen.tsx
Normal file
452
src/screens/trip/TripDetailsScreen.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
6
src/services/googleMaps.ts
Normal file
6
src/services/googleMaps.ts
Normal 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
6
src/services/ollama.ts
Normal 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
10
src/services/spotify.ts
Normal 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
15
src/services/supabase.ts
Normal 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
34
src/types/index.ts
Normal 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
14
src/utils/colors.ts
Normal 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',
|
||||
};
|
||||
Reference in New Issue
Block a user