diff --git a/Backend/app/routes/ai.py b/Backend/app/routes/ai.py index a21a482..8427dce 100644 --- a/Backend/app/routes/ai.py +++ b/Backend/app/routes/ai.py @@ -11,22 +11,53 @@ # Initialize router router = APIRouter() -# Load environment variables for Supabase and Gemini -SUPABASE_URL = os.environ.get("SUPABASE_URL") -SUPABASE_KEY = os.environ.get("SUPABASE_KEY") -GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") +# Cache for the lazily-created Supabase client so it is built only once. +_supabase_client: Client | None = None -# Validate required environment variables -if not all([SUPABASE_URL, SUPABASE_KEY, GEMINI_API_KEY]): - raise ValueError("Missing required environment variables: SUPABASE_URL, SUPABASE_KEY, GEMINI_API_KEY") -supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) +def get_supabase_client() -> Client: + """ + Lazily create and return the Supabase client. + + Reading the environment variables and constructing the client is deferred + to request time so that importing this module never crashes the whole app + when the required variables are missing. If the configuration is absent we + raise an HTTPException so only the AI endpoints fail (with a clear 503), + not unrelated routes. + """ + global _supabase_client + if _supabase_client is not None: + return _supabase_client + + supabase_url = os.environ.get("SUPABASE_URL") + supabase_key = os.environ.get("SUPABASE_KEY") + if not supabase_url or not supabase_key: + raise HTTPException( + status_code=503, + detail="AI service is not configured: missing SUPABASE_URL and/or SUPABASE_KEY.", + ) + + _supabase_client = create_client(supabase_url, supabase_key) + return _supabase_client + + +def get_gemini_api_key() -> str: + """Return the Gemini API key, raising a clear 503 if it is not configured.""" + gemini_api_key = os.environ.get("GEMINI_API_KEY") + if not gemini_api_key: + raise HTTPException( + status_code=503, + detail="AI service is not configured: missing GEMINI_API_KEY.", + ) + return gemini_api_key + def fetch_from_gemini(): prompt = ( "List the top 6 trending content niches for creators and brands this week. For each, provide: name (the niche), insight (a short qualitative reason why it's trending), and global_activity (a number from 1 to 5, where 5 means very high global activity in this category, and 1 means low).Return as a JSON array of objects with keys: name, insight, global_activity." ) - url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key={GEMINI_API_KEY}" + gemini_api_key = get_gemini_api_key() + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key={gemini_api_key}" # Set up retry strategy retry_strategy = Retry( total=3, @@ -60,6 +91,7 @@ def trending_niches(): - Otherwise, fetch from Gemini, store in Supabase, and return the new data. - If Gemini fails, fallback to the most recent data available. """ + supabase = get_supabase_client() today = str(date.today()) # Check if today's data exists in Supabase result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute() @@ -75,6 +107,8 @@ def trending_niches(): "fetched_at": today }).execute() result = supabase.table("trending_niches").select("*").eq("fetched_at", today).execute() + except HTTPException: + raise except Exception as e: print("Gemini fetch failed:", e) # fallback: serve most recent data diff --git a/Frontend/src/pages/Login.tsx b/Frontend/src/pages/Login.tsx index 875567a..ab220bb 100644 --- a/Frontend/src/pages/Login.tsx +++ b/Frontend/src/pages/Login.tsx @@ -1,12 +1,9 @@ import { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import { Eye, EyeOff, Rocket } from "lucide-react"; import { supabase } from "../utils/supabase"; -import { useAuth } from "../context/AuthContext"; export default function LoginPage() { - const Navigate = useNavigate(); - const { login } = useAuth(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); @@ -19,7 +16,7 @@ export default function LoginPage() { setError(""); try { - const { data, error } = await supabase.auth.signInWithPassword({ + const { error } = await supabase.auth.signInWithPassword({ email, password, }); @@ -32,7 +29,7 @@ export default function LoginPage() { // AuthContext will handle navigation based on user onboarding status and role setIsLoading(false); - } catch (err) { + } catch { setError("Invalid email or password. Please try again."); } finally { setIsLoading(false); @@ -40,7 +37,7 @@ export default function LoginPage() { }; const handleGoogleLogin = async () => { - const { data, error } = await supabase.auth.signInWithOAuth({ + const { error } = await supabase.auth.signInWithOAuth({ provider: "google", }); @@ -53,190 +50,204 @@ export default function LoginPage() { }; return ( -
-
+
+ {/* Cinematic Ambient Background Glow */} +
+
+ + {/* Top Navigation Header */} +
- - +
+ +
+ Inpact -
- +
+ Don't have an account? Sign up
-
- -
-
-
-
-

+

+ + {/* Centered Form Area */} +
+ {/* Double-Bezel Nested Card Enclosure */} +
+
+ {/* Header / Brand Welcome */} +
+

Welcome back

-

+

Sign in to your account to continue

+
- {error && ( -
- {error} -
- )} + {error && ( +
+ {error} +
+ )} + +
+ {/* Email Address Field */} +
+ + setEmail(e.target.value)} + required + className="w-full px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-950 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-500 focus:border-transparent transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] font-medium" + placeholder="you@example.com" + /> +
- -
+ {/* Password Field */} +
+
+ + Forgot password? + +
+
setEmail(e.target.value)} + id="password" + type={showPassword ? "text" : "password"} + value={password} + onChange={(e) => setPassword(e.target.value)} required - className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200" - placeholder="you@example.com" + className="w-full px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-950 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-500 focus:border-transparent transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] pr-12 font-medium" + placeholder="••••••••" /> -
- -
-
- - - Forgot password? - -
-
- setPassword(e.target.value)} - required - className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200" - placeholder="••••••••" - /> - -
-
- - - - -
-
-
-
-
-
- - Or continue with - -
-
- -
-
+
+ + {/* Login CTA (Submit Button) */} + -
+ Signing in... +
+ ) : ( + "Sign in" + )} + + + + {/* Separator / Divider */} +
+
+
+
+ + Or continue with + +
+
+ + {/* OAuth Buttons (Centered Grid) */} +
+ +
-
+ -