zerito commited on
Commit
90a13c0
·
1 Parent(s): 9bb9164

Deploy backend update Wed Dec 3 21:14:46 CET 2025

Browse files
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
- static async get(req: Request, res: Response) {
 
 
 
 
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 rows = await TrackModel.findAll();
13
- console.log(`Fetched ${rows.length} rows from the database.`);
14
 
15
- let features = rows.map(row => {
 
16
  return {
17
  type: 'Feature',
18
- geometry: typeof row.geojson === 'string' ? JSON.parse(row.geojson) : row.geojson,
 
 
19
  properties: {
20
- id: row.id,
21
- name: row.name,
22
- description: row.description,
23
- ...row.properties, // Spread existing properties
24
- difficulty: row.difficulty,
25
- type: row.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 route for distance check
47
  // GeoJSON coordinates are [lng, lat]
48
- const routePoint = feature.geometry.coordinates[0];
49
- const routeLng = routePoint[0];
50
- const routeLat = routePoint[1];
51
 
52
- const dist = getDistanceFromLatLonInKm(centerLat, centerLng, routeLat, routeLng);
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
- res.json({
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).send('Server Error');
88
  console.log('--- End getTracks Controller (Error) ---');
89
  }
90
  }
91
 
92
- static async create(req: Request, res: Response) {
93
- console.log('--- Start createTrack Controller ---');
94
- let { name, geojson, properties, difficulty, type } = req.body;
95
-
96
- // Extract difficulty and type from geojson.properties if not provided directly
97
- if (!difficulty && geojson?.properties?.['piste:difficulty']) {
98
- difficulty = geojson.properties['piste:difficulty'];
99
- }
100
- if (!type && geojson?.properties?.['piste:type']) {
101
- type = geojson.properties['piste:type'];
102
- }
103
 
104
- console.log(`Attempting to create track: Name='${name}', Difficulty='${difficulty}', Type='${type}'.`);
105
 
106
- if (!geojson || !geojson.geometry || !geojson.geometry.coordinates) {
107
- console.error('Validation Error: Invalid or missing GeoJSON in request body.');
108
- return res.status(400).json({ error: 'Invalid GeoJSON' });
109
- }
110
- console.log('GeoJSON structure validated.');
111
 
112
- try {
113
- const trackData = {
114
- name,
115
- geojson: geojson.geometry,
116
- properties: geojson.properties || properties,
117
- difficulty,
118
- type
 
 
 
 
 
 
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({ success: true, id });
126
- console.log('--- End createTrack Controller (Success) ---');
127
  } catch (err) {
128
- console.error('Error in createTrack:', err);
129
- res.status(500).send('Server Error');
130
- console.log('--- End createTrack Controller (Error) ---');
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
- // Get all tracks (with optional location filtering)
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
- export interface Track {
4
- id: number;
5
- name: string;
6
- description: string;
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 id, name, description, path as geojson, properties, difficulty, type
 
 
 
 
 
 
 
 
17
  FROM routes
 
18
  `;
19
  return tracks;
20
  }
21
 
22
- static async create(track: Partial<Track>): Promise<number> {
23
- const { name, geojson, properties, difficulty, type } = track;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- // Ensure table exists with new schema (Migration logic from original index.ts)
 
 
 
 
26
  await sql`
27
- CREATE TABLE IF NOT EXISTS routes (
28
- id SERIAL PRIMARY KEY,
29
- name VARCHAR(255),
30
- description TEXT,
31
- path JSONB,
32
- properties JSONB,
33
- difficulty VARCHAR(50),
34
- type VARCHAR(50),
35
- created_at TIMESTAMP DEFAULT NOW()
36
- );
37
  `;
38
 
39
- // Attempt to add columns if they don't exist
40
  try {
41
- await sql`ALTER TABLE routes ADD COLUMN IF NOT EXISTS properties JSONB;`;
42
- await sql`ALTER TABLE routes ADD COLUMN IF NOT EXISTS difficulty VARCHAR(50);`;
43
- await sql`ALTER TABLE routes ADD COLUMN IF NOT EXISTS type VARCHAR(50);`;
44
  } catch (e) {
45
- // Ignore errors if columns exist
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
- router.get('/', TrackController.get);
7
- router.post('/', TrackController.create);
 
 
 
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
+ }