Skip to main content

Spots

Community-driven skate spot and action sports location database.

Overview

The Spots feature allows users to discover, share, and organize action sports locations. Users can browse a global map of spots, create personal spot lists, and contribute new locations. The Chrome Extension enables bulk importing from Google Maps.

Frontend Implementation

Website (Next.js)

Location: /pages/spots/

FilePurpose
index.jsMain spots page with interactive map
[state]/index.jsState-specific spot listing
[state]/[city].jsCity-specific spot listing
[state]/[city]/[spot].jsIndividual spot detail page

Components:

ComponentLocationPurpose
SpotCard.js/components/SpotCard.jsSpot preview with image, name, rating
SpotMap.js/components/SpotMap.jsInteractive Google Maps with markers
SpotFilters.js/components/SpotFilters.jsFilter by type, amenities, rating
StateCard.js/components/spots/StateCard.jsState navigation card

Key Features:

  • Interactive map with clustered markers
  • Filter by spot type (park, street, DIY, etc.)
  • Filter by amenities (lights, bathroom, free entry)
  • State/city hierarchical navigation
  • User ratings and reviews
  • Photo galleries
  • Directions integration (Google Maps, Apple Maps)

Map Integration

Google Maps Setup:

// /components/SpotMap.js
import { GoogleMap, Marker, MarkerClusterer } from "@react-google-maps/api";

const SpotMap = ({ spots, center, zoom }) => {
return (
<GoogleMap
mapContainerStyle={{ width: "100%", height: "500px" }}
center={center}
zoom={zoom}
>
<MarkerClusterer>
{(clusterer) =>
spots.map((spot) => (
<Marker
key={spot._id}
position={{ lat: spot.latitude, lng: spot.longitude }}
clusterer={clusterer}
onClick={() => handleMarkerClick(spot)}
/>
))
}
</MarkerClusterer>
</GoogleMap>
);
};

Mobile App (React Native)

Location: /TrickList/screens/

ScreenPurpose
SpotsScreen.jsBrowse spots with map view
SpotDetailScreen.jsView spot details, photos, directions
SpotListsScreen.jsUser's personal spot collections
AddSpotScreen.jsSubmit new spot with location picker

Map Library: react-native-maps

Backend Implementation

Database Schema

Collection: spots

{
_id: ObjectId,
name: String, // "Venice Beach Skatepark"
slug: String, // "venice-beach-skatepark" (URL-safe)
description: String,
latitude: Number, // 33.9850
longitude: Number, // -118.4695
address: String, // Full street address
city: String, // "Los Angeles"
state: String, // "CA" or "California"
country: String, // "USA"
postalCode: String,

// Classification
type: String, // "park", "street", "diy", "plaza"
sportTypes: [String], // ["skateboarding", "bmx"]
tags: [String], // ["bowl", "street", "transitions"]

// Amenities
amenities: {
lights: Boolean,
bathroom: Boolean,
water: Boolean,
shade: Boolean,
freeEntry: Boolean,
rental: Boolean
},

// Media
images: [String], // S3 URLs
thumbnailUrl: String, // Primary image

// Ratings
rating: Number, // Average (1-5)
ratingCount: Number, // Number of ratings

// Meta
googlePlaceId: String, // For Google Maps integration
submittedBy: ObjectId, // User who added spot
isVerified: Boolean, // Admin verified
isActive: Boolean, // Soft delete flag

createdAt: Date,
updatedAt: Date
}

Indexes:

  • { latitude: 1, longitude: 1 } - Geospatial queries
  • { state: 1, city: 1 } - Location hierarchy
  • { slug: 1 } - URL lookups
  • { sportTypes: 1, type: 1 } - Filtered queries
  • { "location": "2dsphere" } - Geospatial index (if using GeoJSON)

Collection: spotlists (User's saved spot collections)

{
_id: ObjectId,
user: ObjectId,
name: String, // "LA Weekend Spots"
description: String,
isPublic: Boolean,
spots: [ObjectId], // References to spots collection
coverImage: String, // S3 URL
createdAt: Date,
updatedAt: Date
}

Collection: spot_ratings

{
_id: ObjectId,
spot: ObjectId,
user: ObjectId,
rating: Number, // 1-5
review: String, // Optional text review
images: [String], // User-submitted photos
createdAt: Date
}

API Endpoints

Base URL: https://api.thetrickbook.com/api

Spots (Public)

MethodEndpointDescriptionAuth
GET/spotsList all spots with filteringNo
GET/spots/:idGet spot by IDNo
GET/spots/slug/:slugGet spot by URL slugNo
GET/spots/state/:stateGet spots by stateNo
GET/spots/city/:state/:cityGet spots by cityNo
GET/spots/nearbyGet spots near coordinatesNo
POST/spotsCreate new spotJWT
PUT/spots/:idUpdate spotJWT/Admin
DELETE/spots/:idDelete spotAdmin
POST/spots/bulkBulk import spotsAdmin

Query Parameters for GET /spots:

  • lat & lng - Center point for nearby search
  • radius - Search radius in miles (default 25)
  • type - Filter by spot type
  • sportType - Filter by sport
  • state - Filter by state
  • city - Filter by city
  • limit - Pagination (default 50)
  • skip - Offset

Spot Lists (User Collections)

MethodEndpointDescriptionAuth
GET/spotlistsGet user's spot listsJWT
POST/spotlistsCreate spot listJWT
GET/spotlists/:idGet spot list detailsJWT
PUT/spotlists/:idUpdate spot listJWT
DELETE/spotlists/:idDelete spot listJWT
POST/spotlists/:id/spotsAdd spot to listJWT
DELETE/spotlists/:id/spots/:spotIdRemove spot from listJWT

Spot Ratings

MethodEndpointDescriptionAuth
GET/spots/:id/ratingsGet spot ratingsNo
POST/spots/:id/ratingsAdd ratingJWT
PUT/spots/:id/ratings/:ratingIdUpdate ratingJWT
DELETE/spots/:id/ratings/:ratingIdDelete ratingJWT

Routes Implementation

File: /Backend/routes/spots.js

// Nearby spots with geospatial query
router.get("/nearby", async (req, res) => {
const { lat, lng, radius = 25 } = req.query;

const spots = await Spot.find({
latitude: {
$gte: parseFloat(lat) - (radius / 69),
$lte: parseFloat(lat) + (radius / 69)
},
longitude: {
$gte: parseFloat(lng) - (radius / 54.6),
$lte: parseFloat(lng) + (radius / 54.6)
},
isActive: true
}).limit(100);

res.json(spots);
});

// Spots by state with city grouping
router.get("/state/:state", async (req, res) => {
const spots = await Spot.aggregate([
{ $match: { state: req.params.state, isActive: true } },
{ $group: {
_id: "$city",
spots: { $push: "$$ROOT" },
count: { $sum: 1 }
}},
{ $sort: { count: -1 } }
]);

res.json(spots);
});

Chrome Extension Integration

The Chrome Extension (Map Scraper) extracts spots from Google Maps and syncs to the backend.

Flow:

  1. User browses Google Maps searching for skate spots
  2. Extension extracts: name, address, coordinates, rating, photos
  3. User categorizes with tags (park, street, lights, etc.)
  4. Bulk sync to TrickBook via /spots/bulk endpoint

Data Extraction:

// Extension extracts from Google Maps DOM
{
name: "Venice Beach Skatepark",
address: "1800 Ocean Front Walk, Venice, CA 90291",
latitude: 33.9850,
longitude: -118.4695,
googlePlaceId: "ChIJ...",
rating: 4.5,
photos: ["url1", "url2"]
}

See Chrome Extension Documentation for details.

Image Storage

Spot images stored in AWS S3:

  • Bucket: trickbook
  • Path: /spots/{spot-id}/
  • Thumbnail generation: Automatic resize to 400x300

Mobile Considerations

Offline Mode

  • Spots in user's lists cached locally
  • Map tiles cached for offline viewing
  • Sync when connection restored

Location Services

  • Request location permission for nearby spots
  • Background location for "spots near me" notifications
  • Geofencing for spot check-ins (future)

Performance

  • Cluster markers for large datasets
  • Lazy load images
  • Pagination with infinite scroll