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
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:
@@ -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;
|
||||
Reference in New Issue
Block a user