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