vp / src /streamlit_app.py
deadshot2003's picture
Update src/streamlit_app.py
4f9da33 verified
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">β™‘ &nbsp; β™‘ &nbsp; β™‘</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)