Skip to content

Commit a14ec70

Browse files
authored
Add files via upload
1 parent cc54b37 commit a14ec70

9 files changed

Lines changed: 1648 additions & 0 deletions

File tree

js/api.js

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,235 @@
1+
/* ============================================================
2+
api.js – All Supabase database operations
3+
Depends on: config.js (sb), state.js (state)
4+
============================================================ */
15

6+
/* ----------------------------------------------------------
7+
Home data
8+
---------------------------------------------------------- */
9+
10+
/**
11+
* Load all tours + the current user's memberships and cache admin usernames.
12+
*/
13+
async function loadHomeData() {
14+
const [toursRes, membershipsRes] = await Promise.all([
15+
sb.from('tours').select('*').order('date', { ascending: true }),
16+
sb.from('tour_members').select('tour_id').eq('user_id', state.currentUser.id),
17+
]);
18+
19+
state.tours = toursRes.data || [];
20+
21+
const memberIds = (membershipsRes.data || []).map(m => m.tour_id);
22+
const adminIds = state.tours
23+
.filter(t => t.admin_id === state.currentUser.id)
24+
.map(t => t.id);
25+
26+
state.myTourIds = new Set([...memberIds, ...adminIds]);
27+
28+
// Cache admin usernames that we haven't seen yet
29+
const uncached = [...new Set(
30+
state.tours.map(t => t.admin_id).filter(id => !state.profileCache[id])
31+
)];
32+
if (uncached.length) {
33+
const { data: profs } = await sb.from('profiles').select('id,username').in('id', uncached);
34+
(profs || []).forEach(p => { state.profileCache[p.id] = p.username; });
35+
}
36+
}
37+
38+
/* ----------------------------------------------------------
39+
Tour detail data
40+
---------------------------------------------------------- */
41+
42+
/**
43+
* Load a single tour, its members, messages and plan dates.
44+
* Populates state.currentTour / tourMembers / tourMessages / tourPlanDates.
45+
* @param {string} tourId
46+
*/
47+
async function loadTourData(tourId) {
48+
const [tourRes, membersRes, msgsRes, datesRes] = await Promise.all([
49+
sb.from('tours').select('*').eq('id', tourId).single(),
50+
sb.from('tour_members').select('user_id').eq('tour_id', tourId),
51+
sb.from('messages').select('*').eq('tour_id', tourId).order('created_at', { ascending: true }),
52+
sb.from('plan_dates').select('*').eq('tour_id', tourId).order('date', { ascending: true }),
53+
]);
54+
55+
state.currentTour = tourRes.data;
56+
state.tourMessages = msgsRes.data || [];
57+
state.tourPlanDates = datesRes.data || [];
58+
59+
// Cache usernames we haven't seen yet
60+
const memberUserIds = (membersRes.data || []).map(m => m.user_id);
61+
const allIds = [...new Set([
62+
...(state.currentTour?.admin_id ? [state.currentTour.admin_id] : []),
63+
...memberUserIds,
64+
].filter(id => !state.profileCache[id]))];
65+
66+
if (allIds.length) {
67+
const { data: profs } = await sb.from('profiles').select('id,username').in('id', allIds);
68+
(profs || []).forEach(p => { state.profileCache[p.id] = p.username; });
69+
}
70+
71+
// Build sorted member list: admin first, then others
72+
const adminId = state.currentTour?.admin_id;
73+
state.tourMembers = [
74+
...(adminId ? [{
75+
user_id: adminId,
76+
username: state.profileCache[adminId] || 'Admin',
77+
isAdmin: true,
78+
}] : []),
79+
...(membersRes.data || [])
80+
.filter(m => m.user_id !== adminId)
81+
.map(m => ({
82+
user_id: m.user_id,
83+
username: state.profileCache[m.user_id] || 'Unbekannt',
84+
isAdmin: false,
85+
})),
86+
];
87+
88+
if (state.currentTour?.date) {
89+
state.tourCalMonth = new Date(state.currentTour.date + 'T12:00:00');
90+
}
91+
}
92+
93+
/**
94+
* Refresh only the messages for the current tour.
95+
*/
96+
async function loadMessages() {
97+
const { data } = await sb
98+
.from('messages')
99+
.select('*')
100+
.eq('tour_id', state.currentTourId)
101+
.order('created_at', { ascending: true });
102+
state.tourMessages = data || [];
103+
}
104+
105+
/* ----------------------------------------------------------
106+
Tour mutations
107+
---------------------------------------------------------- */
108+
109+
/**
110+
* Create a new tour (admin = current user).
111+
* @param {object} data – tour fields (without admin_id)
112+
* @returns {object} created tour row
113+
*/
114+
async function createTour(data) {
115+
const { data: t, error } = await sb
116+
.from('tours')
117+
.insert({ ...data, admin_id: state.currentUser.id })
118+
.select()
119+
.single();
120+
if (error) throw new Error(error.message);
121+
return t;
122+
}
123+
124+
/**
125+
* Join a tour after password check.
126+
* @param {string} tourId
127+
* @param {string} password
128+
*/
129+
async function joinTour(tourId, password) {
130+
const tour = state.tours.find(t => t.id === tourId);
131+
if (!tour) throw new Error('Tour nicht gefunden.');
132+
if (tour.join_password !== password) throw new Error('Falsches Passwort!');
133+
134+
const { error } = await sb
135+
.from('tour_members')
136+
.insert({ tour_id: tourId, user_id: state.currentUser.id });
137+
138+
// Ignore "duplicate key" error – user already joined
139+
if (error && !error.message.includes('duplicate')) throw new Error(error.message);
140+
state.myTourIds.add(tourId);
141+
}
142+
143+
/**
144+
* Update editable tour fields (destination, distance, description).
145+
* @param {object} updates
146+
*/
147+
async function updateTourInfo(updates) {
148+
const { error } = await sb
149+
.from('tours')
150+
.update(updates)
151+
.eq('id', state.currentTourId);
152+
if (error) throw new Error(error.message);
153+
if (state.currentTour) Object.assign(state.currentTour, updates);
154+
}
155+
156+
/* ----------------------------------------------------------
157+
Messages
158+
---------------------------------------------------------- */
159+
160+
/**
161+
* Insert a chat message and return the new row.
162+
* @param {string} text
163+
* @returns {object}
164+
*/
165+
async function sendMessage(text) {
166+
const { data, error } = await sb
167+
.from('messages')
168+
.insert({
169+
tour_id: state.currentTourId,
170+
user_id: state.currentUser.id,
171+
username: state.currentUser.username,
172+
text,
173+
})
174+
.select()
175+
.single();
176+
if (error) throw new Error(error.message);
177+
return data;
178+
}
179+
180+
/* ----------------------------------------------------------
181+
Plan dates
182+
---------------------------------------------------------- */
183+
184+
/**
185+
* Add a planning date to the current tour.
186+
* @param {string} date – ISO date string (YYYY-MM-DD)
187+
* @param {string} label – optional description
188+
*/
189+
async function addPlanDate(date, label) {
190+
const { data, error } = await sb
191+
.from('plan_dates')
192+
.insert({ tour_id: state.currentTourId, date, label })
193+
.select()
194+
.single();
195+
if (error) throw new Error(error.message);
196+
state.tourPlanDates.push(data);
197+
}
198+
199+
/**
200+
* Delete a planning date by its UUID.
201+
* @param {string} id
202+
*/
203+
async function deletePlanDate(id) {
204+
await sb.from('plan_dates').delete().eq('id', id);
205+
state.tourPlanDates = state.tourPlanDates.filter(d => d.id !== id);
206+
}
207+
208+
/* ----------------------------------------------------------
209+
GPX route
210+
---------------------------------------------------------- */
211+
212+
/**
213+
* Persist a GPX route (array of [lat, lon] pairs) to the DB.
214+
* @param {Array} route
215+
*/
216+
async function saveGPX(route) {
217+
const { error } = await sb
218+
.from('tours')
219+
.update({ gpx_route: route })
220+
.eq('id', state.currentTourId);
221+
if (error) throw new Error(error.message);
222+
if (state.currentTour) state.currentTour.gpx_route = route;
223+
}
224+
225+
/**
226+
* Remove the GPX route from the current tour.
227+
*/
228+
async function deleteGPX() {
229+
const { error } = await sb
230+
.from('tours')
231+
.update({ gpx_route: null })
232+
.eq('id', state.currentTourId);
233+
if (error) throw new Error(error.message);
234+
if (state.currentTour) state.currentTour.gpx_route = null;
235+
}

js/app.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/* ============================================================
2+
app.js – Application router, render dispatcher and boot
3+
This file is loaded last and calls init() to start the app.
4+
Depends on: all other modules
5+
============================================================ */
6+
7+
/* ----------------------------------------------------------
8+
Router
9+
---------------------------------------------------------- */
10+
11+
/**
12+
* Navigate to a new view.
13+
* Optionally merges extra state params (e.g. currentTourId, currentTab).
14+
* Destroys the Leaflet map if we're leaving the tour page.
15+
*
16+
* @param {string} view – target view name
17+
* @param {object} [params] – extra state fields to merge
18+
*/
19+
async function navigateTo(view, params = {}) {
20+
// Tear down map when leaving the tour detail page
21+
if (mapInstance && view !== 'tour') destroyMap();
22+
23+
// Merge any extra params into global state
24+
Object.assign(state, params);
25+
26+
// Load data required for the target view
27+
try {
28+
if (view === 'home' && state.currentUser) await loadHomeData();
29+
if (view === 'tour' && state.currentTourId) await loadTourData(state.currentTourId);
30+
} catch (e) {
31+
console.error('[navigateTo] data fetch error:', e);
32+
}
33+
34+
state.view = view;
35+
render();
36+
}
37+
38+
/* ----------------------------------------------------------
39+
Render dispatcher
40+
---------------------------------------------------------- */
41+
42+
/**
43+
* Re-render the entire #app element based on state.view.
44+
* After injecting HTML, wire up event listeners.
45+
*/
46+
function render() {
47+
const app = document.getElementById('app');
48+
if (!app) return;
49+
50+
// Navigation bar is shown on every view except auth / loading
51+
const showNav = !['auth', 'loading'].includes(state.view);
52+
53+
let html = showNav ? renderNav() : '';
54+
55+
switch (state.view) {
56+
case 'auth': html += renderAuth(); break;
57+
case 'home': html += renderHome(); break;
58+
case 'create': html += renderCreate(); break;
59+
case 'join': html += renderJoin(); break;
60+
case 'tour': html += renderTour(); break;
61+
default: html += '<div class="loading-screen">…</div>';
62+
}
63+
64+
app.innerHTML = html;
65+
attachEvents();
66+
}
67+
68+
/* ----------------------------------------------------------
69+
Boot
70+
---------------------------------------------------------- */
71+
72+
/**
73+
* Application entry point.
74+
* Checks for an existing Supabase session and routes accordingly.
75+
*/
76+
async function init() {
77+
try {
78+
const { data: { session } } = await sb.auth.getSession();
79+
80+
if (session) {
81+
const { data: profile } = await sb
82+
.from('profiles')
83+
.select('username')
84+
.eq('id', session.user.id)
85+
.single();
86+
87+
if (profile) {
88+
state.currentUser = { id: session.user.id, username: profile.username };
89+
state.profileCache[session.user.id] = profile.username;
90+
await navigateTo('home');
91+
return;
92+
}
93+
}
94+
} catch (e) {
95+
console.error('[init] session check failed:', e);
96+
}
97+
98+
// No valid session → show auth screen
99+
state.authMode = 'login';
100+
state.view = 'auth';
101+
render();
102+
}
103+
104+
// Start the application
105+
init();

0 commit comments

Comments
 (0)