{isVerified ? 'ANALYSIS METRICS' : 'TOP CONCERNS'}
Sign up to access overall analysis report
// src/pages/AnalysisResultPage.jsx import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { loadFromDB, saveToDB } from './db'; import jsPDF from 'jspdf'; import html2canvas from 'html2canvas'; import API_BASE_URL from './config'; const AnalysisResultPage = () => { const navigate = useNavigate(); const errorDescriptions = { 'error_below_min_image_size': 'Input image resolution is too small', 'error_exceed_max_image_size': 'Input image resolution is too large', 'error_src_skin_too_small': 'The skin area in the uploaded image is too small. The width of the skin needs to be greater than 60% of the width of the image.', 'error_src_skin_out_of_bound': 'The skin area in the uploaded image is out of bound', 'error_lighting_dark': 'The lighting in the uploaded image is too dark' }; const [analysisData, setAnalysisData] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [selectedMetric, setSelectedMetric] = useState(null); const [activeMask, setActiveMask] = useState(null); const [showSignupModal, setShowSignupModal] = useState(false); const [showHelpModal, setShowHelpModal] = useState(false); const [isVerified, setIsVerified] = useState(false); // Form State const [formData, setFormData] = useState({ name: '', phone: '' }); const [formErrors, setFormErrors] = useState({ name: '', phone: '' }); // Report Ref const reportRef = useRef(null); // OTP State const [showOtpModal, setShowOtpModal] = useState(false); const [otp, setOtp] = useState(new Array(6).fill('')); const [otpError, setOtpError] = useState(''); const [isVerifying, setIsVerifying] = useState(false); const [isSendingReport, setIsSendingReport] = useState(false); const [isSubmittingSignup, setIsSubmittingSignup] = useState(false); const [toast, setToast] = useState({ show: false, message: '' }); // WhatsApp API configuration has been moved to backend // Track the generated OTP for verification (frontend-only simulation) const [generatedOtp, setGeneratedOtp] = useState(''); // Validation Regex const nameRegex = /^[a-zA-Z\s]*$/; const phoneRegex = /^\d{0,10}$/; // Ref for PDF capture const pdfRef = useRef(null); useEffect(() => { const loadAnalysisData = async () => { try { const urlParams = new URLSearchParams(window.location.search); const phone = urlParams.get('phone'); const reportId = urlParams.get('report'); if (phone && reportId) { // We are coming from a verification link try { const res = await fetch(`${API_BASE_URL}/api/reports/${reportId}`); if (res.ok) { const savedReport = await res.json(); setAnalysisData(savedReport.data); setFormData(prev => ({ ...prev, phone: phone, name: savedReport.userName || '' })); // Skip OTP when clicking the direct report link or from admin panel setIsVerified(true); setIsLoading(false); return; } } catch (e) { console.error("Failed to load report", e); } } const storedData = await loadFromDB('analysisData'); if (!storedData) { throw new Error('No analysis data found. Please start a new analysis.'); } setAnalysisData(storedData); setIsLoading(false); } catch (err) { setError(err.message); setIsLoading(false); } }; loadAnalysisData(); }, []); const handleNewAnalysis = async () => { await saveToDB('analysisData', null); navigate('/'); }; const getScoreColor = (score) => { if (!score && score !== 0) return 'bg-gray-600'; if (score >= 70) return 'bg-emerald-500'; if (score >= 40) return 'bg-yellow-500'; return 'bg-[#0097C3]'; }; const getMetricDisplayName = (metricKey) => { const displayNames = { wrinkle: 'Wrinkles', radiance: 'Radiance', firmness: 'Firmness', oiliness: 'Oiliness', redness: 'Redness', dark_circle_v2: 'Dark Circles', pore: 'Pores', eye_bag: 'Eye Bags', texture: 'Texture', age_spot: 'Age Spots', droopy_lower_eyelid: 'Droopy Lower Eyelid', moisture: 'Moisture', droopy_upper_eyelid: 'Droopy Upper Eyelid', acne: 'Acne' }; return displayNames[metricKey] || metricKey.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); }; const handleMetricClick = (metric) => { if (metric.output_mask_name) { const maskData = analysisData?.maskImages?.[metric.output_mask_name]; if (maskData) { setActiveMask(maskData); setSelectedMetric(metric.key); } else { setActiveMask(null); setSelectedMetric(null); } } else { setActiveMask(null); setSelectedMetric(null); } }; const handleClearMask = () => { setActiveMask(null); setSelectedMetric(null); }; const handleNameChange = (e) => { const val = e.target.value; if (nameRegex.test(val)) { setFormData(prev => ({ ...prev, name: val })); if (formErrors.name) setFormErrors(prev => ({ ...prev, name: '' })); } }; const handlePhoneChange = (e) => { const val = e.target.value; if (phoneRegex.test(val)) { setFormData(prev => ({ ...prev, phone: val })); if (formErrors.phone) setFormErrors(prev => ({ ...prev, phone: '' })); } }; // OTP Handlers const handleOtpChange = (element, index) => { if (isNaN(element.value)) return false; setOtp([...otp.map((d, idx) => (idx === index ? element.value : d))]); // Focus next input if (element.nextSibling && element.value !== "") { element.nextSibling.focus(); } }; const sendWhatsappOtp = async (phone) => { // Generate a unique ID for this specific analysis report const reportId = Math.random().toString(36).substring(7); const currentUrl = window.location.origin + window.location.pathname; const verificationLink = `${currentUrl}?phone=${phone}&report=${reportId}`; let otpToUse = ''; let isExisting = false; try { // 1. Create/Update User & Get OTP from Backend const userRes = await fetch(`${API_BASE_URL}/api/users`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: formData.name, phone: phone }) }); const userResText = await userRes.text(); let userData; try { userData = JSON.parse(userResText); } catch (e) { console.error('User API returned non-JSON:', userResText); throw new Error('Could not connect to user API properly. Please try again.'); } if (userData.success) { otpToUse = userData.user.otp; isExisting = userData.user.isExisting || false; setGeneratedOtp(otpToUse); } else if (userData.message && userData.message.includes('already registered')) { // If backend says already registered, we treat as existing user isExisting = true; console.log("User already registered, proceeding as existing user"); } else { const errorMsg = userData.message || 'Failed to register user'; throw new Error(errorMsg); } // 2. Save Report to Backend await fetch(`${API_BASE_URL}/api/reports`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reportId, phone, userName: formData.name, data: analysisData, link: verificationLink }) }); console.log(`Sending WhatsApp message request to backend for ${phone}`); const response = await fetch(`${API_BASE_URL}/api/whatsapp/send-otp`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: phone, name: formData.name, otpToUse: otpToUse, verificationLink: verificationLink, isExisting: isExisting }) }); if (!response.ok) { const waErrText = await response.text(); let errorData; try { errorData = JSON.parse(waErrText); } catch (e) { errorData = { message: waErrText.substring(0, 100) }; } console.error('Backend WhatsApp API Error:', errorData); throw new Error(errorData.message || 'Failed to send WhatsApp message'); } console.log('WhatsApp message sent successfully via backend'); return { ...(userData.user || {}), isExisting: isExisting }; } catch (err) { console.error('Error in sendWhatsappOtp:', err); throw err; } }; const verifyWhatsappOtp = async (enteredOtp) => { try { const res = await fetch(`${API_BASE_URL}/api/verify-otp`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: formData.phone, otp: enteredOtp }) }); const data = await res.json(); return data.success; } catch (e) { console.error("OTP verification error", e); return false; } }; const sendReportToWhatsapp = async () => { setIsSendingReport(true); try { // Sending a follow-up message with the overall score via backend const response = await fetch(`${API_BASE_URL}/api/whatsapp/send-report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: formData.phone, name: formData.name, overallScore: overallScore, origin: window.location.origin }) }); if (!response.ok) throw new Error('Failed to send report'); alert("Report details sent to your WhatsApp!"); } catch (err) { console.error('Error sending report:', err); alert("Failed to send report. Please try again."); } finally { setIsSendingReport(false); } }; const handleSignupSubmit = async () => { // const db = JSON.parse(localStorage.getItem('limelite_users') || '{}'); // const existingUser = db[formData.phone]; const existingUser = false; // logic moved to backend, or we can check via api but it's fine let errors = {}; if (!formData.name.trim()) errors.name = 'Name is required'; if (formData.phone.length !== 10) errors.phone = 'Number must be exactly 10 digits'; // We allow both new and existing users to proceed // If they are existing, the backend call in sendWhatsappOtp will handle it if (Object.keys(errors).length > 0) { setFormErrors(errors); return; } setIsSubmittingSignup(true); try { const user = await sendWhatsappOtp(formData.phone); setShowSignupModal(false); if (user && user.isExisting) { setIsVerified(true); setToast({ show: true, message: "You're already an user, welcome back!" }); setTimeout(() => setToast({ show: false, message: '' }), 4000); } else { setShowOtpModal(true); setOtp(new Array(6).fill('')); } } catch (err) { setFormErrors({ phone: err.message || 'Failed to send message. Please try again.' }); } finally { setIsSubmittingSignup(false); } }; const handleVerifyOtp = async () => { const enteredOtp = otp.join(''); if (enteredOtp.length !== 6) { setOtpError('Please enter a valid 6-digit OTP'); return; } setIsVerifying(true); setOtpError(''); try { const isValid = await verifyWhatsappOtp(enteredOtp); if (isValid) { alert("OTP Verified Successfully!"); setShowOtpModal(false); setIsVerified(true); } else { setOtpError('Invalid OTP. Please try again.'); } } catch (error) { setOtpError('Verification failed. Please try again.'); } finally { setIsVerifying(false); } }; const getScoreValue = (data, key) => { if (!data || typeof data !== 'object') return null; if (data[key] && typeof data[key] === 'object') return data[key].ui_score || data[key].score || data[key].raw_score || null; if (data[key] !== undefined && data[key] !== null) return data[key]; return null; }; const hasDetailedData = (data, key) => { return data && data[key] && typeof data[key] === 'object' && data[key].ui_score !== undefined; }; // --- START OF MODIFICATION --- /** * Adjusts the score by -5 and ensures it's not negative. * @param {number|string} score The original score. * @returns {number|null} The adjusted score or null. */ const getAdjustedScore = (score) => { const numScore = Number(score); if (isNaN(numScore) || score === null || score === undefined) return null; // Apply -5 adjustment and ensure the score is not negative. return Math.max(0, numScore - 5); }; // --- END OF MODIFICATION --- const getDisplayMetrics = (scoreData) => { if (!scoreData) return []; return Object.keys(scoreData) .filter(key => !['all', 'skin_age', 'takeaways', 'resize', 'image', 'url', 'resize_image', 'image_size'].includes(key)) .map(key => { const rawScore = hasDetailedData(scoreData, key) ? scoreData[key].ui_score : getScoreValue(scoreData, key); // --- START OF MODIFICATION --- const adjustedUiScore = getAdjustedScore(rawScore); // --- END OF MODIFICATION --- return { key, name: getMetricDisplayName(key), // Use the adjusted score for display ui_score: adjustedUiScore, raw_score: hasDetailedData(scoreData, key) ? scoreData[key].raw_score : null, output_mask_name: hasDetailedData(scoreData, key) ? scoreData[key].output_mask_name : null, hasDetails: hasDetailedData(scoreData, key) }; }); }; const renderUserImage = () => { const originalImage = analysisData?.originalImage; if (!originalImage) return null; return (
Analyzing skin profile...
{errorDescriptions[error] || error}
No analysis data available.
Sign up to access overall analysis report
{formErrors.name}
}{formErrors.phone}
}Please enter the OTP sent to you on whatsapp.
{otpError}
}To access previous reports, please contact us at hello@limelite.com
If you experience any technical issues, please contact us at support@example.com
Name
{formData.name || 'Anonymous'}
Contact Number
+91 {formData.phone || 'N/A'}
Analysis Date
{(() => { const d = new Date(); const day = d.getDate(); const suffix = (day > 3 && day < 21) || day % 10 > 3 ? 'th' : ['th', 'st', 'nd', 'rd'][day % 10]; return `${day}${suffix} ${d.toLocaleString('en-GB', { month: 'long' })} ${d.getFullYear()}`; })()}
Skin Health
Advanced Hydration: Apply hyaluronic acid serum daily to stabilize moisture levels.
Dermal Protection: Broad-spectrum physical sunshade is mandatory for UV protection.
Clinical Intervention: Visit your specialist for focused treatment on top priority zones.
* This diagnostic data is interpreted by Limelite AI Skin Vision. For detailed medical prescriptions, please book a 1-on-1 therapist consultation.
LIMELITE CLINICAL SYSTEMS
support@limelite.com | limelite.com