DEV Community

Cover image for Building a Real-Time Location Sharing App with React, Socket.io, and Leaflet
Sindbad_X
Sindbad_X

Posted on • Edited on

Building a Real-Time Location Sharing App with React, Socket.io, and Leaflet

There are a lot of tutorials on building Chat App with react and socket io. While building a Chat App is a cool way to explore real-time applications, I wanted to be a bit more creative. So, I came up with the idea of creating a location sharing app. Plus, I needed a project for my empty resume that may compensate for my lack of experience.

So, here we go.

LocShare: a real time location sharing app that allows you to share your location to multiple users.

Image description

Image description

You will also get notified when someone joins or leaves the room and the current number of connected users.

Image description

As soon as you stop sharing the location, the room will get destroyed, and the connected users won’t be able to see your location any more.

Image description


This is a location sharing app, which means in order to test this application you have to walk/run/drive miles. Hopefully not. Because browsers provides a way to manipulate your coordinates without you having to leave your couch.

Navigate to the more tools and find the sensor option. From there, you can manipulate your current coordinates.

Image description

Image description

You can try out this app from here. The backend is hosted on render’s free tier, so it will take some time for the first connection and sometime it's even not available for few hours.


If you're coding along, you might find it a bit challenging. My emphasis is on the crucial bits of code, the logical flow, and the structure of this app. I encourage you to visit the GitHub repository for the complete code.

Setting up the project

In the frontend, I've configured my React app with Vite, TypeScript, and Tailwind CSS. To map our coordinates on a map, we'll use a well-known open-source library named Leaflet.

I've recently begun using TypeScript and applied it in this project. However, it's important to note that TypeScript is optional if you're coding along.

Here are the dependencies of frontend:

{
  "name": "client",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@types/leaflet": "^1.9.6",
    "@types/react-leaflet": "^3.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^4.11.0",
    "react-leaflet": "^4.2.1",
    "react-router-dom": "^6.16.0",
    "react-toastify": "^9.1.3",
    "socket.io-client": "^4.7.2"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "autoprefixer": "^10.4.15",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "postcss": "^8.4.29",
    "tailwindcss": "^3.3.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}
Enter fullscreen mode Exit fullscreen mode

For the backend, we have a basic node-express server with the following dependencies.

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "socket.io": "^4.7.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
    "concurrently": "^8.2.0",
    "nodemon": "^3.0.1",
    "typescript": "^5.2.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

Folder Structure

React App:

πŸ“ src
β”œβ”€β”€ πŸ“ components
β”‚   β”œβ”€β”€ πŸ“ Element
β”‚   β”‚   β”œβ”€β”€ πŸ“„ Header
β”‚   β”‚   β”œβ”€β”€ πŸ“„ Map
β”‚   β”‚   β”œβ”€β”€ πŸ“„ Status
β”‚   β”‚   β”œβ”€β”€ πŸ“„ StatusPanel
β”‚   β”œβ”€β”€ πŸ“ Layout
β”œβ”€β”€ πŸ“ context
β”‚   β”œβ”€β”€ πŸ“„ socket.tsx
β”œβ”€β”€ πŸ“ pages
β”‚   β”œβ”€β”€ πŸ“„ Home.tsx
β”‚   β”œβ”€β”€ πŸ“„ Location.tsx
β”œβ”€β”€ πŸ“ types
β”œβ”€β”€ πŸ“„ App.tsx
β”œβ”€β”€ πŸ“„ main.tsx
β”œβ”€β”€ πŸ“„ index.css
...rest
Enter fullscreen mode Exit fullscreen mode

Node server:

πŸ“ src
β”œβ”€β”€ πŸ“„ index.ts
Enter fullscreen mode Exit fullscreen mode

Create Socket Context

By storing socket in context, we can make it globally available to rest of our app. The connectSocket function handles the connection. If socket is null(no existing connection), it establishes a new socket connection. If socket is not null(disconnected), it connects to socket.

// SocketProvider.js
import {useState, createContext, useContext, JSX} from 'react'
import {io, Socket} from 'socket.io-client'
import { SOCKET_URL } from '../config'

type SocketContextType = {
  socket: Socket | null;
  connectSocket: () => void;
}

type SocketProviderProps = {
  children: JSX.Element
}

export const SocketContext = createContext<SocketContextType | null>(null)

export const SocketProvider = ({children}: SocketProviderProps) => {
  const [socket, setSocket] = useState<Socket | null>(null)

  const connectSocket = () => {
    if(!socket) {
      const newSocket: Socket = io(SOCKET_URL)
      setSocket(newSocket)
      return
    }
    socket.connect()
  }

  return (
    <SocketContext.Provider value={{socket, connectSocket}}>
      {children}
    </SocketContext.Provider>
  )
}

export const useSocket = () => {
  const context = useContext(SocketContext)
  if(!context) {
    throw new Error('Something went wrong!')
  }
  return context
}

Enter fullscreen mode Exit fullscreen mode

Here, I've made things easier by adding a custom hook called useSocket, which handles the logic of accessing SocketContext along with SocketProvider. This approach simplifies the code, making it easier to read and understand when working with context values in your components.

Wrapping app with socket context

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { SocketProvider } from './context/socket.tsx'
import {ToastContainer} from 'react-toastify'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <SocketProvider>
      <>
        <App />
        <ToastContainer newestOnTop/>
      </>
    </SocketProvider>
  </React.StrictMode>,
)
Enter fullscreen mode Exit fullscreen mode

App Routes

Here, we have our two pages: home and location. From homepage, we can create room and channel our location. The Location page is displayed when the URL matches the pattern /location/1242, indicating that 1242 is a specific room ID or parameter.

These pages are wrapped in a layout component.

import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import Location from './pages/Location'

function App() {

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="location/:roomId" element={<Location />}/>
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Layout Component

import { Outlet } from 'react-router-dom'
import Header from '../Elements/Header'

function index() {
  return (
    <div className='flex justify-center px-3 py-2'>
      <div className='flex flex-col w-full md:min-w-full xl:min-w-[1100px] xl:max-w-[1200px] mb-4'>
        <Header />
        <main>
          <Outlet />
        </main>
      </div>
    </div>
  )
}

export default index
Enter fullscreen mode Exit fullscreen mode

Access User Location

To get the current location of the user, we can use the geolocation interface provided by the browser to obtain the current position. This will open a popup, asking user the location permission.

// get the current position
navigator.geolocation.getCurrentPosition(success, error, options)
Enter fullscreen mode Exit fullscreen mode

success, error(optional), options(optional) are the callback function that are being passed, and through them, we can access the the coordinates or error or pass additional parameters. You can check out the mozilla doc for more.

We don't just need the current position; we also want to track the user's movement from point A to B

We can have a setInterval function that repeatedly calls getCurrentPosition(). That’s not required here, because geolocation interface provide an another method called watchPosition(). And this method is called each time the position changes.

// watch the current position
let id = navigator.geolocation.watchPosition(success, error, options)

// clear the resource
navigator.geolocation.clearWatch(id)
Enter fullscreen mode Exit fullscreen mode

This watchPosition method is like setTimeout or setInterval, a part of browser features, that runs asynchronously in the background and monitor the user’s current position. By storing it in a variable, we are keeping a reference. So later when we no longer want to track the user location, we will clear the variable or set it null. If we don’t do that, it will keep running in the background and that can lead to memory issues and unnecessary resource consumption.

type GeolocationPosition = {
  lat: number
  lng: number
}
type LocationStatus = 'accessed' | 'denied' | 'unknown' | 'error'

export default function Home() {
  const [locationStatus, setLocationStatus] = useState<LocationStatus>('unknown')
  const [position, setPosition] = useState<GeolocationPosition | null>(null)

  useEffect(() => {
    let watchId: number | null = null
        // check for geolocation support in browser
    if('geolocation' in navigator) {
      watchId = navigator.geolocation.watchPosition((position) => {
      setPosition({
        lat: position.coords.latitude,
        lng: position.coords.longitude
      })
      setLocationStatus('accessed')
      }, (error) => {
        switch (error.code) {
          case error.PERMISSION_DENIED:
            setLocationStatus('denied')
            break
          case error.POSITION_UNAVAILABLE:
            setLocationStatus('unknown')
            break
          case error.TIMEOUT:
            setLocationStatus('error')
            break
          default:
            setLocationStatus('error')
            break
        }
      })
      return () => {
        if(watchId) {
          navigator.geolocation.clearWatch(watchId)
        }
      }
    }
  }, [])
...
...
Enter fullscreen mode Exit fullscreen mode

When the page is loaded, the location prompt appears and ask for location permission. Based on that it handles whether user have allowed location permission or not. Once location is allowed, we have access to the position object that contains the coordinates of user.

With the error code provided by geolocation, we can nicely handle the error and know exactly why user can’t access location.

Locate coordinates on map

Once we have the user’s coordinates, we can pass them into our Map component and the location will shown in the map.

We want the map marker to automatically move to the passed coordinates. In our Location Marker component, we put the map.flyTo method, provided by the leaflet instance, inside the useEffect hook with position as the dependency. Whenever, location change, the marker will fly there.

import { useState, useEffect } from 'react'
import { MapContainer, TileLayer, useMapEvents, Marker, Popup } from 'react-leaflet'
import { GeolocationPosition } from '../../../types' 
import 'leaflet/dist/leaflet.css'

function Map({ location }: { location: GeolocationPosition }) {
  if (!location) return 'No location found'

  return (
    <div className='w-full bg-gray-100 h-[600px] md:h-[550px]'>
      <MapContainer center={[location.lat, location.lng]} zoom={30} scrollWheelZoom={true} className='h-screen'>
        <TileLayer
          url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' // OpenStreetMap tile layer URL
        />
        <LocationMarker location={location} />
      </MapContainer>
    </div>
  )
}

function LocationMarker({ location }: { location: GeolocationPosition }) {

  const map = useMapEvents({})  // Use map events to access the Leaflet map instance
  const [position, setPosition] = useState({
    lat: location.lat,
    lng: location.lng
  })

  // Effect to update marker position and fly to the new location when location data changes
  useEffect(() => {
    setPosition({
      lat: location.lat,
      lng: location.lng
    })
    map.flyTo([location.lat, location.lng]) // Fly to the new location on the map
  }, [location])

  return position === null ? null : (
    <Marker position={position}>
      <Popup>User is here!</Popup>
    </Marker>
  )
}

export default Map
Enter fullscreen mode Exit fullscreen mode

Now, we can use this component in our homepage.

export default function Home() {
  const [locationStatus, setLocationStatus] = useState<LocationStatus>('unknown')
  const [position, setPosition] = useState<GeolocationPosition | null>(null)

    // ... rest

return(
<>  
{/** ...rest **/}
                {
            position && (<Map location={position}/>)
        }
</>
)
Enter fullscreen mode Exit fullscreen mode

Setup node-express socket server

In this code, we've set up a simple HTTP server using Node.js and Express. Then, we've created a Socket.io server instance (io) and passed our Express server to it. Even though, HTTP and Web Sockets are different communication protocols, socket io enables both the HTTP server and the WebSocket server to share the same server instance, allowing them to communicate over the same network port.

import express, { Express, Request, Response } from 'express'
import {Socket, Server} from 'socket.io'
import cors from 'cors'
import dotenv from 'dotenv'

dotenv.config()

const app: Express = express()
const port = process.env.PORT || 5000

app.use(cors())
app.use(express.json())

app.get('/', (req: Request, res: Response) => {
  res.send('Welcome to LocShare!')
})


const server = app.listen(port, () => {
  console.log(`Server is running`)
})

const io: Server = new Server(server, {
  cors: {
    origin: '*',
  },
})

io.on('connection', (socket: Socket) => {
  console.log(`User connected: ${socket.id}`)
})
Enter fullscreen mode Exit fullscreen mode

When a user share location, we are essentially going to create the room.

AΒ roomΒ is an arbitrary channel that sockets canΒ joinΒ andΒ leave. It can be used to broadcast events to a subset of clients. read more

This room will have a unique id. Once the room is created, we’ll send the roomId in response to the user.

Here’s the flow

  1. user share location: emit create room event
  2. server receive the event
    1. generate room id
    2. join the room
    3. attach room id to current socket client
    4. emit room created event and roomid
    5. store room creator

πŸ’‘I’m extending my Socket and attaching an additional property to the socket called roomId. Later we’ll use this when user left the room.

// Define a custom interface extending the Socket interface
interface CustomSocket extends Socket {
  roomId?: string
}
const roomCreator = new Map<string, string>() // roomid => socketid

io.on('connection', (socket: CustomSocket) => {
  console.log(`User connected: ${socket.id}`)

  socket.on('createRoom', (data) => {
    const roomId = Math.random().toString(36).substring(2, 7)
    socket.join(roomId) // joining room in sockets
        socket.roomId = roomId //  attach roomId to socket
    const totalRoomUsers = io.sockets.adapter.rooms.get(roomId)
    socket.emit('roomCreated', { 
      roomId,
      position: data.position,
      totalConnectedUsers: Array.from(totalRoomUsers || []),
    })
    roomCreator.set(roomId, socket.id) // map roomid with socket
  })
})
Enter fullscreen mode Exit fullscreen mode

Joining room

In order to join the room, our client will emit an event β€˜joinRoom’ with the roomId.

  1. User emit β€˜joinRoom’ with roomId
  2. Check if room exist:
    • exist
      1. join room
      2. attach roomid to socket
      3. get the room creator and notify him
      4. message the room joiner(socket)
    • !exist
      1. notify the socket
...
...
socket.on('joinRoom', (data: {roomId: string}) => {

    // check if room exists
    const roomExists = io.sockets.adapter.rooms.has(data.roomId)
    if (roomExists) {
      socket.join(data.roomId)
      socket.roomId = data.roomId //  attach roomId to socket

      // Notify the room creator about the new user     
      const creatorSocketID = roomCreator.get(data.roomId)
      if (creatorSocketID) {
        const creatorSocket = io.sockets.sockets.get(creatorSocketID) // get socket instance of creator
        if (creatorSocket) {
          const totalRoomUsers = io.sockets.adapter.rooms.get(data.roomId)
          creatorSocket.emit('userJoinedRoom', {
            userId: socket.id,
            totalConnectedUsers: Array.from(totalRoomUsers || [])
          })
        }
      }
      // msg to joiner
      io.to(`${socket.id}`).emit('roomJoined', {
        status: 'OK',
      })

    } else {
      io.to(`${socket.id}`).emit('roomJoined', {
        status: 'ERROR'
      })
    }
  })
Enter fullscreen mode Exit fullscreen mode

For location update

socket.on('updateLocation', (data) => {
    io.emit('updateLocationResponse', data)
})
Enter fullscreen mode Exit fullscreen mode

Home page: Connect Sockets & Create Room

Now from the home page, we can connect to the socket server. To share location, user must share his location first. Once we have the coordinates, we can connect to the server and when the user have successfully established the connection, we’ll be automatically emit the createRoom event.

I chose this approach to prevent automatic connection to the server when a user visits the page. In our application, there might be additional pages like a login or register page in the future. To avoid unnecessary connections, I've designed it this way to ensure that we only establish a connection when it's explicitly needed.

type SocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error'
type RoomInfo = {
  roomId: string
  position: GeolocationPosition
  totalConnectedUsers: string[]
}

export default function Home() {
    // ...location related states

  const {socket, connectSocket} = useSocket()
  const [socketStatus, setSocketStatus] = useState<SocketStatus>('disconnected')
    const [roomLink, setRoomLink] = useState<string>('')
    const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null)

  function connectToSocketServer() {
    connectSocket()
    setSocketStatus('connecting')
  }

  useEffect(() => {
    let watchId: number | null = null
    // ...geolocation logic
  }, [])

  useEffect(() => {
    if(socket) {
      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('createRoom', {
          position
        })
      })

      socket.on('roomCreated', (data: RoomInfo) => {
        toast.success('You are live!', {
          autoClose: 2000,
          })
        setRoomInfo(data)
      })

      socket.on('userJoinedRoom', (data: {userId: string, totalConnectedUsers: string[]}) => {
        setRoomInfo((prev) => {
          if(prev) {
            return {
              ...prev,
              totalConnectedUsers: data.totalConnectedUsers
            }
          }
          return null
        })

        toast.info(`${data.userId} joined the room`, {
          autoClose: 2000,
        })

        position && socket.emit('updateLocation', {
          position
        })
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }
  }, [socket])

  useEffect(() => {
    if(socket) {
      socket.emit('updateLocation', {
        position
      })
    }
  }, [position])


  return (
    <>
            {/* ...rest */}
            {
              socketStatus === 'disconnected' && (
                <div className='flex flex-col gap-6 items-start w-full'>
                  <button 
                    className={`${locationStatus === 'accessed' ? 'bg-purple-800' : 'bg-gray-600 cursor-not-allowed'}`}
                    onClick={() => {
                      if(locationStatus === 'accessed') {
                        connectToSocketServer()
                      } else {
                        toast.error('Please allow location access', {
                          autoClose: 2000,
                        })
                      }
                    }}
                    disabled={locationStatus !== 'accessed'}
                    >Share Location</button>
                  {/*  ...rest */}
              </div>
              )
Enter fullscreen mode Exit fullscreen mode

Location page: access roomId and join room

When the page load, we extract the room ID from the URL. Subsequently, we establish a connection with the socket. Once the connection is successfully established, we trigger the 'join room' event to indicate that the user has joined the specified room.

import React, {useState, useEffect} from 'react'
import { useParams } from 'react-router-dom'
import {useSocket} from '../context/socket'

type RoomStatus = 'unknown' | 'joined' | 'not-exist'

function Location() {
  const { roomId } = useParams()
  const { socket, connectSocket } = useSocket()
  const [socketStatus, setSocketStatus] = useState<SocketStatus>('disconnected')
  const [roomStatus, setRoomStatus] = useState<RoomStatus>('unknown')
  const [position, setPosition] = useState<GeolocationPosition | null>(null)

  useEffect(() => {
    connectSocket()
    setSocketStatus('connecting')
    return () => {
      if(socket) {
        socket.disconnect()
        setSocketStatus('disconnected')
      }
    }
  }, [])

  useEffect(() => {

    if(socket){      

      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('joinRoom', {
          roomId
        })
      })

      socket.on('roomJoined', ({status}: {status: string}) => {
        if(status === 'OK') {
          setRoomStatus('joined')
        } else if (status === 'ERROR') {
          setRoomStatus('not-exist')
        } else {
          setRoomStatus('unknown')
        }
      })

      socket.on('updateLocationResponse', ({position}:{position: GeolocationPosition}) => {
        if(position) {
          setPosition(position)
        }
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }

  }, [socket])

// ...rest
Enter fullscreen mode Exit fullscreen mode

Server: leave room

Here, the logic when user leaves the room:

User Leaves the Room:

  1. For Room Creator:
    • If the leaving user is the creator:
      1. Destroy the Room:
      2. Notify Other Users
  2. For Room Joiner:
    • If the leaving user is a participant:
      1. Notify the Creator:
        • Inform the room creator about the departure.
      2. Leave the Room:
        • Ensure the leaving user is removed from the room's participant list.
io.on('connection', (socket: CustomSocket) => {
  console.log(`User connected: ${socket.id}`)

// ...rest code

socket.on('disconnect', () => {
    console.log(`User disconnected: ${socket.id}`)

    const roomId = socket.roomId
    if(roomId){
      // if disconnected user is creator, destroy room
      if(roomCreator.get(roomId) === socket.id){
        // notify users in room that room is destroyed
        const roomUsers = io.sockets.adapter.rooms.get(roomId)
        if(roomUsers){
          for (const socketId of roomUsers) {
            io.to(`${socketId}`).emit('roomDestroyed', {
              status: 'OK'
            })
          }
        }
        io.sockets.adapter.rooms.delete(roomId)
        roomCreator.delete(roomId)    
      } else{
        socket.leave(roomId)
        // notify creator that user left room
        const creatorSocketId = roomCreator.get(roomId)
        if(creatorSocketId){
          const creatorSocket = io.sockets.sockets.get(creatorSocketId)
          if(creatorSocket){
            creatorSocket.emit('userLeftRoom', {
              userId: socket.id,
              totalConnectedUsers: Array.from(io.sockets.adapter.rooms.get(roomId) || [])
            })
          }
        }
      }
    }

  })

})
Enter fullscreen mode Exit fullscreen mode

Update home and location page: leave room

home.tsx

export default function Home() {

    // ...rest

  useEffect(() => {
    if(socket) {
      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('createRoom', {
          position
        })
      })
            // ...rest

            socket.on('userLeftRoom', (data: {userId: string, totalConnectedUsers: string[]}) => {
        setRoomInfo((prev) => {
          if(prev) {
            return {
                ...prev,
                totalConnectedUsers: data.totalConnectedUsers
              }
            }
            return null
          })
          toast.info(`${data.userId} left the room`, {
            autoClose: 2000,
          })
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }
  }, [socket])
// ...rest
Enter fullscreen mode Exit fullscreen mode

location.tsx



function Location() {

  // ...rest

  useEffect(() => {

    if(socket){      

      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('joinRoom', {
          roomId
        })
      })

      // ...rest

            socket.on('roomDestroyed', () => {
        setRoomStatus('not-exist')
        socket.disconnect()
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }

  }, [socket])

function stopSharingLocation() {
    if(socket){
      socket.disconnect()
      setSocketStatus('disconnected')
      setRoomInfo(null)
      toast.success('You are no longer live!', {
        autoClose: 2000,
      })
    }
  }

// ...rest
Enter fullscreen mode Exit fullscreen mode

We're done!πŸ‘πŸ½

We've covered the fundamental flow and logic of the location sharing application, focusing on the essential aspects. While I haven't provided the JSX and UI implementation code here, you can find the complete source code in the GitHub repository.


Final Words

In this project, I’ve touched the barebones. In the real world, we will have something more robust, with added authentication, database and more. Most importantly the scalability of the app, the amount of users it can handle.

Hopefully, if I get enough time, I would continue this series and try building those features.

You are welcomed to make a pull request, raise an issue or suggest changes.

Top comments (5)

Collapse
 
appurist profile image
Paul / Appurist

Kudos for using Leaflet rather than Google Maps!
(And websockets.)

Collapse
 
sahilverma_dev profile image
Sahil Verma

Don't mind if I steal your project😏

Collapse
 
sindbad_x profile image
Sindbad_X

@backslant there are already apps out there for Android which allows you to share your location. Check out on play store.

If you want to build something like this project, I don't think you would need anything extra. You will just have to build the UI for Android and the server code will be the same.

Collapse
 
androaddict profile image
androaddict

Nice article

Collapse
 
sindbad_x profile image
Sindbad_X

Thanks