Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import sys, os | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from utils.auth import initiate_signup, complete_signup, login | |
| st.set_page_config( | |
| page_title="FitPro AI", | |
| page_icon="β‘", | |
| layout="wide", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| if st.session_state.get("logged_in"): | |
| st.switch_page("pages/2_Dashboard.py") | |
| for k, v in [("mode","landing"),("signup_step","form"), | |
| ("pending_email",""),("login_err",""), | |
| ("signup_err",""),("otp_err","")]: | |
| if k not in st.session_state: | |
| st.session_state[k] = v | |
| login_err = st.session_state.pop("login_err", "") | |
| signup_err = st.session_state.pop("signup_err", "") | |
| otp_err = st.session_state.pop("otp_err", "") | |
| mode = st.session_state.mode | |
| step = st.session_state.signup_step | |
| pending_email = st.session_state.get("pending_email","") | |
| # ββ Auth Pages CSS (only for login/signup) ββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&display=swap'); | |
| /* Reset all Streamlit default spacing */ | |
| #MainMenu,footer,header, | |
| [data-testid="stHeader"],[data-testid="stToolbar"], | |
| [data-testid="stToolbarActions"],[data-testid="stDecoration"], | |
| [data-testid="stStatusWidget"],[data-testid="stSidebarNav"], | |
| [data-testid="collapsedControl"],[data-testid="baseButton-header"], | |
| .stDeployButton,button[kind="header"], | |
| section[data-testid="stSidebar"] { | |
| display:none!important;height:0!important;overflow:hidden!important; | |
| position:absolute!important; | |
| } | |
| /* Natural document flow for auth pages */ | |
| html, body { | |
| margin:0 !important; | |
| padding:0 !important; | |
| width:100% !important; | |
| background:#07070f !important; | |
| font-family:'DM Sans', sans-serif !important; | |
| overflow-x: hidden !important; | |
| overflow-y: auto !important; | |
| } | |
| .stApp { | |
| background:#07070f !important; | |
| min-height: 100vh !important; | |
| } | |
| [data-testid="stAppViewContainer"] { | |
| background:#07070f !important; | |
| width:100% !important; | |
| min-height: 100vh !important; | |
| } | |
| [data-testid="stAppViewContainer"] > section { | |
| padding:0 !important; | |
| width:100% !important; | |
| } | |
| [data-testid="stAppViewContainer"] > section > div.block-container { | |
| padding:0 !important; | |
| margin:0 !important; | |
| max-width:100% !important; | |
| width:100% !important; | |
| } | |
| /* Remove all extra gaps */ | |
| [data-testid="stVerticalBlock"] { gap:0 !important; } | |
| [data-testid="element-container"] { margin:0 !important; padding:0 !important; } | |
| /* Buttons */ | |
| .stButton > button { | |
| background:linear-gradient(135deg,#6366f1,#8b5cf6) !important; | |
| border:none !important; | |
| color:#fff !important; | |
| border-radius:10px !important; | |
| font-family:'DM Sans', sans-serif !important; | |
| font-size:0.95rem !important; | |
| font-weight:600 !important; | |
| padding:13px 28px !important; | |
| width:100% !important; | |
| box-shadow:0 4px 18px rgba(99,102,241,0.32) !important; | |
| transition:all 0.2s !important; | |
| cursor:pointer !important; | |
| } | |
| .stButton > button:hover { | |
| opacity:0.88 !important; | |
| transform:translateY(-2px) !important; | |
| box-shadow:0 8px 28px rgba(99,102,241,0.50) !important; | |
| } | |
| /* Ghost button variant */ | |
| .ghost-btn .stButton > button { | |
| background:transparent !important; | |
| border:1.5px solid rgba(99,102,241,0.40) !important; | |
| color:rgba(241,240,255,0.65) !important; | |
| box-shadow:none !important; | |
| } | |
| .ghost-btn .stButton > button:hover { | |
| border-color:#818cf8 !important; | |
| color:#f1f0ff !important; | |
| background:rgba(99,102,241,0.08) !important; | |
| transform:translateY(-1px) !important; | |
| box-shadow:none !important; | |
| } | |
| /* Inputs */ | |
| div[data-baseweb="input"] > div { | |
| background:#0f0f1e !important; | |
| border:1.5px solid rgba(99,102,241,0.22) !important; | |
| border-radius:10px !important; | |
| } | |
| div[data-baseweb="input"]:focus-within > div { | |
| border-color:#6366f1 !important; | |
| box-shadow:0 0 0 3px rgba(99,102,241,0.12) !important; | |
| } | |
| div[data-baseweb="input"] input { | |
| background:transparent !important; | |
| color:#f1f0ff !important; | |
| font-family:'DM Sans', sans-serif !important; | |
| font-size:0.95rem !important; | |
| height:48px !important; | |
| } | |
| [data-testid="stWidgetLabel"] p { | |
| color:rgba(241,240,255,0.55) !important; | |
| font-size:0.72rem !important; | |
| font-weight:600 !important; | |
| letter-spacing:1.5px !important; | |
| text-transform:uppercase !important; | |
| } | |
| .stAlert { | |
| border-radius:10px !important; | |
| font-size:0.88rem !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LANDING PAGE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if mode == "landing": | |
| # ββ Topnav βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| nav_l, nav_r = st.columns([6, 2]) | |
| with nav_l: | |
| st.markdown(""" | |
| <div style="padding:20px 40px 16px 40px"> | |
| <span style="font-family:'Syne',sans-serif;font-size:1.55rem;font-weight:800; | |
| background:linear-gradient(135deg,#6366f1,#a78bfa); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent; | |
| letter-spacing:1px">β‘ FitPro AI</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with nav_r: | |
| st.markdown("<div style='padding:14px 40px 0 0;display:flex;gap:10px;justify-content:flex-end'>", unsafe_allow_html=True) | |
| nb1, nb2 = st.columns(2) | |
| with nb1: | |
| st.markdown("<div class='ghost-btn'>", unsafe_allow_html=True) | |
| if st.button("Sign In", key="nav_signin"): | |
| st.session_state.mode = "login"; st.rerun() | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with nb2: | |
| if st.button("Get Started", key="nav_start"): | |
| st.session_state.mode = "signup"; st.session_state.signup_step = "form"; st.rerun() | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("<hr style='margin:0;border-color:rgba(99,102,241,0.12)'>", unsafe_allow_html=True) | |
| # ββ Hero ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <div style=" | |
| background: | |
| radial-gradient(ellipse 55% 60% at 10% 20%, rgba(99,102,241,0.13) 0%, transparent 55%), | |
| radial-gradient(ellipse 45% 50% at 90% 80%, rgba(139,92,246,0.10) 0%, transparent 55%), | |
| radial-gradient(circle, rgba(255,255,255,0.025) 1px, transparent 1px) 0 0 / 30px 30px; | |
| padding: 80px 40px 60px; | |
| text-align: center; | |
| "> | |
| <div style="font-size:0.62rem;font-weight:700;letter-spacing:5px; | |
| text-transform:uppercase;color:#818cf8;margin-bottom:18px; | |
| display:flex;align-items:center;justify-content:center;gap:10px"> | |
| <span style="width:28px;height:1px;background:rgba(99,102,241,0.4);display:inline-block"></span> | |
| AI-Powered Personal Training | |
| <span style="width:28px;height:1px;background:rgba(99,102,241,0.4);display:inline-block"></span> | |
| </div> | |
| <h1 style="font-family:'Syne',sans-serif; | |
| font-size:clamp(2.8rem,5vw,5rem);font-weight:800; | |
| line-height:0.92;letter-spacing:-1.5px;margin-bottom:22px;margin-top:0"> | |
| <span style="background:linear-gradient(135deg,#ffffff,rgba(255,255,255,0.75)); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent"> | |
| TRAIN SMARTER. | |
| </span><br> | |
| <span style="background:linear-gradient(135deg,#6366f1,#a78bfa); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent"> | |
| GET REAL RESULTS. | |
| </span> | |
| </h1> | |
| <p style="font-size:1.05rem;color:rgba(241,240,255,0.45);font-weight:300; | |
| max-width:500px;margin:0 auto 44px;line-height:1.8"> | |
| Your AI coach builds a fully personalised workout & nutrition plan | |
| tailored to your body, goals and equipment β generated in seconds. | |
| </p> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:20px"> | |
| <span style="font-size:0.60rem;font-weight:600;letter-spacing:1.8px; | |
| text-transform:uppercase;color:rgba(241,240,255,0.22); | |
| border:1px solid rgba(99,102,241,0.15);border-radius:20px;padding:5px 14px">Strength</span> | |
| <span style="font-size:0.60rem;font-weight:600;letter-spacing:1.8px; | |
| text-transform:uppercase;color:rgba(241,240,255,0.22); | |
| border:1px solid rgba(99,102,241,0.15);border-radius:20px;padding:5px 14px">Fat Loss</span> | |
| <span style="font-size:0.60rem;font-weight:600;letter-spacing:1.8px; | |
| text-transform:uppercase;color:rgba(241,240,255,0.22); | |
| border:1px solid rgba(99,102,241,0.15);border-radius:20px;padding:5px 14px">Muscle Build</span> | |
| <span style="font-size:0.60rem;font-weight:600;letter-spacing:1.8px; | |
| text-transform:uppercase;color:rgba(241,240,255,0.22); | |
| border:1px solid rgba(99,102,241,0.15);border-radius:20px;padding:5px 14px">Nutrition Plans</span> | |
| <span style="font-size:0.60rem;font-weight:600;letter-spacing:1.8px; | |
| text-transform:uppercase;color:rgba(241,240,255,0.22); | |
| border:1px solid rgba(99,102,241,0.15);border-radius:20px;padding:5px 14px">Progress Tracking</span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # CTA buttons | |
| _, cta_l, cta_r, _ = st.columns([2, 1.2, 1.2, 2]) | |
| with cta_l: | |
| if st.button("β‘ Start for Free", key="hero_start", use_container_width=True): | |
| st.session_state.mode = "signup"; st.session_state.signup_step = "form"; st.rerun() | |
| with cta_r: | |
| st.markdown("<div class='ghost-btn'>", unsafe_allow_html=True) | |
| if st.button("Sign In β", key="hero_signin", use_container_width=True): | |
| st.session_state.mode = "login"; st.rerun() | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # ββ Feature cards βββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.markdown("<div style='padding:0 40px'>", unsafe_allow_html=True) | |
| st.markdown("<hr style='border-color:rgba(99,102,241,0.10);margin:40px 0 32px'>", | |
| unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div style="text-align:center;font-size:0.60rem;font-weight:700;letter-spacing:3px; | |
| text-transform:uppercase;color:rgba(241,240,255,0.25);margin-bottom:20px"> | |
| Everything you need | |
| </div>""", unsafe_allow_html=True) | |
| f1, f2, f3 = st.columns(3, gap="medium") | |
| for col, icon, title, desc in [ | |
| (f1,"π€","AI-Generated Plans", | |
| "Groq LLaMA 3 generates your full workout + diet plan in under 60 seconds, personalised to your exact stats, goals and equipment."), | |
| (f2,"π","Progress Tracking", | |
| "Track daily workouts, build streaks, visualise your weekly activity and mark sessions complete right from the dashboard."), | |
| (f3,"π₯","Nutrition Plans", | |
| "Full macro breakdown, sample meal plan, supplement guide and foods to prioritise β all tailored to your BMI and goal."), | |
| ]: | |
| with col: | |
| st.markdown(f""" | |
| <div style="background:rgba(14,14,26,0.9);border:1px solid rgba(99,102,241,0.14); | |
| border-radius:16px;padding:28px 24px;height:100%"> | |
| <div style="font-size:1.8rem;margin-bottom:14px">{icon}</div> | |
| <div style="font-family:'Syne',sans-serif;font-size:1rem;font-weight:700; | |
| color:#f1f0ff;margin-bottom:8px">{title}</div> | |
| <div style="font-size:0.82rem;color:rgba(241,240,255,0.38);line-height:1.7">{desc}</div> | |
| </div>""", unsafe_allow_html=True) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| st.markdown("<div style='height:60px'></div>", unsafe_allow_html=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # AUTH PAGES (Login / Signup / OTP) β simplified natural flow | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| else: | |
| # Simple header | |
| st.markdown(""" | |
| <div style="padding:20px 40px 16px;border-bottom:1px solid rgba(99,102,241,0.10); | |
| background:#07070f"> | |
| <span style="font-family:'Syne',sans-serif;font-size:1.55rem;font-weight:800; | |
| background:linear-gradient(135deg,#6366f1,#a78bfa); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:1px"> | |
| β‘ FitPro AI | |
| </span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Background with subtle gradient (no flex, just padding) | |
| st.markdown(""" | |
| <div style=" | |
| background: | |
| radial-gradient(ellipse 55% 45% at 15% 25%,rgba(99,102,241,0.11) 0%,transparent 60%), | |
| radial-gradient(ellipse 45% 40% at 85% 75%,rgba(139,92,246,0.08) 0%,transparent 55%), | |
| radial-gradient(circle,rgba(255,255,255,0.025) 1px,transparent 1px) 0 0/30px 30px, | |
| #07070f; | |
| width: 100%; | |
| padding: 40px 24px; | |
| "> | |
| """, unsafe_allow_html=True) | |
| # Centered card using columns | |
| _, mid, _ = st.columns([1, 1.4, 1]) | |
| with mid: | |
| # Back home button | |
| back_col, _ = st.columns([1, 3]) | |
| with back_col: | |
| if st.button("β Home", key="back_home"): | |
| st.session_state.mode = "landing"; st.rerun() | |
| # Auth card | |
| st.markdown(""" | |
| <div style="background:rgba(10,10,20,0.97); | |
| border:1px solid rgba(99,102,241,0.30);border-radius:20px; | |
| padding:36px 40px 32px;position:relative; | |
| box-shadow:0 32px 80px rgba(0,0,0,0.60),0 0 0 1px rgba(99,102,241,0.06); | |
| margin: 20px 0;"> | |
| <div style="position:absolute;top:0;left:0;right:0;height:2px; | |
| background:linear-gradient(90deg,transparent,#6366f1 30%, | |
| #a78bfa 50%,#6366f1 70%,transparent)"></div> | |
| """, unsafe_allow_html=True) | |
| # ββ LOGIN ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if mode == "login": | |
| st.markdown(""" | |
| <div style="font-size:0.58rem;font-weight:700;letter-spacing:4px; | |
| text-transform:uppercase;color:#818cf8;margin-bottom:6px"> | |
| Welcome back | |
| </div> | |
| <div style="font-family:'Syne',sans-serif;font-size:2.2rem;font-weight:800; | |
| color:#f1f0ff;margin-bottom:24px;line-height:1">Sign In</div> | |
| """, unsafe_allow_html=True) | |
| if login_err: | |
| st.error(login_err) | |
| with st.form("login_form", clear_on_submit=False): | |
| li_u = st.text_input("Email or Username", placeholder="your@email.com") | |
| li_p = st.text_input("Password", type="password", placeholder="β’β’β’β’β’β’β’β’") | |
| sub = st.form_submit_button("Sign In β", use_container_width=True) | |
| if sub: | |
| if not li_u.strip() or not li_p: | |
| st.error("Please fill all fields.") | |
| else: | |
| ok, token, real_u, msg = login(li_u.strip(), li_p) | |
| if ok: | |
| st.session_state.update(logged_in=True, username=real_u, auth_token=token) | |
| st.switch_page("pages/2_Dashboard.py") | |
| else: | |
| st.error(msg) | |
| st.markdown("<div style='height:16px'></div>", unsafe_allow_html=True) | |
| st.markdown("<div style='text-align:center;font-size:0.85rem;color:rgba(241,240,255,0.35);margin-bottom:8px'>New here?</div>", unsafe_allow_html=True) | |
| if st.button("Create an account β", key="to_signup", use_container_width=True): | |
| st.session_state.mode = "signup"; st.session_state.signup_step = "form"; st.rerun() | |
| # ββ OTP ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| elif mode == "signup" and step == "otp": | |
| st.markdown(f""" | |
| <div style="font-size:0.58rem;font-weight:700;letter-spacing:4px; | |
| text-transform:uppercase;color:#818cf8;margin-bottom:6px">Almost there</div> | |
| <div style="font-family:'Syne',sans-serif;font-size:2.2rem;font-weight:800; | |
| color:#f1f0ff;margin-bottom:16px;line-height:1">Verify Email</div> | |
| <div style="background:rgba(99,102,241,0.08);border:1px solid rgba(99,102,241,0.25); | |
| border-radius:10px;padding:14px;text-align:center;margin-bottom:20px; | |
| font-size:0.85rem;color:rgba(241,240,255,0.45)"> | |
| 6-digit code sent to | |
| <strong style="color:#818cf8;display:block;font-size:0.95rem;margin-top:4px"> | |
| {pending_email} | |
| </strong> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if otp_err: | |
| st.error(otp_err) | |
| with st.form("otp_form", clear_on_submit=False): | |
| otp_val = st.text_input("Verification Code", placeholder="000000", max_chars=6) | |
| sub = st.form_submit_button("Verify & Create Account β", use_container_width=True) | |
| if sub: | |
| if len(otp_val.strip()) < 6: | |
| st.error("Please enter the 6-digit code.") | |
| else: | |
| ok, token, result = complete_signup(pending_email, otp_val.strip()) | |
| if ok: | |
| st.session_state.update( | |
| logged_in=True, username=result, auth_token=token, | |
| mode="landing", signup_step="form", pending_email="" | |
| ) | |
| st.switch_page("pages/2_Dashboard.py") | |
| else: | |
| st.error(result) | |
| st.markdown("<div style='height:8px'></div>", unsafe_allow_html=True) | |
| if st.button("β Wrong email? Go back", key="back_otp"): | |
| st.session_state.signup_step = "form"; st.rerun() | |
| # ββ SIGNUP βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| else: | |
| st.markdown(""" | |
| <div style="font-size:0.58rem;font-weight:700;letter-spacing:4px; | |
| text-transform:uppercase;color:#818cf8;margin-bottom:6px">Get started free</div> | |
| <div style="font-family:'Syne',sans-serif;font-size:2.2rem;font-weight:800; | |
| color:#f1f0ff;margin-bottom:24px;line-height:1">Create Account</div> | |
| """, unsafe_allow_html=True) | |
| if signup_err: | |
| st.error(signup_err) | |
| with st.form("signup_form", clear_on_submit=False): | |
| su_u = st.text_input("Username", placeholder="yourname") | |
| su_e = st.text_input("Email Address", placeholder="your@email.com") | |
| c1, c2 = st.columns(2) | |
| with c1: | |
| su_p = st.text_input("Password", type="password", placeholder="min 6 chars") | |
| with c2: | |
| su_p2 = st.text_input("Confirm Password", type="password", placeholder="repeat") | |
| sub = st.form_submit_button("Create Account β", use_container_width=True) | |
| if sub: | |
| err = "" | |
| if not su_u.strip() or not su_e.strip() or not su_p or not su_p2: | |
| err = "Please fill all fields." | |
| elif "@" not in su_e: | |
| err = "Enter a valid email address." | |
| elif su_p != su_p2: | |
| err = "Passwords don't match." | |
| elif len(su_p) < 6: | |
| err = "Password must be at least 6 characters." | |
| if err: | |
| st.error(err) | |
| else: | |
| ok, mode_flag, token = initiate_signup(su_u.strip(), su_e.strip(), su_p) | |
| if ok and mode_flag == "__DIRECT__": | |
| st.session_state.update( | |
| logged_in=True, username=su_u.strip(), auth_token=token, | |
| mode="landing", signup_step="form", pending_email="" | |
| ) | |
| st.switch_page("pages/2_Dashboard.py") | |
| elif ok and mode_flag == "__OTP__": | |
| st.session_state.pending_email = su_e.strip() | |
| st.session_state.signup_step = "otp" | |
| st.rerun() | |
| else: | |
| st.error(mode_flag) | |
| st.markdown("<div style='height:16px'></div>", unsafe_allow_html=True) | |
| st.markdown("<div style='text-align:center;font-size:0.85rem;color:rgba(241,240,255,0.35);margin-bottom:8px'>Already have an account?</div>", unsafe_allow_html=True) | |
| if st.button("Sign in β", key="to_login", use_container_width=True): | |
| st.session_state.mode = "login"; st.rerun() | |
| st.markdown("</div>", unsafe_allow_html=True) # close auth card | |
| st.markdown("</div>", unsafe_allow_html=True) # close background div |