Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import base64 | |
| import os | |
| import time | |
| st.set_page_config(page_title="A Special Invitation πΈ", page_icon="πΈ", layout="centered") | |
| # ββ session state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PASSWORD = "0207" | |
| for k, v in { | |
| "authenticated": False, | |
| "show_yes": False, | |
| "showing_no": False, | |
| "no_index": 0, | |
| }.items(): | |
| if k not in st.session_state: | |
| st.session_state[k] = v | |
| # ββ image helper βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| def img_to_b64(stem): | |
| for ext in (".jpeg", ".jpg", ".png"): | |
| for base in (SCRIPT_DIR, os.getcwd()): | |
| p = os.path.join(base, stem + ext) | |
| if os.path.exists(p): | |
| mime = "image/png" if ext == ".png" else "image/jpeg" | |
| with open(p, "rb") as f: | |
| return base64.b64encode(f.read()).decode(), mime | |
| return None, None | |
| # ββ CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Lato:wght@300;400;700&display=swap'); | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body, .stApp { background: #080508 !important; min-height: 100vh; } | |
| header, footer, #MainMenu, .stDeployButton, | |
| [data-testid="stToolbar"], [data-testid="stDecoration"], | |
| [data-testid="stStatusWidget"] { display: none !important; } | |
| .block-container { | |
| padding: 1rem 1rem 1rem !important; | |
| max-width: 480px !important; | |
| margin: 0 auto !important; | |
| } | |
| #heart-canvas { | |
| position: fixed; top:0; left:0; | |
| width:100vw; height:100vh; | |
| pointer-events:none; z-index:0; | |
| } | |
| /* ββ CARD ββ */ | |
| .card { | |
| position: relative; z-index: 10; | |
| background: linear-gradient(155deg, rgba(28,8,20,0.96), rgba(14,3,11,0.98)); | |
| border: 1px solid rgba(255,105,180,0.25); | |
| border-radius: 24px; | |
| padding: 36px 24px 28px; | |
| text-align: center; | |
| box-shadow: 0 0 60px rgba(255,20,147,0.1), inset 0 1px 0 rgba(255,182,193,0.07); | |
| margin-bottom: 20px; | |
| animation: floatCard 7s ease-in-out infinite; | |
| } | |
| @keyframes floatCard { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} } | |
| .rose { font-size:44px; display:block; margin-bottom:14px; | |
| animation:roseGlow 2.5s ease-in-out infinite; | |
| filter:drop-shadow(0 0 12px rgba(255,20,147,0.5)); } | |
| @keyframes roseGlow { 0%,100%{transform:scale(1) rotate(-3deg)} 50%{transform:scale(1.08) rotate(3deg)} } | |
| .eyebrow { font-family:'Lato',sans-serif; font-size:9px; letter-spacing:4px; | |
| text-transform:uppercase; color:#ff69b4; opacity:.7; margin-bottom:10px; } | |
| .deco { width:40px; height:1px; | |
| background:linear-gradient(90deg,transparent,#ff69b4 50%,transparent); | |
| margin:0 auto 16px; } | |
| .question { font-family:'Playfair Display',serif; font-size:22px; font-weight:700; | |
| color:#fff; line-height:1.45; margin-bottom:6px; } | |
| .question em { color:#ff69b4; font-style:italic; } | |
| .hearts-deco { font-size:11px; color:#ff69b4; opacity:.3; letter-spacing:8px; margin:10px 0 12px; } | |
| .card-sub { font-family:'Lato',sans-serif; font-size:12px; | |
| color:rgba(255,182,193,.4); line-height:1.6; } | |
| /* ββ BUTTONS (Global Styles) ββ */ | |
| div[data-testid="stButton"] > button { | |
| background: linear-gradient(135deg, #ff1493, #ff69b4); | |
| color: #fff; | |
| border: none; | |
| border-radius: 50px; | |
| font-family: 'Lato', sans-serif; | |
| font-size: 16px; | |
| font-weight: 700; | |
| letter-spacing: 1.5px; | |
| text-transform: uppercase; | |
| padding: 16px 32px; | |
| box-shadow: 0 4px 18px rgba(255,20,147,0.38); | |
| transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| width: 100%; | |
| } | |
| div[data-testid="stButton"] > button:active { | |
| transform: scale(0.96); | |
| } | |
| /* ββ OVERLAY (No image + Yes image) ββ */ | |
| .overlay { | |
| position: fixed; top:0; left:0; width:100vw; height:100vh; | |
| background: rgba(0,0,0,0.92); | |
| backdrop-filter: blur(12px); | |
| z-index: 2000; | |
| display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| padding: 24px; gap: 14px; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { from{opacity:0} to{opacity:1} } | |
| .overlay img { | |
| max-width: 280px; width: 85%; | |
| border-radius: 18px; | |
| border: 1px solid rgba(255,105,180,0.2); | |
| box-shadow: 0 0 50px rgba(255,20,147,0.18), 0 16px 40px rgba(0,0,0,0.5); | |
| animation: popIn 0.35s cubic-bezier(0.34,1.56,0.64,1); | |
| margin-top: -20px; | |
| } | |
| @keyframes popIn { from{transform:scale(0.75);opacity:0} to{transform:scale(1);opacity:1} } | |
| .overlay-title { | |
| font-family: 'Playfair Display', serif; | |
| font-size: 24px; color: #ff69b4; | |
| font-style: italic; text-align: center; | |
| margin-top: 10px; | |
| } | |
| .overlay-title.white { color: #fff; font-style: normal; font-weight: 700; } | |
| .overlay-sub { | |
| font-family: 'Lato', sans-serif; | |
| font-size: 13px; color: rgba(255,182,193,0.6); | |
| text-align: center; line-height: 1.6; | |
| } | |
| /* ββ NO MSG UNDER BUTTONS ββ */ | |
| .no-msg { | |
| position: relative; z-index: 10; | |
| font-family: 'Playfair Display', serif; | |
| font-style: italic; font-size: 13px; | |
| color: rgba(255,182,193,0.4); | |
| text-align: center; margin-top: 10px; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| /* ββ LOGIN ββ */ | |
| .login-wrap { | |
| position: relative; z-index: 10; | |
| min-height: 100vh; | |
| display: flex; flex-direction: column; | |
| align-items: center; justify-content: center; | |
| padding: 20px; | |
| } | |
| .login-card { | |
| background: linear-gradient(145deg, rgba(28,8,20,0.97), rgba(12,3,10,0.99)); | |
| border: 1px solid rgba(255,105,180,0.3); | |
| border-radius: 24px; | |
| padding: 40px 32px 36px; | |
| max-width: 340px; width: 100%; | |
| text-align: center; | |
| box-shadow: 0 0 60px rgba(255,20,147,0.1); | |
| animation: floatCard 6s ease-in-out infinite; | |
| } | |
| .login-icon { font-size:42px; display:block; margin-bottom:12px; | |
| filter:drop-shadow(0 0 10px rgba(255,20,147,0.5)); } | |
| .login-eyebrow { font-family:'Lato',sans-serif; font-size:9px; letter-spacing:4px; | |
| color:#ff69b4; opacity:.7; text-transform:uppercase; margin-bottom:8px; } | |
| .login-heading { font-family:'Playfair Display',serif; font-size:22px; color:#fff; margin-bottom:24px; } | |
| div[data-testid="stTextInput"] input { | |
| background: rgba(255,182,193,0.06) !important; | |
| border: 1px solid rgba(255,105,180,0.3) !important; | |
| border-radius: 50px !important; color: #fff !important; | |
| padding: 12px 20px !important; | |
| font-family: 'Lato',sans-serif !important; | |
| letter-spacing: 2px !important; text-align: center !important; | |
| font-size: 14px !important; | |
| } | |
| div[data-testid="stTextInput"] input::placeholder { color:rgba(255,182,193,0.35) !important; } | |
| div[data-testid="stTextInput"] input:focus { border-color:rgba(255,20,147,0.6) !important; outline:none !important; } | |
| div[data-testid="stTextInput"] label { display:none !important; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ floating hearts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| show_confetti = "true" if st.session_state.show_yes else "false" | |
| st.markdown(f""" | |
| <canvas id="heart-canvas"></canvas> | |
| <script> | |
| (function(){{ | |
| const c=document.getElementById('heart-canvas'); | |
| if(!c)return; | |
| const ctx=c.getContext('2d'); | |
| function resize(){{c.width=window.innerWidth;c.height=window.innerHeight;}} | |
| resize(); window.addEventListener('resize',resize); | |
| const cols=['#ff69b4','#ff1493','#ffb6c1','#ff85a2','#ff4d8f']; | |
| const H=function(){{this.reset();}}; | |
| H.prototype.reset=function(){{ | |
| this.x=Math.random()*c.width;this.y=c.height+20; | |
| this.s=Math.random()*12+4;this.sp=Math.random()*0.9+0.25; | |
| this.op=Math.random()*0.35+0.06;this.sw=Math.random()*1.4-0.7; | |
| this.sa=Math.random()*Math.PI*2;this.col=cols[Math.floor(Math.random()*cols.length)]; | |
| this.rot=Math.random()*Math.PI*2; | |
| }}; | |
| const hearts=[]; | |
| for(let i=0;i<30;i++){{const h=new H();h.y=Math.random()*c.height;hearts.push(h);}} | |
| function dh(x,y,s,col,op,rot){{ | |
| ctx.save();ctx.translate(x,y);ctx.rotate(rot); | |
| ctx.globalAlpha=op;ctx.fillStyle=col; | |
| ctx.beginPath();ctx.moveTo(0,-s*.25); | |
| ctx.bezierCurveTo(s*.5,-s*.7,s,s*.1,0,s*.6); | |
| ctx.bezierCurveTo(-s,s*.1,-s*.5,-s*.7,0,-s*.25); | |
| ctx.fill();ctx.restore(); | |
| }} | |
| function animate(){{ | |
| ctx.clearRect(0,0,c.width,c.height); | |
| hearts.forEach(h=>{{ | |
| h.sa+=0.025;h.x+=Math.sin(h.sa)*h.sw;h.y-=h.sp;h.rot+=0.004; | |
| if(h.y<-25)h.reset(); | |
| dh(h.x,h.y,h.s,h.col,h.op,h.rot); | |
| }}); | |
| requestAnimationFrame(animate); | |
| }} | |
| animate(); | |
| if({show_confetti}){{ | |
| const cc=document.createElement('canvas'); | |
| cc.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:1999;'; | |
| cc.width=window.innerWidth;cc.height=window.innerHeight; | |
| document.body.appendChild(cc); | |
| const cctx=cc.getContext('2d'); | |
| const pc=['#ff1493','#ff69b4','#ffb6c1','#ffd700','#ff4500','#fff0f5']; | |
| const pieces=[]; | |
| for(let i=0;i<120;i++) pieces.push({{ | |
| x:Math.random()*cc.width,y:-10-Math.random()*200, | |
| sz:Math.random()*8+3,col:pc[Math.floor(Math.random()*pc.length)], | |
| sp:Math.random()*4+2,sw:Math.random()*2-1,rot:Math.random()*360,rs:Math.random()*4-2,op:1 | |
| }}); | |
| function ac(){{ | |
| cctx.clearRect(0,0,cc.width,cc.height);let alive=false; | |
| pieces.forEach(p=>{{ | |
| if(p.y<cc.height+10){{alive=true;p.y+=p.sp;p.x+=p.sw;p.rot+=p.rs; | |
| if(p.y>cc.height*.5)p.op-=0.012; | |
| cctx.save();cctx.translate(p.x,p.y);cctx.rotate(p.rot*Math.PI/180); | |
| cctx.globalAlpha=Math.max(0,p.op);cctx.fillStyle=p.col; | |
| cctx.fillRect(-p.sz/2,-p.sz/2,p.sz,p.sz*.55);cctx.restore(); | |
| }} | |
| }}); | |
| if(alive)requestAnimationFrame(ac);else cc.remove(); | |
| }} | |
| ac(); | |
| }} | |
| }})(); | |
| </script> | |
| """, unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LOGIN | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not st.session_state.authenticated: | |
| st.markdown(""" | |
| <div class="login-wrap"> | |
| <div class="login-card"> | |
| <span class="login-icon">π</span> | |
| <p class="login-eyebrow">Private Access</p> | |
| <p class="login-heading">Enter your secret code</p> | |
| </div> | |
| </div> | |
| <style>.block-container { margin-top: -20vh !important; }</style> | |
| """, unsafe_allow_html=True) | |
| _, mid, _ = st.columns([1, 4, 1]) | |
| with mid: | |
| pwd = st.text_input("code", type="password", | |
| placeholder="β¦ Access Code β¦", | |
| label_visibility="collapsed") | |
| if st.button("β¦ Unlock β¦", use_container_width=True): | |
| if pwd == PASSWORD: | |
| st.session_state.authenticated = True | |
| st.rerun() | |
| else: | |
| st.error("Incorrect code π") | |
| st.stop() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # YES OVERLAY | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if st.session_state.show_yes: | |
| b64, mime = img_to_b64("yes_image") | |
| img_tag = f'<img src="data:{mime};base64,{b64}"/>' if b64 \ | |
| else '<div style="font-size:72px;margin-bottom:4px">πΈ</div>' | |
| st.markdown(f""" | |
| <div class="overlay"> | |
| {img_tag} | |
| <p class="overlay-title white">She said YES! ππΉπ€</p> | |
| <p class="overlay-sub"> YAYYYYYY!!!! <br>You've made this Valentine's Day absolutely perfect. π€</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.stop() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # NO OVERLAY (Strictly Paired Logic) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if st.session_state.showing_no: | |
| # 1. Define strict pairs so they never desync | |
| # Format: (Image_Name, Text_Message) | |
| pairs = [ | |
| ("no1", "Why though... π₯Ί"), # 1st Click (Index 0) | |
| ("no2", "Really? π"), # 2nd Click (Index 1) | |
| ("no3", "My poor heart... π"), # 3rd Click (Index 2) | |
| ] | |
| # 2. Calculate Index | |
| # If index is 3, modulo 3 = 0 (Back to start). | |
| # If index is 50, modulo 3 = 2 (Correctly shows 3rd pair). | |
| idx = st.session_state.no_index % len(pairs) | |
| stem, t = pairs[idx] | |
| # 3. Load Image | |
| b64, mime = img_to_b64(stem) | |
| img_tag = f'<img src="data:{mime};base64,{b64}"/>' if b64 \ | |
| else '<div style="font-size:72px;margin-bottom:4px">π’</div>' | |
| # 4. Show Overlay | |
| st.markdown(f""" | |
| <div class="overlay"> | |
| {img_tag} | |
| <p class="overlay-title">{t}</p> | |
| <p class="overlay-sub">I'm not letting you leave until you say Yes. π€<br>(Tap the X to try again)</p> | |
| </div> | |
| <style> | |
| /* X Button Styling */ | |
| div[data-testid="stButton"] button {{ | |
| position: fixed !important; top: 24px !important; right: 24px !important; | |
| z-index: 2005 !important; background: rgba(0,0,0,0.5) !important; | |
| color: rgba(255,255,255,0.8) !important; border: 1px solid rgba(255,255,255,0.3) !important; | |
| border-radius: 50% !important; width: 44px !important; height: 44px !important; | |
| padding: 0 !important; font-size: 20px !important; | |
| display: flex !important; align-items: center !important; justify-content: center !important; | |
| }} | |
| div[data-testid="stButton"] button:hover {{ | |
| background: rgba(255,20,147,0.8) !important; border-color: #ff69b4 !important; color: white !important; | |
| }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| if st.button("β", key="close_overlay_btn"): | |
| st.session_state.showing_no = False | |
| st.session_state.no_index += 1 # Increment Counter | |
| st.rerun() | |
| st.stop() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # MAIN PAGE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div class="card"> | |
| <span class="rose">πΉ</span> | |
| <p class="eyebrow">A Special Request</p> | |
| <div class="deco"></div> | |
| <p class="question">Chondi, will you give me the honor of being your<br><em>platonic Valentine</em><br>this year?</p> | |
| <p class="hearts-deco">β‘ β‘ β‘</p> | |
| <p class="card-sub">No pressure... but also, just a little. πΈ</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ββ Dynamic Shrinking Logic ββ | |
| click_count = st.session_state.no_index | |
| # Start at 100%, shrink by 15% each time, don't go below 20% | |
| scale_factor = max(0.2, 1.0 - (click_count * 0.15)) | |
| # CSS: Target NO button (2nd col) and YES button (1st col) | |
| st.markdown(f""" | |
| <style> | |
| /* Target the Second Column (NO Button) */ | |
| div[data-testid="column"]:nth-of-type(2) button {{ | |
| transform: scale({scale_factor}); | |
| transform-origin: center center; | |
| transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); | |
| }} | |
| /* Target the First Column (YES Button) */ | |
| div[data-testid="column"]:nth-of-type(1) button {{ | |
| animation: pulseBtn 2s infinite; | |
| }} | |
| @keyframes pulseBtn {{ | |
| 0% {{ transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 20, 147, 0.7); }} | |
| 70% {{ transform: scale(1.05); box-shadow: 0 0 0 10px rgba(255, 20, 147, 0); }} | |
| 100% {{ transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 20, 147, 0); }} | |
| }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββ Buttons ββ | |
| c1, c2 = st.columns(2) | |
| with c1: | |
| if st.button("β¦ Yes! β¦", key="yes_btn", use_container_width=True): | |
| st.session_state.show_yes = True | |
| st.rerun() | |
| with c2: | |
| if st.button("No", key="no_btn", use_container_width=True): | |
| st.session_state.showing_no = True | |
| st.rerun() | |
| # Guilt Trip Text (Synced Logic) | |
| # Define same messages for bottom text (can be different or same, but cycled safely) | |
| no_messages = [ | |
| "Are you sure? π₯Ί", "Please reconsider... π", | |
| "My heart is shattering...", "I'll wait forever πΈ", | |
| "Pretty please? π", "This wasn't supposed to happen π’", | |
| ] | |
| if st.session_state.no_index > 0: | |
| # Use modulo length of messages to ensure it never breaks | |
| idx = (st.session_state.no_index - 1) % len(no_messages) | |
| st.markdown(f"<p class='no-msg'>{no_messages[idx]}</p>", unsafe_allow_html=True) |