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