diff --git a/README.md b/README.md index 48dd63f..e65f9d7 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,10 @@ -# Welcome to your Expo app 👋 +# Hot Potato Club -This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). +Hot Potato Club is a fun party game where players pass a ticking "potato" around until the timer runs out. The player holding the potato when the timer goes off loses the round. The game is played in rounds, the player with least number of losses at the end of the game wins. The game is designed to be played with a group of friends or family, and can be played in person or virtually. The game is easy to set up and can be played in a short amount of time, making it perfect for parties or gatherings. -## Get started - -1. Install dependencies - - ```bash - npm install - ``` - -2. Start the app - - ```bash - npx expo start - ``` - -In the output, you'll find options to open the app in a - -- [development build](https://docs.expo.dev/develop/development-builds/introduction/) -- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) -- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) -- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo - -You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). - -## Get a fresh project - -When you're ready, run: - -```bash -npm run reset-project -``` - -This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. - -## Learn more - -To learn more about developing your project with Expo, look at the following resources: - -- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). -- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. - -## Join the community - -Join our community of developers creating universal apps. - -- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. -- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. +### How to play: +1. Read the rules of the game and instructions carefully. +2. Enter the names of the players in the game. +3. Choose the category of the game. +4. Set the number of rounds and the time limit for each round. +5. Start the game and pass the potato around while the timer counts down. \ No newline at end of file diff --git a/app.json b/app.json index ea6a146..5880e95 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "slug": "hot-potato-club", "version": "1.0.0", "orientation": "portrait", - "icon": "./app/assets/images/icon.png", + "icon": "./app/assets/images/logo_t.png", "scheme": "hotpotatoclub", "userInterfaceStyle": "automatic", "newArchEnabled": true, @@ -33,6 +33,10 @@ } ] ], + "splash": { + "image": "./app/assets/images/logo.png", + "resizeMode": "contain" + }, "experiments": { "typedRoutes": true }, diff --git a/app/_context/GameContext.tsx b/app/_context/GameContext.tsx index f853605..288a6e2 100644 --- a/app/_context/GameContext.tsx +++ b/app/_context/GameContext.tsx @@ -17,6 +17,7 @@ type GameContextType = { nextRound: () => void; resetGame: () => void; isGameSetupComplete: () => boolean; + setRoundLoser: (playerIndex: number) => void; }; export const GameContext = createContext(undefined); @@ -34,6 +35,9 @@ export const GameProvider: React.FC<{ children: ReactNode }> = ({ children }) => const explode = () => dispatch({ type: 'EXPLODE' }); const nextRound = () => dispatch({ type: 'NEXT_ROUND' }); const resetGame = () => dispatch({ type: 'RESET_GAME' }); + const setRoundLoser = (playerIndex: number) => { + dispatch({ type: 'SET_ROUND_LOSER', payload: playerIndex }); + }; const value = useMemo(() => { const isGameSetupComplete = () => { @@ -55,6 +59,7 @@ export const GameProvider: React.FC<{ children: ReactNode }> = ({ children }) => nextRound, resetGame, isGameSetupComplete, + setRoundLoser, }; }, [state]); diff --git a/app/_context/gameReducer.ts b/app/_context/gameReducer.ts index 9a09e8a..de8b3df 100644 --- a/app/_context/gameReducer.ts +++ b/app/_context/gameReducer.ts @@ -56,6 +56,25 @@ export function gameReducer(state: GameState = initialGameState, action: GameAct ...state, exploded: true, timerRunning: false, + }; + } + case 'SET_ROUND_LOSER': { + const updatedPlayers = [...state.settings.players]; + updatedPlayers[action.payload] = { + ...updatedPlayers[action.payload], + roundsLost: (updatedPlayers[action.payload].roundsLost || 0) + 1 + }; + + const playerId = state.settings.players[action.payload].id; + + return { + ...state, + settings: { + ...state.settings, + players: updatedPlayers + }, + exploded: true, + timerRunning: false, roundResults: [...state.roundResults, { playerId, exploded: true }], }; } diff --git a/app/_layout.tsx b/app/_layout.tsx index e00b6a4..cfa692d 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,6 +2,7 @@ import { GameProvider } from '@/_context/GameContext'; import * as Font from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; +import { StatusBar } from 'expo-status-bar'; import { useCallback, useEffect, useState } from 'react'; import { View } from 'react-native'; @@ -48,6 +49,7 @@ export default function RootLayout() { // If we're ready to show the app, render it with our custom splash screen animation return ( + { + const navigateTo = (path: "/screens/setup-players" | "/screens/how-to-play" | "/screens/feedback") => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); router.push(path); }; + const handleDonate = async () => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + + const donateUrl = 'https://www.hawkslab.online/donate'; + + try { + const supported = await Linking.canOpenURL(donateUrl); + if (supported) { + await Linking.openURL(donateUrl); + } else { + Alert.alert( + 'Unable to Open', + 'Sorry, we couldn\'t open the donation page. Please visit https://www.hawkslab.online/donate directly in your browser.' + ); + } + } catch (error) { + console.error('Error opening donation URL:', error); + Alert.alert( + 'Error', + 'Something went wrong. Please visit https://www.hawkslab.online/donate directly in your browser.' + ); + } + }; + return ( - + How to Play + + + navigateTo('/screens/feedback')} + > + 💬 Feedback + + + + ❤️ Donate + + ); @@ -110,4 +151,34 @@ const styles = StyleSheet.create({ secondaryButtonText: { color: '#FE6244', }, + bottomButtonsContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 20, + }, + smallButton: { + backgroundColor: '#F0F0F0', + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 20, + flex: 0.48, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + feedbackButton: { + backgroundColor: '#E8F5E8', + }, + donateButton: { + backgroundColor: '#FFE8E8', + }, + smallButtonText: { + color: '#666', + fontSize: 14, + fontFamily: 'SpaceMono', + fontWeight: 'bold', + }, }); \ No newline at end of file diff --git a/app/screens/feedback.tsx b/app/screens/feedback.tsx new file mode 100644 index 0000000..76b6070 --- /dev/null +++ b/app/screens/feedback.tsx @@ -0,0 +1,272 @@ +import { Ionicons } from '@expo/vector-icons'; +import * as Haptics from 'expo-haptics'; +import * as MailComposer from 'expo-mail-composer'; +import { router } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import React, { useState } from 'react'; +import { + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import Animated, { FadeIn } from 'react-native-reanimated'; + +export default function Feedback() { + const [feedback, setFeedback] = useState(''); + const [email, setEmail] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmitFeedback = async () => { + if (!feedback.trim()) { + Alert.alert('Oops!', 'Please enter your feedback before submitting.'); + return; + } + + setIsSubmitting(true); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + + try { + const isAvailable = await MailComposer.isAvailableAsync(); + + if (!isAvailable) { + Alert.alert( + 'Email Not Available', + `Please set up an email account on your device to send feedback, or contact us directly at abhinavgupta4505@gmail.com\n\nYour feedback:\n${feedback}` + ); + setIsSubmitting(false); + return; + } + + const emailBody = `Feedback from Hot Potato Club App: + +${feedback} + +${email ? `Contact Email: ${email}` : 'No contact email provided'} + +--- +Sent from Hot Potato Club Mobile App`; + + const result = await MailComposer.composeAsync({ + recipients: ['abhinavgupta4505@gmail.com'], + subject: 'Hot Potato Club - User Feedback', + body: emailBody, + }); + + if (result.status === 'sent') { + Alert.alert( + 'Thank You!', + 'Your feedback has been sent successfully! We appreciate you taking the time to help us improve Hot Potato Club.', + [ + { + text: 'OK', + onPress: () => router.back(), + }, + ] + ); + } else if (result.status === 'saved') { + Alert.alert( + 'Feedback Saved!', + 'Your feedback has been saved as a draft. Please send it when you\'re ready!', + [ + { + text: 'OK', + onPress: () => router.back(), + }, + ] + ); + } else if (result.status === 'cancelled') { + // User cancelled, just reset the state + setIsSubmitting(false); + return; + } + } catch (error) { + console.error('Error sending feedback:', error); + Alert.alert( + 'Error', + 'Something went wrong while preparing your feedback email. Please try again or contact us directly at abhinavgupta4505@gmail.com' + ); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + router.back(); + }} + > + + + Feedback + + + + + + We'd Love Your Feedback! + + Help us make Hot Potato Club even better. Share your thoughts, suggestions, or report any bugs you've encountered. + + + + Your Feedback * + + + Your Email (Optional) + + + Only provide your email if you'd like us to follow up with you about your feedback. + + + + + {isSubmitting ? 'Preparing Email...' : 'Send Feedback'} + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingTop: 60, + paddingBottom: 20, + backgroundColor: '#fff', + }, + backButton: { + padding: 8, + width: 40, + }, + headerTitle: { + fontSize: 20, + fontFamily: 'SpaceMono', + fontWeight: 'bold', + color: '#333', + }, + content: { + flex: 1, + paddingHorizontal: 20, + }, + title: { + fontSize: 28, + fontFamily: 'SpaceMono', + fontWeight: 'bold', + textAlign: 'center', + marginBottom: 15, + color: '#333', + }, + subtitle: { + fontSize: 16, + fontFamily: 'SpaceMono', + textAlign: 'center', + color: '#666', + marginBottom: 30, + lineHeight: 24, + }, + form: { + marginBottom: 40, + }, + label: { + fontSize: 16, + fontFamily: 'SpaceMono', + fontWeight: 'bold', + marginBottom: 8, + color: '#333', + }, + input: { + borderWidth: 2, + borderColor: '#E0E0E0', + borderRadius: 12, + padding: 15, + fontSize: 16, + fontFamily: 'SpaceMono', + marginBottom: 15, + backgroundColor: '#F9F9F9', + }, + textArea: { + borderWidth: 2, + borderColor: '#E0E0E0', + borderRadius: 12, + padding: 15, + fontSize: 16, + fontFamily: 'SpaceMono', + marginBottom: 15, + backgroundColor: '#F9F9F9', + minHeight: 120, + }, + helperText: { + fontSize: 12, + fontFamily: 'SpaceMono', + color: '#999', + marginBottom: 25, + marginTop: -10, + }, + submitButton: { + backgroundColor: '#FE6244', + paddingVertical: 15, + borderRadius: 30, + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, + }, + submitButtonDisabled: { + backgroundColor: '#CCC', + }, + submitButtonText: { + color: 'white', + fontSize: 18, + fontFamily: 'SpaceMono', + fontWeight: 'bold', + }, +}); \ No newline at end of file diff --git a/app/screens/game-settings.tsx b/app/screens/game-settings.tsx index 4546fb1..55b5edf 100644 --- a/app/screens/game-settings.tsx +++ b/app/screens/game-settings.tsx @@ -36,7 +36,7 @@ export default function GameSettings() { return ( - + - + Game Over! Final Results diff --git a/app/screens/gameplay.tsx b/app/screens/gameplay.tsx index 0e98dba..6dd94b2 100644 --- a/app/screens/gameplay.tsx +++ b/app/screens/gameplay.tsx @@ -1,5 +1,5 @@ import { useGame } from '@/_context/GameContext'; -import { FontAwesome5, MaterialIcons } from '@expo/vector-icons'; +import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { router } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; @@ -18,7 +18,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function Gameplay() { const { gameState, - nextPlayer, skipQuestion, explode, resetGame @@ -162,22 +161,17 @@ export default function Gameplay() { try { Alert.alert( 'BOOM!', - `The bomb exploded with ${gameState.settings.players[gameState.currentPlayer].name}!`, - [{ text: 'Continue', onPress: () => { + 'The bomb exploded! Time to select who was holding it.', + [{ text: 'Select Player', onPress: () => { setTimeElapsed(0); setExplosionTime(null); - router.push('/screens/round-result'); + router.push('/screens/select-loser'); }}] ); } catch (error) { console.error('Error showing alert:', error); } - }, [explode, gameState.settings.players, gameState.currentPlayer]); - - const handleNextPlayer = () => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - nextPlayer(); - }; + }, [explode]); const handleSkipQuestion = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); @@ -206,8 +200,8 @@ export default function Gameplay() { ); }; - // Get current player name - const currentPlayer = gameState.settings.players[gameState.currentPlayer]; + // Get current player name - now just for display, not for actual game logic + const currentPlayer = gameState.settings.players[gameState.currentPlayer] || gameState.settings.players[0]; // Safety check if (!currentPlayer) { @@ -220,13 +214,13 @@ export default function Gameplay() { return ( - + {showCountdown ? ( - Get Ready {currentPlayer.name}! + Get Ready! - You're starting Round {gameState.currentRound} + Round {gameState.currentRound} is starting... @@ -269,9 +263,12 @@ export default function Gameplay() { - {`${currentPlayer.name}'s turn`} + Round {gameState.currentRound} {gameState.currentCategory} {gameState.currentQuestion} + + Discuss the question with your group. When the bomb explodes, select who was holding it! + @@ -280,15 +277,7 @@ export default function Gameplay() { onPress={handleSkipQuestion} > - Skip - - - - - Pass + Skip Question @@ -376,8 +365,8 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginVertical: 20, }, - playerName: { - fontSize: 28, + currentRoundText: { + fontSize: 24, fontWeight: 'bold', color: '#FE6244', marginBottom: 10, @@ -393,12 +382,20 @@ const styles = StyleSheet.create({ fontSize: 24, textAlign: 'center', marginHorizontal: 20, + marginBottom: 30, lineHeight: 34, fontFamily: 'SpaceMono', }, + instructionText: { + fontSize: 16, + textAlign: 'center', + marginHorizontal: 20, + color: '#666', + fontStyle: 'italic', + fontFamily: 'SpaceMono', + }, footer: { - flexDirection: 'row', - justifyContent: 'space-between', + alignItems: 'center', paddingVertical: 20, }, skipButton: { @@ -417,20 +414,6 @@ const styles = StyleSheet.create({ marginLeft: 8, fontFamily: 'SpaceMono', }, - passButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#FE6244', - paddingVertical: 12, - paddingHorizontal: 24, - borderRadius: 30, - }, - passButtonText: { - color: 'white', - fontSize: 16, - marginLeft: 8, - fontFamily: 'SpaceMono', - }, errorText: { fontSize: 18, textAlign: 'center', diff --git a/app/screens/how-to-play.tsx b/app/screens/how-to-play.tsx index aa4d97f..2b5aaa5 100644 --- a/app/screens/how-to-play.tsx +++ b/app/screens/how-to-play.tsx @@ -4,18 +4,18 @@ import { router } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { - Image, - ScrollView, - StyleSheet, - Text, - TouchableOpacity, - View + Image, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View } from 'react-native'; export default function HowToPlay() { return ( - + - + - + (null); + const insets = useSafeAreaInsets(); + + const handlePlayerSelect = (playerIndex: number) => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + setSelectedPlayer(playerIndex); + }; + + const handleConfirmSelection = () => { + if (selectedPlayer === null) { + Alert.alert('No Player Selected', 'Please select which player was holding the bomb when it exploded.'); + return; + } + + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + + // Set the loser for this round + setRoundLoser(selectedPlayer); + + // Navigate to round result + router.push('/screens/round-result'); + }; + + const handleGoBack = () => { + Alert.alert( + "Go Back?", + "Are you sure you want to go back? You'll need to play this round again.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Go Back", + onPress: () => router.back() + } + ] + ); + }; + + return ( + + + + + + + + Who Lost? + + + + + 💥 BOOM! + + Round {gameState.currentRound} has ended.{'\n'} + Select which player was holding the bomb when it exploded: + + + + {gameState.settings.players.map((player, index) => ( + handlePlayerSelect(index)} + > + + + + {player.name.charAt(0).toUpperCase()} + + + + + {player.name} + + + + {selectedPlayer === index && ( + + )} + + ))} + + + + + Confirm Selection + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: '#f0f0f0', + }, + backButton: { + padding: 8, + }, + headerTitle: { + fontSize: 18, + fontWeight: 'bold', + fontFamily: 'SpaceMono', + }, + placeholder: { + width: 40, + }, + content: { + flex: 1, + padding: 20, + }, + title: { + fontSize: 48, + textAlign: 'center', + marginBottom: 20, + }, + subtitle: { + fontSize: 18, + textAlign: 'center', + marginBottom: 30, + lineHeight: 26, + color: '#666', + fontFamily: 'SpaceMono', + }, + playersList: { + flex: 1, + marginBottom: 20, + }, + playerButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + marginBottom: 12, + borderRadius: 12, + borderWidth: 2, + borderColor: '#f0f0f0', + backgroundColor: 'white', + }, + playerButtonSelected: { + borderColor: '#FE6244', + backgroundColor: '#fff5f4', + }, + playerInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + playerAvatar: { + width: 50, + height: 50, + borderRadius: 25, + justifyContent: 'center', + alignItems: 'center', + marginRight: 16, + }, + playerAvatarSelected: { + borderWidth: 2, + borderColor: '#FE6244', + }, + playerAvatarText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + fontFamily: 'SpaceMono', + }, + playerAvatarTextSelected: { + color: 'white', + }, + playerDetails: { + flex: 1, + }, + playerName: { + fontSize: 18, + fontWeight: 'bold', + fontFamily: 'SpaceMono', + }, + playerNameSelected: { + color: '#FE6244', + }, + confirmButton: { + backgroundColor: '#FE6244', + paddingVertical: 16, + borderRadius: 12, + alignItems: 'center', + }, + confirmButtonDisabled: { + backgroundColor: '#ccc', + }, + confirmButtonText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + fontFamily: 'SpaceMono', + }, + confirmButtonTextDisabled: { + color: '#999', + }, +}); \ No newline at end of file diff --git a/app/screens/setup-players.tsx b/app/screens/setup-players.tsx index 2ada557..5f98d20 100644 --- a/app/screens/setup-players.tsx +++ b/app/screens/setup-players.tsx @@ -1,20 +1,19 @@ import { useGame } from '@/_context/GameContext'; -import { Player } from '@/_types/GameTypes'; import { AntDesign, MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { router } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import React, { useState } from 'react'; import { - Alert, - KeyboardAvoidingView, - Platform, - ScrollView, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - View + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View } from 'react-native'; export default function SetupPlayers() { @@ -45,28 +44,27 @@ export default function SetupPlayers() { }; const handleNext = () => { - // Validate player names - const emptyNameIndex = playerNames.findIndex(name => !name.trim()); - if (emptyNameIndex !== -1) { - Alert.alert('Missing Name', `Please enter a name for Player ${emptyNameIndex + 1}`); + if (playerNames.length < 2) { + Alert.alert('Not enough players', 'You need at least 2 players to start a game.'); return; } - - // Check for duplicate names - const uniqueNames = new Set(playerNames.map(name => name.trim())); - if (uniqueNames.size !== playerNames.length) { - Alert.alert('Duplicate Names', 'All players must have unique names!'); - return; - } - - // Create player objects with IDs and save to context - const players: Player[] = playerNames.map((name, index) => ({ - id: `player-${index + 1}`, - name: name.trim(), + + // Colors for players + const playerColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', + '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F' + ]; + + // Create player objects with all required properties + const playersWithDetails = playerNames.map((name, index) => ({ + id: `player-${index}`, + name, + color: playerColors[index % playerColors.length], + roundsLost: 0 })); - - setPlayers(players); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + setPlayers(playersWithDetails); router.push('/screens/select-categories'); }; @@ -76,7 +74,7 @@ export default function SetupPlayers() { style={{ flex: 1 }} > - +