| | import os |
| | import re |
| | import uuid |
| | import pytz |
| | import psycopg2 |
| | import requests |
| | import gradio as gr |
| | import json |
| | import pickle |
| | import base64 |
| |
|
| | from google.auth.transport.requests import Request |
| | from googleapiclient.discovery import build |
| | from google.oauth2.credentials import Credentials |
| |
|
| | from email.mime.text import MIMEText |
| | from datetime import datetime, date |
| | from dotenv import load_dotenv |
| |
|
| |
|
| | |
| |
|
| | load_dotenv() |
| |
|
| | DB_URL = os.getenv("DB_URL") |
| | GMAIL_USER = os.getenv("GMAIL_USER") |
| | CRON_SECRET = os.getenv("CRON") |
| | HF_URL = os.getenv("HF_URL") |
| |
|
| | LEETCODE_API = "https://leetcode-api-vercel.vercel.app" |
| |
|
| | SCOPES = ["https://www.googleapis.com/auth/gmail.send"] |
| | TOKEN_FILE = "token.pkl" |
| |
|
| |
|
| | |
| |
|
| | def get_db(): |
| | return psycopg2.connect(DB_URL, sslmode="require") |
| |
|
| |
|
| | |
| |
|
| | def get_gmail_service(): |
| |
|
| | creds = None |
| |
|
| | if os.path.exists(TOKEN_FILE): |
| | try: |
| | with open(TOKEN_FILE, "rb") as f: |
| | creds = pickle.load(f) |
| | except: |
| | creds = None |
| |
|
| | if creds and creds.expired and creds.refresh_token: |
| | try: |
| | creds.refresh(Request()) |
| |
|
| | with open(TOKEN_FILE, "wb") as f: |
| | pickle.dump(creds, f) |
| |
|
| | except: |
| | creds = None |
| |
|
| | if not creds: |
| | raise Exception("β token.pkl missing. Upload valid token.") |
| |
|
| | return build("gmail", "v1", credentials=creds) |
| |
|
| |
|
| | def send_email(to, subject, html): |
| |
|
| | try: |
| | service = get_gmail_service() |
| |
|
| | msg = MIMEText(html, "html") |
| | msg["To"] = to |
| | msg["From"] = GMAIL_USER |
| | msg["Subject"] = subject |
| |
|
| | raw = base64.urlsafe_b64encode( |
| | msg.as_bytes() |
| | ).decode() |
| |
|
| | body = {"raw": raw} |
| |
|
| | service.users().messages().send( |
| | userId="me", |
| | body=body |
| | ).execute() |
| |
|
| | print("β
Sent:", to) |
| | return True |
| |
|
| | except Exception as e: |
| | print("β Gmail error:", e) |
| | return False |
| |
|
| |
|
| | def create_email_template(title, content, unsubscribe_link, email_type="reminder", problem_link=None, difficulty=None): |
| | """Create a beautiful HTML email template""" |
| | |
| | |
| | colors = { |
| | "morning": {"primary": "#4CAF50", "secondary": "#81C784", "bg": "#E8F5E8"}, |
| | "afternoon": {"primary": "#FF9800", "secondary": "#FFB74D", "bg": "#FFF3E0"}, |
| | "night": {"primary": "#F44336", "secondary": "#EF5350", "bg": "#FFEBEE"}, |
| | "verification": {"primary": "#2196F3", "secondary": "#64B5F6", "bg": "#E3F2FD"} |
| | } |
| | |
| | color = colors.get(email_type, colors["morning"]) |
| | |
| | |
| | difficulty_colors = { |
| | "Easy": "#00B04F", |
| | "Medium": "#FFA116", |
| | "Hard": "#FF375F" |
| | } |
| | |
| | |
| | if email_type == "verification": |
| | problem_button = f""" |
| | <div style="text-align: center; margin: 30px 0;"> |
| | <a href="{unsubscribe_link}" |
| | style="display: inline-block; background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); |
| | color: white; padding: 15px 30px; text-decoration: none; border-radius: 25px; |
| | font-weight: bold; font-size: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); |
| | transition: transform 0.2s;"> |
| | β
Verify Email |
| | </a> |
| | </div> |
| | """ |
| | tips_section = "" |
| | motivation_section = "" |
| | difficulty_badge = "" |
| | else: |
| | |
| | if problem_link: |
| | link_url = problem_link |
| | else: |
| | link_url = f"https://leetcode.com/problems/{title.lower().replace(' ', '-').replace('.', '')}" |
| | |
| | |
| | if difficulty: |
| | diff_color = difficulty_colors.get(difficulty, "#666") |
| | difficulty_badge = f""" |
| | <div style="text-align: center; margin: 10px 0;"> |
| | <span style="background-color: {diff_color}; color: white; padding: 4px 12px; |
| | border-radius: 12px; font-size: 12px; font-weight: bold;"> |
| | {difficulty} |
| | </span> |
| | </div> |
| | """ |
| | else: |
| | difficulty_badge = "" |
| | |
| | problem_button = f""" |
| | <div style="text-align: center; margin: 30px 0;"> |
| | <a href="{link_url}" |
| | style="display: inline-block; background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); |
| | color: white; padding: 15px 30px; text-decoration: none; border-radius: 25px; |
| | font-weight: bold; font-size: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); |
| | transition: transform 0.2s;"> |
| | π Solve Problem |
| | </a> |
| | </div> |
| | """ |
| | |
| | |
| | if difficulty == "Easy": |
| | tips_content = """ |
| | <li>Focus on understanding the problem clearly</li> |
| | <li>Think about the simplest approach first</li> |
| | <li>Test with the given examples</li> |
| | <li>Consider edge cases like empty inputs</li> |
| | """ |
| | elif difficulty == "Medium": |
| | tips_content = """ |
| | <li>Break the problem into smaller subproblems</li> |
| | <li>Consider multiple approaches (greedy, DP, etc.)</li> |
| | <li>Think about time and space complexity</li> |
| | <li>Use appropriate data structures</li> |
| | """ |
| | else: |
| | tips_content = """ |
| | <li>Study similar problems and patterns</li> |
| | <li>Don't rush - take time to understand</li> |
| | <li>Consider advanced algorithms and techniques</li> |
| | <li>Break it down step by step</li> |
| | """ |
| | |
| | tips_section = f""" |
| | <!-- Tips Section --> |
| | <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0;"> |
| | <h3 style="color: #333; margin: 0 0 15px 0; font-size: 18px;">π‘ {difficulty} Problem Tips</h3> |
| | <ul style="color: #666; margin: 0; padding-left: 20px; line-height: 1.6;"> |
| | {tips_content} |
| | </ul> |
| | </div> |
| | """ |
| | |
| | motivation_quotes = [ |
| | "The expert in anything was once a beginner.", |
| | "Every problem is a step forward in your journey.", |
| | "Consistency beats perfection every time.", |
| | "Code today, conquer tomorrow.", |
| | "Small progress is still progress." |
| | ] |
| | import random |
| | quote = random.choice(motivation_quotes) |
| | |
| | motivation_section = f""" |
| | <!-- Stats or Motivation --> |
| | <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; margin: 25px 0;"> |
| | <p style="color: white; margin: 0; font-size: 16px; font-style: italic;"> |
| | "{quote}" πͺ |
| | </p> |
| | </div> |
| | """ |
| | |
| | return f""" |
| | <!DOCTYPE html> |
| | <html> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>LeetCode Daily Tracker</title> |
| | </head> |
| | <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5;"> |
| | <div style="max-width: 600px; margin: 0 auto; background-color: white; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> |
| | |
| | <!-- Header --> |
| | <div style="background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); padding: 30px; text-align: center;"> |
| | <h1 style="color: white; margin: 0; font-size: 28px; font-weight: 300;"> |
| | π§ LeetCode Daily Tracker |
| | </h1> |
| | <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 16px;"> |
| | Your coding journey companion |
| | </p> |
| | </div> |
| | |
| | <!-- Content --> |
| | <div style="padding: 40px 30px;"> |
| | <div style="background-color: {color['bg']}; border-left: 4px solid {color['primary']}; padding: 20px; margin-bottom: 25px; border-radius: 0 8px 8px 0;"> |
| | {content} |
| | {difficulty_badge} |
| | </div> |
| | |
| | {problem_button} |
| | |
| | {tips_section} |
| | |
| | {motivation_section} |
| | </div> |
| | |
| | <!-- Footer --> |
| | <div style="background-color: #f8f9fa; padding: 25px 30px; border-top: 1px solid #eee;"> |
| | <div style="text-align: center;"> |
| | <p style="color: #666; margin: 0 0 15px 0; font-size: 14px;"> |
| | Keep coding, keep growing! π± |
| | </p> |
| | <div style="margin: 15px 0;"> |
| | <a href="https://leetcode.com" style="color: {color['primary']}; text-decoration: none; margin: 0 10px;">π LeetCode</a> |
| | <span style="color: #ccc;">|</span> |
| | <a href="https://github.com" style="color: {color['primary']}; text-decoration: none; margin: 0 10px;">π» GitHub</a> |
| | <span style="color: #ccc;">|</span> |
| | <a href="{unsubscribe_link if email_type != 'verification' else '#'}" style="color: #999; text-decoration: none; margin: 0 10px; font-size: 12px;">{'Unsubscribe' if email_type != 'verification' else ''}</a> |
| | </div> |
| | <p style="color: #999; font-size: 12px; margin: 15px 0 0 0;"> |
| | Β© 2024 LeetCode Daily Tracker. Made with β€οΈ for coders. |
| | </p> |
| | </div> |
| | </div> |
| | </div> |
| | </body> |
| | </html> |
| | """ |
| |
|
| |
|
| | |
| |
|
| | EMAIL_REGEX = re.compile( |
| | r"^[\w\.-]+@[\w\.-]+\.\w+$" |
| | ) |
| |
|
| |
|
| | def valid_email(email): |
| | return bool(email and EMAIL_REGEX.match(email)) |
| |
|
| |
|
| | def valid_leetcode(username): |
| |
|
| | if not username or len(username) < 3: |
| | return False |
| |
|
| | try: |
| | r = requests.get( |
| | f"{LEETCODE_API}/{username}", |
| | timeout=8 |
| | ) |
| | return r.status_code == 200 |
| |
|
| | except: |
| | return False |
| |
|
| |
|
| | |
| |
|
| | def get_daily_problem(): |
| | try: |
| | r = requests.get(f"{LEETCODE_API}/daily", timeout=10) |
| | r.raise_for_status() |
| |
|
| | d = r.json() |
| | print("π‘ Daily API response keys:", list(d.keys())) |
| |
|
| | |
| | if "questionTitle" in d and "titleSlug" in d: |
| | title = d["questionTitle"] |
| | slug = d["titleSlug"] |
| | link = d.get("questionLink", f"https://leetcode.com/problems/{slug}/") |
| | difficulty = d.get("difficulty", "Unknown") |
| | print(f"β
Found daily problem: {title} ({difficulty}) - {slug}") |
| | return title, slug, link, difficulty |
| |
|
| | |
| | if "title" in d and "titleSlug" in d: |
| | title = d["title"] |
| | slug = d["titleSlug"] |
| | link = f"https://leetcode.com/problems/{slug}/" |
| | difficulty = d.get("difficulty", "Unknown") |
| | print(f"β
Found daily problem (fallback): {title} ({difficulty}) - {slug}") |
| | return title, slug, link, difficulty |
| |
|
| | |
| | print("β Available fields in API response:", list(d.keys())) |
| | raise Exception("Could not find title and slug in daily API response") |
| |
|
| | except requests.exceptions.RequestException as e: |
| | print(f"β Network error calling daily API: {e}") |
| | raise Exception(f"Failed to fetch daily problem: {e}") |
| | |
| | except json.JSONDecodeError as e: |
| | print(f"β JSON decode error: {e}") |
| | raise Exception("Invalid JSON response from daily API") |
| |
|
| |
|
| | def solved_today(username, slug): |
| |
|
| | try: |
| | r = requests.get( |
| | f"{LEETCODE_API}/{username}/acSubmission?limit=20", |
| | timeout=10 |
| | ) |
| |
|
| | if r.status_code != 200: |
| | return False |
| |
|
| | d = r.json() |
| |
|
| | if "submission" in d: |
| | subs = d["submission"] |
| | elif "data" in d: |
| | subs = d["data"] |
| | elif isinstance(d, list): |
| | subs = d |
| | else: |
| | return False |
| |
|
| | today = date.today() |
| |
|
| | for s in subs: |
| |
|
| | if not isinstance(s, dict): |
| | continue |
| |
|
| | if s.get("titleSlug") != slug: |
| | continue |
| |
|
| | ts = s.get("timestamp") |
| |
|
| | if not ts: |
| | continue |
| |
|
| | solved = datetime.fromtimestamp( |
| | int(ts), |
| | tz=pytz.utc |
| | ).date() |
| |
|
| | if solved == today: |
| | return True |
| |
|
| | return False |
| |
|
| | except: |
| | return False |
| |
|
| |
|
| | |
| |
|
| | def subscribe(username, email, timezone): |
| |
|
| | if not valid_leetcode(username): |
| | return "β Invalid LeetCode username" |
| |
|
| | if not valid_email(email): |
| | return "β Invalid email" |
| |
|
| | conn = get_db() |
| | cur = conn.cursor() |
| |
|
| | cur.execute(""" |
| | SELECT email_verified, verification_token, unsubscribed |
| | FROM users WHERE email=%s |
| | """, (email,)) |
| |
|
| | row = cur.fetchone() |
| |
|
| | |
| | if row: |
| | verified, token, unsub = row |
| |
|
| | if verified and not unsub: |
| | cur.close() |
| | conn.close() |
| | return "β οΈ Already subscribed" |
| |
|
| | if verified and unsub: |
| | cur.execute(""" |
| | UPDATE users |
| | SET unsubscribed=false, |
| | leetcode_username=%s, |
| | timezone=%s, |
| | last_sent_date=NULL, |
| | last_sent_slot=NULL |
| | WHERE email=%s |
| | """, (username, timezone, email)) |
| |
|
| | conn.commit() |
| | cur.close() |
| | conn.close() |
| | return "β
Re-subscribed" |
| |
|
| | |
| | verification_content = f""" |
| | <h2 style="color: #2196F3; margin: 0 0 15px 0;">Welcome back! π</h2> |
| | <p style="color: #333; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;"> |
| | We're excited to have you on your coding journey again! |
| | </p> |
| | <p style="color: #666; font-size: 14px; margin: 0;"> |
| | Click the verification button above to activate your daily LeetCode reminders. |
| | </p> |
| | """ |
| | |
| | html_email = create_email_template( |
| | "Email Verification", |
| | verification_content, |
| | f"{HF_URL}?verify={token}", |
| | "verification" |
| | ) |
| |
|
| | send_email(email, "π Please verify your email", html_email) |
| | cur.close() |
| | conn.close() |
| | return "π© Verification re-sent" |
| |
|
| | |
| | token = uuid.uuid4().hex |
| |
|
| | cur.execute(""" |
| | INSERT INTO users( |
| | leetcode_username,email,timezone, |
| | email_verified,verification_token,unsubscribed |
| | ) |
| | VALUES(%s,%s,%s,false,%s,false) |
| | """, (username, email, timezone, token)) |
| |
|
| | conn.commit() |
| | cur.close() |
| | conn.close() |
| |
|
| | |
| | welcome_content = f""" |
| | <h2 style="color: #4CAF50; margin: 0 0 15px 0;">Welcome to the club! π</h2> |
| | <p style="color: #333; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;"> |
| | You're about to embark on an amazing coding journey with daily LeetCode challenges! |
| | </p> |
| | <div style="background-color: white; border: 2px dashed #4CAF50; padding: 15px; border-radius: 8px; margin: 15px 0;"> |
| | <p style="color: #4CAF50; font-weight: bold; margin: 0 0 5px 0;">π
Your Schedule:</p> |
| | <p style="color: #666; font-size: 14px; margin: 0;"> |
| | π
<strong>9 AM</strong> - Daily problem<br> |
| | π <strong>3 PM</strong> - Gentle reminder<br> |
| | π <strong>8 PM</strong> - Final reminder |
| | </p> |
| | </div> |
| | <p style="color: #666; font-size: 14px; margin: 0;"> |
| | Click the verification button above to start receiving your personalized reminders! |
| | </p> |
| | """ |
| | |
| | html_email = create_email_template( |
| | "Welcome", |
| | welcome_content, |
| | f"{HF_URL}?verify={token}", |
| | "verification" |
| | ) |
| |
|
| | send_email(email, "π― Verify your LeetCode journey!", html_email) |
| | return "π© Verification sent" |
| |
|
| |
|
| | |
| |
|
| | def verify_user(token): |
| |
|
| | conn = get_db() |
| | cur = conn.cursor() |
| |
|
| | cur.execute(""" |
| | UPDATE users |
| | SET email_verified=true |
| | WHERE verification_token=%s |
| | AND email_verified=false |
| | """, (token,)) |
| |
|
| | ok = cur.rowcount |
| |
|
| | conn.commit() |
| | cur.close() |
| | conn.close() |
| |
|
| | if ok == 0: |
| | return "β Invalid link" |
| |
|
| | return "β
Email verified" |
| |
|
| |
|
| | |
| |
|
| | def unsubscribe_user(token): |
| |
|
| | conn = get_db() |
| | cur = conn.cursor() |
| |
|
| | cur.execute(""" |
| | UPDATE users |
| | SET unsubscribed=true |
| | WHERE verification_token=%s |
| | """, (token,)) |
| |
|
| | ok = cur.rowcount |
| |
|
| | conn.commit() |
| | cur.close() |
| | conn.close() |
| |
|
| | if ok == 0: |
| | return "β Invalid link" |
| |
|
| | return "β
Unsubscribed" |
| |
|
| |
|
| | |
| |
|
| | def handle_url(request: gr.Request): |
| |
|
| | try: |
| | params = request.query_params |
| |
|
| | if "verify" in params: |
| | return verify_user(params["verify"]) |
| |
|
| | if "unsubscribe" in params: |
| | return unsubscribe_user(params["unsubscribe"]) |
| |
|
| | return "" |
| |
|
| | except Exception as e: |
| | print("URL error:", e) |
| | return "" |
| |
|
| |
|
| |
|
| | def run_scheduler(secret): |
| |
|
| | if secret != CRON_SECRET: |
| | return "β Unauthorized" |
| |
|
| | conn = get_db() |
| | cur = conn.cursor() |
| |
|
| | cur.execute(""" |
| | SELECT id,leetcode_username,email,timezone, |
| | last_sent_date,last_sent_slot,verification_token |
| | FROM users |
| | WHERE email_verified=true |
| | AND unsubscribed=false |
| | """) |
| |
|
| | users = cur.fetchall() |
| |
|
| | try: |
| | title, slug, problem_link, difficulty = get_daily_problem() |
| | except Exception as e: |
| | cur.close() |
| | conn.close() |
| | return f"β Failed to get daily problem: {e}" |
| |
|
| | now = datetime.now(pytz.utc) |
| | sent = 0 |
| |
|
| | for uid, user, mail, tz, last_d, last_s, token in users: |
| |
|
| | try: |
| | local = now.astimezone(pytz.timezone(tz)) |
| | h = local.hour |
| |
|
| | |
| | if 8 <= h <= 9: |
| | slot = "morning" |
| | subject = f"π
Today's LeetCode Challenge: {title}" |
| | content = f""" |
| | <h2 style="color: #4CAF50; margin: 0 0 15px 0;">Good morning, coder! βοΈ</h2> |
| | <p style="color: #333; font-size: 18px; font-weight: bold; margin: 0 0 10px 0;"> |
| | Today's Problem: <span style="color: #4CAF50;">{title}</span> |
| | </p> |
| | <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0;"> |
| | Start your day with a fresh challenge! This {difficulty} problem is perfect for warming up |
| | your coding muscles. Take your time to understand the requirements! π |
| | </p> |
| | """ |
| | email_type = "morning" |
| |
|
| | elif 14 <= h <= 15: |
| | slot = "afternoon" |
| | subject = f"β° Afternoon Coding Break: {title}" |
| | content = f""" |
| | <h2 style="color: #FF9800; margin: 0 0 15px 0;">Time for a coding break! β‘</h2> |
| | <p style="color: #333; font-size: 16px; margin: 0 0 15px 0;"> |
| | Haven't tackled <strong style="color: #FF9800;">{title}</strong> yet? No worries! |
| | </p> |
| | <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0;"> |
| | This {difficulty} problem is waiting for you. Sometimes a fresh afternoon perspective |
| | can lead to breakthrough solutions! π‘ |
| | </p> |
| | """ |
| | email_type = "afternoon" |
| |
|
| | elif 19 <= h <= 20: |
| | slot = "night" |
| | subject = f"π Last Call: {title}" |
| | content = f""" |
| | <h2 style="color: #F44336; margin: 0 0 15px 0;">Final reminder! π₯</h2> |
| | <p style="color: #333; font-size: 16px; margin: 0 0 15px 0;"> |
| | <strong style="color: #F44336;">{title}</strong> ({difficulty}) is still waiting for you! |
| | </p> |
| | <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;"> |
| | Don't let the day end without giving it a try. Even reading through the problem |
| | and thinking about approaches counts as progress! |
| | </p> |
| | <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 8px;"> |
| | <p style="color: #856404; margin: 0; font-size: 14px;"> |
| | πͺ <strong>Remember:</strong> Consistency beats perfection. Every attempt makes you stronger! |
| | </p> |
| | </div> |
| | """ |
| | email_type = "night" |
| |
|
| | else: |
| | continue |
| |
|
| | today = date.today() |
| |
|
| | |
| | if last_d == today and last_s == slot: |
| | print(f"βοΈ Skipping {mail} - already sent {slot} email today") |
| | continue |
| |
|
| | |
| | if solved_today(user, slug): |
| | print(f"β
{user} already solved {slug} - skipping email") |
| | continue |
| |
|
| | |
| | html_email = create_email_template( |
| | title, |
| | content, |
| | f"{HF_URL}?unsubscribe={token}", |
| | email_type, |
| | problem_link, |
| | difficulty |
| | ) |
| |
|
| | ok = send_email(mail, subject, html_email) |
| |
|
| | if not ok: |
| | print(f"β Failed to send email to {mail}") |
| | continue |
| |
|
| | |
| | cur.execute(""" |
| | UPDATE users |
| | SET last_sent_date=%s, |
| | last_sent_slot=%s |
| | WHERE id=%s |
| | """, (today, slot, uid)) |
| |
|
| | sent += 1 |
| |
|
| | except Exception as e: |
| | print(f"β Error processing user {user} ({mail}): {e}") |
| | continue |
| |
|
| | conn.commit() |
| | cur.close() |
| | conn.close() |
| |
|
| | return f"β
Scheduler completed. Sent: {sent} emails" |
| |
|
| |
|
| |
|
| | |
| | with gr.Blocks( |
| | title="LeetCode Notifier", |
| | theme=gr.themes.Soft(), |
| | css=""" |
| | .gradio-container { |
| | max-width: 800px !important; |
| | margin: auto !important; |
| | } |
| | """ |
| | ) as app: |
| |
|
| | gr.Markdown(""" |
| | # π¬ LeetCode Daily Email Notifier |
| | |
| | Get personalized daily LeetCode problem reminders sent directly to your inbox! |
| | Never miss a day of coding practice. |
| | """) |
| |
|
| | with gr.Row(): |
| | with gr.Column(): |
| | u = gr.Textbox( |
| | label="π§βπ» LeetCode Username", |
| | placeholder="Enter your LeetCode username", |
| | info="We'll verify this username exists on LeetCode" |
| | ) |
| | m = gr.Textbox( |
| | label="π§ Email Address", |
| | placeholder="your.email@gmail.com", |
| | info="You'll receive a verification email" |
| | ) |
| | tz = gr.Dropdown( |
| | choices=sorted(pytz.all_timezones), |
| | value="Asia/Kolkata", |
| | label="π Timezone", |
| | info="Choose your timezone for proper scheduling" |
| | ) |
| |
|
| | with gr.Row(): |
| | subscribe_btn = gr.Button("π Subscribe", variant="primary", scale=2) |
| | |
| | out = gr.Textbox(label="π Status", interactive=False, lines=2) |
| | |
| | subscribe_btn.click(subscribe, [u, m, tz], out) |
| |
|
| | gr.Markdown(""" |
| | --- |
| | ### β° Email Schedule |
| | - **π
9:00 AM** - Daily problem notification |
| | - **π 3:00 PM** - Gentle reminder (if not solved) |
| | - **π 8:00 PM** - Final reminder (if not solved) |
| | |
| | ### π Admin Panel |
| | """) |
| |
|
| | with gr.Row(): |
| | sec = gr.Textbox( |
| | label="π Secret Key", |
| | type="password", |
| | placeholder="Enter scheduler secret key" |
| | ) |
| | run_btn = gr.Button("βΆοΈ Run Scheduler", variant="secondary") |
| |
|
| | run_btn.click(run_scheduler, sec, out) |
| |
|
| | |
| | app.load(handle_url, outputs=out) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | app.launch(debug=True) |