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