feat: setup S2S integration, tailwind fixes, admin routing, nginx fallback
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user