1009 lines
51 KiB
React
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; |