Mobile AppBasketballReact Native

Building Hangtime: Basketball Meets Software Engineering

April 15, 2024 β€’ 10 min read

App Features & Technology

  • πŸ“±React Native – Cross-platform mobile development
  • πŸ—ΊοΈInteractive Maps – 200+ basketball courts mapped
  • πŸ‘₯Social Features – Player profiles and game organizing
  • πŸ€Game Matching – Find players and organize pickup games

Growth & Community Impact

  • πŸ‘€10,000+ Active Players – Organic growth through word-of-mouth
  • πŸ€5,000+ Games Organized – Real community connections
  • ⭐4.8/5 App Store Rating – High user satisfaction
  • 🌏Melbourne-wide Coverage – All major basketball venues

Combining passion for basketball with engineering skills to solve real problems. Hangtime Melbourne connects over 10,000 basketball players across Melbourne, but the real story is how we built a community-driven platform that scaled organically through word-of-mouth and solid engineering practices. This project taught me that the best software solutions come from solving problems you're passionate about.

Hangtime App Architecture

Mobile App Layer (React Native)
iOS App
Android App
Maps
Chat
Profile
↕️
Backend Services (Node.js + Firebase)
REST API
Push Notifications
Firebase Auth & Firestore
Real-time Chat
Location Services
↕️
Data & External Services
Google Maps
Firebase Storage
Analytics

React Native enabled cross-platform development while Firebase provided real-time features and scalable backend infrastructure.

The Problem We Solved

Melbourne has hundreds of basketball courts, but finding active players and games was always a challenge. Players would show up to empty courts or miss out on games because they didn't know where the action was.

Before Hangtime
β€’ Empty courts or packed games
β€’ No way to find other players
β€’ Inconsistent pickup games
β€’ Limited social connections
After Hangtime
β€’ Organized games with confirmed players
β€’ Real-time court activity
β€’ Strong basketball community
β€’ Lasting friendships formed

By solving a real problem I personally experienced, Hangtime grew organically to become Melbourne's go-to basketball community app.

React Native Implementation & Cross-Platform Architecture

React Native allowed us to build for both iOS and Android with a single codebase while maintaining native performance:

// Court Map Component with Real-time Updates
import React, { useState, useEffect } from 'react';
import MapView, { Marker, Callout } from 'react-native-maps';
import firestore from '@react-native-firebase/firestore';
import { useNavigation } from '@react-navigation/native';

const CourtMapScreen = () => {
  const [courts, setCourts] = useState([]);
  const [activeGames, setActiveGames] = useState([]);
  const navigation = useNavigation();

  useEffect(() => {
    // Load basketball courts from Firestore
    const unsubscribeCourts = firestore()
      .collection('courts')
      .onSnapshot(snapshot => {
        const courtsData = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        }));
        setCourts(courtsData);
      });

    // Listen to active games in real-time
    const unsubscribeGames = firestore()
      .collection('games')
      .where('status', '==', 'active')
      .where('date', '>=', new Date())
      .onSnapshot(snapshot => {
        const gamesData = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        }));
        setActiveGames(gamesData);
      });

    return () => {
      unsubscribeCourts();
      unsubscribeGames();
    };
  }, []);

  const getActiveGameForCourt = (courtId) => {
    return activeGames.find(game => game.courtId === courtId);
  };

  return (
    <MapView
      style={{ flex: 1 }}
      initialRegion={{
        latitude: -37.8136,
        longitude: 144.9631,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      }}
    >
      {courts.map(court => {
        const activeGame = getActiveGameForCourt(court.id);
        return (
          <Marker
            key={court.id}
            coordinate={{
              latitude: court.location.latitude,
              longitude: court.location.longitude,
            }}
            pinColor={activeGame ? '#22c55e' : '#6b7280'}
          >
            <Callout onPress={() => navigation.navigate('CourtDetail', { 
              courtId: court.id 
            })}>
              <View style={{ padding: 10, minWidth: 200 }}>
                <Text style={{ fontWeight: 'bold', fontSize: 16 }}>
                  {court.name}
                </Text>
                <Text style={{ color: '#666', marginTop: 4 }}>
                  {court.address}
                </Text>
                {activeGame && (
                  <View style={{ 
                    marginTop: 8, 
                    padding: 8, 
                    backgroundColor: '#22c55e', 
                    borderRadius: 4 
                  }}>
                    <Text style={{ color: 'white', fontWeight: 'bold' }}>
                      πŸ€ Active Game
                    </Text>
                    <Text style={{ color: 'white', fontSize: 12 }}>
                      {activeGame.players.length}/{activeGame.maxPlayers} players
                    </Text>
                  </View>
                )}
              </View>
            </Callout>
          </Marker>
        );
      })}
    </MapView>
  );
};

export default CourtMapScreen;

Firebase Real-time Game Management

Firebase Firestore enables real-time game updates and player coordination:

// Game Service - Creating and Managing Basketball Games
import firestore from '@react-native-firebase/firestore';
import messaging from '@react-native-firebase/messaging';

class GameService {
  static async createGame(gameData) {
    const { courtId, date, time, maxPlayers, description, createdBy } = gameData;
    
    try {
      const gameRef = await firestore().collection('games').add({
        courtId,
        date: firestore.Timestamp.fromDate(new Date(date)),
        time,
        maxPlayers,
        description,
        createdBy,
        players: [createdBy], // Creator automatically joins
        status: 'active',
        createdAt: firestore.FieldValue.serverTimestamp(),
        updatedAt: firestore.FieldValue.serverTimestamp()
      });

      // Send notification to nearby players
      await this.notifyNearbyPlayers(courtId, gameRef.id);
      
      return gameRef.id;
    } catch (error) {
      console.error('Error creating game:', error);
      throw error;
    }
  }

  static async joinGame(gameId, userId) {
    const gameRef = firestore().collection('games').doc(gameId);
    
    try {
      await firestore().runTransaction(async (transaction) => {
        const gameDoc = await transaction.get(gameRef);
        
        if (!gameDoc.exists) {
          throw new Error('Game not found');
        }

        const gameData = gameDoc.data();
        
        if (gameData.players.includes(userId)) {
          throw new Error('Already joined this game');
        }

        if (gameData.players.length >= gameData.maxPlayers) {
          throw new Error('Game is full');
        }

        transaction.update(gameRef, {
          players: firestore.FieldValue.arrayUnion(userId),
          updatedAt: firestore.FieldValue.serverTimestamp()
        });

        // Add to user's joined games
        transaction.update(
          firestore().collection('users').doc(userId),
          {
            joinedGames: firestore.FieldValue.arrayUnion(gameId)
          }
        );
      });

      // Notify other players
      await this.notifyGamePlayers(gameId, `New player joined the game!`);
      
    } catch (error) {
      console.error('Error joining game:', error);
      throw error;
    }
  }

  static async notifyNearbyPlayers(courtId, gameId) {
    // Get users who have played at this court before
    const nearbyUsers = await firestore()
      .collection('users')
      .where('favoriteCourts', 'array-contains', courtId)
      .get();

    const tokens = nearbyUsers.docs
      .map(doc => doc.data().fcmToken)
      .filter(token => token);

    if (tokens.length > 0) {
      await messaging().sendMulticast({
        tokens,
        notification: {
          title: 'πŸ€ New Game Started!',
          body: 'A basketball game is starting near your favorite court'
        },
        data: {
          type: 'new_game',
          gameId,
          courtId
        }
      });
    }
  }

  static async updateGameStatus(gameId, status) {
    await firestore().collection('games').doc(gameId).update({
      status,
      updatedAt: firestore.FieldValue.serverTimestamp()
    });

    if (status === 'completed') {
      // Update player statistics
      const game = await firestore().collection('games').doc(gameId).get();
      const gameData = game.data();
      
      for (const playerId of gameData.players) {
        await firestore().collection('users').doc(playerId).update({
          gamesPlayed: firestore.FieldValue.increment(1),
          lastGameDate: firestore.FieldValue.serverTimestamp()
        });
      }
    }
  }
}

export default GameService;

Real-time Chat Implementation

In-app messaging enables players to coordinate and build community:

// Chat Component for Game Coordination
import React, { useState, useEffect, useCallback } from 'react';
import { GiftedChat } from 'react-native-gifted-chat';
import firestore from '@react-native-firebase/firestore';
import auth from '@react-native-firebase/auth';

const GameChatScreen = ({ route }) => {
  const { gameId } = route.params;
  const [messages, setMessages] = useState([]);
  const currentUser = auth().currentUser;

  useEffect(() => {
    const unsubscribe = firestore()
      .collection('games')
      .doc(gameId)
      .collection('messages')
      .orderBy('createdAt', 'desc')
      .limit(50)
      .onSnapshot(snapshot => {
        const messagesData = snapshot.docs.map(doc => {
          const data = doc.data();
          return {
            _id: doc.id,
            text: data.text,
            createdAt: data.createdAt.toDate(),
            user: {
              _id: data.user._id,
              name: data.user.name,
              avatar: data.user.avatar
            }
          };
        });
        setMessages(messagesData);
      });

    return unsubscribe;
  }, [gameId]);

  const onSend = useCallback((messages = []) => {
    const message = messages[0];
    
    firestore()
      .collection('games')
      .doc(gameId)
      .collection('messages')
      .add({
        text: message.text,
        createdAt: firestore.FieldValue.serverTimestamp(),
        user: {
          _id: currentUser.uid,
          name: currentUser.displayName,
          avatar: currentUser.photoURL
        }
      });
  }, [gameId, currentUser]);

  return (
    <GiftedChat
      messages={messages}
      onSend={onSend}
      user={{
        _id: currentUser.uid,
        name: currentUser.displayName,
        avatar: currentUser.photoURL
      }}
      placeholder="Type a message..."
      showUserAvatar
      alwaysShowSend
    />
  );
};

export default GameChatScreen;

User Growth & Community Building Strategy

Organic growth through community-first features and basketball culture:

// User Analytics & Growth Tracking
const UserAnalytics = {
  // Track user engagement metrics
  trackUserAction: async (userId, action, metadata = {}) => {
    await firestore().collection('analytics').add({
      userId,
      action,
      metadata,
      timestamp: firestore.FieldValue.serverTimestamp(),
      appVersion: getAppVersion(),
      platform: Platform.OS
    });
  },

  // Community building features
  calculateUserReputation: async (userId) => {
    const userDoc = await firestore().collection('users').doc(userId).get();
    const userData = userDoc.data();
    
    let reputation = 0;
    
    // Points for games played
    reputation += userData.gamesPlayed * 2;
    
    // Points for games organized
    reputation += userData.gamesOrganized * 5;
    
    // Points for positive reviews
    reputation += userData.positiveReviews * 3;
    
    // Bonus for consistent play
    const daysActive = userData.activeDays || 0;
    if (daysActive > 30) reputation += 20;
    if (daysActive > 90) reputation += 50;
    
    return reputation;
  },

  // Growth metrics
  getGrowthMetrics: async () => {
    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
    
    const newUsers = await firestore()
      .collection('users')
      .where('createdAt', '>=', thirtyDaysAgo)
      .get();
    
    const activeGames = await firestore()
      .collection('games')
      .where('date', '>=', thirtyDaysAgo)
      .get();
    
    const totalUsers = await firestore()
      .collection('users')
      .get();
    
    return {
      newUsersThisMonth: newUsers.size,
      gamesThisMonth: activeGames.size,
      totalUsers: totalUsers.size,
      avgGamesPerUser: activeGames.size / totalUsers.size
    };
  }
};

// Community Features
const CommunityFeatures = {
  // Player skill matching
  findCompatiblePlayers: async (userId) => {
    const user = await firestore().collection('users').doc(userId).get();
    const userSkill = user.data().skillLevel;
    
    return firestore()
      .collection('users')
      .where('skillLevel', '>=', userSkill - 1)
      .where('skillLevel', '<=', userSkill + 1)
      .where('isActive', '==', true)
      .limit(20)
      .get();
  },

  // Court recommendations
  recommendCourts: async (userId) => {
    const userGames = await firestore()
      .collection('games')
      .where('players', 'array-contains', userId)
      .get();
    
    const playedCourts = userGames.docs.map(doc => doc.data().courtId);
    
    // Recommend courts similar players use
    const similarPlayerCourts = await firestore()
      .collection('courts')
      .where('popularWith', 'array-contains-any', playedCourts)
      .limit(10)
      .get();
    
    return similarPlayerCourts.docs.map(doc => ({
      id: doc.id,
      ...doc.data()
    }));
  }
};

The Business Side: Passion Project to Sustainable App

Development Timeline:
Month 1-2: MVP development (core features)
Month 3-4: Beta testing with local basketball groups  
Month 5-6: App Store launch and initial user acquisition
Month 7-12: Feature expansion based on user feedback
Month 13+: Sustainable growth and community building

Growth Strategy:
πŸ€ Partner with local basketball courts and leagues
πŸ‘₯ User referral system and community events
πŸ“± App Store optimization and social media presence
🎯 Focus on user retention over acquisition

Key Metrics:
β€’ 10,000+ active monthly users
β€’ 4.8/5 average App Store rating
β€’ 85% weekly user retention rate
β€’ 5,000+ games successfully organized
β€’ Featured in local Melbourne sports media

Building Hangtime taught me that the most successful projects combine technical skills with genuine passion. When you're solving a problem you care about, the long hours of coding, debugging, and user support don't feel like workβ€”they feel like building something meaningful for your community.

Download:App Store
Share: