Skip to content
Open

Goat #31

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,6 @@ export default function App() {

// Tutorial state
const [tutorialStep, setTutorialStep] = useState(0);
const [tutorialForceSidebar, setTutorialForceSidebar] = useState(false);
const [tutorialForceInfo, setTutorialForceInfo] = useState(false);

const {
location: gpsLocation,
Expand Down Expand Up @@ -372,19 +370,8 @@ export default function App() {
{tutorialStep > 0 && (
<TutorialOverlay
theme={mapTheme}
triggerSidebar={setTutorialForceSidebar}
triggerInfo={setTutorialForceInfo}
onStepChange={setTutorialStep}
onComplete={() => {
setTutorialStep(0);
setTutorialForceSidebar(false);
setTutorialForceInfo(false);
if (isFirstLaunch()) setMedicalOpen(true);
}}
onSkip={() => {
setTutorialStep(0);
setTutorialForceSidebar(false);
setTutorialForceInfo(false);
if (isFirstLaunch()) setMedicalOpen(true);
}}
/>
Expand All @@ -396,6 +383,7 @@ export default function App() {
landmark={searchData?.landmark}
countryCode={countryCode}
contacts={searchData?.contacts || []}
cat={cat}
topContact={topContact}
isOnline={isOnline}
gpsLost={gpsLost}
Expand All @@ -410,8 +398,6 @@ export default function App() {
usingFallbackData={!!searchData && !searchHasRealData}
mapTheme={mapTheme}
onToggleTheme={() => setMapTheme(prev => prev === 'dark' ? 'light' : 'dark')}
forceSidebarOpen={tutorialForceSidebar}
forceInfoOpen={tutorialForceInfo}
onTutorialStart={() => setTutorialStep(1)}
/>

Expand Down Expand Up @@ -507,6 +493,7 @@ export default function App() {
onSubmit={handleTriage}
onSkip={() => setTriageOpen(false)}
location={activeLocation}
countryCode={countryCode}
landmark={searchData?.landmark}
topContact={topContact}
/>
Expand Down Expand Up @@ -543,6 +530,7 @@ export default function App() {
open={dispatchOpen}
onClose={() => { setDispatchOpen(false); setScenePhoto(null); }}
location={activeLocation}
countryCode={countryCode}
landmark={searchData?.landmark}
contacts={searchData?.contacts || []}
topContact={topContact}
Expand Down
64 changes: 31 additions & 33 deletions frontend/src/components/ContactCard.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Hospital, Shield, Ambulance, Truck, Wrench, Cog, Car, PhoneCall, Navigation, Zap } from 'lucide-react';
import { Hospital, Shield, Ambulance, Truck, Wrench, Cog, Car, PhoneCall, Phone, Navigation, Zap } from 'lucide-react';
import { guardedTelDial } from '../utils/demoMode';

const CATEGORY_CONFIG = {
Expand Down Expand Up @@ -36,40 +36,38 @@ export default function ContactCard({ contact, isLast, variant }) {
else if (typeof distance === 'number' && distance >= 4) statusAttr = 'far';

if (variant === 'popup') {
let catClass = 'cat-neutral';
if (cat === 'hospital' || cat === 'ambulance') catClass = 'cat-medical';
else if (cat === 'police') catClass = 'cat-police';
else if (cat === 'fire') catClass = 'cat-fire';

return (
<div className="popup-variant-card">
<div className="popup-name">{name}</div>
<div className="popup-phone-row">
{callHref ? (
<a
href={callHref}
className="call-btn"
id={`call-btn-${phoneClean}`}
onClick={(e) => guardedTelDial(e, phoneClean, name)}
>
<PhoneCall size={13} strokeWidth={2.4} fill="#fff" />
<span className="call-btn-num">{phone}</span>
</a>
) : (
<div className="call-btn disabled" style={{ opacity: 0.5 }}>
<PhoneCall size={13} strokeWidth={2.4} />
<span className="call-btn-num">{t('actions.no_phone')}</span>
</div>
)}
</div>
<div className="popup-bottom-row">
<div className="popup-km-text">{kmValue} {t('card.km', 'KM')}</div>
{mapsHref && (
<a
href={mapsHref}
className="maps-link"
target="_blank"
rel="noopener noreferrer"
>
<Navigation size={13} color="#1D4ED8" strokeWidth={2.4} />
{t('actions.directions')}
</a>
<div className={`rs-popup-card-v2 ${catClass}`}>
<div className="card-name">{name}</div>
<div className="card-rule"></div>
<div className="card-actions">
{callHref && (
<>
<button
className="ic-btn btn-call"
title="Call"
onClick={(e) => guardedTelDial(e, phoneClean, name)}
>
<svg className="rs-popup-svg" viewBox="0 0 24 24" fill="none" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 1.27h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.91a16 16 0 0 0 6.06 6.06l.91-.91a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7a2 2 0 0 1 1.72 2.01z"/></svg>
</button>
<div className="ic-sep"></div>
</>
)}

<button
className="ic-btn"
title="Directions"
onClick={() => {
if (mapsHref) window.open(mapsHref, '_blank');
}}
>
<svg className="rs-popup-svg" viewBox="0 0 24 24" fill="none" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>
</button>
</div>
</div>
);
Expand Down
53 changes: 52 additions & 1 deletion frontend/src/components/ContactList.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import ContactCard from './ContactCard';
import { ChevronUp } from 'lucide-react';
import { CATS } from '../constants';

// Map filter chip keys to i18n keys. "All" + "Puncture" are filter-only;
Expand Down Expand Up @@ -30,6 +31,39 @@ export default function ContactList({ contacts, loading, error, cachedAt, cat, s
});
}, [contacts, cat]);

const [showTakeUp, setShowTakeUp] = useState(false);

useEffect(() => {
if (filtered.length <= 15) {
setShowTakeUp(false);
return;
}

// 25% threshold, e.g., 19 * 0.25 = 4.75 -> 5th card -> index 4
let thresholdIndex = Math.ceil(filtered.length * 0.25) - 1;

// Cap at the 30th card for large lists (> 120 cards)
if (filtered.length > 120) {
thresholdIndex = 29;
}

const handleScroll = () => {
// Find the card element at the threshold index
const cardEl = document.querySelector(`.svc-list > *:nth-child(${thresholdIndex + 1})`);
if (cardEl) {
const rect = cardEl.getBoundingClientRect();
// Visible if the top of the card has come above the bottom of the viewport
setShowTakeUp(rect.top < window.innerHeight);
}
};

window.addEventListener('scroll', handleScroll, { passive: true });
// Check initially
handleScroll();

return () => window.removeEventListener('scroll', handleScroll);
}, [filtered.length]);

// ── Loading state ─────────────────────────────────────────────────────────
if (loading) {
return (
Expand Down Expand Up @@ -96,6 +130,23 @@ export default function ContactList({ contacts, loading, error, cachedAt, cat, s
))
)}
</div>

{/* Floating Take Up Button */}
{showTakeUp && (
<div className="scroll-up-arrow" onClick={() => {
const firstCard = document.querySelector('.svc-list > *:first-child');
if (firstCard) {
firstCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}}>
<div className="scroll-arrow-pill">
<ChevronUp size={16} strokeWidth={2.5} />
<span>Take up</span>
</div>
</div>
)}
</>
);
}
66 changes: 0 additions & 66 deletions frontend/src/components/CountryEmergency.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,7 @@ const EMERGENCY_CONFIG = [
{ key: 'general', i18nKey: 'emergency.disaster', short: 'DIS', Icon: Phone, color: '#22C55E' },
];

// Rulebook lists six mandatory service categories. The four cards above
// cover police, ambulance and the disaster/general line. These four close
// the gap (hospital, towing, tyre, showroom) by surfacing the nearest
// contact's phone as a one-tap dial when /search returned one. If no
// contact in the category has a phone, the card is skipped (no fake dials).
const NEARBY_CONFIG = [
{ key: 'hospital', i18nKey: 'category.hospital', short: 'HOSP', Icon: Hospital, color: '#DC2626', match: ['hospital'] },
{ key: 'towing', i18nKey: 'category.towing', short: 'TOW', Icon: Truck, color: '#7C3AED', match: ['towing'] },
{ key: 'tyre', i18nKey: 'category.puncture', short: 'TYRE', Icon: Disc, color: '#0284C7', match: ['tyre', 'puncture'] },
{ key: 'showroom', i18nKey: 'category.showroom', short: 'SHOW', Icon: Car, color: '#0F766E', match: ['showroom'] },
];

function pickPhoneForCategory(contacts, match) {
if (!Array.isArray(contacts)) return null;
for (const c of contacts) {
if (!c?.phone) continue;
const cat = (c.category || '').toLowerCase();
if (match.includes(cat)) return c;
}
return null;
}

export default function CountryEmergency({ numbers, contacts }) {
const { t } = useTranslation();
Expand All @@ -39,17 +19,6 @@ export default function CountryEmergency({ numbers, contacts }) {
const { police, ambulance, fire, general, highway } = numbers;
const vals = { police, ambulance, fire, general };

const nearbyCards = NEARBY_CONFIG
.map((cfg) => {
let contact = pickPhoneForCategory(contacts, cfg.match);
// No live towing contact (e.g. fully offline): fall back to the national
// highway assistance helpline so towing stays covered with zero network.
if (!contact && cfg.key === 'towing' && highway) {
contact = { phone: highway };
}
return { cfg, contact };
})
.filter((row) => row.contact);

return (
<>
Expand Down Expand Up @@ -80,41 +49,6 @@ export default function CountryEmergency({ numbers, contacts }) {
);
})}
</div>

{nearbyCards.length > 0 && (
<div className="national-grid" style={{ marginTop: 8 }}>
{nearbyCards.map(({ cfg, contact }) => {
const { key, i18nKey, short, Icon, color } = cfg;
const label = t(i18nKey);
const phone = contact.phone;
return (
<a
key={key}
href={`tel:${phone}`}
className="nat-card"
id={`ce-btn-${key}`}
data-num={phone}
aria-label={`Call ${label}: ${contact.name || phone}`}
title={contact.name || ''}
onClick={(e) => guardedTelDial(e, phone, label)}
>
<div className="nat-icon" style={{ background: color + '22' }}>
<Icon size={20} color={color} strokeWidth={2.3} />
</div>
<div className="nat-body">
<div className="nat-label" data-short={short}>{label}</div>
<div
className="nat-num"
style={{ fontSize: 13, color, letterSpacing: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{phone}
</div>
</div>
</a>
);
})}
</div>
)}
</>
);
}
8 changes: 6 additions & 2 deletions frontend/src/components/DispatchScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import React, { useEffect, useState } from 'react';
import { Check, Ambulance, Navigation2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { DEMO_MODE } from '../utils/demoMode';
import { getEmergencyNumbers } from '../utils/emergencyNumbers';

/**
* DispatchScreen — full-screen "Help is on the way" overlay.
* Shown after SOS dispatched. Mirrors the FinalDispatch (Glass) design.
*/
export default function DispatchScreen({ open, onClose, location, landmark, contacts = [], topContact, dispatchedAt, isCrash = false, triageReason = null, scenePhoto = null }) {
export default function DispatchScreen({ open, onClose, location, landmark, contacts = [], topContact, dispatchedAt, isCrash = false, triageReason = null, scenePhoto = null, countryCode }) {
const { t, i18n } = useTranslation();
const isEnglish = i18n.language.startsWith('en');
const [elapsed, setElapsed] = useState(0);
const [batteryPct, setBatteryPct] = useState(null);
const [batteryCharging, setBatteryCharging] = useState(false);

const numbers = getEmergencyNumbers(countryCode || location?.country_code || 'IN') || { general: '112' };
const emergencyNum = numbers.general || numbers.police || '112';

useEffect(() => {
if (!open) return;
const start = Date.now();
Expand Down Expand Up @@ -264,7 +268,7 @@ export default function DispatchScreen({ open, onClose, location, landmark, cont
</svg>
</div>
<div className="dx-circle-body">
<div className="dx-circle-name">112 Unified</div>
<div className="dx-circle-name">{emergencyNum} Unified</div>
<div className="dx-circle-role">{t('dispatch.role_emergency', 'Emergency')}</div>
</div>
<div className="dx-circle-status">
Expand Down
Loading
Loading