More changes than expected. Mobile version improvements & PWA
Build & Push Docker Images / test-backend (push) Successful in 44s
Build & Push Docker Images / test-frontend (push) Successful in 52s
Build & Push Docker Images / build-backend (push) Failing after 34s
Build & Push Docker Images / build-frontend (push) Failing after 30s

This commit is contained in:
Ichitux
2026-05-20 22:28:17 +02:00
parent 7b288d33cb
commit 2eff93e66a
118 changed files with 13780 additions and 209 deletions
+222
View File
@@ -0,0 +1,222 @@
import React, { useEffect, useState } from 'react';
import './ProfileView.css';
import { getUserPosition } from '../utils/geo';
function ProfileView({ currentUser, onProfileSaved, onShowSaved, onLogout }) {
const [address, setAddress] = useState(currentUser?.address || '');
const [latitude, setLatitude] = useState(
currentUser?.latitude != null ? String(currentUser.latitude) : ''
);
const [longitude, setLongitude] = useState(
currentUser?.longitude != null ? String(currentUser.longitude) : ''
);
const [saving, setSaving] = useState(false);
const [geocoding, setGeocoding] = useState(false);
const [locating, setLocating] = useState(false);
const [feedback, setFeedback] = useState(null);
useEffect(() => {
setAddress(currentUser?.address || '');
setLatitude(currentUser?.latitude != null ? String(currentUser.latitude) : '');
setLongitude(currentUser?.longitude != null ? String(currentUser.longitude) : '');
setFeedback(null);
}, [currentUser?.id]);
async function handleSave(e) {
e?.preventDefault();
setSaving(true);
setFeedback(null);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
address: address.trim() || null,
latitude: latitude === '' ? null : latitude,
longitude: longitude === '' ? null : longitude,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `Save failed (HTTP ${res.status})`);
}
const updated = await res.json();
onProfileSaved?.(updated);
setFeedback({ type: 'ok', text: 'Profile saved.' });
} catch (err) {
setFeedback({ type: 'err', text: err.message || 'Save failed' });
} finally {
setSaving(false);
}
}
async function handleUseLocation() {
setLocating(true);
setFeedback(null);
try {
const pos = await getUserPosition();
setLatitude(String(pos.lat));
setLongitude(String(pos.lon));
setFeedback({ type: 'ok', text: 'Got your current location — remember to Save.' });
} catch (err) {
let msg = err && err.message ? err.message : 'Could not get location';
if (err && typeof err.code === 'number') {
if (err.code === 1) msg = 'Location permission denied — allow it in your browser settings.';
else if (err.code === 2) msg = 'Location unavailable — check OS location services.';
else if (err.code === 3) msg = 'Location request timed out — try again.';
}
setFeedback({ type: 'err', text: msg });
} finally {
setLocating(false);
}
}
async function handleGeocode() {
const q = address.trim();
if (!q) {
setFeedback({ type: 'err', text: 'Enter an address first.' });
return;
}
setGeocoding(true);
setFeedback(null);
try {
const res = await fetch(`/api/geocode?q=${encodeURIComponent(q)}`, {
credentials: 'include',
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `Lookup failed (HTTP ${res.status})`);
}
const data = await res.json();
setLatitude(String(data.lat));
setLongitude(String(data.lon));
setFeedback({
type: 'ok',
text: `Located: ${data.displayName} — remember to Save.`,
});
} catch (err) {
setFeedback({ type: 'err', text: err.message || 'Lookup failed' });
} finally {
setGeocoding(false);
}
}
const hasSavedCoords =
currentUser?.latitude != null && currentUser?.longitude != null;
return (
<div className="profile-view">
<header className="profile-header">
<div className="profile-avatar" aria-hidden="true">👤</div>
<div className="profile-info">
<h2>{currentUser?.username}</h2>
<p className="profile-role">
{currentUser?.is_admin ? 'Administrator' : 'Member'}
{hasSavedCoords && <span className="profile-pill"> · Location saved</span>}
</p>
</div>
</header>
<section className="profile-section">
<h3>Your location</h3>
<p className="profile-section-sub">
Save your address so "Sort by distance" uses it without asking the browser every time.
</p>
<form onSubmit={handleSave} className="profile-form">
<div className="profile-field">
<label htmlFor="profile-address">Address</label>
<input
id="profile-address"
type="text"
placeholder="123 Main St, Madrid, Spain"
value={address}
onChange={(e) => setAddress(e.target.value)}
autoComplete="street-address"
disabled={saving}
/>
</div>
<div className="profile-field-row">
<div className="profile-field">
<label htmlFor="profile-lat">Latitude</label>
<input
id="profile-lat"
type="number"
step="any"
placeholder="40.4168"
value={latitude}
onChange={(e) => setLatitude(e.target.value)}
disabled={saving}
/>
</div>
<div className="profile-field">
<label htmlFor="profile-lon">Longitude</label>
<input
id="profile-lon"
type="number"
step="any"
placeholder="-3.7038"
value={longitude}
onChange={(e) => setLongitude(e.target.value)}
disabled={saving}
/>
</div>
</div>
<div className="profile-helper-actions">
<button
type="button"
className="profile-btn-secondary"
onClick={handleGeocode}
disabled={geocoding || saving}
>
{geocoding ? 'Finding…' : '📍 Find from address'}
</button>
<button
type="button"
className="profile-btn-secondary"
onClick={handleUseLocation}
disabled={locating || saving}
>
{locating ? 'Locating…' : '🛰️ Use my current location'}
</button>
</div>
{feedback && (
<p className={`profile-feedback profile-feedback--${feedback.type}`}>
{feedback.text}
</p>
)}
<div className="profile-actions">
<button type="submit" className="profile-btn-primary" disabled={saving}>
{saving ? 'Saving…' : 'Save profile'}
</button>
</div>
</form>
</section>
<section className="profile-section">
<h3>Account</h3>
<button type="button" className="profile-link-row" onClick={onShowSaved}>
<span className="profile-link-row-icon">🔔</span>
<span className="profile-link-row-label">Saved notifications</span>
<span className="profile-link-row-chevron" aria-hidden="true"></span>
</button>
<button
type="button"
className="profile-link-row profile-link-row--danger"
onClick={onLogout}
>
<span className="profile-link-row-icon"></span>
<span className="profile-link-row-label">Logout</span>
<span className="profile-link-row-chevron" aria-hidden="true"></span>
</button>
</section>
</div>
);
}
export default ProfileView;