ajit3259 commited on
Commit
c103e9f
·
1 Parent(s): b862a8b

feat(frontend): cinematic loading, page-load entrance, mobile polish

Browse files

Within the existing amber/polaroid identity:
- loading cycles poetic phrases so the ~40s read feels like attention paid
- staggered rise of wordmark -> tagline -> rule -> dropzone on load
- stronger drag-over state (amber border + inner glow)
- mobile breakpoint for the polaroid stack and film cards
- prefers-reduced-motion guard

Files changed (1) hide show
  1. frontend/src/Chalchitra.jsx +53 -7
frontend/src/Chalchitra.jsx CHANGED
@@ -13,6 +13,16 @@ const STACK = [
13
 
14
  const fanOffset = (i, n) => (i - (n - 1) / 2) * 36;
15
 
 
 
 
 
 
 
 
 
 
 
16
  // Read file → resize via canvas → compressed JPEG data URL.
17
  // Keeps the payload to the model small; the backend accepts data URLs directly.
18
  const processImage = (file) => new Promise((res) => {
@@ -56,13 +66,16 @@ const CSS = `
56
  .chi{max-width:700px;margin:0 auto;padding:60px 24px 80px;position:relative;z-index:2}
57
 
58
  .ch-hd{text-align:center;margin-bottom:52px}
59
- .ch-wm{font-family:var(--fd);font-size:clamp(56px,11vw,92px);font-weight:300;font-style:italic;letter-spacing:.05em;color:var(--tx);line-height:1;text-shadow:0 0 80px rgba(201,146,62,.14);display:block}
60
- .ch-tg{font-family:var(--fd);font-size:10.5px;letter-spacing:.32em;text-transform:uppercase;color:rgba(201,146,62,.88);margin-top:14px}
61
- .ch-rl{width:28px;height:1px;background:var(--bd);margin:18px auto 0}
 
62
 
63
- .ch-dp{border:1px solid var(--bd);padding:56px 40px;text-align:center;cursor:pointer;transition:all .3s;background:var(--am3);position:relative;overflow:hidden}
64
  .ch-dp::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0,rgba(201,146,62,.06) 0%,transparent 65%);pointer-events:none}
65
- .ch-dp:hover,.ch-dp-ov{border-color:var(--am2)}
 
 
66
  .ch-dp-ic{display:block;font-size:24px;color:var(--am);opacity:.45;margin-bottom:18px}
67
  .ch-dp-mn{font-family:var(--fd);font-size:23px;font-style:italic;font-weight:300;color:var(--tx);opacity:.6}
68
  .ch-dp-hn{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--tx2);margin-top:8px}
@@ -108,7 +121,8 @@ const CSS = `
108
  .ch-ld{text-align:center;padding:72px 0}
109
  .ch-lic{display:block;font-size:30px;color:var(--am);animation:chP 2.2s ease-in-out infinite;margin-bottom:22px}
110
  @keyframes chP{0%,100%{opacity:.2;transform:scale(.95)}50%{opacity:.9;transform:scale(1.05)}}
111
- .ch-ltx{font-family:var(--fd);font-size:18px;font-style:italic;font-weight:300;color:var(--tx2);letter-spacing:.12em}
 
112
 
113
  .ch-mini-stack{position:relative;height:116px;margin-bottom:40px}
114
  .ch-mini-photo{position:absolute;width:112px;background:#f0ebe1;padding:3px 3px 14px;box-shadow:0 5px 18px rgba(0,0,0,.6)}
@@ -137,6 +151,26 @@ const CSS = `
137
  .ch-ag button:hover{color:var(--am);border-bottom-color:var(--am2)}
138
 
139
  .ch-ft-cr{margin-top:64px;text-align:center;font-family:var(--fm);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--tx2);opacity:.5}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  `;
141
 
142
  export default function Chalchitra() {
@@ -149,6 +183,7 @@ export default function Chalchitra() {
149
  // Titles already offered this session — sent as `exclude` so refreshing or
150
  // re-rolling brings different films. Stateless server; the memory lives here.
151
  const [seen, setSeen] = useState([]);
 
152
  const ref = useRef(null);
153
 
154
  useEffect(() => {
@@ -158,6 +193,17 @@ export default function Chalchitra() {
158
  document.head.appendChild(link);
159
  }, []);
160
 
 
 
 
 
 
 
 
 
 
 
 
161
  const addFiles = async (fs) => {
162
  const fileArray = Array.from(fs)
163
  .filter(f => f.type.startsWith("image/"))
@@ -302,7 +348,7 @@ export default function Chalchitra() {
302
  {phase === "loading" && (
303
  <div className="ch-ld">
304
  <span className="ch-lic">✦</span>
305
- <p className="ch-ltx">reading your moment</p>
306
  </div>
307
  )}
308
 
 
13
 
14
  const fanOffset = (i, n) => (i - (n - 1) / 2) * 36;
15
 
16
+ // Shown one at a time while the oracle reads — turns a ~40s wait into something
17
+ // that feels like attention being paid, not a spinner.
18
+ const LOADING_PHRASES = [
19
+ "reading your moment",
20
+ "studying the quality of light",
21
+ "listening for the mood beneath",
22
+ "searching the reels",
23
+ "finding what belongs to you",
24
+ ];
25
+
26
  // Read file → resize via canvas → compressed JPEG data URL.
27
  // Keeps the payload to the model small; the backend accepts data URLs directly.
28
  const processImage = (file) => new Promise((res) => {
 
66
  .chi{max-width:700px;margin:0 auto;padding:60px 24px 80px;position:relative;z-index:2}
67
 
68
  .ch-hd{text-align:center;margin-bottom:52px}
69
+ .ch-wm{font-family:var(--fd);font-size:clamp(56px,11vw,92px);font-weight:300;font-style:italic;letter-spacing:.05em;color:var(--tx);line-height:1;text-shadow:0 0 80px rgba(201,146,62,.14);display:block;animation:chRise 1.2s cubic-bezier(.2,.7,.2,1) both}
70
+ .ch-tg{font-family:var(--fd);font-size:10.5px;letter-spacing:.32em;text-transform:uppercase;color:rgba(201,146,62,.88);margin-top:14px;animation:chRise 1.2s .14s cubic-bezier(.2,.7,.2,1) both}
71
+ .ch-rl{width:28px;height:1px;background:var(--bd);margin:18px auto 0;animation:chRise 1.2s .28s cubic-bezier(.2,.7,.2,1) both}
72
+ @keyframes chRise{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}
73
 
74
+ .ch-dp{border:1px solid var(--bd);padding:56px 40px;text-align:center;cursor:pointer;transition:border-color .3s,box-shadow .3s,background .3s;background:var(--am3);position:relative;overflow:hidden;animation:chRise 1.2s .42s cubic-bezier(.2,.7,.2,1) both}
75
  .ch-dp::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 50% 0,rgba(201,146,62,.06) 0%,transparent 65%);pointer-events:none}
76
+ .ch-dp:hover{border-color:var(--am2)}
77
+ .ch-dp-ov{border-color:var(--am);background:var(--am2);box-shadow:inset 0 0 70px rgba(201,146,62,.12)}
78
+ .ch-dp-ov .ch-dp-ic{opacity:.85}
79
  .ch-dp-ic{display:block;font-size:24px;color:var(--am);opacity:.45;margin-bottom:18px}
80
  .ch-dp-mn{font-family:var(--fd);font-size:23px;font-style:italic;font-weight:300;color:var(--tx);opacity:.6}
81
  .ch-dp-hn{font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--tx2);margin-top:8px}
 
121
  .ch-ld{text-align:center;padding:72px 0}
122
  .ch-lic{display:block;font-size:30px;color:var(--am);animation:chP 2.2s ease-in-out infinite;margin-bottom:22px}
123
  @keyframes chP{0%,100%{opacity:.2;transform:scale(.95)}50%{opacity:.9;transform:scale(1.05)}}
124
+ .ch-ltx{font-family:var(--fd);font-size:18px;font-style:italic;font-weight:300;color:var(--tx2);letter-spacing:.12em;animation:chPhrase .9s ease both}
125
+ @keyframes chPhrase{0%{opacity:0;transform:translateY(6px)}100%{opacity:.85;transform:none}}
126
 
127
  .ch-mini-stack{position:relative;height:116px;margin-bottom:40px}
128
  .ch-mini-photo{position:absolute;width:112px;background:#f0ebe1;padding:3px 3px 14px;box-shadow:0 5px 18px rgba(0,0,0,.6)}
 
151
  .ch-ag button:hover{color:var(--am);border-bottom-color:var(--am2)}
152
 
153
  .ch-ft-cr{margin-top:64px;text-align:center;font-family:var(--fm);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--tx2);opacity:.5}
154
+
155
+ @media (max-width:600px){
156
+ .chi{padding:42px 18px 64px}
157
+ .ch-hd{margin-bottom:38px}
158
+ .ch-dp{padding:46px 26px}
159
+ .ch-stack{height:300px}
160
+ .ch-photo{width:74%;left:13%}
161
+ .ch-fi{font-size:18px}
162
+ .ch-ip{padding-left:18px;margin-bottom:34px}
163
+ .ch-ipt{font-size:19px;line-height:1.66}
164
+ .ch-fc{padding:22px 18px}
165
+ .ch-ft{font-size:23px}
166
+ .ch-fc:first-child .ch-ft{font-size:26px}
167
+ .ch-fw{font-size:16px}
168
+ .ch-bn{padding:13px 34px}
169
+ .ch-ag{gap:22px;margin-top:42px}
170
+ }
171
+ @media (prefers-reduced-motion:reduce){
172
+ *{animation-duration:.001ms!important;animation-iteration-count:1!important}
173
+ }
174
  `;
175
 
176
  export default function Chalchitra() {
 
183
  // Titles already offered this session — sent as `exclude` so refreshing or
184
  // re-rolling brings different films. Stateless server; the memory lives here.
185
  const [seen, setSeen] = useState([]);
186
+ const [phraseI, setPhraseI] = useState(0);
187
  const ref = useRef(null);
188
 
189
  useEffect(() => {
 
193
  document.head.appendChild(link);
194
  }, []);
195
 
196
+ // Cycle the loading phrases only while reading.
197
+ useEffect(() => {
198
+ if (phase !== "loading") return;
199
+ setPhraseI(0);
200
+ const id = setInterval(
201
+ () => setPhraseI(i => (i + 1) % LOADING_PHRASES.length),
202
+ 2800
203
+ );
204
+ return () => clearInterval(id);
205
+ }, [phase]);
206
+
207
  const addFiles = async (fs) => {
208
  const fileArray = Array.from(fs)
209
  .filter(f => f.type.startsWith("image/"))
 
348
  {phase === "loading" && (
349
  <div className="ch-ld">
350
  <span className="ch-lic">✦</span>
351
+ <p className="ch-ltx" key={phraseI}>{LOADING_PHRASES[phraseI]}…</p>
352
  </div>
353
  )}
354