Logos & scanner changes
Run Tests on Branches / Backend Tests (push) Successful in 3m41s
Run Tests on Branches / Frontend Tests (push) Successful in 3m27s

This commit is contained in:
Antoni Nuñez Romeu
2026-07-04 14:46:42 +02:00
parent 3b725df118
commit df0b114058
15 changed files with 414 additions and 19 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+1 -1
View File
@@ -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;
+2 -5
View File
@@ -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) {
+2 -2
View File
@@ -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>
+84
View File
@@ -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;
}
+141 -7
View File
@@ -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">