diff --git a/shatter-mobile/app/(tabs)/EventsPage.tsx b/shatter-mobile/app/(tabs)/EventsPage.tsx index 9ee1a68..8634098 100644 --- a/shatter-mobile/app/(tabs)/EventsPage.tsx +++ b/shatter-mobile/app/(tabs)/EventsPage.tsx @@ -11,8 +11,8 @@ import { View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import AnimatedTab from "../../src/components/AnimatedTab"; import EventCard from "../../src/components/events/EventCard"; +import AnimatedTab from "../../src/components/general/AnimatedTab"; import EventIB from "../../src/interfaces/Event"; import { getUserEvents } from "../../src/services/event.service"; import { EventPageStyling as styles } from "../../src/styling/EventPage.styles"; diff --git a/shatter-mobile/app/(tabs)/JoinEventPage.tsx b/shatter-mobile/app/(tabs)/JoinEventPage.tsx index edb1382..11eb4e4 100644 --- a/shatter-mobile/app/(tabs)/JoinEventPage.tsx +++ b/shatter-mobile/app/(tabs)/JoinEventPage.tsx @@ -4,16 +4,16 @@ import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; import { useState } from "react"; import { - ActivityIndicator, - ImageBackground, - Text, - TextInput, - TouchableOpacity, - View, + ActivityIndicator, + ImageBackground, + Text, + TextInput, + TouchableOpacity, + View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import AnimatedTab from "../../src/components/AnimatedTab"; import { useAuth } from "../../src/components/context/AuthContext"; +import AnimatedTab from "../../src/components/general/AnimatedTab"; import QRScannerBox from "../../src/components/new-events/QRScannerBox"; import { JoinEventStyling as styles } from "../../src/styling/JoinEventPage.styles"; diff --git a/shatter-mobile/app/(tabs)/ProfilePage.tsx b/shatter-mobile/app/(tabs)/ProfilePage.tsx index cdb1ae4..9629efc 100644 --- a/shatter-mobile/app/(tabs)/ProfilePage.tsx +++ b/shatter-mobile/app/(tabs)/ProfilePage.tsx @@ -1,17 +1,17 @@ import { useFocusEffect, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; import { - Image, - ImageBackground, - ScrollView, - Text, - TouchableOpacity, - View, + Image, + ImageBackground, + ScrollView, + Text, + TouchableOpacity, + View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { SvgUri } from "react-native-svg"; -import AnimatedTab from "../../src/components/AnimatedTab"; import { useAuth } from "../../src/components/context/AuthContext"; +import AnimatedTab from "../../src/components/general/AnimatedTab"; import { ProfilePageStyling as styles } from "../../src/styling/ProfilePage.styles"; export default function Profile() { diff --git a/shatter-mobile/app/EventPages/EventLobby.tsx b/shatter-mobile/app/EventPages/EventLobby.tsx index 4956f1d..eb86aba 100644 --- a/shatter-mobile/app/EventPages/EventLobby.tsx +++ b/shatter-mobile/app/EventPages/EventLobby.tsx @@ -28,6 +28,7 @@ export default function EventLobby() { router.replace({ pathname: "/GamePages/Game", + params: { eventId: event._id }, }); } }, POLL_INTERVAL); diff --git a/shatter-mobile/app/GamePages/Game.tsx b/shatter-mobile/app/GamePages/Game.tsx index 3b25be2..c8ee984 100644 --- a/shatter-mobile/app/GamePages/Game.tsx +++ b/shatter-mobile/app/GamePages/Game.tsx @@ -2,8 +2,8 @@ import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useCallback, useState } from "react"; import { ImageBackground, Text, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -import FullPageLoader from "../../src/components/FullPageLoader"; import IcebreakerGame from "../../src/components/games/IcebreakerGame"; +import FullPageLoader from "../../src/components/general/FullPageLoader"; import { getEventById } from "../../src/services/event.service"; import { GamePageStyling as styles } from "../../src/styling/GamePage.styles"; @@ -60,7 +60,7 @@ const GamePage = () => { - + diff --git a/shatter-mobile/app/UserPages/Guest.tsx b/shatter-mobile/app/UserPages/Guest.tsx index 7481315..1a09600 100644 --- a/shatter-mobile/app/UserPages/Guest.tsx +++ b/shatter-mobile/app/UserPages/Guest.tsx @@ -1,10 +1,12 @@ +import SocialSpinner from "@/src/components/login-signup/SocialSpinner"; import { SocialLink } from "@/src/interfaces/User"; import { colors } from "@/src/styling/constants"; import { GuestStyling as styles } from "@/src/styling/Guest.styles"; -import { useRouter } from "expo-router"; +import { Stack, useRouter } from "expo-router"; import { useState } from "react"; import { ImageBackground, + Modal, Text, TextInput, TouchableOpacity, @@ -17,12 +19,13 @@ export default function GuestPage() { const { continueAsGuest } = useAuth(); const [name, setName] = useState(""); const [contactLink, setContactLink] = useState(""); + const [showConfirmModal, setShowConfirmModal] = useState(false); const [error, setError] = useState(""); const router = useRouter(); const handleContinue = async () => { //need name and social link - if (!name.trim() || !contactLink) { + if (!name.trim() || !contactLink.trim()) { setError("Name and Social Link Cannot Be Empty"); return; } @@ -39,64 +42,133 @@ export default function GuestPage() { } setError(""); - await continueAsGuest(name.trim(), socialLink); + await continueAsGuest(name.trim(), socialLink, ""); //no organization on this page, handled on GuestConfirm router.replace("/JoinEventPage"); }; return ( - - - - Guest Access - Enter your details to continue - - - - Your Name - - - Contact Link - - - Your contact link can be your LinkedIn profile URL, a portfolio - link, or another relevant personal link. - - - {error ? {error} : null} - - - Continue - - - router.push("/UserPages/Signup")} - > - Back - - - - + <> + + + + + Guest Access + Enter your details to continue + + + + Your Name + + + Contact Link + + + Your contact link can be your LinkedIn profile URL, a portfolio + link, or another relevant personal link. + + + {error ? {error} : null} + + + Continue + + + router.push("/UserPages/Signup")} + > + Back + + + { + setShowConfirmModal(true); + }} + > + No Contact Link? + + + + + + + + Continue Without a Contact Link? + + + + + + Users tend to connect better when you include a contact link + like LinkedIn or a portfolio. + + + + Adding a contact link helps others learn more about you and + improves networking during events. + + + {/* Add link */} + setShowConfirmModal(false)} + > + Add Contact Link + + + {/* Continue anyway */} + { + setShowConfirmModal(false); + router.push("/UserPages/GuestNoLink"); + }} + > + + Continue Without Link + + + + + + + + ); } diff --git a/shatter-mobile/app/UserPages/GuestNoLink.tsx b/shatter-mobile/app/UserPages/GuestNoLink.tsx new file mode 100644 index 0000000..c0a55f9 --- /dev/null +++ b/shatter-mobile/app/UserPages/GuestNoLink.tsx @@ -0,0 +1,92 @@ +import { colors } from "@/src/styling/constants"; +import { GuestStyling as styles } from "@/src/styling/Guest.styles"; +import { Stack, useRouter } from "expo-router"; +import { useState } from "react"; +import { + ImageBackground, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useAuth } from "../../src/components/context/AuthContext"; + +export default function GuestConfirm() { + const { continueAsGuest } = useAuth(); + const [name, setName] = useState(""); + const [organization, setOrganization] = useState(""); + const [error, setError] = useState(""); + const router = useRouter(); + + const handleContinue = async () => { + if (!name.trim() || !organization.trim()) { + setError("Name and Organization cannot be empty"); + return; + } + + setError(""); + + await continueAsGuest( + name.trim(), + { label: "", url: "" }, + organization.trim(), + ); + + router.replace("/JoinEventPage"); + }; + + return ( + <> + + + + + Guest Access + Continue without a contact link + + + + Your Name + + + Organization + + + {error ? {error} : null} + + + Continue + + + router.back()} + > + Back + + + + + + ); +} diff --git a/shatter-mobile/app/UserPages/UpdateProfile.tsx b/shatter-mobile/app/UserPages/UpdateProfile.tsx index 2b91bf1..51d63a3 100644 --- a/shatter-mobile/app/UserPages/UpdateProfile.tsx +++ b/shatter-mobile/app/UserPages/UpdateProfile.tsx @@ -1,4 +1,5 @@ import { getStoredAuth } from "@/src/components/context/AsyncStorage"; +import { SocialLinksModal } from "@/src/components/general/SocialLinksModal"; import { userUpdate } from "@/src/services/user.service"; import { colors } from "@/src/styling/constants"; import { useRouter } from "expo-router"; @@ -35,32 +36,19 @@ export default function UpdateProfile() { const [name, setName] = useState(user?.name || ""); const [email, setEmail] = useState(user?.email || ""); const [password, setPassword] = useState(""); + const [title, setTitle] = useState(user?.title || ""); + const [organization, setOrganization] = useState(user?.organization || ""); const [bio, setBio] = useState(user?.bio || ""); const [profilePhoto, setProfilePhoto] = useState(user?.profilePhoto || ""); const [socialLinks, setSocialLinks] = useState< { label: string; url: string }[] >(user?.socialLinks || []); + const [socialModalVisible, setSocialModalVisible] = useState(false); useEffect(() => { if (!user) router.replace("/UserPages/Login"); }, [user]); - const handleLinkChange = ( - index: number, - field: "label" | "url", - value: string, - ) => { - const updated = [...socialLinks]; - updated[index] = { ...updated[index], [field]: value }; - setSocialLinks(updated); - }; - - const addNewLink = () => - setSocialLinks([...socialLinks, { label: "", url: "" }]); - - const removeLink = (index: number) => - setSocialLinks(socialLinks.filter((_, i) => i !== index)); - const handleSave = async () => { if (!user || !user._id) return; if ((!email && password) || (email && !password && user.isGuest)) { @@ -90,10 +78,12 @@ export default function UpdateProfile() { bio, profilePhoto, socialLinks, + organization, + title, }); //local update const res = await userUpdate( user._id, - { name, email, bio, profilePhoto, socialLinks }, + { name, email, bio, profilePhoto, socialLinks, organization, title }, stored.accessToken, ); //remote update @@ -167,6 +157,29 @@ export default function UpdateProfile() { Password must be at least 8 characters + {/* Title */} + Title + + + Your title at your organization, like Project Manager + + + {/* Organization */} + Organization + + {/* Non-guest only */} {!user?.isGuest && ( <> @@ -220,43 +233,21 @@ export default function UpdateProfile() { )} - {/* Social Links */} - - Social Links - - {socialLinks.map((link, index) => ( - - - handleLinkChange(index, "label", text) - } - /> - - handleLinkChange(index, "url", text) - } - /> - removeLink(index)} - > - Remove - - - ))} - - - + Add Social Link + {/* Social link modal */} + setSocialModalVisible(true)} + > + Manage Social Links + + Save Changes diff --git a/shatter-mobile/app/_layout.tsx b/shatter-mobile/app/_layout.tsx index 65ced73..d9eb1eb 100644 --- a/shatter-mobile/app/_layout.tsx +++ b/shatter-mobile/app/_layout.tsx @@ -1,6 +1,6 @@ import { getStoredAuth } from "@/src/components/context/AsyncStorage"; import { GameProvider } from "@/src/components/context/GameContext"; -import FullPageLoader from "@/src/components/FullPageLoader"; +import FullPageLoader from "@/src/components/general/FullPageLoader"; import { Poppins_600SemiBold, useFonts } from "@expo-google-fonts/poppins"; import { WorkSans_400Regular } from "@expo-google-fonts/work-sans"; import { Asset } from "expo-asset"; @@ -22,16 +22,18 @@ export default function RootLayout() { const [assetReady, setAssetReady] = useState(false); const redirectTo = useRef("/GetStarted"); + //preload fonts const [fontsLoaded] = useFonts({ "Poppins-SemiBold": Poppins_600SemiBold, "WorkSans-Regular": WorkSans_400Regular, }); - // Preload background image + //preload background image useEffect(() => { Asset.loadAsync([BG_IMAGE]).finally(() => setAssetReady(true)); }, []); + //check if user is logged in useEffect(() => { const checkAuth = async () => { try { @@ -46,12 +48,14 @@ export default function RootLayout() { checkAuth(); }, []); + //hold user if not loaded yet useEffect(() => { if (!fontsLoaded || !authReady || !assetReady) return; router.replace(redirectTo.current as any); SplashScreen.hideAsync(); }, [fontsLoaded, authReady, assetReady]); + //user isn't loaded yet if (!fontsLoaded || !authReady || !assetReady) { return ( diff --git a/shatter-mobile/src/api/users/user.api.tsx b/shatter-mobile/src/api/users/user.api.tsx index edca881..082133e 100644 --- a/shatter-mobile/src/api/users/user.api.tsx +++ b/shatter-mobile/src/api/users/user.api.tsx @@ -92,14 +92,6 @@ export async function UserFetchApi( { headers: { Authorization: `Bearer ${token}` } }, ); - //TODO: Remove profile photo assigning here - if (!response.data.user.profilePhoto) { - const encodedName = encodeURIComponent( - response.data.user.name ?? "Unknown", - ); - response.data.user.profilePhoto = `https://api.dicebear.com/9.x/initials/svg?seed=${encodedName}`; - } - return response.data; } catch (error) { const err = error as AxiosError; @@ -290,6 +282,8 @@ export async function UserUpdateApi( bio: updates.bio, profilePhoto: updates.profilePhoto, socialLinks: updates.socialLinks, + organization: updates.organization, + title: updates.title, }; const response: AxiosResponse = await axios.put( diff --git a/shatter-mobile/src/components/context/AsyncStorage.tsx b/shatter-mobile/src/components/context/AsyncStorage.tsx index aa0ce1d..ba17218 100644 --- a/shatter-mobile/src/components/context/AsyncStorage.tsx +++ b/shatter-mobile/src/components/context/AsyncStorage.tsx @@ -7,7 +7,7 @@ export type AuthDataStorage = { userId: string | null; accessToken: string; isGuest: boolean; - guestInfo: { name: string; socialLinks: SocialLink[] }; + guestInfo: { name: string; socialLinks?: SocialLink[], organization?: string }; }; export const getStoredAuth = async (): Promise => { diff --git a/shatter-mobile/src/components/context/AuthContext.tsx b/shatter-mobile/src/components/context/AuthContext.tsx index 031d484..0ab5420 100644 --- a/shatter-mobile/src/components/context/AuthContext.tsx +++ b/shatter-mobile/src/components/context/AuthContext.tsx @@ -12,7 +12,11 @@ type AuthContextType = { accessToken: string, isGuest: boolean, ) => Promise; - continueAsGuest: (name: string, socialLink: SocialLink) => Promise; + continueAsGuest: ( + name: string, + socialLink: SocialLink, + organization: string, + ) => Promise; logout: () => Promise; updateUser: (updates: Partial) => User | undefined; }; @@ -24,7 +28,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { userId: "", accessToken: "", isGuest: true, - guestInfo: { name: "", socialLinks: [] }, + guestInfo: { name: "", socialLinks: [], organization: "" }, }); const [user, setUser] = useState(undefined); @@ -44,6 +48,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { email: res.user.email, isGuest: res.user.isGuest, socialLinks: res.user.socialLinks, + organization: res.user.organization, + title: res.user.title, profilePhoto: res.user.profilePhoto, }; @@ -59,6 +65,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { email: "", isGuest: savedData.isGuest, socialLinks: savedData.guestInfo.socialLinks, + organization: savedData.guestInfo.organization, }; setUser(mappedUser); } @@ -79,18 +86,27 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { userId: user?._id, accessToken, isGuest: isGuest, - guestInfo: { name: user.name, socialLinks: user.socialLinks }, + guestInfo: { + name: user.name, + socialLinks: user.socialLinks || [], + organization: user.organization, + }, }; setAuthStorage(storageData); await saveStoredAuth(storageData); }; //when user initially creates a guest account - const continueAsGuest = async (name: string, socialLink: SocialLink) => { + const continueAsGuest = async ( + name: string, + socialLink: SocialLink, + organization: string, + ) => { const guestUser: User = { _id: null, name: name, socialLinks: [{ label: socialLink.label, url: socialLink.url }], + organization: organization, isGuest: true, }; @@ -100,7 +116,11 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { userId: guestUser._id, accessToken: "", isGuest: true, - guestInfo: { name: guestUser.name, socialLinks: guestUser.socialLinks }, + guestInfo: { + name: guestUser.name, + socialLinks: guestUser.socialLinks || [], + organization: organization || "", + }, }; setAuthStorage(storageData); diff --git a/shatter-mobile/src/components/events/UserModal.tsx b/shatter-mobile/src/components/events/UserModal.tsx index 4e60ef1..e0e4c7d 100644 --- a/shatter-mobile/src/components/events/UserModal.tsx +++ b/shatter-mobile/src/components/events/UserModal.tsx @@ -29,15 +29,20 @@ const UserModal = ({ user, onRequestClose }: UserModalProps) => { /> - {user.name} + + {user.name} + {user.title ? ` - ${user.title}` : ""} + + {user.organization && {user.organization}} + {user.bio && {user.bio}} - {user.socialLinks?.length > 0 && ( + {user.socialLinks && user.socialLinks?.length > 0 && ( - {user.socialLinks.map((link, index) => ( + {user.socialLinks?.map((link, index) => ( { diff --git a/shatter-mobile/src/components/games/NameBingo.tsx b/shatter-mobile/src/components/games/NameBingo.tsx index 5b25d8a..61d6185 100644 --- a/shatter-mobile/src/components/games/NameBingo.tsx +++ b/shatter-mobile/src/components/games/NameBingo.tsx @@ -2,21 +2,21 @@ import { useGame } from "@/src/components/context/GameContext"; import { EventState, Participant } from "@/src/interfaces/Event"; import { BingoTile } from "@/src/interfaces/Game"; import { - getBingoCategories, - getParticipantsByEventId, + getBingoCategories, + getParticipantsByEventId, } from "@/src/services/game.service"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { useEffect, useState } from "react"; import { - DimensionValue, - ScrollView, - Text, - TextInput, - TouchableOpacity, - View, + DimensionValue, + ScrollView, + Text, + TextInput, + TouchableOpacity, + View, } from "react-native"; import { NameBingoStyling as styles } from "../../styling/NameBingo.styles"; -import FullPageLoader from "../FullPageLoader"; +import FullPageLoader from "../general/FullPageLoader"; type NameBingoProps = { eventId: string; @@ -319,16 +319,22 @@ const NameBingo = ({ eventId, onConnect }: NameBingoProps) => { setActiveCardId(card.cardId); }} > - + - {card.tile?.shortQuestion || "?"} + {card.tile?.shortQuestion || "?"} {card.assignedParticipantId && ( - {card.assignedName} + {card.assignedName} )} diff --git a/shatter-mobile/src/components/AnimatedTab.tsx b/shatter-mobile/src/components/general/AnimatedTab.tsx similarity index 100% rename from shatter-mobile/src/components/AnimatedTab.tsx rename to shatter-mobile/src/components/general/AnimatedTab.tsx diff --git a/shatter-mobile/src/components/FullPageLoader.tsx b/shatter-mobile/src/components/general/FullPageLoader.tsx similarity index 100% rename from shatter-mobile/src/components/FullPageLoader.tsx rename to shatter-mobile/src/components/general/FullPageLoader.tsx diff --git a/shatter-mobile/src/components/general/SocialLinksModal.tsx b/shatter-mobile/src/components/general/SocialLinksModal.tsx new file mode 100644 index 0000000..ef80336 --- /dev/null +++ b/shatter-mobile/src/components/general/SocialLinksModal.tsx @@ -0,0 +1,114 @@ +import { colors } from "@/src/styling/constants"; +import { + Modal, + ScrollView, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import { UpdateProfileStyling as styles } from "../../styling/UpdateProfile.styles"; + +type SocialLinkModalProps = { + socialModalVisible: boolean; + setSocialModalVisible: (visible: boolean) => void; + socialLinks: { label: string; url: string }[]; + setSocialLinks: (links: { label: string; url: string }[]) => void; +}; + +export function SocialLinksModal({ + socialModalVisible, + setSocialModalVisible, + socialLinks, + setSocialLinks, +}: SocialLinkModalProps) { + const handleLinkChange = ( + index: number, + field: "label" | "url", + value: string, + ) => { + const updated = [...socialLinks]; + updated[index] = { ...updated[index], [field]: value }; + setSocialLinks(updated); + }; + + const addNewLink = () => + setSocialLinks([...socialLinks, { label: "", url: "" }]); + + const removeLink = (index: number) => + setSocialLinks(socialLinks.filter((_, i) => i !== index)); + return ( + <> + setSocialModalVisible(false)} + > + + + + Manage Social Links + + + {socialLinks.map((link, index) => ( + + + handleLinkChange(index, "label", text) + } + /> + + handleLinkChange(index, "url", text) + } + /> + removeLink(index)} + > + Remove + + + ))} + + + Add Social Link + + + + setSocialModalVisible(false)} + > + Done + + + + + + ); +} diff --git a/shatter-mobile/src/components/login-signup/LoginForm.tsx b/shatter-mobile/src/components/login-signup/LoginForm.tsx index 975634c..d506354 100644 --- a/shatter-mobile/src/components/login-signup/LoginForm.tsx +++ b/shatter-mobile/src/components/login-signup/LoginForm.tsx @@ -58,10 +58,12 @@ export default function LoginForm() { const user: User = { _id: userResponse.userId, - name: userData?.user.name, + name: userData.user.name, email, - socialLinks: userData?.user.socialLinks ?? [], + socialLinks: userData.user.socialLinks ?? [], profilePhoto: userData.user.profilePhoto, + organization: userData.user.organization, + title: userData.user.title, isGuest: false, }; diff --git a/shatter-mobile/src/components/login-signup/SignupForm.tsx b/shatter-mobile/src/components/login-signup/SignupForm.tsx index e688c1e..d4130c0 100644 --- a/shatter-mobile/src/components/login-signup/SignupForm.tsx +++ b/shatter-mobile/src/components/login-signup/SignupForm.tsx @@ -6,7 +6,6 @@ import * as WebBrowser from "expo-web-browser"; import { useState } from "react"; import { ActivityIndicator, - Button, ImageBackground, KeyboardAvoidingView, Platform, @@ -190,11 +189,17 @@ export default function SignUpForm() { Already have an Account?{" "} Log In -