Spaces:
Sleeping
Sleeping
Update client/src/App.jsx
Browse files- client/src/App.jsx +90 -24
client/src/App.jsx
CHANGED
|
@@ -8,8 +8,8 @@ const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBh
|
|
| 8 |
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
| 9 |
|
| 10 |
const AI_MODELS = [
|
| 11 |
-
{ id: 'openai/dall-e-3', name: 'DALL-E 3
|
| 12 |
-
{ id: 'google/imagen-4.0-preview', name: 'Imagen 4
|
| 13 |
{ id: 'gemini-2.5-flash-image', name: 'Nano Banana' },
|
| 14 |
];
|
| 15 |
|
|
@@ -26,17 +26,54 @@ function App() {
|
|
| 26 |
const [selectedRatio, setSelectedRatio] = useState(ASPECT_RATIOS[0].id);
|
| 27 |
const [generating, setGenerating] = useState(false);
|
| 28 |
const [imageSrc, setImageSrc] = useState(null);
|
| 29 |
-
|
| 30 |
const [inputImage, setInputImage] = useState(null);
|
| 31 |
const [previewUrl, setPreviewUrl] = useState(null);
|
| 32 |
const fileInputRef = useRef(null);
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
useEffect(() => {
|
| 35 |
supabase.auth.getSession().then(({ data: { session } }) => setSession(session));
|
| 36 |
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => setSession(session));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
return () => subscription.unsubscribe();
|
| 38 |
}, []);
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
const handleLogin = async () => {
|
| 41 |
const { error } = await supabase.auth.signInAnonymously();
|
| 42 |
if (error) alert("Login failed: " + error.message);
|
|
@@ -77,10 +114,7 @@ function App() {
|
|
| 77 |
|
| 78 |
if (!session) {
|
| 79 |
return (
|
| 80 |
-
<div style={{
|
| 81 |
-
height: '100vh', display: 'flex', flexDirection: 'column',
|
| 82 |
-
alignItems: 'center', justifyContent: 'center', textAlign: 'center'
|
| 83 |
-
}}>
|
| 84 |
<div className="glass-panel" style={{padding: '40px', maxWidth: '350px'}}>
|
| 85 |
<h1 style={{fontSize:'2rem', margin:'0 0 10px 0', fontWeight:'200'}}>Liquid AI</h1>
|
| 86 |
<button onClick={handleLogin} className="glass-pill" style={{fontSize:'1.1rem', padding:'12px 30px'}}>Enter</button>
|
|
@@ -92,52 +126,85 @@ function App() {
|
|
| 92 |
return (
|
| 93 |
<div style={{
|
| 94 |
minHeight: '100vh', display: 'flex', flexDirection: 'column',
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
}}>
|
| 97 |
|
| 98 |
-
{/* HEADER */}
|
| 99 |
<div style={{
|
| 100 |
padding: '15px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
| 101 |
-
position: 'sticky', top: 0, zIndex: 10
|
|
|
|
| 102 |
}}>
|
| 103 |
-
{/* EXPANDING LOGO */}
|
| 104 |
<div className="logo-expand-container">
|
| 105 |
<span className="logo-main">TEK</span>
|
| 106 |
<span className="logo-hidden">BUILD</span>
|
| 107 |
</div>
|
| 108 |
|
| 109 |
-
<div style={{display: 'flex', gap: '8px'}}>
|
| 110 |
<select className="glass-pill" value={selectedRatio} onChange={(e) => setSelectedRatio(e.target.value)}>
|
| 111 |
{ASPECT_RATIOS.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
| 112 |
</select>
|
| 113 |
-
<select className="glass-pill" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} style={{maxWidth:'
|
| 114 |
{AI_MODELS.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
| 115 |
</select>
|
| 116 |
</div>
|
| 117 |
</div>
|
| 118 |
|
| 119 |
-
{/* VIEWPORT */}
|
| 120 |
-
<div style={{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
{generating ? (
|
| 122 |
<div className="glass-panel" style={{padding: '20px', borderRadius:'50%'}}>
|
| 123 |
<div style={{fontSize:'40px', animation:'spin 2s infinite'}}>⏳</div>
|
| 124 |
</div>
|
| 125 |
) : imageSrc ? (
|
| 126 |
-
<div className="glass-panel" style={{padding: '10px', maxWidth: '100%'}}>
|
| 127 |
-
|
|
|
|
| 128 |
</div>
|
| 129 |
) : (
|
| 130 |
-
// Center is now clean. The "Ready" text moved down.
|
| 131 |
<div style={{opacity: 0.1, transform:'scale(0.8)'}}>❖</div>
|
| 132 |
)}
|
| 133 |
</div>
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
{/* INPUT DOCK */}
|
| 136 |
<div style={{
|
| 137 |
position: 'fixed', bottom: 0, left: 0, right: 0,
|
| 138 |
-
padding: '10px 20px 30px 20px', zIndex: 20
|
|
|
|
|
|
|
| 139 |
}}>
|
| 140 |
-
{/* 3. RELOCATED STATUS TEXT */}
|
| 141 |
<div style={{
|
| 142 |
textAlign: 'center', fontSize: '12px', color: 'rgba(255,255,255,0.6)',
|
| 143 |
marginBottom: '10px', letterSpacing: '1px', textTransform: 'uppercase'
|
|
@@ -145,12 +212,11 @@ function App() {
|
|
| 145 |
{generating ? "Dreaming..." : (imageSrc ? "Done" : "Ready to dream")}
|
| 146 |
</div>
|
| 147 |
|
| 148 |
-
{/* Upload Preview */}
|
| 149 |
{previewUrl && (
|
| 150 |
<div style={{position: 'absolute', bottom: '110px', left: '20px'}}>
|
| 151 |
-
<div className="holo-box">
|
| 152 |
-
<img src={previewUrl} style={{height: '60px', borderRadius: '
|
| 153 |
-
<button onClick={() => {setPreviewUrl(null); setInputImage(null);}} style={{position: 'absolute', top: -
|
| 154 |
</div>
|
| 155 |
</div>
|
| 156 |
)}
|
|
|
|
| 8 |
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
| 9 |
|
| 10 |
const AI_MODELS = [
|
| 11 |
+
{ id: 'openai/dall-e-3', name: 'DALL-E 3' },
|
| 12 |
+
{ id: 'google/imagen-4.0-preview', name: 'Imagen 4' },
|
| 13 |
{ id: 'gemini-2.5-flash-image', name: 'Nano Banana' },
|
| 14 |
];
|
| 15 |
|
|
|
|
| 26 |
const [selectedRatio, setSelectedRatio] = useState(ASPECT_RATIOS[0].id);
|
| 27 |
const [generating, setGenerating] = useState(false);
|
| 28 |
const [imageSrc, setImageSrc] = useState(null);
|
|
|
|
| 29 |
const [inputImage, setInputImage] = useState(null);
|
| 30 |
const [previewUrl, setPreviewUrl] = useState(null);
|
| 31 |
const fileInputRef = useRef(null);
|
| 32 |
|
| 33 |
+
// PWA State
|
| 34 |
+
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
| 35 |
+
const [showInstallBanner, setShowInstallBanner] = useState(false);
|
| 36 |
+
const [isIOS, setIsIOS] = useState(false);
|
| 37 |
+
|
| 38 |
useEffect(() => {
|
| 39 |
supabase.auth.getSession().then(({ data: { session } }) => setSession(session));
|
| 40 |
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => setSession(session));
|
| 41 |
+
|
| 42 |
+
// PWA Detection Logic
|
| 43 |
+
const isIosDevice = /ipad|iphone|ipod/.test(navigator.userAgent.toLowerCase()) && !window.MSStream;
|
| 44 |
+
// Check if running in standalone (already installed) mode
|
| 45 |
+
const isIn standaloneMode = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
|
| 46 |
+
|
| 47 |
+
if (isIosDevice && !isInStandaloneMode) {
|
| 48 |
+
setIsIOS(true);
|
| 49 |
+
// Show iOS hint after a short delay so it doesn't overlap login immediately
|
| 50 |
+
setTimeout(() => setShowInstallBanner(true), 3000);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
window.addEventListener('beforeinstallprompt', (e) => {
|
| 54 |
+
// Prevent Chrome 67 and earlier from automatically showing the prompt
|
| 55 |
+
e.preventDefault();
|
| 56 |
+
// Stash the event so it can be triggered later.
|
| 57 |
+
setDeferredPrompt(e);
|
| 58 |
+
// Update UI to notify the user they can add to home screen
|
| 59 |
+
if (!isInStandaloneMode) {
|
| 60 |
+
setShowInstallBanner(true);
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
return () => subscription.unsubscribe();
|
| 65 |
}, []);
|
| 66 |
|
| 67 |
+
const handleInstallClick = async () => {
|
| 68 |
+
if (deferredPrompt) {
|
| 69 |
+
deferredPrompt.prompt();
|
| 70 |
+
const { outcome } = await deferredPrompt.userChoice;
|
| 71 |
+
console.log(`User response to install prompt: ${outcome}`);
|
| 72 |
+
setDeferredPrompt(null);
|
| 73 |
+
setShowInstallBanner(false);
|
| 74 |
+
}
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
const handleLogin = async () => {
|
| 78 |
const { error } = await supabase.auth.signInAnonymously();
|
| 79 |
if (error) alert("Login failed: " + error.message);
|
|
|
|
| 114 |
|
| 115 |
if (!session) {
|
| 116 |
return (
|
| 117 |
+
<div style={{height: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center'}}>
|
|
|
|
|
|
|
|
|
|
| 118 |
<div className="glass-panel" style={{padding: '40px', maxWidth: '350px'}}>
|
| 119 |
<h1 style={{fontSize:'2rem', margin:'0 0 10px 0', fontWeight:'200'}}>Liquid AI</h1>
|
| 120 |
<button onClick={handleLogin} className="glass-pill" style={{fontSize:'1.1rem', padding:'12px 30px'}}>Enter</button>
|
|
|
|
| 126 |
return (
|
| 127 |
<div style={{
|
| 128 |
minHeight: '100vh', display: 'flex', flexDirection: 'column',
|
| 129 |
+
// Added padding bottom to offset the fixed bottom dock and center image visually
|
| 130 |
+
paddingBottom: '150px',
|
| 131 |
+
boxSizing: 'border-box'
|
| 132 |
}}>
|
| 133 |
|
| 134 |
+
{/* HEADER with professional spacing */}
|
| 135 |
<div style={{
|
| 136 |
padding: '15px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
| 137 |
+
position: 'sticky', top: 0, zIndex: 10,
|
| 138 |
+
gap: '20px' // Crucial gap to prevent overlapping
|
| 139 |
}}>
|
|
|
|
| 140 |
<div className="logo-expand-container">
|
| 141 |
<span className="logo-main">TEK</span>
|
| 142 |
<span className="logo-hidden">BUILD</span>
|
| 143 |
</div>
|
| 144 |
|
| 145 |
+
<div style={{display: 'flex', gap: '8px', flexShrink: 0}}>
|
| 146 |
<select className="glass-pill" value={selectedRatio} onChange={(e) => setSelectedRatio(e.target.value)}>
|
| 147 |
{ASPECT_RATIOS.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
| 148 |
</select>
|
| 149 |
+
<select className="glass-pill" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} style={{maxWidth:'130px'}}>
|
| 150 |
{AI_MODELS.map(m => <option key={m.id} value={m.id}>{m.name}</option>)}
|
| 151 |
</select>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
+
{/* VIEWPORT - Perfectly Centered */}
|
| 156 |
+
<div style={{
|
| 157 |
+
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 158 |
+
padding: '20px', flexDirection: 'column',
|
| 159 |
+
// Ensures it takes available space but doesn't push out of bounds
|
| 160 |
+
minHeight: 0
|
| 161 |
+
}}>
|
| 162 |
{generating ? (
|
| 163 |
<div className="glass-panel" style={{padding: '20px', borderRadius:'50%'}}>
|
| 164 |
<div style={{fontSize:'40px', animation:'spin 2s infinite'}}>⏳</div>
|
| 165 |
</div>
|
| 166 |
) : imageSrc ? (
|
| 167 |
+
<div className="glass-panel" style={{padding: '10px', maxWidth: '100%', maxHeight: '100%', display:'flex'}}>
|
| 168 |
+
{/* Image constrained to viewport height minus header/footer space */}
|
| 169 |
+
<img src={imageSrc} alt="Generated" style={{maxWidth: '100%', maxHeight: '65vh', borderRadius: '16px', objectFit:'contain'}} />
|
| 170 |
</div>
|
| 171 |
) : (
|
|
|
|
| 172 |
<div style={{opacity: 0.1, transform:'scale(0.8)'}}>❖</div>
|
| 173 |
)}
|
| 174 |
</div>
|
| 175 |
|
| 176 |
+
{/* PWA INSTALL BANNER */}
|
| 177 |
+
{showInstallBanner && (
|
| 178 |
+
<div className="pwa-banner">
|
| 179 |
+
{isIOS ? (
|
| 180 |
+
// iOS Instructions
|
| 181 |
+
<div style={{display:'flex', alignItems:'center', gap:'10px'}}>
|
| 182 |
+
<span style={{fontSize:'24px'}}>📲</span>
|
| 183 |
+
<div style={{textAlign:'left', fontSize:'14px'}}>
|
| 184 |
+
Tap <img src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master Emojis/Objects/Share%20Icon.png" alt="share" width="18" style={{verticalAlign:'middle'}}/> then "Add to Home Screen" for full screen experience.
|
| 185 |
+
</div>
|
| 186 |
+
<button onClick={() => setShowInstallBanner(false)} style={{background:'none', border:'none', color:'#aaa', fontSize:'20px', padding:'0 10px'}}>×</button>
|
| 187 |
+
</div>
|
| 188 |
+
) : (
|
| 189 |
+
// Android Install Button
|
| 190 |
+
<div style={{display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%'}}>
|
| 191 |
+
<span style={{fontSize:'14px', fontWeight:'500'}}>Install App for best experience?</span>
|
| 192 |
+
<div style={{display:'flex', gap:'10px'}}>
|
| 193 |
+
<button onClick={() => setShowInstallBanner(false)} className="glass-pill" style={{padding:'6px 12px', background:'transparent'}}>Later</button>
|
| 194 |
+
<button onClick={handleInstallClick} className="run-btn" style={{borderRadius:'20px', padding:'6px 15px', height:'auto', width:'auto', fontSize:'14px'}}>Install</button>
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
)}
|
| 198 |
+
</div>
|
| 199 |
+
)}
|
| 200 |
+
|
| 201 |
{/* INPUT DOCK */}
|
| 202 |
<div style={{
|
| 203 |
position: 'fixed', bottom: 0, left: 0, right: 0,
|
| 204 |
+
padding: '10px 20px 30px 20px', zIndex: 20,
|
| 205 |
+
// Ensure bottom padding respects iPhone home swipe bar
|
| 206 |
+
paddingBottom: 'calc(30px + env(safe-area-inset-bottom))'
|
| 207 |
}}>
|
|
|
|
| 208 |
<div style={{
|
| 209 |
textAlign: 'center', fontSize: '12px', color: 'rgba(255,255,255,0.6)',
|
| 210 |
marginBottom: '10px', letterSpacing: '1px', textTransform: 'uppercase'
|
|
|
|
| 212 |
{generating ? "Dreaming..." : (imageSrc ? "Done" : "Ready to dream")}
|
| 213 |
</div>
|
| 214 |
|
|
|
|
| 215 |
{previewUrl && (
|
| 216 |
<div style={{position: 'absolute', bottom: '110px', left: '20px'}}>
|
| 217 |
+
<div className="holo-box" style={{padding:'5px', marginBottom:0, display:'inline-block'}}>
|
| 218 |
+
<img src={previewUrl} style={{height: '60px', borderRadius: '4px'}} />
|
| 219 |
+
<button onClick={() => {setPreviewUrl(null); setInputImage(null);}} style={{position: 'absolute', top: -8, right: -8, borderRadius:'50%', width:'20px', height:'20px', border:'none', background:'red', color:'white', fontSize:'12px', display:'flex', alignItems:'center', justifyContent:'center'}}>×</button>
|
| 220 |
</div>
|
| 221 |
</div>
|
| 222 |
)}
|