Logos & scanner changes
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 34 KiB |
@@ -30,7 +30,7 @@
|
||||
--inverse-surface: #2e3132;
|
||||
--inverse-on-surface: #eff1f2;
|
||||
--inverse-primary: #91d78a;
|
||||
--surface-tint: #2a6b2c;
|
||||
--surface-tint: #8ccb8e;
|
||||
--surface-dim: #d8dadb;
|
||||
--surface-bright: #f8fafb;
|
||||
--surface-variant: #e1e3e4;
|
||||
|
||||
@@ -133,11 +133,8 @@
|
||||
}
|
||||
|
||||
.home-brand-name {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media (max-height: 700px) {
|
||||
|
||||
@@ -6,8 +6,8 @@ function HomeView({ onScanClick, onSearchClick }) {
|
||||
<div className="home-view">
|
||||
<div className="home-hero">
|
||||
<div className="home-logo-wrapper">
|
||||
<img src="/logo.png" alt="FarmaClic" className="home-logo" />
|
||||
<h1 className="home-brand-name">FarmaClic</h1>
|
||||
<img src="/farmaclic_logo.png" alt="FarmaClic" className="home-logo" />
|
||||
<img src="/farmaclic_text.png" alt="FarmaClic" className="home-brand-name" />
|
||||
</div>
|
||||
<p className="home-desc">Encuentra tus medicamentos en farmacias cercanas</p>
|
||||
</div>
|
||||
|
||||
@@ -358,3 +358,87 @@
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Photo upload section */
|
||||
.upload-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.upload-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--on-surface-variant);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.upload-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.upload-option {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.25rem 0.75rem;
|
||||
background: var(--surface-container-lowest);
|
||||
border: 2px solid var(--outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.upload-option:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-container);
|
||||
}
|
||||
|
||||
.upload-option:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.upload-option svg {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.upload-hidden-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Photo preview */
|
||||
.photo-preview-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.photo-preview-container {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
max-height: 22rem;
|
||||
}
|
||||
|
||||
.photo-preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
max-height: 22rem;
|
||||
}
|
||||
|
||||
.photo-preview-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
const [loadingPrescriptions, setLoadingPrescriptions] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
// Photo upload state
|
||||
const [previewUrl, setPreviewUrl] = useState(null);
|
||||
const [ocrLoading, setOcrLoading] = useState(false);
|
||||
const cameraInputRef = useRef(null);
|
||||
const galleryInputRef = useRef(null);
|
||||
|
||||
const isNative = Capacitor.isNativePlatform();
|
||||
|
||||
const fetchPrescriptions = useCallback(async (cip) => {
|
||||
@@ -237,6 +243,61 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
fetchPrescriptions(cip);
|
||||
}
|
||||
|
||||
// Photo upload handlers
|
||||
function handlePhotoSelect(e) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setErrorMsg('');
|
||||
setPreviewUrl(URL.createObjectURL(file));
|
||||
setPhase('photo-preview');
|
||||
// Reset input so selecting the same file again triggers onChange
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
async function handlePhotoOcr() {
|
||||
if (!previewUrl) return;
|
||||
setOcrLoading(true);
|
||||
setErrorMsg('');
|
||||
try {
|
||||
const res = await fetch(previewUrl);
|
||||
const blob = await res.blob();
|
||||
const formData = new FormData();
|
||||
formData.append('photo', blob, 'tsi.jpg');
|
||||
|
||||
const ocrRes = await fetch('/api/tsi/ocr', { method: 'POST', body: formData });
|
||||
const data = await ocrRes.json();
|
||||
|
||||
if (!ocrRes.ok) {
|
||||
setErrorMsg(data.error || 'No se pudo leer la imagen. Intenta con otra foto.');
|
||||
setPhase('error');
|
||||
return;
|
||||
}
|
||||
|
||||
const cip = data.cip;
|
||||
if (!CIP_REGEX.test(cip)) {
|
||||
setErrorMsg(`CIP detectado "${cip}" no tiene formato válido. Introduce el código manualmente.`);
|
||||
setPhase('error');
|
||||
return;
|
||||
}
|
||||
|
||||
playBeep();
|
||||
setScannedCip(cip);
|
||||
setPreviewUrl(null);
|
||||
setPhase('prescriptions');
|
||||
fetchPrescriptions(cip);
|
||||
} catch (err) {
|
||||
setErrorMsg(`Error al procesar la imagen: ${err.message || 'Error desconocido'}`);
|
||||
setPhase('error');
|
||||
} finally {
|
||||
setOcrLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePhotoDiscard() {
|
||||
setPreviewUrl(null);
|
||||
setPhase('idle');
|
||||
}
|
||||
|
||||
function handlePickPrescription(rx) {
|
||||
stopCamera();
|
||||
onSelectMedicine(rx.name);
|
||||
@@ -248,6 +309,9 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
setPhase('idle');
|
||||
setPrescriptions([]);
|
||||
setScannedCip(null);
|
||||
} else if (phase === 'photo-preview') {
|
||||
setPreviewUrl(null);
|
||||
setPhase('idle');
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
@@ -285,13 +349,51 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
)}
|
||||
|
||||
{phase === 'idle' && (
|
||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handleStartScan}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
Abrir cámara
|
||||
</button>
|
||||
<>
|
||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handleStartScan}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
Abrir cámara
|
||||
</button>
|
||||
|
||||
<div className="upload-section">
|
||||
<label className="upload-label">O sube una foto de tu TSI</label>
|
||||
<div className="upload-row">
|
||||
<button className="upload-option" onClick={() => cameraInputRef.current?.click()}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
|
||||
<circle cx="12" cy="13" r="4" />
|
||||
</svg>
|
||||
<span>Hacer foto</span>
|
||||
</button>
|
||||
<button className="upload-option" onClick={() => galleryInputRef.current?.click()}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
</svg>
|
||||
<span>Subir de galería</span>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref={cameraInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={handlePhotoSelect}
|
||||
className="upload-hidden-input"
|
||||
/>
|
||||
<input
|
||||
ref={galleryInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handlePhotoSelect}
|
||||
className="upload-hidden-input"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{phase === 'scanning' && !isNative && (
|
||||
@@ -314,6 +416,38 @@ function ScannerView({ onClose, onSelectMedicine }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === 'photo-preview' && (
|
||||
<div className="photo-preview-panel">
|
||||
<div className="photo-preview-container">
|
||||
<img src={previewUrl} alt="TSI capturada" className="photo-preview-img" />
|
||||
</div>
|
||||
{ocrLoading ? (
|
||||
<div className="scanning-active">
|
||||
<div className="scanner-spinner" />
|
||||
<p>Procesando imagen…</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="photo-preview-actions">
|
||||
<button className="scan-btn scan-btn--primary scan-btn--start" onClick={handlePhotoOcr}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="4 7 4 4 20 4 20 7" />
|
||||
<line x1="9" y1="20" x2="15" y2="20" />
|
||||
<line x1="12" y1="4" x2="12" y2="20" />
|
||||
</svg>
|
||||
Escanear imagen
|
||||
</button>
|
||||
<button className="scan-btn scan-btn--ghost" onClick={handlePhotoDiscard}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
Descartar
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="cip-form" onSubmit={handleManualSubmit}>
|
||||
<label className="cip-label" htmlFor="cip-input">O introduce el código CIP manualmente</label>
|
||||
<div className="cip-row">
|
||||
|
||||