Shinhati2023 commited on
Commit
0e556f2
·
verified ·
1 Parent(s): 68e4c91

Update client/src/App.jsx

Browse files
Files changed (1) hide show
  1. 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 (Best)' },
12
- { id: 'google/imagen-4.0-preview', name: 'Imagen 4 (Real)' },
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
- maxWidth: '100%', margin: '0 auto', paddingBottom: '140px'
 
 
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:'140px'}}>
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={{flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px', flexDirection: 'column'}}>
 
 
 
 
 
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
- <img src={imageSrc} alt="Generated" style={{maxWidth: '100%', maxHeight: '60vh', borderRadius: '16px', display:'block'}} />
 
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: '8px'}} />
153
- <button onClick={() => {setPreviewUrl(null); setInputImage(null);}} style={{position: 'absolute', top: -10, right: -10, borderRadius:'50%', width:'24px', height:'24px', border:'none', background:'red', color:'white'}}>×</button>
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
  )}