refactor: remove extract.js and checkin.js
All extraction and check-in interactivity is now handled by Datastar signals and inline FileReader JS. No separate JavaScript files remain.
This commit is contained in:
parent
e4a304b406
commit
2a94b87d49
2 changed files with 0 additions and 314 deletions
|
|
@ -1,257 +0,0 @@
|
||||||
// Check-in page — minimal JS for browser APIs and async operations.
|
|
||||||
// All state management and UI logic lives in Datastar signals (checkin.html).
|
|
||||||
// JS dispatches custom events to bridge async results back to Datastar.
|
|
||||||
|
|
||||||
const _root = () => document.getElementById('checkin-root');
|
|
||||||
|
|
||||||
const emit = (name, detail = {}) =>
|
|
||||||
_root().dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
|
|
||||||
|
|
||||||
// --- State mirror ---
|
|
||||||
// Tracks signal values via event listeners so submitCheckIn() can read them
|
|
||||||
// without accessing Datastar internals.
|
|
||||||
|
|
||||||
let _cafeId = '';
|
|
||||||
let _cafeName = '';
|
|
||||||
let _cafeCity = '';
|
|
||||||
let _cafeCountry = '';
|
|
||||||
let _cafeLat = 0;
|
|
||||||
let _cafeLng = 0;
|
|
||||||
let _cafeWebsite = '';
|
|
||||||
let _roastId = '';
|
|
||||||
let _rating = 0;
|
|
||||||
let _submitting = false;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const root = _root();
|
|
||||||
if (!root) return;
|
|
||||||
|
|
||||||
root.addEventListener('cafe-selected', (e) => {
|
|
||||||
_cafeId = String(e.detail.id);
|
|
||||||
_cafeName = e.detail.name;
|
|
||||||
_cafeCity = e.detail.city || '';
|
|
||||||
_cafeCountry = e.detail.country || '';
|
|
||||||
_cafeLat = e.detail.lat || 0;
|
|
||||||
_cafeLng = e.detail.lng || 0;
|
|
||||||
_cafeWebsite = e.detail.website || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
root.addEventListener('scan-complete', (e) => {
|
|
||||||
_roastId = e.detail.roastId;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track roast selection from the dropdown
|
|
||||||
root.addEventListener('change', (e) => {
|
|
||||||
if (e.target.tagName === 'SELECT' && e.target.value) {
|
|
||||||
_roastId = e.target.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track rating clicks via aria-label convention
|
|
||||||
root.addEventListener('click', (e) => {
|
|
||||||
const star = e.target.closest('[aria-label^="Rate"]');
|
|
||||||
if (!star) return;
|
|
||||||
const match = star.getAttribute('aria-label')?.match(/Rate (\d)/);
|
|
||||||
if (match) _rating = parseInt(match[1], 10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Geolocation (browser API) ---
|
|
||||||
|
|
||||||
const locateUser = () => {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
emit('location-error', { message: 'Geolocation is not supported by your browser.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('location-start');
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(pos) => emit('location-found', { lat: pos.coords.latitude, lng: pos.coords.longitude }),
|
|
||||||
(err) => emit('location-error', {
|
|
||||||
message: err.code === 1
|
|
||||||
? 'Location access denied. You can search by name instead.'
|
|
||||||
: 'Could not determine your location. You can search by name instead.',
|
|
||||||
}),
|
|
||||||
{ enableHighAccuracy: true, timeout: 15000 },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Nearby cafe search (Foursquare API) ---
|
|
||||||
|
|
||||||
let _nearbyCafes = [];
|
|
||||||
|
|
||||||
const searchNearbyCafes = async (query, lat, lng) => {
|
|
||||||
if (!lat || !query || query.length < 2) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(
|
|
||||||
`/api/v1/nearby-cafes?lat=${lat}&lng=${lng}&q=${encodeURIComponent(query)}`,
|
|
||||||
{ credentials: 'same-origin' },
|
|
||||||
);
|
|
||||||
if (!resp.ok) throw new Error(`${resp.status}`);
|
|
||||||
|
|
||||||
const cafes = await resp.json();
|
|
||||||
_nearbyCafes = cafes;
|
|
||||||
renderNearbyCafes(cafes);
|
|
||||||
} catch {
|
|
||||||
emit('checkin-error', { message: 'Nearby search failed. Please try again.' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNearbyCafes = (cafes) => {
|
|
||||||
const el = document.getElementById('nearby-results');
|
|
||||||
|
|
||||||
if (!cafes.length) {
|
|
||||||
el.innerHTML = '<p class="px-3 py-2 text-sm text-stone-500">No nearby cafes found.</p>';
|
|
||||||
} else {
|
|
||||||
let html = '<h3 class="px-3 py-2 text-xs font-semibold text-stone-500 uppercase tracking-wide">Nearby</h3>';
|
|
||||||
cafes.forEach((cafe, i) => {
|
|
||||||
const dist = cafe.distance_meters < 1000
|
|
||||||
? `${cafe.distance_meters} m`
|
|
||||||
: `${(cafe.distance_meters / 1000).toFixed(1)} km`;
|
|
||||||
const loc = [cafe.city, cafe.country].filter(Boolean).join(', ');
|
|
||||||
html += `<button type="button" class="w-full px-3 py-2 text-left text-sm hover:bg-amber-100 transition" onclick="selectNearby(${i})">`;
|
|
||||||
html += `<span class="font-medium text-amber-900">${esc(cafe.name)}</span>`;
|
|
||||||
html += `<span class="ml-2 text-xs text-stone-500">${esc(loc)} · ${dist}</span>`;
|
|
||||||
html += `</button>`;
|
|
||||||
});
|
|
||||||
el.innerHTML = html;
|
|
||||||
}
|
|
||||||
el.classList.remove('hidden');
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectNearby = (i) => {
|
|
||||||
const c = _nearbyCafes[i];
|
|
||||||
if (!c) return;
|
|
||||||
emit('cafe-selected', {
|
|
||||||
id: '',
|
|
||||||
name: c.name,
|
|
||||||
city: c.city || '',
|
|
||||||
country: c.country || '',
|
|
||||||
lat: c.latitude || 0,
|
|
||||||
lng: c.longitude || 0,
|
|
||||||
website: c.website || '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Client-side cafe filtering ---
|
|
||||||
|
|
||||||
const filterExistingCafes = (query) => {
|
|
||||||
const container = document.getElementById('existing-cafes');
|
|
||||||
if (!container) return;
|
|
||||||
const lower = query.toLowerCase();
|
|
||||||
let visible = 0;
|
|
||||||
container.querySelectorAll('[data-cafe-name]').forEach((btn) => {
|
|
||||||
const show = !query || btn.dataset.cafeName.toLowerCase().includes(lower);
|
|
||||||
btn.style.display = show ? '' : 'none';
|
|
||||||
if (show) visible++;
|
|
||||||
});
|
|
||||||
const noMatch = container.querySelector('[data-no-match]');
|
|
||||||
if (noMatch) noMatch.style.display = query && !visible ? '' : 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Scan callback (extract.js integration) ---
|
|
||||||
|
|
||||||
const onScanExtracted = async (data) => {
|
|
||||||
emit('scan-start');
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
roaster_name: data.roaster?.name || '',
|
|
||||||
roaster_country: data.roaster?.country || '',
|
|
||||||
roaster_city: data.roaster?.city || '',
|
|
||||||
roaster_homepage: data.roaster?.homepage || '',
|
|
||||||
roast_name: data.roast?.name || '',
|
|
||||||
origin: data.roast?.origin || '',
|
|
||||||
region: data.roast?.region || '',
|
|
||||||
producer: data.roast?.producer || '',
|
|
||||||
process: data.roast?.process || '',
|
|
||||||
tasting_notes: (data.roast?.tasting_notes || []).join(', '),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/v1/scan', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!resp.ok) {
|
|
||||||
const err = await resp.json().catch(() => ({}));
|
|
||||||
throw new Error(err.message || `Server returned ${resp.status}`);
|
|
||||||
}
|
|
||||||
const result = await resp.json();
|
|
||||||
emit('scan-complete', { roastId: String(result.roast_id), name: body.roast_name });
|
|
||||||
} catch (e) {
|
|
||||||
emit('scan-error', { message: `Scan failed: ${e.message}` });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Submit check-in ---
|
|
||||||
|
|
||||||
const submitCheckIn = async () => {
|
|
||||||
if (_submitting) return;
|
|
||||||
if (!_cafeName) { emit('checkin-error', { message: 'Please select a cafe.' }); return; }
|
|
||||||
if (!_roastId) { emit('checkin-error', { message: 'Please select or scan a coffee.' }); return; }
|
|
||||||
|
|
||||||
_submitting = true;
|
|
||||||
emit('submit-start');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let cafeId = _cafeId;
|
|
||||||
|
|
||||||
// Create cafe from Foursquare if no existing ID
|
|
||||||
if (!cafeId) {
|
|
||||||
const cafeResp = await fetch('/api/v1/cafes', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: _cafeName,
|
|
||||||
city: _cafeCity || null,
|
|
||||||
country: _cafeCountry || null,
|
|
||||||
latitude: _cafeLat || null,
|
|
||||||
longitude: _cafeLng || null,
|
|
||||||
website: _cafeWebsite || null,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!cafeResp.ok) {
|
|
||||||
const err = await cafeResp.json().catch(() => ({}));
|
|
||||||
throw new Error(err.message || `Failed to create cafe (${cafeResp.status})`);
|
|
||||||
}
|
|
||||||
const newCafe = await cafeResp.json();
|
|
||||||
cafeId = String(newCafe.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cupBody = {
|
|
||||||
roast_id: parseInt(_roastId, 10),
|
|
||||||
cafe_id: parseInt(cafeId, 10),
|
|
||||||
};
|
|
||||||
if (_rating) cupBody.rating = _rating;
|
|
||||||
|
|
||||||
const cupResp = await fetch('/api/v1/cups', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify(cupBody),
|
|
||||||
});
|
|
||||||
if (!cupResp.ok) {
|
|
||||||
const err = await cupResp.json().catch(() => ({}));
|
|
||||||
throw new Error(err.message || `Failed to create cup (${cupResp.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = '/';
|
|
||||||
} catch (e) {
|
|
||||||
_submitting = false;
|
|
||||||
emit('checkin-error', { message: `Check-in failed: ${e.message}` });
|
|
||||||
emit('submit-error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Utility ---
|
|
||||||
|
|
||||||
const esc = (t) => {
|
|
||||||
const el = document.createElement('span');
|
|
||||||
el.textContent = t;
|
|
||||||
return el.innerHTML;
|
|
||||||
};
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
let _extracting = false;
|
|
||||||
|
|
||||||
const triggerPhotoExtract = (formId, endpoint, onSuccess) => {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.accept = 'image/*';
|
|
||||||
input.capture = 'environment';
|
|
||||||
input.onchange = () => {
|
|
||||||
if (input.files.length === 0) return;
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
doExtract(formId, endpoint, { image: reader.result }, onSuccess);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(input.files[0]);
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractFromText = (formId, endpoint, onSuccess) => {
|
|
||||||
const input = document.getElementById(`${formId}-extract-text`);
|
|
||||||
const prompt = input.value.trim();
|
|
||||||
if (prompt.length < 3) return;
|
|
||||||
doExtract(formId, endpoint, { prompt }, onSuccess);
|
|
||||||
};
|
|
||||||
|
|
||||||
const doExtract = async (formId, endpoint, body, onSuccess) => {
|
|
||||||
if (_extracting) return;
|
|
||||||
_extracting = true;
|
|
||||||
const errorEl = document.getElementById(`${formId}-extract-error`);
|
|
||||||
const controlsEl = document.getElementById(`${formId}-extract-controls`);
|
|
||||||
const waitingEl = document.getElementById(`${formId}-extract-waiting`);
|
|
||||||
errorEl.classList.add('hidden');
|
|
||||||
controlsEl.classList.add('hidden');
|
|
||||||
waitingEl.classList.remove('hidden');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!resp.ok) {
|
|
||||||
const errData = await resp.json().catch(() => ({}));
|
|
||||||
throw new Error(errData.message || `Server returned ${resp.status}`);
|
|
||||||
}
|
|
||||||
const data = await resp.json();
|
|
||||||
onSuccess(data);
|
|
||||||
} catch (e) {
|
|
||||||
errorEl.textContent = `Extraction failed: ${e.message}`;
|
|
||||||
errorEl.classList.remove('hidden');
|
|
||||||
} finally {
|
|
||||||
waitingEl.classList.add('hidden');
|
|
||||||
controlsEl.classList.remove('hidden');
|
|
||||||
_extracting = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Loading…
Reference in a new issue