feat: setup S2S integration, tailwind fixes, admin routing, nginx fallback

This commit is contained in:
2026-09-22 16:40:42 +05:30
parent 45d13fa7be
commit 05358be6c1
17 changed files with 522 additions and 149 deletions
+1
View File
@@ -19,6 +19,7 @@ RUN npm run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80 for Nginx
EXPOSE 80
+10
View File
@@ -0,0 +1,10 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
+88 -7
View File
@@ -17,6 +17,7 @@
"face-api.js": "^0.22.2",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.14.0",
"js-encrypt": "^2.3.4",
"jsencrypt": "^3.5.4",
"jspdf": "^3.0.1",
@@ -2053,9 +2054,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -3826,6 +3827,12 @@
"integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==",
"license": "MIT"
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -6891,11 +6898,10 @@
}
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz",
"integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -8100,6 +8106,33 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"license": "MIT"
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-png/node_modules/pako": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "(MIT AND Zlib)"
},
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -9158,6 +9191,34 @@
"node": ">=8.0.0"
}
},
"node_modules/html2pdf.js": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.14.0.tgz",
"integrity": "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==",
"license": "MIT",
"dependencies": {
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0",
"jspdf": "^4.0.0"
}
},
"node_modules/html2pdf.js/node_modules/jspdf": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz",
"integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@@ -9439,6 +9500,12 @@
"node": ">= 0.4"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -16764,6 +16831,20 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+1
View File
@@ -12,6 +12,7 @@
"face-api.js": "^0.22.2",
"file-saver": "^2.0.5",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.14.0",
"js-encrypt": "^2.3.4",
"jsencrypt": "^3.5.4",
"jspdf": "^3.0.1",
+66 -36
View File
@@ -102,7 +102,7 @@ const AnalysisResultPage = () => {
if (!score && score !== 0) return 'bg-gray-600';
if (score >= 70) return 'bg-emerald-500';
if (score >= 40) return 'bg-yellow-500';
return 'bg-[limelite-red]';
return 'bg-limelite-red';
};
const getMetricDisplayName = (metricKey) => {
@@ -212,18 +212,48 @@ const AnalysisResultPage = () => {
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
})
});
// 2. Generate PDF and Save to Backend
let generatedPdfLink = verificationLink;
if (reportRef.current) {
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);
const pdfBlob = pdf.output('blob');
const fd = new FormData();
fd.append('pdf', pdfBlob, `Limelite_Report_${phone}.pdf`);
fd.append('phone', phone);
fd.append('userName', formData.name);
const saveRes = await fetch(`${API_BASE_URL}/api/reports/save-pdf`, {
method: 'POST',
body: fd
});
const saveData = await saveRes.json();
if (saveData.success) {
generatedPdfLink = `${API_BASE_URL}${saveData.link}`;
}
} catch(err) {
console.error("Failed to generate/upload PDF during save:", err);
} finally {
reportElement.style.display = 'none';
}
}
verificationLink = generatedPdfLink;
console.log(`Sending WhatsApp message request to backend for ${phone}`);
@@ -469,7 +499,7 @@ const AnalysisResultPage = () => {
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-[limelite-red] rounded-full animate-spin"></div>
<div className="w-12 h-12 border-4 border-white/10 border-t-limelite-red rounded-full animate-spin"></div>
<p className="text-gray-300 animate-pulse">Analyzing skin profile...</p>
</div>
</div>
@@ -478,12 +508,12 @@ const AnalysisResultPage = () => {
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-[limelite-red]/20 rounded-none flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-[limelite-red]" 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 className="w-16 h-16 bg-limelite-red/20 rounded-none flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-limelite-red" 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-[limelite-red] hover:bg-[limelite-maroon] text-white rounded-none font-bold transition-colors shadow-lg shadow-[limelite-red]/30">
<button onClick={handleNewAnalysis} className="w-full py-3 bg-limelite-red hover:bg-limelite-maroon text-white rounded-none font-bold transition-colors shadow-lg shadow-limelite-red/30">
Try Again
</button>
</div>
@@ -494,7 +524,7 @@ const AnalysisResultPage = () => {
<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-[limelite-red] text-white rounded-none shadow-lg">Start New Analysis</button>
<button onClick={handleNewAnalysis} className="px-6 py-2 bg-limelite-red text-white rounded-none shadow-lg">Start New Analysis</button>
</div>
</div>
);
@@ -530,7 +560,7 @@ const AnalysisResultPage = () => {
<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-[limelite-red] shadow-lg">
<header className="fixed top-0 left-0 right-0 z-50 py-4 px-6 flex items-center justify-between bg-limelite-red shadow-lg">
<div className="flex items-center gap-4">
<img src={process.env.PUBLIC_URL + '/assets/images/logo.jpg'} alt="Limelite Logo" className="h-12 md:h-16 w-auto brightness-0 invert" />
</div>
@@ -574,7 +604,7 @@ const AnalysisResultPage = () => {
{activeMask && (
<button
onClick={handleClearMask}
className="text-white hover:text-[limelite-red] transition-colors flex items-center mb-2"
className="text-white hover:text-limelite-red 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">
@@ -608,7 +638,7 @@ const AnalysisResultPage = () => {
{/* 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-[limelite-red]/20 text-[limelite-red] border border-[limelite-red]/30'
'bg-limelite-red/20 text-limelite-red border border-limelite-red/30'
}`}>
{metric.ui_score >= 70 ? 'Good' : metric.ui_score >= 40 ? 'Average' : 'Bad'}
</div>
@@ -631,7 +661,7 @@ const AnalysisResultPage = () => {
<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-[limelite-red] hover:bg-[limelite-maroon] 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"
className="w-full bg-limelite-red hover:bg-limelite-maroon 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">
@@ -646,7 +676,7 @@ const AnalysisResultPage = () => {
<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-[limelite-red] hover:bg-[limelite-maroon] 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"
className="w-full bg-limelite-red hover:bg-limelite-maroon 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">
@@ -680,36 +710,36 @@ const AnalysisResultPage = () => {
<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-[limelite-red]">*</span>
Full Name<span className="text-limelite-red">*</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-[limelite-red]' : 'border-gray-400 focus:border-[limelite-red]'}`}
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-limelite-red' : 'border-gray-400 focus:border-limelite-red'}`}
/>
{formErrors.name && <p className="text-[limelite-red] text-xs mt-1">{formErrors.name}</p>}
{formErrors.name && <p className="text-limelite-red 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-[limelite-red]">*</span>
Whatsapp Number<span className="text-limelite-red">*</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-[limelite-red]' : 'border-gray-400 focus:border-[limelite-red]'}`}
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-limelite-red' : 'border-gray-400 focus:border-limelite-red'}`}
/>
{formErrors.phone && <p className="text-[limelite-red] text-[13px] mt-2 font-medium leading-tight">{formErrors.phone}</p>}
{formErrors.phone && <p className="text-limelite-red text-[13px] mt-2 font-medium leading-tight">{formErrors.phone}</p>}
</div>
<button
onClick={handleSignupSubmit}
disabled={isSubmittingSignup}
className="w-full bg-[limelite-red] hover:bg-[limelite-maroon] text-white py-3 font-semibold text-lg mt-4 transition-colors disabled:opacity-70 flex items-center justify-center h-14"
className="w-full bg-limelite-red hover:bg-limelite-maroon 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">
@@ -748,7 +778,7 @@ const AnalysisResultPage = () => {
<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-[limelite-red] focus:outline-none font-medium text-black"
className="w-11 h-11 border border-gray-300 rounded-md text-center text-xl focus:border-limelite-red focus:outline-none font-medium text-black"
type="text"
name="otp"
maxLength="1"
@@ -760,12 +790,12 @@ const AnalysisResultPage = () => {
))}
</div>
{otpError && <p className="text-[limelite-red] text-sm mb-4 text-center">{otpError}</p>}
{otpError && <p className="text-limelite-red text-sm mb-4 text-center">{otpError}</p>}
<button
onClick={handleVerifyOtp}
disabled={isVerifying}
className="w-full bg-[limelite-red] hover:bg-[limelite-maroon] text-white py-4 font-semibold text-lg transition-colors disabled:opacity-70 flex items-center justify-center"
className="w-full bg-limelite-red hover:bg-limelite-maroon 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>
@@ -813,9 +843,9 @@ const AnalysisResultPage = () => {
{/* Toast Notification */}
{toast.show && (
<div className="fixed top-24 right-6 z-[200] animate-[fadeIn_0.3s_ease-out]">
<div className="bg-white text-[limelite-red] px-6 py-4 shadow-[0_10px_40px_rgba(0,0,0,0.15)] flex items-center gap-3 border-l-4 border-[limelite-red]">
<div className="bg-[limelite-red]/10 p-1.5 rounded-full">
<svg className="w-5 h-5 text-[limelite-red]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className="bg-white text-limelite-red px-6 py-4 shadow-[0_10px_40px_rgba(0,0,0,0.15)] flex items-center gap-3 border-l-4 border-limelite-red">
<div className="bg-limelite-red/10 p-1.5 rounded-full">
<svg className="w-5 h-5 text-limelite-red" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
+45 -49
View File
@@ -61,9 +61,14 @@ const App = () => {
let intervalId;
if (view === 'camera' && videoRef.current && modelsLoaded) {
const detectskin = async () => {
const detections = await skinapi.detectSingleFace(videoRef.current, new skinapi.TinyFaceDetectorOptions());
if (detections) setskinBox(detections.box);
else setskinBox(null);
if (!videoRef.current || videoRef.current.readyState < 2) return;
try {
const detections = await skinapi.detectSingleFace(videoRef.current, new skinapi.TinyFaceDetectorOptions());
if (detections) setskinBox(detections.box);
else setskinBox(null);
} catch (e) {
console.error("Face detection error:", e);
}
};
intervalId = setInterval(detectskin, 300);
}
@@ -332,38 +337,37 @@ const App = () => {
throw new Error(`Backend Analysis Request failed: ${e.message}`);
}
// --- Process ZIP & save analysis ---
// --- Process New S2S JSON Response ---
try {
const maskZipUrl = result.result?.results?.[0]?.data?.[0]?.url || result.result?.url;
if (!maskZipUrl) throw new Error("No mask ZIP URL found in result");
setZipUrl(maskZipUrl);
const zipRes = await fetch(maskZipUrl);
if (!zipRes.ok) throw new Error(`Failed to download ZIP file (status: ${zipRes.status})`);
const zipBlob = await zipRes.blob();
const zip = await JSZip.loadAsync(zipBlob);
const scoreFile = zip.file("skinanalysisResult/score_info.json") || zip.file("score_info.json");
const scores = scoreFile ? JSON.parse(await scoreFile.async("text")) : result.result?.results?.[0]?.scores || {};
if (Object.keys(scores).length === 0) throw new Error("Could not extract scores from analysis result.");
const newMaskMap = {};
for (const key of Object.keys(scores)) {
const maskName = scores[key].output_mask_name;
if (!maskName) continue;
const f = zip.file(`skinanalysisResult/${maskName}`) || zip.file(`masks/${maskName}`) || zip.file(maskName);
if (!f) continue;
const b = await f.async('blob');
newMaskMap[maskName] = await blobToBase64(b);
}
const scores = result.result?.scores;
if (!scores) throw new Error("No scores found in result from backend.");
const originalBase64 = await blobToBase64(await fetch(previewUrl).then(r => r.blob()));
const newMaskMap = {};
for (const key of Object.keys(scores)) {
const maskUrl = scores[key].output_mask_name;
if (!maskUrl) continue;
try {
// Fetch the image from S3 to cache it locally as base64
const imgRes = await fetch(maskUrl);
if (imgRes.ok) {
const b = await imgRes.blob();
newMaskMap[maskUrl] = await blobToBase64(b);
} else {
newMaskMap[maskUrl] = maskUrl; // Fallback to URL
}
} catch(err) {
newMaskMap[maskUrl] = maskUrl;
}
}
await saveToDB('analysisData', {
scoreData: scores,
maskImages: newMaskMap,
originalImage: originalBase64,
zipDownloadUrl: maskZipUrl
zipDownloadUrl: null
});
setAnalysisResults(scores);
@@ -395,9 +399,9 @@ const App = () => {
<div className="container mx-auto px-4 py-10 md:py-16 relative z-10 min-h-screen flex flex-col items-center justify-start gap-4 md:gap-8">
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-[60] py-4 px-6 flex items-center justify-between bg-[limelite-red] shadow-lg">
<header className="fixed top-0 left-0 right-0 z-[60] py-4 px-6 flex items-center justify-between bg-transparent">
<div className="flex items-center gap-4">
<img src={process.env.PUBLIC_URL + '/assets/images/logo.jpg'} alt="Limelite Logo" className="h-12 md:h-16 w-auto " />
<img src={process.env.PUBLIC_URL + '/assets/images/logo.jpg'} alt="Limelite Logo" className="h-10 md:h-14 w-auto" />
</div>
<div className="flex items-center gap-4">
@@ -442,7 +446,7 @@ const App = () => {
{/* Landing View */}
{view === 'landing' && (
<div className="flex flex-col items-center justify-center w-full px-4 gap-8 fade-in mt-20 md:mt-32">
<div className="flex flex-col items-center justify-center w-full px-4 gap-8 fade-in mt-16 md:mt-32">
<div className="text-center space-y-4 mb-2">
<h1 className="text-4xl md:text-5xl font-serif italic text-white leading-tight tracking-[0.02em]">
Step into your style
@@ -453,11 +457,11 @@ const App = () => {
</div>
{/* Action Buttons Container */}
<div className="w-full flex flex-row items-center justify-center gap-4 max-w-md mx-auto">
<div className="w-full flex flex-col sm:flex-row items-center justify-center gap-4 max-w-sm sm:max-w-md mx-auto">
{/* Take Photo Card */}
<button
onClick={() => setView('camera')}
className="flex-1 bg-white text-[#1A1A1A] rounded-xl px-8 py-4 font-bold text-sm tracking-widest uppercase shadow-lg hover:bg-gray-100 transition-all text-center border-2 border-transparent"
className="w-full sm:flex-1 bg-white text-[#1A1A1A] rounded-xl px-6 py-4 font-bold text-sm tracking-widest uppercase shadow-lg hover:bg-gray-100 transition-all text-center border-2 border-transparent"
>
Take Photo
</button>
@@ -465,7 +469,7 @@ const App = () => {
{/* Upload Photo Card */}
<button
onClick={() => document.getElementById('uploadInput').click()}
className="flex-1 bg-[#1A1A1A] text-white border border-gray-700 rounded-xl px-8 py-4 font-bold text-sm tracking-widest uppercase shadow-lg hover:bg-black transition-all text-center"
className="w-full sm:flex-1 bg-[#1A1A1A] text-white border border-gray-700 rounded-xl px-6 py-4 font-bold text-sm tracking-widest uppercase shadow-lg hover:bg-black transition-all text-center"
>
Upload Photo
</button>
@@ -478,15 +482,7 @@ const App = () => {
/>
</div>
{/* Admin Link */}
<div className="mt-8">
<button
onClick={() => navigate('/login')}
className="text-gray-400 hover:text-[limelite-red] transition-colors text-xs uppercase tracking-widest font-bold"
>
Admin Login
</button>
</div>
</div>
)}
{
@@ -504,7 +500,7 @@ const App = () => {
<div className="absolute bottom-12 left-0 right-0 flex justify-center items-center z-20">
<button
onClick={handleCaptureskin}
className="transform active:scale-95 transition-all cursor-pointer bg-[limelite-red] w-36 h-18 py-4 px-10 rounded-full shadow-[0_0_20px_rgba(0,151,195,0.3)] flex items-center justify-center border-none"
className="transform active:scale-95 transition-all cursor-pointer bg-limelite-red w-36 h-18 py-4 px-10 rounded-full shadow-[0_0_20px_rgba(0,151,195,0.3)] flex items-center justify-center border-none"
>
<IoMdAperture className="w-12 h-12 text-white" />
</button>
@@ -528,8 +524,8 @@ const App = () => {
{/* Scanning Animation only when loading */}
{isLoading && (
<div className="absolute inset-0 z-20 pointer-events-none overflow-hidden">
<div className="w-full h-[2px] bg-[limelite-red] shadow-[0_0_20px_rgba(0,151,195,0.8)] absolute top-0 animate-[scan_3s_linear_infinite]"></div>
<div className="w-full h-32 bg-gradient-to-t from-[limelite-red]/20 to-transparent absolute top-0 -translate-y-full animate-[scanTrace_3s_linear_infinite]"></div>
<div className="w-full h-[2px] bg-limelite-red shadow-[0_0_20px_rgba(0,151,195,0.8)] absolute top-0 animate-[scan_3s_linear_infinite]"></div>
<div className="w-full h-32 bg-gradient-to-t from-limelite-red/20 to-transparent absolute top-0 -translate-y-full animate-[scanTrace_3s_linear_infinite]"></div>
</div>
)}
@@ -553,7 +549,7 @@ const App = () => {
<button
onClick={handleSubmitClick}
disabled={isLoading}
className="h-14 w-20 bg-[limelite-red] text-white rounded-none flex items-center justify-center shadow-lg hover:bg-[limelite-maroon] transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
className="h-14 w-20 bg-limelite-red text-white rounded-none flex items-center justify-center shadow-lg hover:bg-limelite-maroon transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Confirm"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -573,7 +569,7 @@ const App = () => {
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-6 animate-[fadeIn_0.2s_ease-out]">
<div className="bg-white rounded-none shadow-2xl w-full max-w-sm p-8 text-center">
<div className="w-16 h-16 bg-cyan-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-8 h-8 text-[limelite-red]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-8 h-8 text-limelite-red" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
@@ -586,7 +582,7 @@ const App = () => {
</p>
<button
onClick={handleRetakeClick}
className="w-full bg-[limelite-red] text-white font-bold py-4 px-6 rounded-none hover:bg-[limelite-maroon] transition-all active:scale-95 uppercase tracking-widest text-sm"
className="w-full bg-limelite-red text-white font-bold py-4 px-6 rounded-none hover:bg-limelite-maroon transition-all active:scale-95 uppercase tracking-widest text-sm"
>
Retake Photo
</button>
+10 -10
View File
@@ -24,7 +24,7 @@ const DashboardPage = () => {
const handleLogout = () => {
// Here you would clear any persisted auth state if it existed
navigate('/login');
navigate('/admin');
};
const [showSupportModal, setShowSupportModal] = useState(false);
const [activeTab, setActiveTab] = useState('reports');
@@ -101,7 +101,7 @@ const DashboardPage = () => {
return (
<div className="flex h-screen bg-[#F8F9FA] font-sans">
{/* Sidebar */}
<aside className="w-64 bg-[limelite-red] text-white flex flex-col shrink-0">
<aside className="w-64 bg-limelite-red text-white flex flex-col shrink-0">
<div className="p-6 mb-4">
<img src={process.env.PUBLIC_URL + '/assets/images/logo.jpg'} alt="LIMELITE" className="h-10 w-auto brightness-0 invert" />
</div>
@@ -109,7 +109,7 @@ const DashboardPage = () => {
<nav className="flex-1 px-0 mt-4">
<div
onClick={() => setActiveTab('reports')}
className={`flex items-center gap-3 px-6 py-4 cursor-pointer transition-colors ${activeTab === 'reports' ? 'bg-white text-[limelite-red]' : 'text-white hover:bg-white/10'}`}
className={`flex items-center gap-3 px-6 py-4 cursor-pointer transition-colors ${activeTab === 'reports' ? 'bg-white text-limelite-red' : 'text-white hover:bg-white/10'}`}
>
<HiOutlineDocumentText className="text-xl" />
<span className="font-bold uppercase tracking-wider text-xs">Reports</span>
@@ -126,14 +126,14 @@ const DashboardPage = () => {
<div className="px-0 space-y-0 mb-6 border-t border-white/10">
<div
onClick={() => setActiveTab('settings')}
className={`flex items-center gap-3 px-6 py-4 cursor-pointer transition-colors ${activeTab === 'settings' ? 'bg-white text-[limelite-red]' : 'text-white hover:bg-white/10'}`}
className={`flex items-center gap-3 px-6 py-4 cursor-pointer transition-colors ${activeTab === 'settings' ? 'bg-white text-limelite-red' : 'text-white hover:bg-white/10'}`}
>
<FiSettings className="text-xl" />
<span className="font-bold uppercase tracking-wider text-xs">Settings</span>
</div>
<div className="hover:bg-white/10 text-white flex items-center gap-3 px-6 py-5 transition-colors cursor-pointer border-t border-white/10">
<div className="flex items-center gap-3 w-full">
<div className="w-10 h-10 bg-[limelite-maroon] rounded-full flex items-center justify-center overflow-hidden border border-white/30 text-white font-bold text-sm">
<div className="w-10 h-10 bg-limelite-maroon rounded-full flex items-center justify-center overflow-hidden border border-white/30 text-white font-bold text-sm">
LA
</div>
<div className="flex-1 min-w-0">
@@ -166,7 +166,7 @@ const DashboardPage = () => {
<input
type="text"
placeholder="Search"
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-10 pr-4 focus:outline-none focus:ring-1 focus:ring-[limelite-red] text-sm text-gray-900"
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-10 pr-4 focus:outline-none focus:ring-1 focus:ring-limelite-red text-sm text-gray-900"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -199,7 +199,7 @@ const DashboardPage = () => {
<td className="px-6 py-4 text-sm text-gray-500 font-medium">{(currentPage - 1) * ITEMS_PER_PAGE + index + 1}</td>
<td className="px-6 py-4 text-sm font-medium text-gray-700">{report.name}</td>
<td className="px-6 py-4 text-sm text-gray-500">{report.phone}</td>
<td className="px-6 py-4 text-sm text-[limelite-red] font-semibold truncate max-w-[200px]">
<td className="px-6 py-4 text-sm text-limelite-red font-semibold truncate max-w-[200px]">
<a href={report.link} target="_blank" rel="noopener noreferrer" className="hover:underline">
{report.link ? report.link.substring(report.link.indexOf('result')) : '-'}
</a>
@@ -244,7 +244,7 @@ const DashboardPage = () => {
<label className="text-sm font-bold text-gray-700 uppercase">Site Name</label>
<input
type="text"
className="col-span-2 border-b-2 py-2 focus:outline-none focus:border-[limelite-red] transition-colors"
className="col-span-2 border-b-2 py-2 focus:outline-none focus:border-limelite-red transition-colors"
value={settings.siteName}
onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
/>
@@ -280,14 +280,14 @@ const DashboardPage = () => {
</div>
<button
onClick={() => setSettings({ ...settings, autoArchive: !settings.autoArchive })}
className={`w-12 h-6 rounded-full transition-colors relative ${settings.autoArchive ? 'bg-[limelite-red]' : 'bg-gray-300'}`}
className={`w-12 h-6 rounded-full transition-colors relative ${settings.autoArchive ? 'bg-limelite-red' : 'bg-gray-300'}`}
>
<div className={`absolute top-1 bg-white w-4 h-4 rounded-full transition-transform ${settings.autoArchive ? 'translate-x-7' : 'translate-x-1'}`}></div>
</button>
</div>
<div className="pt-6">
<button className="bg-[limelite-red] text-white px-8 py-3 font-bold uppercase tracking-widest hover:bg-[limelite-maroon] transition-colors">
<button className="bg-limelite-red text-white px-8 py-3 font-bold uppercase tracking-widest hover:bg-limelite-maroon transition-colors">
Save Changes
</button>
</div>
+6 -6
View File
@@ -13,7 +13,7 @@ const LoginPage = () => {
setError('');
if (credentials.email === 'admin@limelite.com' && credentials.password === 'admin@2026') {
navigate('/dashboard');
navigate('/admin/dashboard');
} else {
setError('Incorrect admin credentials');
}
@@ -23,12 +23,12 @@ const LoginPage = () => {
<div className="min-h-screen bg-[#18181b] flex items-center justify-center p-4 font-sans">
<div className="bg-white w-full max-w-md p-10 shadow-2xl rounded-none">
<div className="flex flex-col items-center mb-10">
<div className="bg-[limelite-red] p-4 mb-6">
<div className="bg-limelite-red p-4 mb-6">
<img src={process.env.PUBLIC_URL + '/assets/images/logo.jpg'} alt="Limelite" className="h-8 brightness-0 invert" />
</div>
</div>
{error && <p className="text-[limelite-red] text-sm font-bold text-center mb-6 animate-shake">{error}</p>}
{error && <p className="text-limelite-red text-sm font-bold text-center mb-6 animate-shake">{error}</p>}
<form onSubmit={handleSubmit} className="space-y-6">
<div>
@@ -36,7 +36,7 @@ const LoginPage = () => {
<input
type="email"
required
className="w-full border-b-2 border-gray-200 py-3 focus:outline-none focus:border-[limelite-red] transition-colors text-black"
className="w-full border-b-2 border-gray-200 py-3 focus:outline-none focus:border-limelite-red transition-colors text-black"
placeholder="olivia@limelite.com"
value={credentials.email}
onChange={(e) => setCredentials({ ...credentials, email: e.target.value })}
@@ -48,7 +48,7 @@ const LoginPage = () => {
<input
type={showPassword ? "text" : "password"}
required
className="w-full border-b-2 border-gray-200 py-3 pr-10 focus:outline-none focus:border-[limelite-red] transition-colors text-black"
className="w-full border-b-2 border-gray-200 py-3 pr-10 focus:outline-none focus:border-limelite-red transition-colors text-black"
placeholder="••••••••"
value={credentials.password}
onChange={(e) => setCredentials({ ...credentials, password: e.target.value })}
@@ -64,7 +64,7 @@ const LoginPage = () => {
</div>
<button
type="submit"
className="w-full bg-[limelite-red] text-white py-4 font-bold uppercase tracking-widest hover:bg-[limelite-maroon] transition-colors mt-4"
className="w-full bg-limelite-red text-white py-4 font-bold uppercase tracking-widest hover:bg-limelite-maroon transition-colors mt-4"
>
Login
</button>
+2 -2
View File
@@ -16,8 +16,8 @@ root.render(
<Routes>
<Route path='/' element={<App />} />
<Route path='/result' element={<AnalysisResultPage />} />
<Route path='/login' element={<LoginPage />} />
<Route path='/dashboard' element={<DashboardPage />} />
<Route path='/admin' element={<LoginPage />} />
<Route path='/admin/dashboard' element={<DashboardPage />} />
</Routes>
</BrowserRouter>