Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions frontend/src/components/ImageQualityPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.quality-panel {
margin-top: 20px;
padding: 20px;
border-radius: 16px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
}

.quality-panel__header h3 {
margin-bottom: 16px;
font-size: 18px;
}

.quality-panel__score {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}

.quality-panel__circle {
width: 90px;
height: 90px;
border-radius: 50%;
background: #22c55e;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
}

.quality-panel__status {
font-size: 18px;
font-weight: 600;
}

.quality-panel__suggestions {
display: flex;
flex-direction: column;
gap: 10px;
}

.quality-panel__suggestion {
padding: 12px;
border-radius: 10px;
background: rgba(255,255,255,0.05);
}

@media (max-width: 768px) {
.quality-panel__score {
flex-direction: column;
align-items: flex-start;
}
}
55 changes: 55 additions & 0 deletions frontend/src/components/ImageQualityPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import './ImageQualityPanel.css';

function ImageQualityPanel({ processedUrl }) {

if (!processedUrl) return null;

const qualityScore = 82;

const suggestions = [
'Good lighting detected',
'Face alignment looks correct',
'Background quality is acceptable',
'Image could be slightly sharper',
];

const getStatus = () => {
if (qualityScore >= 80) return 'Excellent';
if (qualityScore >= 60) return 'Good';
return 'Needs Improvement';
};

return (
<div className="quality-panel">

<div className="quality-panel__header">
<h3>AI Image Quality Analysis</h3>
</div>

<div className="quality-panel__score">
<div className="quality-panel__circle">
{qualityScore}%
</div>

<p className="quality-panel__status">
{getStatus()}
</p>
</div>

<div className="quality-panel__suggestions">
{suggestions.map((item) => (
<div
key={item}
className="quality-panel__suggestion"
>
✓ {item}
</div>
))}
</div>

</div>
);
}

export default ImageQualityPanel;
237 changes: 146 additions & 91 deletions frontend/src/components/PhotoPreview.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import LoadingSpinner from './LoadingSpinner';
import './PhotoPreview.css';
import ImageQualityPanel from './ImageQualityPanel';

/**
* PhotoPreview — displays original and processed passport photo side by side.
Expand All @@ -16,103 +17,157 @@ function PhotoPreview({ originalUrl, processedUrl, isProcessing }) {
const [imageLoaded, setImageLoaded] = useState(false);

return (
<div className="photo-preview">
{/* Original */}
{originalUrl && (
<div className="photo-preview__panel">
<span className="photo-preview__label">Original</span>
<div className="photo-preview__frame photo-preview__frame--original">
<img
src={originalUrl}
alt="Original uploaded — before processing"
className="photo-preview__img"
/>

{/* Floating Glassmorphic Guidelines Toggle */}
<button
type="button"
className={`photo-preview__toggle-btn ${showGuidelines ? 'photo-preview__toggle-btn--active' : ''}`}
onClick={() => setShowGuidelines(!showGuidelines)}
title={showGuidelines ? 'Hide Passport Guidelines Overlay' : 'Show Passport Guidelines Overlay'}
return (
<div className="photo-preview">
{/* Original */}
{originalUrl && (
<div className="photo-preview__panel">
<span className="photo-preview__label">Original</span>

<div className="photo-preview__frame photo-preview__frame--original">
<img
src={originalUrl}
alt="Original uploaded — before processing"
className="photo-preview__img"
/>

{/* Floating Glassmorphic Guidelines Toggle */}
<button
type="button"
className={`photo-preview__toggle-btn ${
showGuidelines
? 'photo-preview__toggle-btn--active'
: ''
}`}
onClick={() =>
setShowGuidelines(!showGuidelines)
}
title={
showGuidelines
? 'Hide Passport Guidelines Overlay'
: 'Show Passport Guidelines Overlay'
}
>
<svg
className="photo-preview__toggle-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<svg className="photo-preview__toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="3" y1="15" x2="21" y2="15" />
</svg>
<span className="photo-preview__toggle-text">Guidelines</span>
</button>

{/* Smart Compliance Guidelines Grid */}
{showGuidelines && (
<div className="photo-preview__overlay-grid">
{/* L-Shaped Outer Crop Corners */}
<div className="photo-preview__corner photo-preview__corner--tl" />
<div className="photo-preview__corner photo-preview__corner--tr" />
<div className="photo-preview__corner photo-preview__corner--bl" />
<div className="photo-preview__corner photo-preview__corner--br" />

{/* Vertical Symmetry Axis */}
<div className="photo-preview__guide photo-preview__guide--center-line" />

{/* Head Crown Target Region (Silhouette Oval) */}
<div className="photo-preview__guide photo-preview__guide--head-oval">
<span className="photo-preview__guide-tag photo-preview__guide-tag--oval">CROWN LEVEL</span>
</div>

{/* Ideal Eye Alignment Level */}
<div className="photo-preview__guide photo-preview__guide--eye-line">
<span className="photo-preview__guide-tag photo-preview__guide-tag--eye">EYES LEVEL</span>
</div>

{/* Ideal Chin Alignment Level */}
<div className="photo-preview__guide photo-preview__guide--chin-line">
<span className="photo-preview__guide-tag photo-preview__guide-tag--chin">CHIN LEVEL</span>
</div>
<rect
x="3"
y="3"
width="18"
height="18"
rx="2"
ry="2"
/>
<line x1="9" y1="3" x2="9" y2="21" />
<line x1="15" y1="3" x2="15" y2="21" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="3" y1="15" x2="21" y2="15" />
</svg>

<span className="photo-preview__toggle-text">
Guidelines
</span>
</button>

{/* Smart Compliance Guidelines Grid */}
{showGuidelines && (
<div className="photo-preview__overlay-grid">

<div className="photo-preview__corner photo-preview__corner--tl" />
<div className="photo-preview__corner photo-preview__corner--tr" />
<div className="photo-preview__corner photo-preview__corner--bl" />
<div className="photo-preview__corner photo-preview__corner--br" />

<div className="photo-preview__guide photo-preview__guide--center-line" />

<div className="photo-preview__guide photo-preview__guide--head-oval">
<span className="photo-preview__guide-tag photo-preview__guide-tag--oval">
CROWN LEVEL
</span>
</div>
)}
</div>
</div>
)}

{/* Processed */}
<div className="photo-preview__panel">
<span className="photo-preview__label">
Processed
{isProcessing && <span className="photo-preview__processing-badge">Processing…</span>}
</span>
<div className={`photo-preview__frame photo-preview__frame--processed${isProcessing ? ' photo-preview__frame--loading' : ''}`}>
{processedUrl && !isProcessing ? (
<>
{!imageLoaded && (
<div className="photo-preview__image-preloader">
<LoadingSpinner size="md" />
</div>
)}
<img
src={processedUrl}
alt="AI-processed — background removed and centred"
className={`photo-preview__img photo-preview__img--processed ${imageLoaded ? 'photo-preview__img--loaded' : 'photo-preview__img--loading'}`}
onLoad={() => setImageLoaded(true)}
/>
</>
) : (
<div className="photo-preview__empty">
{isProcessing ? (
<LoadingSpinner size="md" />
) : (
<p className="photo-preview__empty-text">
Upload and process a photo to preview the AI-generated result
</p>
)}
<div className="photo-preview__guide photo-preview__guide--eye-line">
<span className="photo-preview__guide-tag photo-preview__guide-tag--eye">
EYES LEVEL
</span>
</div>

<div className="photo-preview__guide photo-preview__guide--chin-line">
<span className="photo-preview__guide-tag photo-preview__guide-tag--chin">
CHIN LEVEL
</span>
</div>
</div>
)}
</div>
</div>
)}

{/* Processed */}
<div className="photo-preview__panel">
<span className="photo-preview__label">
Processed

{isProcessing && (
<span className="photo-preview__processing-badge">
Processing…
</span>
)}
</span>

<div
className={`photo-preview__frame photo-preview__frame--processed${
isProcessing
? ' photo-preview__frame--loading'
: ''
}`}
>
{processedUrl && !isProcessing ? (
<>
{!imageLoaded && (
<div className="photo-preview__image-preloader">
<LoadingSpinner size="md" />
</div>
)}

<img
src={processedUrl}
alt="AI-processed — background removed and centred"
className={`photo-preview__img photo-preview__img--processed ${
imageLoaded
? 'photo-preview__img--loaded'
: 'photo-preview__img--loading'
}`}
onLoad={() => setImageLoaded(true)}
/>
</>
) : (
<div className="photo-preview__empty">
{isProcessing ? (
<LoadingSpinner size="md" />
) : (
<p className="photo-preview__empty-text">
Upload and process a photo to preview the AI-generated result
</p>
)}
</div>
)}
</div>
</div>
);
}

export default PhotoPreview;
{/* AI QUALITY PANEL */}
<ImageQualityPanel
processedUrl={processedUrl}
/>

</div>
);

export default PhotoPreview;}