diff --git a/crux-frontend/.env b/crux-frontend/.env new file mode 100644 index 0000000..85af4f3 --- /dev/null +++ b/crux-frontend/.env @@ -0,0 +1,2 @@ +EXPO_PUBLIC_COGNITO_USER_POOL_ID=eu-west-2_6fPDsUm5T +EXPO_PUBLIC_COGNITO_CLIENT_ID=samt4bt8omlj5o4jj8vm1o9ig \ No newline at end of file diff --git a/crux-frontend/app.json b/crux-frontend/app.json index 373cc4b..3976ead 100644 --- a/crux-frontend/app.json +++ b/crux-frontend/app.json @@ -31,14 +31,12 @@ "plugins": [ "expo-router", [ - "expo-build-properties", + "expo-splash-screen", { - "ios": { - "newArchEnabled": true - }, - "android": { - "newArchEnabled": true - } + "image": "./assets/images/splash.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff" } ] ], diff --git a/crux-frontend/app/(authenticated)/(tabs)/_layout.tsx b/crux-frontend/app/(authenticated)/(tabs)/_layout.tsx index 561a8c6..86b2dd0 100644 --- a/crux-frontend/app/(authenticated)/(tabs)/_layout.tsx +++ b/crux-frontend/app/(authenticated)/(tabs)/_layout.tsx @@ -3,7 +3,7 @@ import {Tabs} from 'expo-router'; import {Home, Folder, Plus, Edit2, User} from 'lucide-react-native'; const {width} = Dimensions.get('window'); -const TAB_BAR_WIDTH = width * 0.8; // 90% of screen width +const TAB_BAR_WIDTH = width * 0.8; // 80% of screen width import {BottomTabBarProps} from '@react-navigation/bottom-tabs'; diff --git a/crux-frontend/app/(authenticated)/(tabs)/home.tsx b/crux-frontend/app/(authenticated)/(tabs)/home.tsx index e2deeb3..e9d854e 100644 --- a/crux-frontend/app/(authenticated)/(tabs)/home.tsx +++ b/crux-frontend/app/(authenticated)/(tabs)/home.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { View, Text, @@ -7,10 +6,13 @@ import { TouchableOpacity, StyleSheet, Dimensions, + Platform, + KeyboardAvoidingView, } from 'react-native'; import {Plus, ChevronRight} from 'lucide-react-native'; import Header from '@/components/Header'; -import HorizontalAccordion from '@/components/horizontal-accordion'; +import {HorizontalAccordion} from '@/components/HorizontalAccordion'; +import {Card} from '@/components/Card'; const {width} = Dimensions.get('window'); const CARD_WIDTH = (width - 48) / 2; @@ -68,94 +70,100 @@ export default function HomeScreen() { ], }; - const renderFolderCard = (folder: (typeof folders)[0]) => ( - - {folder.avatar ? ( - - - {folder.type} - - ) : ( - {folder.type} - )} - - {folder.name} - {folder.photos} photos - - - - ); + const keyboardVerticalOffset = Platform.OS === 'ios' ? 80 : 0; return ( - -
- - - Camera roll - + + +
- Legacy folders - {folders.map(renderFolderCard)} + + Camera roll + - - Nominee Progress - - + Legacy folders + + {folders.map((fold) => ( + + ))} - 38% left - - Your nominees - - - - - {nominees.map((nominee) => ( - - ))} - + + Nominee Progress + + + + 38% left + - Recents - {folders.map(renderFolderCard)} + Your nominees + + + + + {nominees.map((nominee) => ( + + ))} + - - - {storage.breakdown.map((item) => ( - - - {item.type} - {item.size} - + Recents + + {folders.map((fold) => ( + ))} - - - 0.3 GB - / 1 GB + + + + {storage.breakdown.map((item) => ( + + + {item.type} + {item.size} + + ))} + + + 0.3 GB + / 1 GB + + + + Upgrade storage ↑ + - - Upgrade storage ↑ - - - - + + + ); } diff --git a/crux-frontend/app/(authenticated)/storage.tsx b/crux-frontend/app/(authenticated)/storage.tsx index cfbdbf2..0e835c4 100644 --- a/crux-frontend/app/(authenticated)/storage.tsx +++ b/crux-frontend/app/(authenticated)/storage.tsx @@ -1,16 +1,15 @@ -import React from "react"; -import { View, Text, Pressable, StyleSheet } from "react-native"; -import { ChevronRight, Trash2 } from "lucide-react-native"; -import Header from "@/components/Header"; +import {View, Text, Pressable, StyleSheet} from 'react-native'; +import {ChevronRight, Trash2} from 'lucide-react-native'; +import Header from '@/components/Header'; export default function StorageScreen() { const storageData = { used: 0.3, free: 0.7, breakdown: [ - { type: "Video", size: "0.2MB", color: "#4A55A2" }, - { type: "Photo", size: "0.1MB", color: "#FF4081" }, - { type: "Files", size: "0.1MB", color: "#00C853" }, + {type: 'Video', size: '0.2MB', color: '#4A55A2'}, + {type: 'Photo', size: '0.1MB', color: '#FF4081'}, + {type: 'Files', size: '0.1MB', color: '#00C853'}, ], }; @@ -53,7 +52,7 @@ export default function StorageScreen() { {storageData.breakdown.map((item, index) => ( - + {item.type} @@ -80,81 +79,81 @@ export default function StorageScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#f8f9fa", + backgroundColor: '#f8f9fa', }, content: { padding: 16, }, title: { fontSize: 28, - fontWeight: "bold", + fontWeight: 'bold', marginBottom: 8, }, subtitle: { fontSize: 16, - color: "#666", + color: '#666', marginBottom: 24, }, card: { - backgroundColor: "#fff", + backgroundColor: '#fff', borderRadius: 16, padding: 16, marginBottom: 24, }, storageBar: { height: 8, - backgroundColor: "#f0f0f0", + backgroundColor: '#f0f0f0', borderRadius: 4, - overflow: "hidden", + overflow: 'hidden', marginBottom: 8, }, storageUsed: { - height: "100%", - backgroundColor: "#4A55A2", + height: '100%', + backgroundColor: '#4A55A2', borderRadius: 4, }, storageInfo: { - flexDirection: "row", - justifyContent: "space-between", + flexDirection: 'row', + justifyContent: 'space-between', marginBottom: 16, }, storageText: { fontSize: 14, - color: "#666", + color: '#666', }, upgradeButton: { - backgroundColor: "#FF4081", + backgroundColor: '#FF4081', borderRadius: 25, paddingVertical: 12, - alignItems: "center", + alignItems: 'center', }, upgradeButtonText: { - color: "#fff", + color: '#fff', fontSize: 16, - fontWeight: "600", + fontWeight: '600', }, breakdownCard: { - backgroundColor: "#fff", + backgroundColor: '#fff', borderRadius: 16, padding: 16, marginBottom: 24, }, breakdownTitle: { fontSize: 20, - fontWeight: "bold", + fontWeight: 'bold', marginBottom: 16, }, breakdownItem: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", + borderBottomColor: '#f0f0f0', }, breakdownLeft: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', }, dot: { width: 8, @@ -166,34 +165,34 @@ const styles = StyleSheet.create({ fontSize: 16, }, breakdownRight: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', gap: 8, }, breakdownSize: { fontSize: 16, - color: "#666", + color: '#666', }, clearCacheButton: { - backgroundColor: "#FFE0F0", + backgroundColor: '#FFE0F0', borderRadius: 25, paddingVertical: 12, - alignItems: "center", + alignItems: 'center', marginBottom: 12, }, clearCacheText: { - color: "#FF4081", + color: '#FF4081', fontSize: 16, - fontWeight: "500", + fontWeight: '500', }, emptyTrashButton: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', gap: 8, }, emptyTrashText: { - color: "#FF4081", + color: '#FF4081', fontSize: 16, }, }); diff --git a/crux-frontend/app/allset.tsx b/crux-frontend/app/allset.tsx index 7642a21..ed4aea2 100644 --- a/crux-frontend/app/allset.tsx +++ b/crux-frontend/app/allset.tsx @@ -1,197 +1,277 @@ -import {useEffect} from 'react'; -import {View, Text, Pressable, StyleSheet, Animated, Image} from 'react-native'; -import {useRouter} from 'expo-router'; -import {ArrowLeft, ArrowRight} from 'lucide-react-native'; +import {useEffect, useRef} from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Animated, + Dimensions, + Image, +} from 'react-native'; +import {ArrowRight} from 'react-native-feather'; +import Svg, {Path} from 'react-native-svg'; -type Nominee = { - id: string; +const {width: SCREEN_WIDTH, height: SCREEN_HEIGHT} = Dimensions.get('window'); + +interface NomineeCardProps { name: string; - type: string; - avatar: string; + relation: string; color: string; + delay: number; + avatarUrl: string; + wavePattern: string; +} + +const WaveBackground = ({color, pattern}: {color: string; pattern: string}) => ( + + + +); + +const NomineeCard = ({ + name, + relation, + color, + delay, + avatarUrl, + wavePattern, +}: NomineeCardProps) => { + const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current; + const opacity = useRef(new Animated.Value(0)).current; + + useEffect(() => { + Animated.sequence([ + Animated.delay(delay), + Animated.parallel([ + Animated.spring(translateY, { + toValue: 0, + damping: 15, + mass: 1, + stiffness: 130, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 1, + duration: 400, + useNativeDriver: true, + }), + ]), + ]).start(); + }, [delay]); + + return ( + + + + + + + {relation} + {name} + + + + + + + + ); }; -export default function AllSet() { - const router = useRouter(); - const nominees: Nominee[] = [ +const AllSet = () => { + const nominees = [ { - id: '1', name: 'Johny', - type: 'Brother', - avatar: '@/assets/images/placeholder1.png', - color: '#E8F5E9', + relation: 'Brother', + color: '#BAFFC9', + delay: 0, + avatarUrl: '@/assets/images/placeholder1.png', + wavePattern: + 'M0,32L48,37.3C96,43,192,53,288,80C384,107,480,149,576,160C672,171,768,149,864,133.3C960,117,1056,107,1152,101.3C1248,96,1344,96,1392,96L1440,96L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z', }, { - id: '2', name: 'Mom', - type: 'Mom', - avatar: '@/assets/images/placeholder1.png', - color: '#E3F2FD', + relation: 'Mom', + color: '#BAE1FF', + delay: 200, + avatarUrl: '@/assets/images/placeholder1.png', + wavePattern: + 'M0,64L48,80C96,96,192,128,288,133.3C384,139,480,117,576,101.3C672,85,768,75,864,90.7C960,107,1056,149,1152,160C1248,171,1344,149,1392,138.7L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z', }, { - id: '3', name: 'Max', - type: 'Friend', - avatar: '@/assets/images/placeholder1.png', - color: '#FCE4EC', + relation: 'Friend', + color: '#FFB3BA', + delay: 400, + avatarUrl: '@/assets/images/placeholder1.png', + wavePattern: + 'M0,96L48,112C96,128,192,160,288,176C384,192,480,192,576,176C672,160,768,128,864,117.3C960,107,1056,117,1152,122.7C1248,128,1344,128,1392,128L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z', }, ]; - // Create animated values for each nominee - const animations = nominees.map(() => new Animated.Value(0)); + const titleOpacity = useRef(new Animated.Value(0)).current; + const buttonOpacity = useRef(new Animated.Value(0)).current; useEffect(() => { - // Sequence the animations - const sequence = nominees.map((_, index) => { - return Animated.timing(animations[index], { + Animated.sequence([ + Animated.timing(titleOpacity, { toValue: 1, duration: 500, - delay: index * 200, // Stagger the animations useNativeDriver: true, - }); - }); - - // Start the sequence after a brief delay - setTimeout(() => { - Animated.sequence(sequence).start(); - }, 500); + }), + Animated.delay(1200), + Animated.timing(buttonOpacity, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }), + ]).start(); }, []); return ( - router.back()}> - - - - + All set! We created folders for your nominees, you can start adding your files. + - - {nominees.map((nominee, index) => ( - - - - - {nominee.type} - {nominee.name} - - - - - - - ))} - + + {nominees.map((nominee) => ( + + ))} + - router.push('/profile')} - > + + Upload files - - + + ); -} +}; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', - }, - backButton: { + backgroundColor: '#FFFFFF', padding: 20, - paddingTop: 40, }, - content: { - flex: 1, - padding: 20, - alignItems: 'center', + header: { + marginTop: 40, + marginBottom: 20, }, title: { - fontSize: 28, + fontSize: 32, fontWeight: 'bold', marginBottom: 8, textAlign: 'center', }, subtitle: { fontSize: 16, - color: '#666', + color: '#666666', textAlign: 'center', - marginBottom: 40, - paddingHorizontal: 20, + marginBottom: 20, }, cardsContainer: { - width: '100%', + flex: 1, + justifyContent: 'center', gap: 16, - marginBottom: 40, }, - card: { + nomineeCard: { + height: 100, borderRadius: 16, - padding: 16, - marginBottom: 16, + overflow: 'hidden', + position: 'relative', }, - cardContent: { + nomineeContent: { + flex: 1, flexDirection: 'row', - justifyContent: 'space-between', alignItems: 'center', + justifyContent: 'space-between', + padding: 16, }, - cardType: { - fontSize: 14, - color: '#666', - marginBottom: 4, + leftContent: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, }, avatar: { width: 40, height: 40, borderRadius: 20, - marginRight: 12, + backgroundColor: '#FFFFFF', }, - cardName: { - fontSize: 24, - fontWeight: '600', + textContent: { + gap: 4, }, - cardButton: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: '#fff', + nomineeRelation: { + fontSize: 14, + color: '#666666', + textTransform: 'uppercase', + borderRadius: 18, + paddingVertical: 4, + paddingHorizontal: 8, + backgroundColor: '#FFFFFF', + alignItems: 'center', justifyContent: 'center', + shadowColor: '#000000', + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + nomineeName: { + fontSize: 24, + fontWeight: 'bold', + }, + arrowButton: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: '#FFFFFF', alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000000', + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, }, uploadButton: { - backgroundColor: '#FF4081', - paddingVertical: 16, - paddingHorizontal: 32, + backgroundColor: '#FF69B4', + height: 50, borderRadius: 25, - marginTop: 'auto', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 20, + shadowColor: '#000000', + shadowOffset: {width: 0, height: 2}, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, }, uploadButtonText: { - color: 'white', + color: '#FFFFFF', fontSize: 16, - fontWeight: '600', + fontWeight: 'bold', }, }); + +export default AllSet; diff --git a/crux-frontend/app/nominees.tsx b/crux-frontend/app/nominees.tsx index 3cb2174..a3e02f2 100644 --- a/crux-frontend/app/nominees.tsx +++ b/crux-frontend/app/nominees.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, {useState} from 'react'; import { View, Text, @@ -6,9 +6,9 @@ import { Pressable, StyleSheet, ScrollView, -} from "react-native"; -import { useRouter } from "expo-router"; -import { ArrowLeft, ChevronDown, X } from "lucide-react-native"; +} from 'react-native'; +import {useRouter} from 'expo-router'; +import {ArrowLeft, ChevronDown, X} from 'lucide-react-native'; type Nominee = { email: string; @@ -19,15 +19,15 @@ type Nominee = { export default function Nominees() { const router = useRouter(); const [nominees, setNominees] = useState([ - { email: "mom@mail.com", relationship: "Mom", color: "#E3F2FD" }, - { email: "max@mail.com", relationship: "Friend", color: "#FCE4EC" }, - { email: "johny@mail.com", relationship: "Brother", color: "#E8F5E9" }, + {email: 'mom@mail.com', relationship: 'Mom', color: '#E3F2FD'}, + {email: 'max@mail.com', relationship: 'Friend', color: '#FCE4EC'}, + {email: 'johny@mail.com', relationship: 'Brother', color: '#E8F5E9'}, ]); - const [newEmail, setNewEmail] = useState(""); - const [newRelationship, setNewRelationship] = useState("Friend"); + const [newEmail, setNewEmail] = useState(''); + const [newRelationship, setNewRelationship] = useState('Friend'); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const relationships = ["Friend", "Mom", "Dad", "Brother", "Sister"]; + const relationships = ['Friend', 'Mom', 'Dad', 'Brother', 'Sister']; const removeNominee = (email: string) => { setNominees(nominees.filter((nominee) => nominee.email !== email)); @@ -35,14 +35,14 @@ export default function Nominees() { const getRelationshipColor = (relationship: string) => { switch (relationship) { - case "Mom": - return "#E3F2FD"; - case "Friend": - return "#FCE4EC"; - case "Brother": - return "#E8F5E9"; + case 'Mom': + return '#E3F2FD'; + case 'Friend': + return '#FCE4EC'; + case 'Brother': + return '#E8F5E9'; default: - return "#F5F5F5"; + return '#F5F5F5'; } }; @@ -111,7 +111,7 @@ export default function Nominees() { {nominees.map((nominee, index) => ( {nominee.email} @@ -134,12 +134,12 @@ export default function Nominees() { router.push("/allset")} + onPress={() => router.push('/allset')} > @@ -149,17 +149,17 @@ export default function Nominees() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#fff", + backgroundColor: '#fff', }, header: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', padding: 20, paddingTop: 40, }, progress: { - color: "#666", + color: '#666', }, content: { flex: 1, @@ -167,18 +167,18 @@ const styles = StyleSheet.create({ }, title: { fontSize: 28, - fontWeight: "bold", + fontWeight: 'bold', marginBottom: 8, }, subtitle: { fontSize: 16, - color: "#666", + color: '#666', marginBottom: 32, lineHeight: 24, }, sectionTitle: { fontSize: 18, - fontWeight: "600", + fontWeight: '600', marginBottom: 16, }, form: { @@ -187,47 +187,47 @@ const styles = StyleSheet.create({ nameInput: { height: 50, borderRadius: 25, - backgroundColor: "#F5F5F5", + backgroundColor: '#F5F5F5', paddingHorizontal: 20, fontSize: 16, marginBottom: 12, }, row: { - flexDirection: "row", + flexDirection: 'row', gap: 12, }, relationshipButton: { height: 50, borderRadius: 25, - backgroundColor: "#F5F5F5", + backgroundColor: '#F5F5F5', paddingHorizontal: 20, - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', gap: 8, flex: 1, }, relationshipButtonText: { fontSize: 16, - color: "#000", + color: '#000', }, emailInput: { height: 50, borderRadius: 25, - backgroundColor: "#F5F5F5", + backgroundColor: '#F5F5F5', paddingHorizontal: 20, fontSize: 16, flex: 2, }, dropdown: { - position: "absolute", + position: 'absolute', top: 110, left: 0, - right: "66%", - backgroundColor: "#fff", + right: '66%', + backgroundColor: '#fff', borderRadius: 12, padding: 8, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, + shadowColor: '#000', + shadowOffset: {width: 0, height: 2}, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, @@ -241,9 +241,9 @@ const styles = StyleSheet.create({ gap: 12, }, inviteItem: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', padding: 16, borderRadius: 12, }, @@ -251,37 +251,37 @@ const styles = StyleSheet.create({ fontSize: 16, }, inviteActions: { - flexDirection: "row", - alignItems: "center", + flexDirection: 'row', + alignItems: 'center', gap: 12, }, relationshipTag: { - backgroundColor: "#fff", + backgroundColor: '#fff', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, }, relationshipTagText: { fontSize: 14, - fontWeight: "500", + fontWeight: '500', }, removeButton: { width: 24, height: 24, borderRadius: 12, - backgroundColor: "#fff", - justifyContent: "center", - alignItems: "center", + backgroundColor: '#fff', + justifyContent: 'center', + alignItems: 'center', }, nextButton: { - position: "absolute", + position: 'absolute', bottom: 20, right: 20, width: 56, height: 56, borderRadius: 28, - backgroundColor: "#FF4081", - justifyContent: "center", - alignItems: "center", + backgroundColor: '#FF4081', + justifyContent: 'center', + alignItems: 'center', }, }); diff --git a/crux-frontend/assets/svg/cardbg.svg b/crux-frontend/assets/svg/cardbg.svg new file mode 100644 index 0000000..0ec3ee1 --- /dev/null +++ b/crux-frontend/assets/svg/cardbg.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/crux-frontend/cognito-config.js b/crux-frontend/cognito-config.js index 04cb430..d082e69 100644 --- a/crux-frontend/cognito-config.js +++ b/crux-frontend/cognito-config.js @@ -1,8 +1,9 @@ -import { CognitoUserPool } from "amazon-cognito-identity-js"; +import {CognitoUserPool} from 'amazon-cognito-identity-js'; +// Ensure these are set in your .env or expo environment variables const poolData = { - UserPoolId: "us-east-2_j5mZ9QZsw", - ClientId: "291imj7jueaf16qabvcbk6k2bn", + UserPoolId: 'eu-west-2_6fPDsUm5T', + ClientId: 'samt4bt8omlj5o4jj8vm1o9ig', }; export const userPool = new CognitoUserPool(poolData); diff --git a/crux-frontend/components/BackgroundPattern.tsx b/crux-frontend/components/BackgroundPattern.tsx new file mode 100644 index 0000000..accbf28 --- /dev/null +++ b/crux-frontend/components/BackgroundPattern.tsx @@ -0,0 +1,24 @@ +import Svg, {Path} from 'react-native-svg'; +import {StyleSheet} from 'react-native'; + +interface BackgroundPatternProps { + color: string; + opacity?: number; +} + +export const BackgroundPattern = ({ + color, + opacity = 0.1, +}: BackgroundPatternProps) => { + return ( + + + + ); +}; diff --git a/crux-frontend/components/Card.tsx b/crux-frontend/components/Card.tsx new file mode 100644 index 0000000..8d432f0 --- /dev/null +++ b/crux-frontend/components/Card.tsx @@ -0,0 +1,113 @@ +import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native'; +import {ChevronRight} from 'lucide-react-native'; + +import {theme} from '@/constants/Colors'; + +interface CardProps { + name: string; + relation: string; + photoCount: number; + avatarUrl: string | undefined; + backgroundColor?: string; + onPress?: () => void; +} + +export const Card: React.FC = ({ + name, + relation, + photoCount, + avatarUrl, + backgroundColor = theme.colors.accent, + onPress, +}) => { + return ( + + + + + + {name} + {relation} + {photoCount} photos + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderRadius: 16, + padding: 16, + minHeight: 100, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + content: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + leftContent: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + avatar: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + }, + textContainer: { + flex: 1, + }, + relation: { + fontSize: 12, + fontWeight: '600', + color: theme.colors.text, + opacity: 0.7, + marginBottom: 4, + letterSpacing: 0.5, + }, + name: { + fontSize: 24, + fontWeight: 'bold', + color: theme.colors.text, + marginBottom: 4, + }, + photoCount: { + fontSize: 14, + color: theme.colors.text, + opacity: 0.6, + }, + arrowContainer: { + width: 32, + height: 32, + backgroundColor: 'white', + borderRadius: 16, + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 1, + }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, +}); diff --git a/crux-frontend/components/Collapsible.tsx b/crux-frontend/components/Collapsible.tsx index 405d393..ed6f358 100644 --- a/crux-frontend/components/Collapsible.tsx +++ b/crux-frontend/components/Collapsible.tsx @@ -4,14 +4,14 @@ import {StyleSheet, Pressable, useColorScheme} from 'react-native'; import {ThemedText} from '@/components/ThemedText'; import {ThemedView} from '@/components/ThemedView'; -import {Colors} from '@/constants/Colors'; +import {theme} from '@/constants/Colors'; export function Collapsible({ children, title, }: PropsWithChildren & {title: string}) { const [isOpen, setIsOpen] = useState(false); - const theme = useColorScheme() ?? 'light'; + const colorScheme = useColorScheme() ?? 'light'; return ( @@ -22,7 +22,9 @@ export function Collapsible({ {title} diff --git a/crux-frontend/components/HorizontalAccordion.tsx b/crux-frontend/components/HorizontalAccordion.tsx new file mode 100644 index 0000000..2e99f39 --- /dev/null +++ b/crux-frontend/components/HorizontalAccordion.tsx @@ -0,0 +1,127 @@ +import React, {useState} from 'react'; +import { + View, + Text, + Image, + TouchableOpacity, + StyleSheet, + ScrollView, +} from 'react-native'; + +const accordionData = [ + { + id: 'boating', + emoji: '🚤', + image: + 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/boat.webp', + title: 'Boating', + content: + 'Port mutiny draught handsomely ye furl flogging line shrouds hulk. Spirits Plate Fleet code of conduct.', + }, + { + id: 'anchoring', + emoji: '⚓️', + image: + 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/anchor.webp', + title: 'Anchoring', + content: + 'Ahoy league hands Sea Legs keelhaul salmagundi fire ship crimp Privateer galleon. Booty boom yard boatswain quarter.', + }, + { + id: 'fishing', + emoji: '🎣', + image: + 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/fishing.webp', + title: 'Fishing', + content: + 'No prey, no pay heave down splice the main brace furl cable snow walk the plank chase guns piracy bucko.', + }, + { + id: 'lighthouses', + emoji: '🔦', + image: + 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/lighthouse.webp', + title: 'Lighthouses', + content: + 'Deadlights squiffy salmagundi cable pinnace parrel topsail Corsair Arr mizzenmast.', + }, + { + id: 'reefs', + emoji: '🪸', + image: + 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/reef.webp', + title: 'Reefs', + content: + 'Keel yard poop deck brigantine gaff six pounders bring a spring upon her cable interloper lad pink.', + }, +]; + +export function HorizontalAccordion() { + const [openId, setOpenId] = useState('boating'); + + const toggleAccordion = (id: string) => { + setOpenId(openId === id ? null : id); + }; + + return ( + + {accordionData.map((item) => ( + + toggleAccordion(item.id)} + style={styles.summary} + > + {item.emoji} + + + {openId === item.id && ( + + {item.title} + {item.content} + + )} + + ))} + + ); +} + +const styles = StyleSheet.create({ + accordionWrapper: { + flexDirection: 'row', + padding: 16, + }, + accordionItem: { + width: 200, + marginRight: 16, + borderRadius: 16, + overflow: 'hidden', + backgroundColor: '#f4f6f9', + }, + summary: { + alignItems: 'center', + padding: 16, + backgroundColor: '#fff', + }, + emoji: { + fontSize: 24, + marginBottom: 8, + }, + image: { + width: '100%', + height: 100, + borderRadius: 16, + }, + detailsContent: { + padding: 16, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 8, + }, + content: { + fontSize: 14, + color: '#444', + }, +}); diff --git a/crux-frontend/components/Input.tsx b/crux-frontend/components/Input.tsx index 7ebf3de..9411c3b 100644 --- a/crux-frontend/components/Input.tsx +++ b/crux-frontend/components/Input.tsx @@ -1,12 +1,12 @@ -import { View, TextInput, Text, StyleSheet } from "react-native"; -import { inputModeOptions } from "react-native"; +import {View, TextInput, Text, StyleSheet} from 'react-native'; +import {InputModeOptions} from 'react-native'; type InputProps = { label: string; placeholder: string; value: string; onChangeText: (text: string) => void; - inputMode: inputModeOptions; + inputMode: InputModeOptions; style?: StyleSheet; }; @@ -39,11 +39,11 @@ const styles = StyleSheet.create({ label: { fontSize: 16, marginBottom: 8, - fontWeight: "500", + fontWeight: '500', }, input: { borderWidth: 1, - borderColor: "#E0E0E0", + borderColor: '#E0E0E0', borderRadius: 8, padding: 12, fontSize: 16, diff --git a/crux-frontend/components/horizontal-accordion.tsx b/crux-frontend/components/horizontal-accordion.tsx deleted file mode 100644 index efe8be1..0000000 --- a/crux-frontend/components/horizontal-accordion.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState } from 'react' -import { View, Text, Image, TouchableOpacity, StyleSheet, ScrollView } from 'react-native' - -const accordionData = [ - { - id: 'boating', - emoji: '🚤', - image: 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/boat.webp', - title: 'Boating', - content: 'Port mutiny draught handsomely ye furl flogging line shrouds hulk. Spirits Plate Fleet code of conduct.', - }, - { - id: 'anchoring', - emoji: '⚓️', - image: 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/anchor.webp', - title: 'Anchoring', - content: 'Ahoy league hands Sea Legs keelhaul salmagundi fire ship crimp Privateer galleon. Booty boom yard boatswain quarter.', - }, - { - id: 'fishing', - emoji: '🎣', - image: 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/fishing.webp', - title: 'Fishing', - content: 'No prey, no pay heave down splice the main brace furl cable snow walk the plank chase guns piracy bucko.', - }, - { - id: 'lighthouses', - emoji: '🔦', - image: 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/lighthouse.webp', - title: 'Lighthouses', - content: 'Deadlights squiffy salmagundi cable pinnace parrel topsail Corsair Arr mizzenmast.', - }, - { - id: 'reefs', - emoji: '🪸', - image: 'https://raw.githubusercontent.com/kevin-powell/accordion/8551c559e3e8d9a9cca7a983c9e8903ef533f189/public/assets/reef.webp', - title: 'Reefs', - content: 'Keel yard poop deck brigantine gaff six pounders bring a spring upon her cable interloper lad pink.', - }, -] - -export default function HorizontalAccordion() { - const [openId, setOpenId] = useState('boating') - - const toggleAccordion = (id: string) => { - setOpenId(openId === id ? null : id) - } - - return ( - - {accordionData.map(item => ( - - toggleAccordion(item.id)} style={styles.summary}> - {item.emoji} - - - {openId === item.id && ( - - {item.title} - {item.content} - - )} - - ))} - - ) -} - -const styles = StyleSheet.create({ - accordionWrapper: { - flexDirection: 'row', - padding: 16, - }, - accordionItem: { - width: 200, - marginRight: 16, - borderRadius: 16, - overflow: 'hidden', - backgroundColor: '#f4f6f9', - }, - summary: { - alignItems: 'center', - padding: 16, - backgroundColor: '#fff', - }, - emoji: { - fontSize: 24, - marginBottom: 8, - }, - image: { - width: '100%', - height: 100, - borderRadius: 16, - }, - detailsContent: { - padding: 16, - }, - title: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 8, - }, - content: { - fontSize: 14, - color: '#444', - }, -}) \ No newline at end of file diff --git a/crux-frontend/constants/Colors.ts b/crux-frontend/constants/Colors.ts index 14e6784..3269c62 100644 --- a/crux-frontend/constants/Colors.ts +++ b/crux-frontend/constants/Colors.ts @@ -6,21 +6,59 @@ const tintColorLight = '#0a7ea4'; const tintColorDark = '#fff'; -export const Colors = { - light: { - text: '#11181C', - background: '#fff', - tint: tintColorLight, - icon: '#687076', - tabIconDefault: '#687076', - tabIconSelected: tintColorLight, - }, - dark: { - text: '#ECEDEE', - background: '#151718', - tint: tintColorDark, - icon: '#9BA1A6', - tabIconDefault: '#9BA1A6', - tabIconSelected: tintColorDark, +export const theme = { + colors: { + background: '#FFFFFF', + text: '#000000', + primary: '#FF69B4', // Pink color used for buttons + secondary: '#F0F0F0', // Light gray for input backgrounds + accent: '#4169E1', // Blue color used for some text and buttons + }, + fontSizes: { + small: 12, + medium: 16, + large: 20, + xlarge: 24, + }, + spacing: { + small: 8, + medium: 16, + large: 24, + }, +}; + +export const globalStyles = { + container: { + flex: 1, + backgroundColor: theme.colors.background, + padding: theme.spacing.medium, + }, + title: { + fontSize: theme.fontSizes.xlarge, + fontWeight: 'bold', + marginBottom: theme.spacing.medium, + }, + subtitle: { + fontSize: theme.fontSizes.medium, + color: '#666', + marginBottom: theme.spacing.large, + }, + input: { + backgroundColor: theme.colors.secondary, + borderRadius: 25, + padding: theme.spacing.medium, + marginBottom: theme.spacing.medium, + }, + button: { + backgroundColor: theme.colors.primary, + borderRadius: 25, + padding: theme.spacing.medium, + alignItems: 'center', + justifyContent: 'center', + }, + buttonText: { + color: theme.colors.background, + fontSize: theme.fontSizes.medium, + fontWeight: 'bold', }, }; diff --git a/crux-frontend/hooks/useAuth.ts b/crux-frontend/hooks/useAuth.ts index 064d768..40d7667 100644 --- a/crux-frontend/hooks/useAuth.ts +++ b/crux-frontend/hooks/useAuth.ts @@ -1,62 +1,105 @@ -import {useState} from 'react'; +import {useState, useCallback} from 'react'; import { AuthenticationDetails, CognitoUser, CognitoUserAttribute, + CognitoUserPool, + CognitoUserSession, } from 'amazon-cognito-identity-js'; import {userPool} from '@/cognito-config'; +// Define an interface for signup data +interface SignupData { + firstName: string; + lastName: string; + email: string; + phone: string; +} -const useAuth = () => { +// Define an interface for authentication context +interface AuthContextType { + user: CognitoUser | null; + isAuthenticated: boolean; + loading: boolean; + error: string | null; + signup: (data: SignupData) => Promise; + signin: (identifier: string) => Promise; + signout: () => Promise; + verifyMFA: (identifier: string, mfaCode: string) => Promise; + resetPassword: (username: string) => Promise; + confirmPasswordReset: ( + username: string, + verificationCode: string, + newPassword: string + ) => Promise; +} + +// First, define the custom user attributes interface +interface CustomUserAttributes { + email: string; + name: string; + id: string; + firstName: string; + lastName: string; + role: string; +} + +const useAuth = (): AuthContextType => { + const [user, setUser] = useState< + | (CognitoUser & { + email: string; + name: string; + id: string; + firstName: string; + lastName: string; + role: string; + }) + | null + >(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const signup = async ({ - firstName, - lastName, - email, - phone, - }: { - firstName: string; - lastName: string; - email: string; - phone: string; - }) => { - setLoading(true); - setError(null); + // Signup method with password input + const signup = useCallback( + async ({ + firstName, + lastName, + email, + phone, + }: SignupData): Promise => { + setLoading(true); + setError(null); + + const attributeList = [ + new CognitoUserAttribute({Name: 'name', Value: firstName}), + new CognitoUserAttribute({Name: 'family_name', Value: lastName}), + new CognitoUserAttribute({Name: 'email', Value: email}), + new CognitoUserAttribute({Name: 'phone_number', Value: phone}), + ]; - const attributeList = [ - new CognitoUserAttribute({Name: 'given_name', Value: firstName}), - new CognitoUserAttribute({Name: 'family_name', Value: lastName}), - new CognitoUserAttribute({Name: 'email', Value: email}), - new CognitoUserAttribute({Name: 'phone_number', Value: phone}), - ]; - - return new Promise((resolve) => { - userPool.signUp( - phone, - 'Password123!', - attributeList, - [], - (err, result) => { + return new Promise((resolve) => { + userPool.signUp(email, 'Password', attributeList, [], (err, result) => { setLoading(false); if (err) { - setError(err.message || 'An error occurred'); + setError(err.message || 'Signup failed'); resolve(false); return; } resolve(true); - } - ); - }); - }; + }); + }); + }, + [] + ); - const signin = async (identifier: string) => { + // Signin method with dynamic credentials + const signin = useCallback(async (identifier: string): Promise => { setLoading(true); setError(null); const authenticationDetails = new AuthenticationDetails({ Username: identifier, - Password: 'Password123!', + Password: 'Password', }); const cognitoUser = new CognitoUser({ @@ -64,53 +107,151 @@ const useAuth = () => { Pool: userPool, }); - return new Promise((resolve) => { + return new Promise((resolve) => { cognitoUser.authenticateUser(authenticationDetails, { - onSuccess: () => { + onSuccess: (session: CognitoUserSession) => { + const idToken = session.getIdToken(); + const payload = idToken.decodePayload(); + // Then modify the user object creation to properly extend CognitoUser + const user = Object.assign(cognitoUser, { + email: payload.email, + name: payload.name, + id: payload.sub, + firstName: payload['custom:firstName'], + lastName: payload['custom:lastName'], + role: payload['custom:role'], + }) as CognitoUser & CustomUserAttributes; + + setUser(user); + setIsAuthenticated(true); setLoading(false); resolve(true); }, onFailure: (err) => { + setError(err.message || 'Authentication failed'); setLoading(false); - setError(err.message || 'An error occurred'); resolve(false); }, mfaRequired: (challengeName, challengeParameters) => { - // Handle MFA challenge + // Indicate MFA is required + setError('MFA Required'); setLoading(false); - // You might want to store the cognitoUser instance to use it later for MFA verification - // You can navigate to MFA verification screen here resolve(false); }, }); }); - }; + }, []); - const verifyMFA = async (identifier: string, mfaCode: string) => { - setLoading(true); - setError(null); + // Signout method + const signout = useCallback(async (): Promise => { + if (user) { + user.signOut(); + setUser(null); + setIsAuthenticated(false); + } + }, [user]); - const cognitoUser = new CognitoUser({ - Username: identifier, - Pool: userPool, - }); + // MFA Verification + const verifyMFA = useCallback( + async (identifier: string, mfaCode: string): Promise => { + setLoading(true); + setError(null); - return new Promise((resolve) => { - cognitoUser.sendMFACode(mfaCode, { - onSuccess: () => { - setLoading(false); - resolve(true); - }, - onFailure: (err) => { - setLoading(false); - setError(err.message || 'MFA verification failed'); - resolve(false); - }, + const cognitoUser = new CognitoUser({ + Username: identifier, + Pool: userPool, + }); + + return new Promise((resolve) => { + cognitoUser.sendMFACode(mfaCode, { + onSuccess: () => { + setIsAuthenticated(true); + setLoading(false); + resolve(true); + }, + onFailure: (err) => { + setError(err.message || 'MFA verification failed'); + setLoading(false); + resolve(false); + }, + }); + }); + }, + [] + ); + + // Password Reset Initiation + const resetPassword = useCallback( + async (username: string): Promise => { + setLoading(true); + setError(null); + + const cognitoUser = new CognitoUser({ + Username: username, + Pool: userPool, }); - }); - }; - return {signup, signin, verifyMFA, loading, error}; + return new Promise((resolve) => { + cognitoUser.forgotPassword({ + onSuccess: () => { + setLoading(false); + resolve(true); + }, + onFailure: (err) => { + setError(err.message || 'Password reset failed'); + setLoading(false); + resolve(false); + }, + }); + }); + }, + [] + ); + + // Password Reset Confirmation + const confirmPasswordReset = useCallback( + async ( + username: string, + verificationCode: string, + newPassword: string + ): Promise => { + setLoading(true); + setError(null); + + const cognitoUser = new CognitoUser({ + Username: username, + Pool: userPool, + }); + + return new Promise((resolve) => { + cognitoUser.confirmPassword(verificationCode, newPassword, { + onSuccess: () => { + setLoading(false); + resolve(true); + }, + onFailure: (err) => { + setError(err.message || 'Password reset confirmation failed'); + setLoading(false); + resolve(false); + }, + }); + }); + }, + [] + ); + + return { + user, + isAuthenticated, + loading, + error, + signup, + signin, + signout, + verifyMFA, + resetPassword, + confirmPasswordReset, + }; }; export default useAuth; diff --git a/crux-frontend/package.json b/crux-frontend/package.json index 6bdd99b..3253ea5 100644 --- a/crux-frontend/package.json +++ b/crux-frontend/package.json @@ -15,43 +15,49 @@ "preset": "jest-expo" }, "dependencies": { - "@expo/vector-icons": "^14.0.4", - "@react-native-async-storage/async-storage": "^2.0.0", - "@react-navigation/native": "^6.0.2", + "@expo/vector-icons": "^14.0.2", + "@react-native-async-storage/async-storage": "2.1.0", + "@react-navigation/bottom-tabs": "^7.0.0", + "@react-navigation/native": "^7.0.0", "amazon-cognito-identity-js": "^6.3.12", - "expo": "^51.0.0", - "expo-build-properties": "~0.11.1", - "expo-constants": "~16.0.2", - "expo-dev-client": "~3.3.9", - "expo-dom": "^27.0.2", - "expo-font": "~12.0.9", - "expo-linking": "~6.3.1", - "expo-router": "~3.5.23", - "expo-splash-screen": "~0.27.5", - "expo-status-bar": "~1.12.1", - "expo-system-ui": "~3.0.7", - "expo-web-browser": "~13.0.3", + "expo": "~52.0.9", + "expo-blur": "~14.0.1", + "expo-constants": "~17.0.3", + "expo-document-picker": "~13.0.1", + "expo-file-system": "~18.0.4", + "expo-font": "~13.0.1", + "expo-haptics": "~14.0.0", + "expo-image-picker": "~16.0.2", + "expo-linking": "~7.0.3", + "expo-router": "~4.0.7", + "expo-splash-screen": "~0.29.11", + "expo-status-bar": "~2.0.0", + "expo-symbols": "~0.2.0", + "expo-system-ui": "~4.0.3", + "expo-web-browser": "~14.0.1", "lucide-react-native": "^0.454.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-native": "0.74.5", - "react-native-gesture-handler": "~2.16.1", - "react-native-reanimated": "~3.10.1", - "react-native-safe-area-context": "4.10.5", - "react-native-screens": "3.31.1", - "react-native-web": "~0.19.10" + "react": "18.3.1", + "react-dom": "18.3.1", + "react-native": "0.76.2", + "react-native-feather": "^1.1.2", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.1", + "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.1.0", + "react-native-web": "~0.19.13", + "react-native-webview": "13.12.4" }, "devDependencies": { - "@babel/core": "^7.20.0", + "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-namespace-from": "^7.18.9", "@types/jest": "^29.5.12", - "@types/react": "~18.2.45", - "@types/react-test-renderer": "^18.0.7", + "@types/react": "~18.3.12", + "@types/react-test-renderer": "^18.3.0", "babel-plugin-module-resolver": "^5.0.0", "jest": "^29.2.1", - "jest-expo": "~51.0.3", - "react-test-renderer": "18.2.0", - "typescript": "~5.3.3" + "jest-expo": "~52.0.2", + "react-test-renderer": "18.3.1", + "typescript": "^5.3.3" }, "private": true } diff --git a/crux-frontend/tsconfig.json b/crux-frontend/tsconfig.json index 4a51e54..fc79723 100644 --- a/crux-frontend/tsconfig.json +++ b/crux-frontend/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { + "baseUrl": ".", "strict": true, "jsx": "react-jsx", "paths": {