Files
limelite_faceanalyzer/frontend/src/AnalysisResultPage.jsx
T

1009 lines
51 KiB
React

// 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 (
<div className="relative w-full h-full flex items-center justify-center overflow-hidden bg-[#18181b]">
<img
src={originalImage}
alt="Skin Analysis"
className={`w-full h-full object-cover transition-opacity duration-500 ${activeMask ? 'opacity-40' : 'opacity-100'}`}
/>
{activeMask && (
<img
src={activeMask}
alt="Analysis Mask"
className="absolute inset-0 w-full h-full object-cover animate-pulse-slow"
/>
)}
</div>
);
};
const handleDownloadPDF = async () => {
if (!reportRef.current) return;
// Show the hidden report briefly for capture
const reportElement = reportRef.current;
reportElement.style.display = 'block';
try {
const canvas = await html2canvas(reportElement, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff'
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
pdf.save(`Limelite_Skin_Report_${formData.name || 'User'}.pdf`);
} catch (err) {
console.error("PDF Generation Error:", err);
alert("Failed to generate PDF. Please try again.");
} finally {
reportElement.style.display = 'none';
}
};
if (isLoading) return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-4 border-white/10 border-t-[#0097C3] rounded-full animate-spin"></div>
<p className="text-gray-300 animate-pulse">Analyzing skin profile...</p>
</div>
</div>
);
if (error) return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="glass-panel p-8 rounded-none shadow-xl max-w-md w-full text-center border border-white/10">
<div className="w-16 h-16 bg-[#0097C3]/20 rounded-none flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-[#0097C3]" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</div>
<h2 className="text-xl font-bold text-white mb-2">Analysis Failed</h2>
<p className="text-gray-300 mb-6">{errorDescriptions[error] || error}</p>
<button onClick={handleNewAnalysis} className="w-full py-3 bg-[#0097C3] hover:bg-[#007ba0] text-white rounded-none font-bold transition-colors shadow-lg shadow-[#0097C3]/30">
Try Again
</button>
</div>
</div>
);
if (!analysisData || !analysisData.scoreData) return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-gray-300 mb-4">No analysis data available.</p>
<button onClick={handleNewAnalysis} className="px-6 py-2 bg-[#0097C3] text-white rounded-none shadow-lg">Start New Analysis</button>
</div>
</div>
);
const { scoreData } = analysisData;
const displayMetrics = getDisplayMetrics(scoreData);
// --- START OF MODIFICATION ---
const rawOverallScore = getScoreValue(scoreData, 'all');
const overallScore = getAdjustedScore(rawOverallScore); // Adjusted overall score
// --- END OF MODIFICATION ---
const skinAge = getScoreValue(scoreData, 'skin_age');
// Displayed skin age is 5 years younger than the calculated skin_age.
// Ensure it's a number and not negative. Round to nearest integer for display.
const displayedSkinAge = (skinAge !== null && skinAge !== undefined && !isNaN(Number(skinAge)))
? Math.max(0, Math.round(Number(skinAge) - 5))
: null;
const takeaways = scoreData.takeaways || [];
// Calculate stroke dasharray for the main score circle
const radius = 54;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - ((overallScore || 0) / 100) * circumference;
// --- SORTING FOR TOP CONCERNS ---
// Filter to get valid scores and sort by lowest score first
const displayedMetricsList = isVerified
? displayMetrics.filter(m => m.ui_score !== null).sort((a, b) => a.ui_score - b.ui_score)
: displayMetrics.filter(m => m.ui_score !== null).sort((a, b) => a.ui_score - b.ui_score).slice(0, 3);
return (
<div className="h-screen overflow-hidden bg-[#18181b] font-sans relative flex flex-col">
{/* Header (Matching App.js) */}
<header className="fixed top-0 left-0 right-0 z-50 py-4 px-6 flex items-center justify-between bg-[#0097C3] shadow-lg">
<div className="flex items-center gap-4">
<img src={process.env.PUBLIC_URL + '/assets/images/limelite_logo.png'} alt="Limelite Logo" className="h-12 md:h-16 w-auto brightness-0 invert" />
</div>
<div className="flex items-center gap-4">
<button
onClick={() => setShowHelpModal(true)}
className="text-white hover:text-white/80 transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<button onClick={handleNewAnalysis} className="text-white hover:text-white/80 transition-colors">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</header>
{/* Main Content */}
<main ref={pdfRef} className="flex-1 flex flex-col relative z-10 min-h-0 bg-[#18181b]">
{/* Image Section */}
<div className="relative w-full flex-1 min-h-0 bg-[#18181b]">
{renderUserImage()}
</div>
{/* Results Panel - Dark Theme (Rectangular) */}
<div className="bg-[#18181b] px-5 pb-4 relative z-20 shrink-0">
{/* Main Score Row - With Border Separator */}
<div className="flex items-center justify-between border-b border-white/20 pb-2 mb-2">
<div className="flex items-center gap-4">
<span className="text-[5rem] leading-none font-normal text-white tracking-tighter">{overallScore?.toFixed(0)}</span>
<div className="flex items-center gap-2 mt-2">
<span className="text-xl text-white font-normal">Skin Health</span>
</div>
</div>
{activeMask && (
<button
onClick={handleClearMask}
className="text-white hover:text-[#0097C3] transition-colors flex items-center mb-2"
title="Show original image"
>
<svg className="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
)}
</div>
{/* Top Concerns */}
<div>
<h4 className="text-[11px] uppercase tracking-widest text-white font-bold mb-2 ml-1">{isVerified ? 'ANALYSIS METRICS' : 'TOP CONCERNS'}</h4>
<div className={`flex gap-2 pb-1 ${isVerified ? 'overflow-x-auto scrollbar-hide -mr-5 pr-5' : 'overflow-hidden -mr-5'}`}>
{displayedMetricsList.map((metric, idx) => (
<div
key={metric.key}
onClick={() => isVerified && handleMetricClick(metric)}
className={`bg-[#2B2B2B] rounded-none flex flex-col items-center justify-between p-2 h-[145px] flex-shrink-0 w-[30%] relative group transition-all duration-300 ${isVerified ? 'cursor-pointer hover:bg-[#333]' : ''}`}
style={{
border: selectedMetric === metric.key ? '1px solid transparent' : '1px solid #333',
backgroundImage: selectedMetric === metric.key ? 'linear-gradient(#2B2B2B, #2B2B2B), linear-gradient(135deg, #ffffff 0%, rgba(255,255,255,0.1) 40%, rgba(255,255,255,0.1) 60%, #ffffff 100%)' : 'none',
backgroundOrigin: 'border-box',
backgroundClip: 'padding-box, border-box'
}}
>
<div className="text-center flex flex-col items-center w-full mt-1 gap-1">
<span className="text-3xl font-medium text-white">{metric.ui_score}</span>
<span className="text-[12px] text-gray-200 font-medium leading-tight text-center h-10 flex items-center justify-center px-1 uppercase tracking-wide">{metric.name}</span>
</div>
{/* Badge */}
<div className={`px-3 py-1 rounded-full text-[11px] font-bold uppercase min-w-[65px] text-center ${metric.ui_score >= 70 ? 'bg-emerald-500/20 text-[#4ade80] border border-emerald-500/30' :
metric.ui_score >= 40 ? 'bg-amber-500/20 text-[#fbbf24] border border-amber-500/30' :
'bg-[#0097C3]/20 text-[#0097C3] border border-[#0097C3]/30'
}`}>
{metric.ui_score >= 70 ? 'Good' : metric.ui_score >= 40 ? 'Average' : 'Bad'}
</div>
</div>
))}
{!isVerified && (
<div className="bg-[#2B2B2B] rounded-none flex items-center justify-center border border-white/5 h-[145px] flex-shrink-0 w-[30%] relative">
<svg className="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
)}
</div>
</div>
</div>
{/* Footer CTA Section */}
{!isVerified ? (
<div className="bg-[#18181b] px-5 py-6 flex flex-col gap-4 relative z-30 shrink-0 border-t border-white/10">
<button
onClick={() => setShowSignupModal(true)}
className="w-full bg-[#0097C3] hover:bg-[#007ba0] text-white h-14 rounded-none font-bold text-lg shadow-sm flex items-center justify-center gap-3 transition-all transform active:scale-95 relative"
>
<span className="text-[1.1rem]">Access Full Report</span>
<div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center">
<svg className="w-3.5 h-3.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
</div>
</button>
<p className="text-center text-xs text-gray-400 font-medium">
Sign up to access overall analysis report
</p>
</div>
) : (
<div className="bg-[#18181b] px-5 py-6 flex flex-col gap-4 relative z-30 shrink-0 border-t border-white/10">
<button
onClick={handleDownloadPDF}
className="w-full bg-[#0097C3] hover:bg-[#007ba0] text-white h-14 rounded-none font-bold text-lg shadow-sm flex items-center justify-center gap-3 transition-all transform active:scale-95 relative"
>
<span className="text-[1.1rem]">Download Report</span>
<div className="w-6 h-6 bg-white/20 rounded-full flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M17.25 9.45L12 14.7l-5.25-5.25M12 3v11.7" />
</svg>
</div>
</button>
</div>
)}
</main>
{/* Signup Modal */}
{showSignupModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white w-full max-w-sm p-6 relative shadow-2xl animate-[fadeIn_0.2s_ease-out]">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-black">Sign Up</h2>
<button
onClick={() => setShowSignupModal(false)}
className="text-black transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="flex flex-col gap-6">
<div>
<label className="block text-gray-700 text-sm font-medium mb-1">
Full Name<span className="text-[#0097C3]">*</span>
</label>
<input
type="text"
placeholder="Enter your First Name"
value={formData.name}
onChange={handleNameChange}
className={`w-full border-b border-dashed py-2 text-base text-black placeholder-gray-400 focus:outline-none transition-colors bg-transparent ${formErrors.name ? 'border-[#0097C3]' : 'border-gray-400 focus:border-[#0097C3]'}`}
/>
{formErrors.name && <p className="text-[#0097C3] text-xs mt-1">{formErrors.name}</p>}
</div>
<div>
<label className="block text-gray-700 text-sm font-medium mb-1">
Whatsapp Number<span className="text-[#0097C3]">*</span>
</label>
<input
type="tel"
placeholder="Enter your Whatsapp Number"
value={formData.phone}
onChange={handlePhoneChange}
className={`w-full border-b border-dashed py-2 text-base text-black placeholder-gray-400 focus:outline-none transition-colors bg-transparent ${formErrors.phone ? 'border-[#0097C3]' : 'border-gray-400 focus:border-[#0097C3]'}`}
/>
{formErrors.phone && <p className="text-[#0097C3] text-[13px] mt-2 font-medium leading-tight">{formErrors.phone}</p>}
</div>
<button
onClick={handleSignupSubmit}
disabled={isSubmittingSignup}
className="w-full bg-[#0097C3] hover:bg-[#007ba0] text-white py-3 font-semibold text-lg mt-4 transition-colors disabled:opacity-70 flex items-center justify-center h-14"
>
{isSubmittingSignup ? (
<div className="flex items-center gap-3">
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<span>Verifying...</span>
</div>
) : 'Submit'}
</button>
</div>
</div>
</div>
)}
{/* OTP Modal */}
{showOtpModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white w-full max-w-sm p-6 relative shadow-2xl animate-[fadeIn_0.2s_ease-out]">
<div className="flex justify-between items-center mb-2">
<h2 className="text-2xl font-bold text-black">OTP Verification</h2>
<button
onClick={() => setShowOtpModal(false)}
className="text-black transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p className="text-sm text-black mb-8 mt-4 font-medium">
Please enter the OTP sent to you on whatsapp.
</p>
<div className="flex justify-between gap-2 mb-8">
{otp.map((data, index) => (
<input
className="w-11 h-11 border border-gray-300 rounded-md text-center text-xl focus:border-[#0097C3] focus:outline-none font-medium text-black"
type="text"
name="otp"
maxLength="1"
key={index}
value={data}
onChange={e => handleOtpChange(e.target, index)}
onFocus={e => e.target.select()}
/>
))}
</div>
{otpError && <p className="text-[#0097C3] text-sm mb-4 text-center">{otpError}</p>}
<button
onClick={handleVerifyOtp}
disabled={isVerifying}
className="w-full bg-[#0097C3] hover:bg-[#007ba0] text-white py-4 font-semibold text-lg transition-colors disabled:opacity-70 flex items-center justify-center"
>
{isVerifying ? (
<div className="w-6 h-6 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
) : 'Verify OTP'}
</button>
</div>
</div>
)}
{/* Help Modal */}
{showHelpModal && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
<div className="bg-white w-full max-w-sm p-8 rounded-none relative">
<div className="flex justify-between items-center mb-6">
<h2 className="text-[28px] font-bold text-black">Help</h2>
<button
onClick={() => setShowHelpModal(false)}
className="text-black transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-6">
<div className="flex gap-3">
<span className="text-black/40 mt-1.5 font-bold">•</span>
<p className="text-[15px] leading-relaxed text-black/70 font-medium">
To access previous reports, please contact us at <span className="text-black font-semibold">hello@limelite.com</span>
</p>
</div>
<div className="flex gap-3">
<span className="text-black/40 mt-1.5 font-bold">•</span>
<p className="text-[15px] leading-relaxed text-black/70 font-medium">
If you experience any technical issues, please contact us at <span className="text-black font-semibold">support@example.com</span>
</p>
</div>
</div>
</div>
</div>
)}
{/* Toast Notification */}
{toast.show && (
<div className="fixed top-24 right-6 z-[200] animate-[fadeIn_0.3s_ease-out]">
<div className="bg-white text-[#0097C3] px-6 py-4 shadow-[0_10px_40px_rgba(0,0,0,0.15)] flex items-center gap-3 border-l-4 border-[#0097C3]">
<div className="bg-[#0097C3]/10 p-1.5 rounded-full">
<svg className="w-5 h-5 text-[#0097C3]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="font-bold tracking-tight text-sm uppercase">{toast.message}</span>
</div>
</div>
)}
{/* Premium Hidden PDF Report Template */}
<div
ref={reportRef}
style={{
display: 'none',
width: '210mm',
minHeight: '297mm',
backgroundColor: '#ffffff',
padding: '15mm',
color: '#1a1a1a',
fontFamily: "'Inter', sans-serif",
boxSizing: 'border-box'
}}
>
{/* Header Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<div style={{ width: '120px' }}>
<img src={process.env.PUBLIC_URL + '/assets/images/limelite_logo.png'} style={{ height: '45px', objectFit: 'contain' }} alt="Logo" />
</div>
<div style={{ flex: 1, textAlign: 'center' }}>
<h1 style={{ fontSize: '28px', fontWeight: '900', margin: 0, textTransform: 'uppercase', letterSpacing: '0.02em', color: '#0097C3', fontFamily: "'Playfair Display', serif" }}>Skin Analysis Report</h1>
</div>
<div style={{ width: '120px' }}></div>
</div>
{/* Accent Line */}
<div style={{ height: '2px', backgroundColor: '#0097C3', marginBottom: '25px' }}></div>
{/* Info Row (Name, Phone, Date) */}
<div style={{ backgroundColor: '#f3f4f6', padding: '18px 25px', display: 'flex', justifyContent: 'space-between', marginBottom: '30px' }}>
<div style={{ flex: 1.5 }}>
<p style={{ margin: 0, fontSize: '10px', color: '#666', textTransform: 'uppercase', fontWeight: '700', letterSpacing: '1px' }}>Name</p>
<p style={{ margin: '4px 0 0 0', fontSize: '16px', fontWeight: '800', color: '#000' }}>{formData.name || 'Anonymous'}</p>
</div>
<div style={{ flex: 1.5 }}>
<p style={{ margin: 0, fontSize: '10px', color: '#666', textTransform: 'uppercase', fontWeight: '700', letterSpacing: '1px' }}>Contact Number</p>
<p style={{ margin: '4px 0 0 0', fontSize: '16px', fontWeight: '800', color: '#000' }}>+91 {formData.phone || 'N/A'}</p>
</div>
<div style={{ flex: 1, textAlign: 'right' }}>
<p style={{ margin: 0, fontSize: '10px', color: '#666', textTransform: 'uppercase', fontWeight: '700', letterSpacing: '1px' }}>Analysis Date</p>
<p style={{ margin: '4px 0 0 0', fontSize: '16px', fontWeight: '800', color: '#000' }}>
{(() => {
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()}`;
})()}
</p>
</div>
</div>
<div style={{ display: 'flex', gap: '20px', marginBottom: '35px' }}>
{/* Primary Imaging */}
<div style={{ flex: '1', position: 'relative' }}>
<div style={{
width: '100%',
height: '320px',
overflow: 'hidden',
border: '4px solid #0097C3'
}}>
<img src={analysisData?.originalImage} style={{ width: '100%', height: '100%', objectFit: 'cover' }} alt="Original Scan" />
</div>
<div style={{
position: 'absolute',
bottom: '15px',
right: '15px',
backgroundColor: '#0097C3',
color: '#fff',
padding: '6px 15px',
fontSize: '10px',
fontWeight: 'bold',
textTransform: 'uppercase'
}}>Analyzed skin Profile</div>
</div>
{/* Overall Score Column */}
<div style={{ flex: '0.6', display: 'flex', flexDirection: 'column' }}>
<div style={{
flex: 1,
backgroundColor: '#0097C3',
color: '#fff',
padding: '20px 15px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '2px'
}}>
<p style={{ margin: 0, fontSize: '11px', fontWeight: '800', textTransform: 'uppercase', letterSpacing: '1px', opacity: 0.9 }}>Skin Health</p>
<div style={{ margin: '15px 0' }}>
<h1 style={{ fontSize: '80px', fontWeight: '900', margin: 0, lineHeight: 0.9 }}>{Math.round(overallScore)}</h1>
</div>
</div>
{/* Estimated Skin Age removed as requested */}
</div>
</div>
{/* Parameters Section */}
<div style={{ marginBottom: '35px' }}>
<h2 style={{
fontSize: '15px',
fontWeight: '900',
marginBottom: '18px',
textTransform: 'uppercase',
letterSpacing: '1.5px',
display: 'flex',
alignItems: 'center',
gap: '10px'
}}>
<span style={{ height: '15px', width: '15px', display: 'inline-block', border: '3px solid #0097C3' }}></span>
Diagnostic Deep-Dive
</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '15px'
}}>
{displayMetrics.filter(m => m.key !== 'all').map((metric, idx) => (
<div key={idx} style={{
padding: '14px',
border: '1px solid #f0f0f0',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: '11px', fontWeight: '800', color: '#1a1a1a', textTransform: 'uppercase', letterSpacing: '0.5px' }}>{metric.name}</span>
<span style={{ fontSize: '13px', fontWeight: '900', color: metric.ui_score >= 70 ? '#10b981' : metric.ui_score >= 40 ? '#f59e0b' : '#ef4444' }}>{metric.ui_score}%</span>
</div>
<div style={{ width: '100%', height: '4px', backgroundColor: '#f5f5f5', borderRadius: '10px', overflow: 'hidden' }}>
<div style={{
width: `${metric.ui_score}%`,
height: '100%',
backgroundColor: metric.ui_score >= 70 ? '#10b981' : metric.ui_score >= 40 ? '#f59e0b' : '#0097C3'
}}></div>
</div>
</div>
))}
</div>
</div>
{/* Specialist Recommendations */}
<div style={{
padding: '25px',
backgroundColor: '#fafafa',
border: '1px solid #eee',
marginBottom: '40px',
borderRadius: '4px'
}}>
<h3 style={{ margin: '0 0 15px 0', fontSize: '14px', fontWeight: '900', color: '#1a1a1a', textTransform: 'uppercase', letterSpacing: '1px' }}>Prescriptive Guidance</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '15px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<span style={{ fontSize: '10px', fontWeight: '900', color: '#0097C3' }}>STEP 01</span>
<p style={{ margin: 0, fontSize: '11px', color: '#555', lineHeight: '1.5' }}><strong>Advanced Hydration:</strong> Apply hyaluronic acid serum daily to stabilize moisture levels.</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<span style={{ fontSize: '10px', fontWeight: '900', color: '#0097C3' }}>STEP 02</span>
<p style={{ margin: 0, fontSize: '11px', color: '#555', lineHeight: '1.5' }}><strong>Dermal Protection:</strong> Broad-spectrum physical sunshade is mandatory for UV protection.</p>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<span style={{ fontSize: '10px', fontWeight: '900', color: '#0097C3' }}>STEP 03</span>
<p style={{ margin: 0, fontSize: '11px', color: '#555', lineHeight: '1.5' }}><strong>Clinical Intervention:</strong> Visit your specialist for focused treatment on top priority zones.</p>
</div>
</div>
</div>
{/* Minimalist Footer */}
<div style={{ marginTop: 'auto', borderTop: '0.5px solid #eee', paddingTop: '15px' }}>
<p style={{ margin: '0 0 10px 0', fontSize: '9px', color: '#b3b3b3', textAlign: 'center', fontStyle: 'italic' }}>
* This diagnostic data is interpreted by Limelite AI Skin Vision. For detailed medical prescriptions, please book a 1-on-1 therapist consultation.
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<p style={{ margin: 0, fontSize: '10px', fontWeight: '800', color: '#1a1a1a' }}>LIMELITE CLINICAL SYSTEMS</p>
<p style={{ margin: 0, fontSize: '9px', color: '#999' }}>support@limelite.com | limelite.com</p>
</div>
</div>
</div>
</div>
);
};
export default AnalysisResultPage;