Spaces:
Sleeping
Sleeping
Deploy backend update Wed Dec 3 21:14:46 CET 2025
Browse files- src/controllers/adventureController.ts +162 -0
- src/controllers/trackController.ts +67 -57
- src/index.ts +20 -1
- src/models/AdventureModel.ts +138 -0
- src/models/TrackModel.ts +70 -36
- src/routes/adventureRoutes.ts +18 -0
- src/routes/trackRoutes.ts +5 -2
- src/seed.ts +325 -5
- src/types/types.ts +110 -0
src/controllers/adventureController.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response } from 'express';
|
| 2 |
+
import { AdventureModel } from '../models/AdventureModel';
|
| 3 |
+
import { AdventureFeature, AdventureFeatureCollection } from '../types/types';
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* AdventureController - Handles HTTP requests for user-saved adventures
|
| 7 |
+
*/
|
| 8 |
+
export class AdventureController {
|
| 9 |
+
/**
|
| 10 |
+
* GET /api/adventures
|
| 11 |
+
* Fetch all adventures, optionally filtered by user_id
|
| 12 |
+
*/
|
| 13 |
+
static async getAdventures(req: Request, res: Response): Promise<void> {
|
| 14 |
+
console.log('--- Start getAdventures Controller ---');
|
| 15 |
+
try {
|
| 16 |
+
const { user_id } = req.query;
|
| 17 |
+
console.log(`Received query parameters: user_id=${user_id}`);
|
| 18 |
+
|
| 19 |
+
const adventures = await AdventureModel.findAll(user_id as string | undefined);
|
| 20 |
+
console.log(`Fetched ${adventures.length} adventures from the database.`);
|
| 21 |
+
|
| 22 |
+
// Convert adventures to GeoJSON features
|
| 23 |
+
const features: AdventureFeature[] = adventures.map(adventure => {
|
| 24 |
+
return {
|
| 25 |
+
type: 'Feature',
|
| 26 |
+
geometry: typeof adventure.path === 'string'
|
| 27 |
+
? JSON.parse(adventure.path)
|
| 28 |
+
: adventure.path,
|
| 29 |
+
properties: {
|
| 30 |
+
id: adventure.id,
|
| 31 |
+
name: adventure.name,
|
| 32 |
+
description: adventure.description,
|
| 33 |
+
user_id: adventure.user_id,
|
| 34 |
+
recorded_at: adventure.recorded_at,
|
| 35 |
+
...adventure.properties
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
const response: AdventureFeatureCollection = {
|
| 41 |
+
type: 'FeatureCollection',
|
| 42 |
+
features
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
res.json(response);
|
| 46 |
+
console.log('--- End getAdventures Controller (Success) ---');
|
| 47 |
+
} catch (err) {
|
| 48 |
+
console.error('Error in getAdventures:', err);
|
| 49 |
+
res.status(500).json({ error: 'Server Error' });
|
| 50 |
+
console.log('--- End getAdventures Controller (Error) ---');
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* GET /api/adventures/:id
|
| 56 |
+
* Fetch a single adventure by ID
|
| 57 |
+
*/
|
| 58 |
+
static async getAdventureById(req: Request, res: Response): Promise<void> {
|
| 59 |
+
console.log('--- Start getAdventureById Controller ---');
|
| 60 |
+
try {
|
| 61 |
+
const id = parseInt(req.params.id);
|
| 62 |
+
console.log(`Fetching adventure with ID: ${id}`);
|
| 63 |
+
|
| 64 |
+
const adventure = await AdventureModel.findById(id);
|
| 65 |
+
|
| 66 |
+
if (!adventure) {
|
| 67 |
+
console.log(`Adventure with ID ${id} not found.`);
|
| 68 |
+
res.status(404).json({ error: 'Adventure not found' });
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
const feature: AdventureFeature = {
|
| 73 |
+
type: 'Feature',
|
| 74 |
+
geometry: typeof adventure.path === 'string'
|
| 75 |
+
? JSON.parse(adventure.path)
|
| 76 |
+
: adventure.path,
|
| 77 |
+
properties: {
|
| 78 |
+
id: adventure.id,
|
| 79 |
+
name: adventure.name,
|
| 80 |
+
description: adventure.description,
|
| 81 |
+
user_id: adventure.user_id,
|
| 82 |
+
recorded_at: adventure.recorded_at,
|
| 83 |
+
...adventure.properties
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
res.json(feature);
|
| 88 |
+
console.log('--- End getAdventureById Controller (Success) ---');
|
| 89 |
+
} catch (err) {
|
| 90 |
+
console.error('Error in getAdventureById:', err);
|
| 91 |
+
res.status(500).json({ error: 'Server Error' });
|
| 92 |
+
console.log('--- End getAdventureById Controller (Error) ---');
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* POST /api/adventures
|
| 98 |
+
* Create a new adventure
|
| 99 |
+
*/
|
| 100 |
+
static async createAdventure(req: Request, res: Response): Promise<void> {
|
| 101 |
+
console.log('--- Start createAdventure Controller ---');
|
| 102 |
+
try {
|
| 103 |
+
const { user_id, name, description, geojson, properties, recorded_at } = req.body;
|
| 104 |
+
|
| 105 |
+
console.log(`Attempting to create adventure: Name='${name}', User ID='${user_id}'.`);
|
| 106 |
+
|
| 107 |
+
if (!geojson || !geojson.geometry || !geojson.geometry.coordinates) {
|
| 108 |
+
console.error('Validation Error: Invalid or missing GeoJSON in request body.');
|
| 109 |
+
res.status(400).json({ error: 'Invalid GeoJSON' });
|
| 110 |
+
return;
|
| 111 |
+
}
|
| 112 |
+
console.log('GeoJSON structure validated.');
|
| 113 |
+
|
| 114 |
+
const adventureData = {
|
| 115 |
+
user_id,
|
| 116 |
+
name,
|
| 117 |
+
description,
|
| 118 |
+
path: geojson.geometry,
|
| 119 |
+
properties: geojson.properties || properties,
|
| 120 |
+
recorded_at: recorded_at ? new Date(recorded_at) : new Date()
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
const id = await AdventureModel.create(adventureData);
|
| 124 |
+
console.log(`Adventure created successfully with ID: ${id}`);
|
| 125 |
+
|
| 126 |
+
res.json({ success: true, id });
|
| 127 |
+
console.log('--- End createAdventure Controller (Success) ---');
|
| 128 |
+
} catch (err) {
|
| 129 |
+
console.error('Error in createAdventure:', err);
|
| 130 |
+
res.status(500).json({ error: 'Server Error' });
|
| 131 |
+
console.log('--- End createAdventure Controller (Error) ---');
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/**
|
| 136 |
+
* DELETE /api/adventures/:id
|
| 137 |
+
* Delete an adventure by ID
|
| 138 |
+
*/
|
| 139 |
+
static async deleteAdventure(req: Request, res: Response): Promise<void> {
|
| 140 |
+
console.log('--- Start deleteAdventure Controller ---');
|
| 141 |
+
try {
|
| 142 |
+
const id = parseInt(req.params.id);
|
| 143 |
+
console.log(`Attempting to delete adventure with ID: ${id}`);
|
| 144 |
+
|
| 145 |
+
const success = await AdventureModel.delete(id);
|
| 146 |
+
|
| 147 |
+
if (!success) {
|
| 148 |
+
console.log(`Adventure with ID ${id} not found or already deleted.`);
|
| 149 |
+
res.status(404).json({ error: 'Adventure not found' });
|
| 150 |
+
return;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
console.log(`Adventure with ID ${id} deleted successfully.`);
|
| 154 |
+
res.json({ success: true });
|
| 155 |
+
console.log('--- End deleteAdventure Controller (Success) ---');
|
| 156 |
+
} catch (err) {
|
| 157 |
+
console.error('Error in deleteAdventure:', err);
|
| 158 |
+
res.status(500).json({ error: 'Server Error' });
|
| 159 |
+
console.log('--- End deleteAdventure Controller (Error) ---');
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
src/controllers/trackController.ts
CHANGED
|
@@ -1,28 +1,39 @@
|
|
| 1 |
import { Request, Response } from 'express';
|
| 2 |
-
import { getDistanceFromLatLonInKm } from '../utils/geoUtils';
|
| 3 |
import { TrackModel } from '../models/TrackModel';
|
|
|
|
|
|
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
export class TrackController {
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
console.log('--- Start getTracks Controller ---');
|
| 8 |
try {
|
| 9 |
const { lat, lng, radius } = req.query;
|
| 10 |
console.log(`Received query parameters: lat=${lat}, lng=${lng}, radius=${radius}`);
|
| 11 |
|
| 12 |
-
const
|
| 13 |
-
console.log(`Fetched ${
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
return {
|
| 17 |
type: 'Feature',
|
| 18 |
-
geometry: typeof
|
|
|
|
|
|
|
| 19 |
properties: {
|
| 20 |
-
id:
|
| 21 |
-
name:
|
| 22 |
-
description:
|
| 23 |
-
...
|
| 24 |
-
difficulty:
|
| 25 |
-
type:
|
| 26 |
}
|
| 27 |
};
|
| 28 |
});
|
|
@@ -39,18 +50,16 @@ export class TrackController {
|
|
| 39 |
const initialFeatureCount = features.length;
|
| 40 |
features = features.filter(feature => {
|
| 41 |
if (!feature.geometry || !feature.geometry.coordinates || feature.geometry.coordinates.length === 0) {
|
| 42 |
-
// console.log(`Skipping feature without coordinates.`); // Use sparingly, can be noisy
|
| 43 |
return false;
|
| 44 |
}
|
| 45 |
|
| 46 |
-
// Use the first point of the
|
| 47 |
// GeoJSON coordinates are [lng, lat]
|
| 48 |
-
const
|
| 49 |
-
const
|
| 50 |
-
const
|
| 51 |
|
| 52 |
-
const dist = getDistanceFromLatLonInKm(centerLat, centerLng,
|
| 53 |
-
// console.log(`Feature distance: ${dist.toFixed(2)} km.`); // Use sparingly, can be noisy
|
| 54 |
return dist <= radiusKm;
|
| 55 |
});
|
| 56 |
console.log(`Features remaining after location filter: ${features.length}. Filtered out ${initialFeatureCount - features.length} features.`);
|
|
@@ -67,11 +76,10 @@ export class TrackController {
|
|
| 67 |
console.log(`Slicing features from index ${startIndex} to ${endIndex}.`);
|
| 68 |
|
| 69 |
const paginatedFeatures = features.slice(startIndex, endIndex);
|
| 70 |
-
|
| 71 |
const totalPages = Math.ceil(features.length / limit);
|
| 72 |
console.log(`Response features count: ${paginatedFeatures.length}. Total pages: ${totalPages}.`);
|
| 73 |
|
| 74 |
-
|
| 75 |
type: 'FeatureCollection',
|
| 76 |
features: paginatedFeatures,
|
| 77 |
pagination: {
|
|
@@ -80,54 +88,56 @@ export class TrackController {
|
|
| 80 |
limit,
|
| 81 |
pages: totalPages
|
| 82 |
}
|
| 83 |
-
}
|
|
|
|
|
|
|
| 84 |
console.log('--- End getTracks Controller (Success) ---');
|
| 85 |
} catch (err) {
|
| 86 |
console.error('Error in getTracks:', err);
|
| 87 |
-
res.status(500).
|
| 88 |
console.log('--- End getTracks Controller (Error) ---');
|
| 89 |
}
|
| 90 |
}
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
type = geojson.properties['piste:type'];
|
| 102 |
-
}
|
| 103 |
|
| 104 |
-
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
};
|
| 120 |
-
// console.log('Data to be created:', trackData); // Use this for debugging the full payload
|
| 121 |
-
|
| 122 |
-
const id = await TrackModel.create(trackData);
|
| 123 |
-
console.log(`Track created successfully with ID: ${id}`);
|
| 124 |
|
| 125 |
-
res.json(
|
| 126 |
-
console.log('--- End
|
| 127 |
} catch (err) {
|
| 128 |
-
console.error('Error in
|
| 129 |
-
res.status(500).
|
| 130 |
-
console.log('--- End
|
| 131 |
}
|
| 132 |
}
|
| 133 |
-
}
|
|
|
|
| 1 |
import { Request, Response } from 'express';
|
|
|
|
| 2 |
import { TrackModel } from '../models/TrackModel';
|
| 3 |
+
import { TrackFeature, TrackFeatureCollection } from '../types/types';
|
| 4 |
+
import { getDistanceFromLatLonInKm } from '../utils/geoUtils';
|
| 5 |
|
| 6 |
+
/**
|
| 7 |
+
* TrackController - Handles HTTP requests for public ski tracks
|
| 8 |
+
*/
|
| 9 |
export class TrackController {
|
| 10 |
+
/**
|
| 11 |
+
* GET /api/tracks
|
| 12 |
+
* Fetch all public tracks with optional location filtering and pagination
|
| 13 |
+
*/
|
| 14 |
+
static async getTracks(req: Request, res: Response): Promise<void> {
|
| 15 |
console.log('--- Start getTracks Controller ---');
|
| 16 |
try {
|
| 17 |
const { lat, lng, radius } = req.query;
|
| 18 |
console.log(`Received query parameters: lat=${lat}, lng=${lng}, radius=${radius}`);
|
| 19 |
|
| 20 |
+
const tracks = await TrackModel.findAll();
|
| 21 |
+
console.log(`Fetched ${tracks.length} tracks from the database.`);
|
| 22 |
|
| 23 |
+
// Convert tracks to GeoJSON features
|
| 24 |
+
let features: TrackFeature[] = tracks.map(track => {
|
| 25 |
return {
|
| 26 |
type: 'Feature',
|
| 27 |
+
geometry: typeof track.geojson === 'string'
|
| 28 |
+
? JSON.parse(track.geojson)
|
| 29 |
+
: track.geojson,
|
| 30 |
properties: {
|
| 31 |
+
id: track.id,
|
| 32 |
+
name: track.name,
|
| 33 |
+
description: track.description,
|
| 34 |
+
...track.properties,
|
| 35 |
+
difficulty: track.difficulty,
|
| 36 |
+
type: track.type
|
| 37 |
}
|
| 38 |
};
|
| 39 |
});
|
|
|
|
| 50 |
const initialFeatureCount = features.length;
|
| 51 |
features = features.filter(feature => {
|
| 52 |
if (!feature.geometry || !feature.geometry.coordinates || feature.geometry.coordinates.length === 0) {
|
|
|
|
| 53 |
return false;
|
| 54 |
}
|
| 55 |
|
| 56 |
+
// Use the first point of the track for distance check
|
| 57 |
// GeoJSON coordinates are [lng, lat]
|
| 58 |
+
const trackPoint = feature.geometry.coordinates[0];
|
| 59 |
+
const trackLng = trackPoint[0];
|
| 60 |
+
const trackLat = trackPoint[1];
|
| 61 |
|
| 62 |
+
const dist = getDistanceFromLatLonInKm(centerLat, centerLng, trackLat, trackLng);
|
|
|
|
| 63 |
return dist <= radiusKm;
|
| 64 |
});
|
| 65 |
console.log(`Features remaining after location filter: ${features.length}. Filtered out ${initialFeatureCount - features.length} features.`);
|
|
|
|
| 76 |
console.log(`Slicing features from index ${startIndex} to ${endIndex}.`);
|
| 77 |
|
| 78 |
const paginatedFeatures = features.slice(startIndex, endIndex);
|
|
|
|
| 79 |
const totalPages = Math.ceil(features.length / limit);
|
| 80 |
console.log(`Response features count: ${paginatedFeatures.length}. Total pages: ${totalPages}.`);
|
| 81 |
|
| 82 |
+
const response: TrackFeatureCollection & { pagination: any } = {
|
| 83 |
type: 'FeatureCollection',
|
| 84 |
features: paginatedFeatures,
|
| 85 |
pagination: {
|
|
|
|
| 88 |
limit,
|
| 89 |
pages: totalPages
|
| 90 |
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
res.json(response);
|
| 94 |
console.log('--- End getTracks Controller (Success) ---');
|
| 95 |
} catch (err) {
|
| 96 |
console.error('Error in getTracks:', err);
|
| 97 |
+
res.status(500).json({ error: 'Server Error' });
|
| 98 |
console.log('--- End getTracks Controller (Error) ---');
|
| 99 |
}
|
| 100 |
}
|
| 101 |
|
| 102 |
+
/**
|
| 103 |
+
* GET /api/tracks/:id
|
| 104 |
+
* Fetch a single track by ID
|
| 105 |
+
*/
|
| 106 |
+
static async getTrackById(req: Request, res: Response): Promise<void> {
|
| 107 |
+
console.log('--- Start getTrackById Controller ---');
|
| 108 |
+
try {
|
| 109 |
+
const id = parseInt(req.params.id);
|
| 110 |
+
console.log(`Fetching track with ID: ${id}`);
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
const track = await TrackModel.findById(id);
|
| 113 |
|
| 114 |
+
if (!track) {
|
| 115 |
+
console.log(`Track with ID ${id} not found.`);
|
| 116 |
+
res.status(404).json({ error: 'Track not found' });
|
| 117 |
+
return;
|
| 118 |
+
}
|
| 119 |
|
| 120 |
+
const feature: TrackFeature = {
|
| 121 |
+
type: 'Feature',
|
| 122 |
+
geometry: typeof track.geojson === 'string'
|
| 123 |
+
? JSON.parse(track.geojson)
|
| 124 |
+
: track.geojson,
|
| 125 |
+
properties: {
|
| 126 |
+
id: track.id,
|
| 127 |
+
name: track.name,
|
| 128 |
+
description: track.description,
|
| 129 |
+
...track.properties,
|
| 130 |
+
difficulty: track.difficulty,
|
| 131 |
+
type: track.type
|
| 132 |
+
}
|
| 133 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
res.json(feature);
|
| 136 |
+
console.log('--- End getTrackById Controller (Success) ---');
|
| 137 |
} catch (err) {
|
| 138 |
+
console.error('Error in getTrackById:', err);
|
| 139 |
+
res.status(500).json({ error: 'Server Error' });
|
| 140 |
+
console.log('--- End getTrackById Controller (Error) ---');
|
| 141 |
}
|
| 142 |
}
|
| 143 |
+
}
|
src/index.ts
CHANGED
|
@@ -2,6 +2,9 @@ import express from 'express';
|
|
| 2 |
import cors from 'cors';
|
| 3 |
import dotenv from 'dotenv';
|
| 4 |
import trackRoutes from './routes/trackRoutes';
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
dotenv.config();
|
| 7 |
|
|
@@ -11,14 +14,30 @@ const port = process.env.PORT || 3000;
|
|
| 11 |
app.use(cors());
|
| 12 |
app.use(express.json());
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
|
|
|
|
|
|
|
|
|
| 15 |
app.get('/api/health', (req, res) => {
|
| 16 |
res.status(200).send('OK');
|
| 17 |
});
|
| 18 |
|
| 19 |
-
//
|
| 20 |
app.use('/api/tracks', trackRoutes);
|
| 21 |
|
|
|
|
|
|
|
|
|
|
| 22 |
app.listen(port, () => {
|
| 23 |
console.log(`Server running on http://localhost:${port}`);
|
| 24 |
});
|
|
|
|
| 2 |
import cors from 'cors';
|
| 3 |
import dotenv from 'dotenv';
|
| 4 |
import trackRoutes from './routes/trackRoutes';
|
| 5 |
+
import adventureRoutes from './routes/adventureRoutes';
|
| 6 |
+
import { TrackModel } from './models/TrackModel';
|
| 7 |
+
import { AdventureModel } from './models/AdventureModel';
|
| 8 |
|
| 9 |
dotenv.config();
|
| 10 |
|
|
|
|
| 14 |
app.use(cors());
|
| 15 |
app.use(express.json());
|
| 16 |
|
| 17 |
+
// Initialize database tables
|
| 18 |
+
async function initializeDatabase() {
|
| 19 |
+
try {
|
| 20 |
+
await TrackModel.ensureTable();
|
| 21 |
+
await AdventureModel.ensureTable();
|
| 22 |
+
console.log('Database tables initialized successfully.');
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error('Error initializing database tables:', error);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
|
| 28 |
+
initializeDatabase();
|
| 29 |
+
|
| 30 |
+
// Health check endpoint
|
| 31 |
app.get('/api/health', (req, res) => {
|
| 32 |
res.status(200).send('OK');
|
| 33 |
});
|
| 34 |
|
| 35 |
+
// Public ski tracks endpoints
|
| 36 |
app.use('/api/tracks', trackRoutes);
|
| 37 |
|
| 38 |
+
// User adventures endpoints
|
| 39 |
+
app.use('/api/adventures', adventureRoutes);
|
| 40 |
+
|
| 41 |
app.listen(port, () => {
|
| 42 |
console.log(`Server running on http://localhost:${port}`);
|
| 43 |
});
|
src/models/AdventureModel.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sql from '../db';
|
| 2 |
+
import { Adventure } from '../types/types';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* AdventureModel - Handles database operations for user-saved adventures
|
| 6 |
+
* These are user-recorded activities that can be created, read, and deleted
|
| 7 |
+
*/
|
| 8 |
+
export class AdventureModel {
|
| 9 |
+
/**
|
| 10 |
+
* Find all adventures, optionally filtered by user ID
|
| 11 |
+
*/
|
| 12 |
+
static async findAll(userId?: string): Promise<Adventure[]> {
|
| 13 |
+
if (userId) {
|
| 14 |
+
const adventures = await sql<Adventure[]>`
|
| 15 |
+
SELECT
|
| 16 |
+
id,
|
| 17 |
+
user_id,
|
| 18 |
+
name,
|
| 19 |
+
description,
|
| 20 |
+
path,
|
| 21 |
+
properties,
|
| 22 |
+
recorded_at,
|
| 23 |
+
created_at
|
| 24 |
+
FROM adventures
|
| 25 |
+
WHERE user_id = ${userId}
|
| 26 |
+
ORDER BY created_at DESC
|
| 27 |
+
`;
|
| 28 |
+
return adventures;
|
| 29 |
+
} else {
|
| 30 |
+
const adventures = await sql<Adventure[]>`
|
| 31 |
+
SELECT
|
| 32 |
+
id,
|
| 33 |
+
user_id,
|
| 34 |
+
name,
|
| 35 |
+
description,
|
| 36 |
+
path,
|
| 37 |
+
properties,
|
| 38 |
+
recorded_at,
|
| 39 |
+
created_at
|
| 40 |
+
FROM adventures
|
| 41 |
+
ORDER BY created_at DESC
|
| 42 |
+
`;
|
| 43 |
+
return adventures;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Find a single adventure by ID
|
| 49 |
+
*/
|
| 50 |
+
static async findById(id: number): Promise<Adventure | null> {
|
| 51 |
+
const adventures = await sql<Adventure[]>`
|
| 52 |
+
SELECT
|
| 53 |
+
id,
|
| 54 |
+
user_id,
|
| 55 |
+
name,
|
| 56 |
+
description,
|
| 57 |
+
path,
|
| 58 |
+
properties,
|
| 59 |
+
recorded_at,
|
| 60 |
+
created_at
|
| 61 |
+
FROM adventures
|
| 62 |
+
WHERE id = ${id}
|
| 63 |
+
`;
|
| 64 |
+
return adventures.length > 0 ? adventures[0] : null;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Create a new adventure
|
| 69 |
+
*/
|
| 70 |
+
static async create(adventure: Partial<Adventure>): Promise<number> {
|
| 71 |
+
const {
|
| 72 |
+
user_id,
|
| 73 |
+
name,
|
| 74 |
+
description,
|
| 75 |
+
path,
|
| 76 |
+
properties,
|
| 77 |
+
recorded_at
|
| 78 |
+
} = adventure;
|
| 79 |
+
|
| 80 |
+
const result = await sql<{ id: number }[]>`
|
| 81 |
+
INSERT INTO adventures (
|
| 82 |
+
user_id,
|
| 83 |
+
name,
|
| 84 |
+
description,
|
| 85 |
+
path,
|
| 86 |
+
properties,
|
| 87 |
+
recorded_at
|
| 88 |
+
)
|
| 89 |
+
VALUES (
|
| 90 |
+
${user_id || null},
|
| 91 |
+
${name || 'Untitled Adventure'},
|
| 92 |
+
${description || null},
|
| 93 |
+
${JSON.stringify(path)},
|
| 94 |
+
${JSON.stringify(properties || {})},
|
| 95 |
+
${recorded_at || new Date()}
|
| 96 |
+
)
|
| 97 |
+
RETURNING id;
|
| 98 |
+
`;
|
| 99 |
+
return result[0].id;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Delete an adventure by ID
|
| 104 |
+
*/
|
| 105 |
+
static async delete(id: number): Promise<boolean> {
|
| 106 |
+
const result = await sql`
|
| 107 |
+
DELETE FROM adventures
|
| 108 |
+
WHERE id = ${id}
|
| 109 |
+
`;
|
| 110 |
+
return result.count > 0;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/**
|
| 114 |
+
* Ensure the adventures table exists with the correct schema
|
| 115 |
+
* This is called during initialization
|
| 116 |
+
*/
|
| 117 |
+
static async ensureTable(): Promise<void> {
|
| 118 |
+
await sql`
|
| 119 |
+
CREATE TABLE IF NOT EXISTS adventures (
|
| 120 |
+
id SERIAL PRIMARY KEY,
|
| 121 |
+
user_id VARCHAR(255),
|
| 122 |
+
name VARCHAR(255),
|
| 123 |
+
description TEXT,
|
| 124 |
+
path JSONB,
|
| 125 |
+
properties JSONB,
|
| 126 |
+
recorded_at TIMESTAMP,
|
| 127 |
+
created_at TIMESTAMP DEFAULT NOW()
|
| 128 |
+
);
|
| 129 |
+
`;
|
| 130 |
+
|
| 131 |
+
// Ensure index exists
|
| 132 |
+
try {
|
| 133 |
+
await sql`CREATE INDEX IF NOT EXISTS idx_adventures_user_id ON adventures(user_id);`;
|
| 134 |
+
} catch (e) {
|
| 135 |
+
// Ignore errors if index already exists
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
}
|
src/models/TrackModel.ts
CHANGED
|
@@ -1,55 +1,89 @@
|
|
| 1 |
import sql from '../db';
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
geojson: any;
|
| 8 |
-
properties: any;
|
| 9 |
-
difficulty: string;
|
| 10 |
-
type: string;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
export class TrackModel {
|
|
|
|
|
|
|
|
|
|
| 14 |
static async findAll(): Promise<Track[]> {
|
| 15 |
const tracks = await sql<Track[]>`
|
| 16 |
-
SELECT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
FROM routes
|
|
|
|
| 18 |
`;
|
| 19 |
return tracks;
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
await sql`
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
`;
|
| 38 |
|
| 39 |
-
//
|
| 40 |
try {
|
| 41 |
-
await sql`
|
| 42 |
-
await sql`
|
| 43 |
-
await sql`ALTER TABLE routes ADD COLUMN IF NOT EXISTS type VARCHAR(50);`;
|
| 44 |
} catch (e) {
|
| 45 |
-
// Ignore errors if
|
| 46 |
}
|
| 47 |
-
|
| 48 |
-
const result = await sql<{ id: number }[]>`
|
| 49 |
-
INSERT INTO routes (name, path, properties, difficulty, type)
|
| 50 |
-
VALUES (${name || 'Untitled Route'}, ${JSON.stringify(geojson)}, ${JSON.stringify(properties || {})}, ${difficulty || null}, ${type || null})
|
| 51 |
-
RETURNING id;
|
| 52 |
-
`;
|
| 53 |
-
return result[0].id;
|
| 54 |
}
|
| 55 |
}
|
|
|
|
| 1 |
import sql from '../db';
|
| 2 |
+
import { Track } from '../types/types';
|
| 3 |
|
| 4 |
+
/**
|
| 5 |
+
* TrackModel - Handles database operations for public ski tracks
|
| 6 |
+
* These are read-only tracks imported from OpenSkiData
|
| 7 |
+
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
export class TrackModel {
|
| 9 |
+
/**
|
| 10 |
+
* Find all tracks in the database
|
| 11 |
+
*/
|
| 12 |
static async findAll(): Promise<Track[]> {
|
| 13 |
const tracks = await sql<Track[]>`
|
| 14 |
+
SELECT
|
| 15 |
+
id,
|
| 16 |
+
name,
|
| 17 |
+
description,
|
| 18 |
+
path as geojson,
|
| 19 |
+
properties,
|
| 20 |
+
difficulty,
|
| 21 |
+
type,
|
| 22 |
+
created_at
|
| 23 |
FROM routes
|
| 24 |
+
ORDER BY created_at DESC
|
| 25 |
`;
|
| 26 |
return tracks;
|
| 27 |
}
|
| 28 |
|
| 29 |
+
/**
|
| 30 |
+
* Find a single track by ID
|
| 31 |
+
*/
|
| 32 |
+
static async findById(id: number): Promise<Track | null> {
|
| 33 |
+
const tracks = await sql<Track[]>`
|
| 34 |
+
SELECT
|
| 35 |
+
id,
|
| 36 |
+
name,
|
| 37 |
+
description,
|
| 38 |
+
path as geojson,
|
| 39 |
+
properties,
|
| 40 |
+
difficulty,
|
| 41 |
+
type,
|
| 42 |
+
created_at
|
| 43 |
+
FROM routes
|
| 44 |
+
WHERE id = ${id}
|
| 45 |
+
`;
|
| 46 |
+
return tracks.length > 0 ? tracks[0] : null;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Find tracks within a geographic radius
|
| 51 |
+
* Note: This is a simple implementation. For production, consider using PostGIS
|
| 52 |
+
*/
|
| 53 |
+
static async findByLocation(
|
| 54 |
+
lat: number,
|
| 55 |
+
lng: number,
|
| 56 |
+
radius: number
|
| 57 |
+
): Promise<Track[]> {
|
| 58 |
+
// For now, we'll fetch all tracks and filter in the controller
|
| 59 |
+
// In production, you'd want to use PostGIS for efficient spatial queries
|
| 60 |
+
return this.findAll();
|
| 61 |
+
}
|
| 62 |
|
| 63 |
+
/**
|
| 64 |
+
* Ensure the routes table exists with the correct schema
|
| 65 |
+
* This is called during initialization
|
| 66 |
+
*/
|
| 67 |
+
static async ensureTable(): Promise<void> {
|
| 68 |
await sql`
|
| 69 |
+
CREATE TABLE IF NOT EXISTS routes (
|
| 70 |
+
id SERIAL PRIMARY KEY,
|
| 71 |
+
name VARCHAR(255),
|
| 72 |
+
description TEXT,
|
| 73 |
+
path JSONB,
|
| 74 |
+
properties JSONB,
|
| 75 |
+
difficulty VARCHAR(50),
|
| 76 |
+
type VARCHAR(50),
|
| 77 |
+
created_at TIMESTAMP DEFAULT NOW()
|
| 78 |
+
);
|
| 79 |
`;
|
| 80 |
|
| 81 |
+
// Ensure indexes exist
|
| 82 |
try {
|
| 83 |
+
await sql`CREATE INDEX IF NOT EXISTS idx_routes_difficulty ON routes(difficulty);`;
|
| 84 |
+
await sql`CREATE INDEX IF NOT EXISTS idx_routes_type ON routes(type);`;
|
|
|
|
| 85 |
} catch (e) {
|
| 86 |
+
// Ignore errors if indexes already exist
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
| 89 |
}
|
src/routes/adventureRoutes.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from 'express';
|
| 2 |
+
import { AdventureController } from '../controllers/adventureController';
|
| 3 |
+
|
| 4 |
+
const router = Router();
|
| 5 |
+
|
| 6 |
+
// GET /api/adventures - Get all adventures (optionally filtered by user_id)
|
| 7 |
+
router.get('/', AdventureController.getAdventures);
|
| 8 |
+
|
| 9 |
+
// GET /api/adventures/:id - Get a specific adventure by ID
|
| 10 |
+
router.get('/:id', AdventureController.getAdventureById);
|
| 11 |
+
|
| 12 |
+
// POST /api/adventures - Create a new adventure
|
| 13 |
+
router.post('/', AdventureController.createAdventure);
|
| 14 |
+
|
| 15 |
+
// DELETE /api/adventures/:id - Delete an adventure
|
| 16 |
+
router.delete('/:id', AdventureController.deleteAdventure);
|
| 17 |
+
|
| 18 |
+
export default router;
|
src/routes/trackRoutes.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { TrackController } from '../controllers/trackController';
|
|
| 3 |
|
| 4 |
const router = Router();
|
| 5 |
|
| 6 |
-
|
| 7 |
-
router.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
export default router;
|
|
|
|
| 3 |
|
| 4 |
const router = Router();
|
| 5 |
|
| 6 |
+
// GET /api/tracks - Get all tracks with optional location filtering
|
| 7 |
+
router.get('/', TrackController.getTracks);
|
| 8 |
+
|
| 9 |
+
// GET /api/tracks/:id - Get a specific track by ID
|
| 10 |
+
router.get('/:id', TrackController.getTrackById);
|
| 11 |
|
| 12 |
export default router;
|
src/seed.ts
CHANGED
|
@@ -38,24 +38,318 @@ const routes = [
|
|
| 38 |
}
|
| 39 |
];
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
async function seed() {
|
| 42 |
try {
|
| 43 |
console.log('Seeding database...');
|
| 44 |
|
| 45 |
-
// Ensure table exists
|
| 46 |
-
// Using JSONB for path to avoid PostGIS dependency issues
|
| 47 |
await sql`
|
| 48 |
CREATE TABLE IF NOT EXISTS routes (
|
| 49 |
id SERIAL PRIMARY KEY,
|
| 50 |
name VARCHAR(255),
|
| 51 |
description TEXT,
|
| 52 |
-
path JSONB,
|
|
|
|
|
|
|
|
|
|
| 53 |
created_at TIMESTAMP DEFAULT NOW()
|
| 54 |
);
|
| 55 |
`;
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
for (const route of routes) {
|
| 58 |
-
// Construct GeoJSON LineString
|
| 59 |
const geojson = {
|
| 60 |
type: 'LineString',
|
| 61 |
coordinates: route.coordinates
|
|
@@ -65,7 +359,33 @@ async function seed() {
|
|
| 65 |
INSERT INTO routes (name, description, path)
|
| 66 |
VALUES (${route.name}, ${route.description}, ${JSON.stringify(geojson)})
|
| 67 |
`;
|
| 68 |
-
console.log(`Inserted: ${route.name}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
console.log('Seeding complete!');
|
|
|
|
| 38 |
}
|
| 39 |
];
|
| 40 |
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
const adventures = [
|
| 44 |
+
{
|
| 45 |
+
name: 'Route Travail MBC',
|
| 46 |
+
description: 'Morning commute route to MBC',
|
| 47 |
+
user_id: 'demo_user',
|
| 48 |
+
difficulty: 'easy',
|
| 49 |
+
type: 'downhill',
|
| 50 |
+
coordinates: [
|
| 51 |
+
[6.8697427, 45.9272432, 1096.9000244140625],
|
| 52 |
+
[6.8698024, 45.9271583, 1096.9000244140625],
|
| 53 |
+
[6.8699343, 45.9271738, 1096.699951171875],
|
| 54 |
+
[6.8700793, 45.9271864, 1095.5999755859375],
|
| 55 |
+
[6.8701594, 45.9272616, 1095.5999755859375],
|
| 56 |
+
[6.8701511, 45.9273519, 1095.5999755859375],
|
| 57 |
+
[6.8701413, 45.9274425, 1095.5999755859375],
|
| 58 |
+
[6.8701461, 45.9275353, 1095.5999755859375],
|
| 59 |
+
[6.8701785, 45.9276224, 1095.5999755859375],
|
| 60 |
+
[6.8701915, 45.9277173, 1096.5999755859375],
|
| 61 |
+
[6.8702202, 45.9278074, 1096.5999755859375],
|
| 62 |
+
[6.8702697, 45.9278918, 1096.5999755859375],
|
| 63 |
+
[6.8703351, 45.9279698, 1096.5999755859375],
|
| 64 |
+
[6.8703026, 45.9280589, 1096.5999755859375],
|
| 65 |
+
[6.8703311, 45.9281596, 1098.300048828125],
|
| 66 |
+
[6.870453, 45.9281889, 1098.300048828125],
|
| 67 |
+
[6.870603, 45.9281428, 1098.300048828125],
|
| 68 |
+
[6.8707213, 45.9281017, 1098.300048828125],
|
| 69 |
+
[6.8708495, 45.9280713, 1098.300048828125],
|
| 70 |
+
[6.8709702, 45.9280302, 1098.300048828125],
|
| 71 |
+
[6.8710862, 45.9279821, 1098],
|
| 72 |
+
[6.8711965, 45.9279322, 1096.0960190128365],
|
| 73 |
+
[6.8713172, 45.927898, 1094],
|
| 74 |
+
[6.8714509, 45.9278981, 1094],
|
| 75 |
+
[6.8715628, 45.9278499, 1094],
|
| 76 |
+
[6.8716834, 45.9278169, 1094],
|
| 77 |
+
[6.8718103, 45.9277842, 1094],
|
| 78 |
+
[6.8719373, 45.9277579, 1094],
|
| 79 |
+
[6.8720694, 45.9277417, 1094],
|
| 80 |
+
[6.8722059, 45.9277352, 1094],
|
| 81 |
+
[6.8723404, 45.927748, 1093.300048828125],
|
| 82 |
+
[6.8724694, 45.9277737, 1093.300048828125],
|
| 83 |
+
[6.8725959, 45.9278143, 1093.300048828125],
|
| 84 |
+
[6.8727277, 45.9278438, 1093.300048828125],
|
| 85 |
+
[6.8728491, 45.9278807, 1093.300048828125],
|
| 86 |
+
[6.8729863, 45.92789, 1093.300048828125],
|
| 87 |
+
[6.8731123, 45.9278689, 1093.300048828125],
|
| 88 |
+
[6.8732376, 45.9278427, 1093.300048828125],
|
| 89 |
+
[6.8733375, 45.9277791, 1093.300048828125],
|
| 90 |
+
[6.8734157, 45.9277013, 1093.300048828125],
|
| 91 |
+
[6.8734976, 45.9276261, 1093.3355591062466],
|
| 92 |
+
[6.8735919, 45.9275631, 1092.9000244140625],
|
| 93 |
+
[6.8736965, 45.9275096, 1093.300048828125],
|
| 94 |
+
[6.8738264, 45.9274944, 1093.300048828125],
|
| 95 |
+
[6.8739655, 45.9274901, 1091.800048828125],
|
| 96 |
+
[6.8740987, 45.9274969, 1091.800048828125],
|
| 97 |
+
[6.8742208, 45.9274648, 1091.800048828125],
|
| 98 |
+
[6.874343, 45.9274361, 1091.800048828125],
|
| 99 |
+
[6.8744692, 45.9274062, 1091.800048828125],
|
| 100 |
+
[6.874587, 45.9273607, 1091.800048828125],
|
| 101 |
+
[6.8747058, 45.9273197, 1091.800048828125],
|
| 102 |
+
[6.8748116, 45.9272661, 1091.800048828125],
|
| 103 |
+
[6.8749095, 45.9272054, 1091.800048828125],
|
| 104 |
+
[6.8750177, 45.927156, 1091.800048828125],
|
| 105 |
+
[6.8751244, 45.9270982, 1091.800048828125],
|
| 106 |
+
[6.8752579, 45.9271073, 1091.800048828125],
|
| 107 |
+
[6.8753906, 45.9271014, 1091.800048828125],
|
| 108 |
+
[6.8754794, 45.9271773, 1091.800048828125],
|
| 109 |
+
[6.8755995, 45.9272159, 1091.800048828125],
|
| 110 |
+
[6.8757281, 45.9272475, 1091.800048828125],
|
| 111 |
+
[6.8757815, 45.9273313, 1091.800048828125],
|
| 112 |
+
[6.8758923, 45.9273866, 1091.800048828125],
|
| 113 |
+
[6.8759039, 45.92748, 1091.800048828125]
|
| 114 |
+
]
|
| 115 |
+
},
|
| 116 |
+
|
| 117 |
+
// "{\"type\":\"LineString\",\"coordinates\":[]}"
|
| 118 |
+
{
|
| 119 |
+
name: 'Randonnée de la Cascade du Dard',
|
| 120 |
+
description: 'Description de la Randonnée de la Cascade du Dard',
|
| 121 |
+
user_id: 'demo_user',
|
| 122 |
+
difficulty: 'easy',
|
| 123 |
+
type: 'hike',
|
| 124 |
+
coordinates: [
|
| 125 |
+
[6.8688945, 45.9157594, 1081.059146391384],
|
| 126 |
+
[6.8687993, 45.9156871, 1081.1997572270068],
|
| 127 |
+
[6.8686657, 45.9156825, 1081.4036592284929],
|
| 128 |
+
[6.8685408, 45.915629, 1082.397348537824],
|
| 129 |
+
[6.8684442, 45.9155681, 1082.9569458509607],
|
| 130 |
+
[6.8683146, 45.9155085, 1083.8154532303688],
|
| 131 |
+
[6.8682309, 45.9154362, 1084.3153061443656],
|
| 132 |
+
[6.8681209, 45.9153795, 1084.983486345155],
|
| 133 |
+
[6.8680333, 45.9153135, 1085.5151355257856],
|
| 134 |
+
[6.8679594, 45.9152368, 1086.0080363904394],
|
| 135 |
+
[6.8678467, 45.9151887, 1086.3440317833956],
|
| 136 |
+
[6.8677568, 45.9151204, 1087.154687009319],
|
| 137 |
+
[6.8676522, 45.9150627, 1088.5409838046298],
|
| 138 |
+
[6.867564, 45.9149893, 1087.9338532751465],
|
| 139 |
+
[6.8674663, 45.9149247, 1090.3846031523801],
|
| 140 |
+
[6.8673608, 45.9148593, 1090.0214557762363],
|
| 141 |
+
[6.8672689, 45.9147919, 1090.1862308992443],
|
| 142 |
+
[6.8671972, 45.9147122, 1090.7392181778926],
|
| 143 |
+
[6.8671259, 45.9146338, 1091.1668282557905],
|
| 144 |
+
[6.8670452, 45.914554, 1090.609045875886],
|
| 145 |
+
[6.8669413, 45.9144926, 1093.199951171875],
|
| 146 |
+
[6.86684, 45.9144293, 1093.5907640714677],
|
| 147 |
+
[6.8667362, 45.9143721, 1092.9802924763503],
|
| 148 |
+
[6.8666481, 45.9142939, 1092.121907866093],
|
| 149 |
+
[6.8665762, 45.9142181, 1092.5488095753228],
|
| 150 |
+
[6.8664866, 45.914145, 1092.8568225420609],
|
| 151 |
+
[6.8664007, 45.914077, 1093.1947403656488],
|
| 152 |
+
[6.8663124, 45.9140028, 1093.674653426672],
|
| 153 |
+
[6.8662081, 45.9139397, 1094.0948209539697],
|
| 154 |
+
[6.8661324, 45.913864, 1094.2890337348153],
|
| 155 |
+
[6.8660427, 45.913796, 1094.758338629102],
|
| 156 |
+
[6.8659641, 45.9137181, 1095.0348936996504],
|
| 157 |
+
[6.8658782, 45.9136447, 1095.4351044545836],
|
| 158 |
+
[6.8657867, 45.9135785, 1095.6289202683129],
|
| 159 |
+
[6.8657001, 45.9135039, 1096.0313616020155],
|
| 160 |
+
[6.8655878, 45.9134521, 1096.5490667514152],
|
| 161 |
+
[6.8654814, 45.9133896, 1096.8325498419586],
|
| 162 |
+
[6.865402, 45.9133117, 1097.3507996690475],
|
| 163 |
+
[6.8653074, 45.9132487, 1096.699951171875],
|
| 164 |
+
[6.8652045, 45.9131939, 1096.0999755859375],
|
| 165 |
+
[6.865116, 45.9131211, 1096.0999755859375],
|
| 166 |
+
[6.8650193, 45.9130553, 1096.0999755859375],
|
| 167 |
+
[6.8648857, 45.913052, 1096.699951171875],
|
| 168 |
+
[6.8647727, 45.9129978, 1096.699951171875],
|
| 169 |
+
[6.8646638, 45.9129417, 1097.0999755859375],
|
| 170 |
+
[6.8645559, 45.9128899, 1097.0999755859375],
|
| 171 |
+
[6.8644565, 45.9128277, 1097.0999755859375],
|
| 172 |
+
[6.8643389, 45.9127878, 1097.0999755859375],
|
| 173 |
+
[6.8642209, 45.9127485, 1097.0999755859375],
|
| 174 |
+
[6.864123, 45.9126859, 1097.0999755859375],
|
| 175 |
+
[6.8640082, 45.9126296, 1097.6987271229448],
|
| 176 |
+
[6.863883, 45.9125935, 1098.0380381656614],
|
| 177 |
+
[6.8638106, 45.912516, 1098.277248683673],
|
| 178 |
+
[6.8636812, 45.9125005, 1098.400975910045],
|
| 179 |
+
[6.8635572, 45.9124745, 1098.79533507723],
|
| 180 |
+
[6.8634366, 45.9124345, 1099.3355886400816],
|
| 181 |
+
[6.8633764, 45.9123533, 1098.94887663412],
|
| 182 |
+
[6.8634471, 45.9122519, 1099.334465347278],
|
| 183 |
+
[6.8634767, 45.9121629, 1099.9805819384444],
|
| 184 |
+
[6.8634276, 45.9120719, 1100.5168661046698],
|
| 185 |
+
[6.863442, 45.9119818, 1101.6319535351283],
|
| 186 |
+
[6.8634325, 45.9118896, 1102.2782625400798],
|
| 187 |
+
[6.8634589, 45.9117891, 1103.0334235043022],
|
| 188 |
+
[6.8634216, 45.9117027, 1103.9590501350813],
|
| 189 |
+
[6.8634198, 45.911608, 1104.8744936324656],
|
| 190 |
+
[6.8633532, 45.9115277, 1105.913546469489],
|
| 191 |
+
[6.8632231, 45.9114953, 1106.2592795372645],
|
| 192 |
+
[6.8631097, 45.9114485, 1106.7834400953755],
|
| 193 |
+
[6.862974, 45.9114356, 1107.448088712898],
|
| 194 |
+
[6.8629228, 45.9113475, 1108.7405101272748],
|
| 195 |
+
[6.8629741, 45.9112612, 1109.1600875213167],
|
| 196 |
+
[6.8630551, 45.9111893, 1109.8827013750902],
|
| 197 |
+
[6.863079, 45.9110941, 1111.2529379732653],
|
| 198 |
+
[6.8630283, 45.9110053, 1111.7883312056974],
|
| 199 |
+
[6.8629303, 45.9109357, 1112.7518912824064],
|
| 200 |
+
[6.8629026, 45.9108428, 1113.3934997681822],
|
| 201 |
+
[6.862939, 45.9107529, 1115.2904673708558],
|
| 202 |
+
[6.863008, 45.9106768, 1116.6572863838849],
|
| 203 |
+
[6.8630739, 45.9105929, 1117.779316663176],
|
| 204 |
+
[6.8630785, 45.9104989, 1118.367700587035],
|
| 205 |
+
[6.8632093, 45.9104501, 1122.7076367556194],
|
| 206 |
+
[6.8633322, 45.9104084, 1121.5885337942984],
|
| 207 |
+
[6.8634663, 45.9103844, 1121.6266247101298],
|
| 208 |
+
[6.8635911, 45.9103537, 1121.6198424705285],
|
| 209 |
+
[6.8637162, 45.9103179, 1121.795843789446],
|
| 210 |
+
[6.8638181, 45.9102568, 1121.9559551414527],
|
| 211 |
+
[6.8639298, 45.9102118, 1122.3771535576504],
|
| 212 |
+
[6.8640544, 45.910243, 1123.8156980438316],
|
| 213 |
+
[6.8641801, 45.9102711, 1125.304478912378],
|
| 214 |
+
[6.8642974, 45.9103199, 1127.2307005231908],
|
| 215 |
+
[6.864232, 45.9102389, 1131.0732677839962],
|
| 216 |
+
[6.8641727, 45.9101559, 1132.6865105632735],
|
| 217 |
+
[6.8641226, 45.9100723, 1134.0755281545366],
|
| 218 |
+
[6.8640815, 45.9099808, 1135.6915737477063],
|
| 219 |
+
[6.8640691, 45.9098891, 1138.1132282046071],
|
| 220 |
+
[6.8640031, 45.9098075, 1139.4874002759907],
|
| 221 |
+
[6.8638927, 45.9097522, 1141.2421705191425],
|
| 222 |
+
[6.863855, 45.9096625, 1143.1267834767593],
|
| 223 |
+
[6.8638765, 45.909572, 1144.0621933953391],
|
| 224 |
+
[6.8637847, 45.9094991, 1145.534466324627],
|
| 225 |
+
[6.8637039, 45.9094266, 1146.9916315229584],
|
| 226 |
+
[6.8636423, 45.9093448, 1147.9774530165907],
|
| 227 |
+
[6.8636167, 45.9092531, 1149.3156147425884],
|
| 228 |
+
[6.8636213, 45.9091554, 1151.4259209197805],
|
| 229 |
+
[6.8636006, 45.9090619, 1151.5267790048365],
|
| 230 |
+
[6.8635101, 45.9089932, 1151.74358575613],
|
| 231 |
+
[6.8634007, 45.9089375, 1152.2040609014698],
|
| 232 |
+
[6.8632593, 45.9089195, 1153.1528849220538],
|
| 233 |
+
[6.8631346, 45.9088902, 1153.0049541366707],
|
| 234 |
+
[6.863087, 45.9088032, 1152.7995268045836],
|
| 235 |
+
[6.8630824, 45.9087124, 1156.3446774600488],
|
| 236 |
+
[6.8630435, 45.9086252, 1158.922561695732],
|
| 237 |
+
[6.862957, 45.9085575, 1160.7090136078941],
|
| 238 |
+
[6.8628931, 45.9084714, 1163.0505113687307],
|
| 239 |
+
[6.8628848, 45.9083805, 1163.98397951416],
|
| 240 |
+
[6.8628238, 45.9083011, 1168.4292803700644],
|
| 241 |
+
[6.8627901, 45.9082069, 1169.900747794902],
|
| 242 |
+
[6.8627282, 45.9081252, 1174.2165031558236],
|
| 243 |
+
[6.862606, 45.9080938, 1176.0866616705248],
|
| 244 |
+
[6.8626716, 45.9080144, 1181.0728643104667],
|
| 245 |
+
[6.8626353, 45.9079268, 1182.1515601443607],
|
| 246 |
+
[6.8626268, 45.9078338, 1184.2375701159237],
|
| 247 |
+
[6.8625365, 45.9077625, 1186.4635136967004],
|
| 248 |
+
[6.862562, 45.9076679, 1189.9840597701184],
|
| 249 |
+
[6.8626656, 45.9076078, 1192.171128467262],
|
| 250 |
+
[6.8626633, 45.9075163, 1194.7407712262575],
|
| 251 |
+
[6.8627494, 45.9074424, 1196.356781211603],
|
| 252 |
+
[6.862771, 45.9073485, 1198.1916958320223],
|
| 253 |
+
[6.8626417, 45.9073265, 1198.2290803715518],
|
| 254 |
+
[6.8627017, 45.9074159, 1199.8748649578085],
|
| 255 |
+
[6.8626896, 45.9073251, 1199.511779286203],
|
| 256 |
+
[6.8626972, 45.9072322, 1201.9547563762574],
|
| 257 |
+
[6.862794, 45.907168, 1203.992082426708],
|
| 258 |
+
[6.8628181, 45.9070732, 1205.9514560626897],
|
| 259 |
+
[6.8627303, 45.9070002, 1209.3273718156995],
|
| 260 |
+
[6.8627545, 45.9069107, 1214.228093208831],
|
| 261 |
+
[6.8627555, 45.9068174, 1215.2585690683013],
|
| 262 |
+
[6.8627467, 45.9067199, 1214.8421159057605],
|
| 263 |
+
[6.8627419, 45.9066261, 1215.5552482700823],
|
| 264 |
+
[6.862723, 45.9065343, 1217.4668126854197],
|
| 265 |
+
[6.8626076, 45.9064859, 1219.4525534519125],
|
| 266 |
+
[6.8626105, 45.9063942, 1223.8877903384719],
|
| 267 |
+
[6.8626526, 45.9063049, 1226.4555866753378],
|
| 268 |
+
[6.8626347, 45.9062097, 1227.2012379532093],
|
| 269 |
+
[6.8626134, 45.9061202, 1227.1461595601897],
|
| 270 |
+
[6.8626837, 45.9060431, 1226.6809385798776],
|
| 271 |
+
[6.8627113, 45.9059514, 1220.5967582426733],
|
| 272 |
+
[6.86265, 45.9058654, 1221.779321839327],
|
| 273 |
+
[6.8625651, 45.9057914, 1226.8725652261244],
|
| 274 |
+
[6.8624669, 45.9057203, 1221.1619895603048],
|
| 275 |
+
[6.8623723, 45.9056485, 1222.0263371758126],
|
| 276 |
+
[6.8622728, 45.9055883, 1221.2221721751157],
|
| 277 |
+
[6.8621356, 45.9055845, 1220.1621979392714],
|
| 278 |
+
[6.8620212, 45.9055336, 1221.800269795361],
|
| 279 |
+
[6.8620631, 45.9054442, 1220.9568260154147],
|
| 280 |
+
[6.8619662, 45.9053766, 1225.19496822729],
|
| 281 |
+
[6.8618778, 45.9053105, 1225.7003474802566],
|
| 282 |
+
[6.861782, 45.9052426, 1226.1530111518266],
|
| 283 |
+
[6.8616612, 45.9052034, 1229.8937884787902],
|
| 284 |
+
[6.8615896, 45.905125, 1229.3889469829403],
|
| 285 |
+
[6.8615994, 45.9050318, 1231.618894736775],
|
| 286 |
+
[6.8615719, 45.9049426, 1235.0859446590077],
|
| 287 |
+
[6.8616801, 45.9048922, 1237.1585239376057],
|
| 288 |
+
[6.8617116, 45.9048014, 1238.2270198005094],
|
| 289 |
+
[6.861662, 45.9047132, 1239.81473378018],
|
| 290 |
+
[6.8616137, 45.9046244, 1243.4683243747838],
|
| 291 |
+
[6.861738, 45.9045886, 1247.4833257883215],
|
| 292 |
+
[6.861705, 45.9045015, 1250.6797443738924],
|
| 293 |
+
[6.8616792, 45.9044097, 1254.7991857101283],
|
| 294 |
+
[6.8618086, 45.9044022, 1259.7396283686414],
|
| 295 |
+
[6.8618767, 45.9043243, 1262.4694378570173],
|
| 296 |
+
[6.8619747, 45.9042654, 1261.5151116779657],
|
| 297 |
+
[6.8620653, 45.9041974, 1260.1425948382714],
|
| 298 |
+
[6.8619417, 45.904165, 1258.667735078068],
|
| 299 |
+
[6.8618501, 45.9040928, 1260.6551140242807],
|
| 300 |
+
[6.861811, 45.9040069, 1260.5299769306173],
|
| 301 |
+
[6.8617482, 45.9039249, 1261.059676443075],
|
| 302 |
+
[6.8618414, 45.9038609, 1262.4840879799542],
|
| 303 |
+
[6.8619723, 45.9038811, 1264.5966908674864],
|
| 304 |
+
[6.8619848, 45.9037911, 1266.1419472398497],
|
| 305 |
+
[6.8618724, 45.9037423, 1268.511885808514],
|
| 306 |
+
[6.8617832, 45.903672, 1271.5656901712935],
|
| 307 |
+
[6.8619164, 45.9036769, 1272.6906004317634],
|
| 308 |
+
[6.8620513, 45.9036957, 1274.755771966626],
|
| 309 |
+
[6.8621612, 45.9036392, 1277.0647415209692],
|
| 310 |
+
[6.8622299, 45.9037197, 1278.9232369875976],
|
| 311 |
+
[6.8623604, 45.9037063, 1275.9682320319018],
|
| 312 |
+
[6.8624479, 45.9036397, 1273.42321468037],
|
| 313 |
+
[6.8625808, 45.9036189, 1272.6875375696866],
|
| 314 |
+
[6.8625842, 45.903713, 1273.2155339369829]
|
| 315 |
+
]
|
| 316 |
+
}
|
| 317 |
+
];
|
| 318 |
+
|
| 319 |
async function seed() {
|
| 320 |
try {
|
| 321 |
console.log('Seeding database...');
|
| 322 |
|
| 323 |
+
// Ensure routes table exists
|
|
|
|
| 324 |
await sql`
|
| 325 |
CREATE TABLE IF NOT EXISTS routes (
|
| 326 |
id SERIAL PRIMARY KEY,
|
| 327 |
name VARCHAR(255),
|
| 328 |
description TEXT,
|
| 329 |
+
path JSONB,
|
| 330 |
+
properties JSONB,
|
| 331 |
+
difficulty VARCHAR(50),
|
| 332 |
+
type VARCHAR(50),
|
| 333 |
created_at TIMESTAMP DEFAULT NOW()
|
| 334 |
);
|
| 335 |
`;
|
| 336 |
|
| 337 |
+
// Ensure adventures table exists
|
| 338 |
+
await sql`
|
| 339 |
+
CREATE TABLE IF NOT EXISTS adventures (
|
| 340 |
+
id SERIAL PRIMARY KEY,
|
| 341 |
+
user_id VARCHAR(255),
|
| 342 |
+
name VARCHAR(255),
|
| 343 |
+
description TEXT,
|
| 344 |
+
path JSONB,
|
| 345 |
+
properties JSONB,
|
| 346 |
+
recorded_at TIMESTAMP,
|
| 347 |
+
created_at TIMESTAMP DEFAULT NOW()
|
| 348 |
+
);
|
| 349 |
+
`;
|
| 350 |
+
|
| 351 |
+
// Seed routes (public ski tracks)
|
| 352 |
for (const route of routes) {
|
|
|
|
| 353 |
const geojson = {
|
| 354 |
type: 'LineString',
|
| 355 |
coordinates: route.coordinates
|
|
|
|
| 359 |
INSERT INTO routes (name, description, path)
|
| 360 |
VALUES (${route.name}, ${route.description}, ${JSON.stringify(geojson)})
|
| 361 |
`;
|
| 362 |
+
console.log(`Inserted route: ${route.name}`);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
// Seed adventures (user-saved activities)
|
| 366 |
+
for (const adventure of adventures) {
|
| 367 |
+
const geojson = {
|
| 368 |
+
type: 'LineString',
|
| 369 |
+
coordinates: adventure.coordinates
|
| 370 |
+
};
|
| 371 |
+
|
| 372 |
+
const properties = {
|
| 373 |
+
difficulty: adventure.difficulty,
|
| 374 |
+
type: adventure.type
|
| 375 |
+
};
|
| 376 |
+
|
| 377 |
+
await sql`
|
| 378 |
+
INSERT INTO adventures (user_id, name, description, path, properties, recorded_at)
|
| 379 |
+
VALUES (
|
| 380 |
+
${adventure.user_id},
|
| 381 |
+
${adventure.name},
|
| 382 |
+
${adventure.description || null},
|
| 383 |
+
${JSON.stringify(geojson)},
|
| 384 |
+
${JSON.stringify(properties)},
|
| 385 |
+
${new Date()}
|
| 386 |
+
)
|
| 387 |
+
`;
|
| 388 |
+
console.log(`Inserted adventure: ${adventure.name}`);
|
| 389 |
}
|
| 390 |
|
| 391 |
console.log('Seeding complete!');
|
src/types/types.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared type definitions for Routes and Adventures
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
// ============================================================================
|
| 6 |
+
// GeoJSON Types
|
| 7 |
+
// ============================================================================
|
| 8 |
+
|
| 9 |
+
export interface GeoJSONPoint {
|
| 10 |
+
type: 'Point';
|
| 11 |
+
coordinates: [number, number]; // [longitude, latitude]
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export interface GeoJSONLineString {
|
| 15 |
+
type: 'LineString';
|
| 16 |
+
coordinates: Array<[number, number]>; // Array of [longitude, latitude]
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface GeoJSONPolygon {
|
| 20 |
+
type: 'Polygon';
|
| 21 |
+
coordinates: Array<Array<[number, number]>>;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export type GeoJSONGeometry = GeoJSONPoint | GeoJSONLineString | GeoJSONPolygon;
|
| 25 |
+
|
| 26 |
+
export interface GeoJSONFeature<G extends GeoJSONGeometry = GeoJSONGeometry, P = any> {
|
| 27 |
+
type: 'Feature';
|
| 28 |
+
geometry: G;
|
| 29 |
+
properties: P;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export interface GeoJSONFeatureCollection<G extends GeoJSONGeometry = GeoJSONGeometry, P = any> {
|
| 33 |
+
type: 'FeatureCollection';
|
| 34 |
+
features: Array<GeoJSONFeature<G, P>>;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// ============================================================================
|
| 38 |
+
// Track Types (Public Ski Tracks)
|
| 39 |
+
// ============================================================================
|
| 40 |
+
|
| 41 |
+
export interface TrackProperties {
|
| 42 |
+
id: number;
|
| 43 |
+
name: string;
|
| 44 |
+
description?: string;
|
| 45 |
+
difficulty?: string; // e.g., 'beginner', 'intermediate', 'advanced', 'expert'
|
| 46 |
+
type?: string; // e.g., 'downhill', 'nordic', 'skitour'
|
| 47 |
+
'piste:difficulty'?: string; // From OpenSkiData
|
| 48 |
+
'piste:type'?: string; // From OpenSkiData
|
| 49 |
+
[key: string]: any; // Allow additional properties from GeoJSON
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export interface Track {
|
| 53 |
+
id: number;
|
| 54 |
+
name: string;
|
| 55 |
+
description?: string;
|
| 56 |
+
geojson: GeoJSONGeometry; // The path geometry
|
| 57 |
+
properties: Record<string, any>; // Additional properties from GeoJSON
|
| 58 |
+
difficulty?: string;
|
| 59 |
+
type?: string;
|
| 60 |
+
created_at?: Date;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
export type TrackFeature = GeoJSONFeature<GeoJSONLineString, TrackProperties>;
|
| 64 |
+
export type TrackFeatureCollection = GeoJSONFeatureCollection<GeoJSONLineString, TrackProperties>;
|
| 65 |
+
|
| 66 |
+
// ============================================================================
|
| 67 |
+
// Adventure Types (User-Saved Activities)
|
| 68 |
+
// ============================================================================
|
| 69 |
+
|
| 70 |
+
export interface AdventureProperties {
|
| 71 |
+
id: number;
|
| 72 |
+
name: string;
|
| 73 |
+
description?: string;
|
| 74 |
+
user_id?: string;
|
| 75 |
+
recorded_at?: Date;
|
| 76 |
+
duration?: number; // Duration in seconds
|
| 77 |
+
distance?: number; // Distance in meters
|
| 78 |
+
[key: string]: any; // Allow additional properties
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
export interface Adventure {
|
| 82 |
+
id: number;
|
| 83 |
+
user_id?: string;
|
| 84 |
+
name: string;
|
| 85 |
+
description?: string;
|
| 86 |
+
path: GeoJSONGeometry; // The recorded path
|
| 87 |
+
properties: Record<string, any>; // Additional properties
|
| 88 |
+
recorded_at?: Date;
|
| 89 |
+
created_at?: Date;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export type AdventureFeature = GeoJSONFeature<GeoJSONLineString, AdventureProperties>;
|
| 93 |
+
export type AdventureFeatureCollection = GeoJSONFeatureCollection<GeoJSONLineString, AdventureProperties>;
|
| 94 |
+
|
| 95 |
+
// ============================================================================
|
| 96 |
+
// API Response Types
|
| 97 |
+
// ============================================================================
|
| 98 |
+
|
| 99 |
+
export interface PaginationInfo {
|
| 100 |
+
total: number;
|
| 101 |
+
page: number;
|
| 102 |
+
limit: number;
|
| 103 |
+
pages: number;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
export interface PaginatedResponse<T> {
|
| 107 |
+
type: 'FeatureCollection';
|
| 108 |
+
features: T[];
|
| 109 |
+
pagination: PaginationInfo;
|
| 110 |
+
}
|