Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import requests | |
| import json | |
| import os | |
| from dotenv import load_dotenv | |
| import time | |
| # Load environment variables from .env file (for local development) | |
| load_dotenv() | |
| # Get environment variables and strip whitespace | |
| organisationNumber = os.getenv("organisationNumber", "").strip() | |
| token = os.getenv("token", "").strip() | |
| postApiUrl = os.getenv("postApiUrl", "").strip() | |
| # RESTORED: ServiceTrigger environment variable | |
| ServiceTrigger = os.getenv("ServiceTrigger", "").strip() | |
| # Validate environment variables on startup | |
| missing_vars = [] | |
| if not organisationNumber: | |
| missing_vars.append("organisationNumber") | |
| if not token: | |
| missing_vars.append("token") | |
| if not postApiUrl: | |
| missing_vars.append("postApiUrl") | |
| # Added validation for ServiceTrigger | |
| if not ServiceTrigger: | |
| missing_vars.append("ServiceTrigger") | |
| if missing_vars: | |
| print(f"⚠️ WARNING: Missing environment variables: {', '.join(missing_vars)}") | |
| print("Please check your Hugging Face Spaces Secrets configuration") | |
| def notify_discord(name, job_post, post_url): | |
| # This URL should be loaded from your environment variables (Discord Webhook URL) | |
| discord_webhook_url = os.getenv("DISCORD_WEBHOOK_URL") | |
| if not discord_webhook_url: | |
| print("⚠️ WARNING: DISCORD_WEBHOOK_URL is not set. Skipping Discord notification.") | |
| return | |
| discord_message = { | |
| "content": "✨ New Job Posted! ✨", | |
| "embeds": [ | |
| { | |
| "title": f"Job Post by {name}", | |
| "description": job_post[:2048] + ('...' if len(job_post) > 2048 else ''), | |
| "url": post_url, | |
| "color": 16744704 # Orange color code | |
| } | |
| ] | |
| } | |
| response = requests.post(discord_webhook_url, json=discord_message) | |
| name = "abcd" | |
| job_post = "efgh" | |
| post_url = "https://www.linkedin.com/feed/update/urn:li:share:1234567890123456789/" | |
| discord_message = { | |
| "content": "✨ New Job Posted! ✨", | |
| "embeds": [ | |
| { | |
| "title": f"Job Post by {name}", | |
| "description": job_post[:2048] + ('...' if len(job_post) > 2048 else ''), | |
| "url": post_url, | |
| "color": 16744704 # Orange color code | |
| } | |
| ] | |
| } | |
| # notify_discord(name, job_post, post_url) | |
| def check_website_status(): | |
| """Check if the FeedHire website is accessible""" | |
| try: | |
| response = requests.get("https://feedhire.me/", timeout=10) | |
| if response.status_code == 200: | |
| return "🟢", "Online", "#10b981" | |
| else: | |
| return "🟡", f"Status {response.status_code}", "#f59e0b" | |
| except requests.exceptions.RequestException: | |
| return "🔴", "Offline", "#ef4444" | |
| def check_api_status(): | |
| """Check if the LinkedIn API is accessible and token is valid""" | |
| if not token or not organisationNumber: | |
| return "🔴", "No Credentials", "#ef4444" | |
| try: | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "LinkedIn-Version": "202510", | |
| "X-Restli-Protocol-Version": "2.0.0" | |
| } | |
| response = requests.get( | |
| f"https://api.linkedin.com/rest/organizations/{organisationNumber}", | |
| headers=headers, | |
| timeout=10 | |
| ) | |
| if response.status_code == 200: | |
| return "🟢", "Connected", "#10b981" | |
| elif response.status_code == 401: | |
| return "🔴", "Auth Failed", "#ef4444" | |
| elif response.status_code == 403: | |
| return "🟡", "Access Denied", "#f59e0b" | |
| else: | |
| return "🟡", f"Error {response.status_code}", "#f59e0b" | |
| except requests.exceptions.RequestException: | |
| return "🔴", "Connection Failed", "#ef4444" | |
| def get_status_display(): | |
| """Get formatted status display for both services""" | |
| web_icon, web_status, web_color = check_website_status() | |
| api_icon, api_status, api_color = check_api_status() | |
| html = f""" | |
| <div class="status-container"> | |
| <div class="status-item"> | |
| <span class="status-icon">{web_icon}</span> | |
| <div> | |
| <div class="status-label">Website</div> | |
| <div class="status-value" style="color: {web_color};">{web_status}</div> | |
| </div> | |
| </div> | |
| <div class="status-divider"></div> | |
| <div class="status-item"> | |
| <span class="status-icon">{api_icon}</span> | |
| <div> | |
| <div class="status-label">API</div> | |
| <div class="status-value" style="color: {api_color};">{api_status}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| return html | |
| def post_to_linkedin(name, job_post): | |
| """Post to LinkedIn via their API with improved UX feedback""" | |
| yield "Verifying inputs...", gr.update(value="", visible=False) | |
| if not organisationNumber or not token or not postApiUrl: | |
| yield f"❌ Configuration Error: Missing LinkedIn API variables. Please check Spaces Secrets.", gr.update(visible=False) | |
| return | |
| # Check for ServiceTrigger (if critical for the process) | |
| if not ServiceTrigger: | |
| yield f"❌ Configuration Error: Missing ServiceTrigger variable. FeedHire update will be manual.", gr.update(visible=False) | |
| # Note: We continue execution here as the LinkedIn post is the primary goal | |
| if not name.strip() or not job_post.strip(): | |
| yield "❌ Please fill in all fields (Name and Job Post).", gr.update(visible=False) | |
| return | |
| headers_dict = { | |
| 'Authorization': f'Bearer {token}', | |
| 'Content-Type': 'application/json', | |
| 'X-Restli-Protocol-Version': '2.0.0', | |
| 'LinkedIn-Version': '202510' | |
| } | |
| data = { | |
| "author": f"urn:li:organization:{organisationNumber}", | |
| "commentary": f"{name}\n\n{job_post}", | |
| "visibility": "PUBLIC", | |
| "lifecycleState": "PUBLISHED", | |
| "distribution": { | |
| "feedDistribution": "MAIN_FEED", | |
| "targetEntities": [], | |
| "thirdPartyDistributionChannels": [] | |
| }, | |
| "isReshareDisabledByAuthor": False | |
| } | |
| try: | |
| yield "🚀 Posting to LinkedIn...", gr.update(visible=False) | |
| response = requests.post( | |
| postApiUrl, | |
| headers=headers_dict, | |
| json=data, | |
| timeout=30 | |
| ) | |
| if response.status_code == 201: | |
| for i in range(10, 0, -1): | |
| yield f"✅ Post successful! Waiting for LinkedIn API update... ({i} seconds remaining)", gr.update(visible=False) | |
| time.sleep(1) | |
| # RESTORED: Service Trigger Logic | |
| if ServiceTrigger: | |
| try: | |
| trigger_resp = requests.post(f"{ServiceTrigger}", timeout=10) | |
| yield "🔄 FeedHire website triggered, waiting for job listing update...", gr.update(visible=False) | |
| if trigger_resp.status_code == 200: | |
| yield "✅ Backend triggered successfully! FeedHire will update the listing shortly.", gr.update(visible=False) | |
| else: | |
| yield f"⚠️ Backend trigger failed (Status: {trigger_resp.status_code}). FeedHire update may be delayed.", gr.update(visible=False) | |
| except Exception as trigger_error: | |
| print(f"Error triggering async fetch: {trigger_error}") | |
| yield f"⚠️ Error triggering FeedHire backend: {trigger_error}", gr.update(visible=False) | |
| yield "✅ Post live! 🔍 Retrieving your post link...", gr.update(visible=False) | |
| try: | |
| post_api_url = "https://api.linkedin.com/rest/posts" | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "LinkedIn-Version": "202510", | |
| "X-Restli-Protocol-Version": "2.0.0" | |
| } | |
| params = { | |
| "q": "author", | |
| "author": f"urn:li:organization:{organisationNumber}", | |
| "count": 1 | |
| } | |
| fetch_response = requests.get(post_api_url, headers=headers, params=params, timeout=30) | |
| if fetch_response.status_code == 200: | |
| fetch_data = fetch_response.json() | |
| if fetch_data.get("elements") and len(fetch_data["elements"]) > 0: | |
| post_id_urn = fetch_data["elements"][0]["id"] | |
| if "share" in post_id_urn: | |
| post_id = post_id_urn.split(":")[-1] | |
| post_url = f"https://www.linkedin.com/feed/update/urn:li:share:{post_id}/" | |
| elif "activity" in post_id_urn: | |
| post_id = post_id_urn.split(":")[-1] | |
| post_url = f"https://www.linkedin.com/feed/update/urn:li:activity:{post_id}/" | |
| else: | |
| raise Exception("Unknown post URN format") | |
| print(f"Post URL: {post_url}") | |
| notify_discord(name, job_post, post_url) | |
| yield "✅ Post published successfully!", gr.update(value=f"### 🎉 [Click here to view your post on LinkedIn →]({post_url})", visible=True) | |
| return | |
| notify_discord(name, job_post, "https://www.linkedin.com/company/109539782/posts/") | |
| yield "✅ Post published! (Could not auto-retrieve post link. Please check the LinkedIn page.)", gr.update(visible=False) | |
| except Exception as fetch_error: | |
| print(f"Error fetching post: {fetch_error}") | |
| notify_discord(name, job_post, "https://www.linkedin.com/company/109539782/posts/") | |
| yield f"✅ Post published! (Error retrieving link: {fetch_error})", gr.update(visible=False) | |
| else: | |
| try: | |
| error_data = response.json() | |
| error_message = error_data.get('message', response.text) | |
| except requests.exceptions.JSONDecodeError: | |
| error_message = response.text | |
| error_msg = f"❌ Error {response.status_code}: {error_message}" | |
| print(error_msg) | |
| yield error_msg, gr.update(visible=False) | |
| except requests.exceptions.Timeout: | |
| yield "❌ Error: Request timed out. Please try again.", gr.update(visible=False) | |
| except requests.exceptions.RequestException as e: | |
| yield f"❌ Network Error: {str(e)}", gr.update(visible=False) | |
| except Exception as e: | |
| yield f"❌ Error: {str(e)}", gr.update(visible=False) | |
| # --- Ultra-Premium Animated Gradio Interface with Orange Theme --- | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap'); | |
| * { | |
| font-family: 'Inter', sans-serif !important; | |
| } | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| margin: auto !important; | |
| } | |
| .transparent-box { | |
| background: transparent !important; | |
| border: none !important; /* Remove border */ | |
| box-shadow: none !important; /* Remove shadow */ | |
| padding: 0 !important; /* Optional: remove extra padding */ | |
| } | |
| /* Transparent container + all children */ | |
| .transparent-box, | |
| .transparent-box > div, | |
| .transparent-box * { | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| } | |
| /* Hero Section Animations (Unchanged) */ | |
| @keyframes fadeInDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes scaleIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.8); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { | |
| transform: scale(1); | |
| } | |
| 50% { | |
| transform: scale(1.05); | |
| } | |
| } | |
| /* Updated Glow to Orange */ | |
| @keyframes glow { | |
| 0%, 100% { | |
| box-shadow: 0 0 20px rgba(255, 126, 0, 0.5); /* Vibrant Orange glow */ | |
| } | |
| 50% { | |
| box-shadow: 0 0 40px rgba(255, 126, 0, 0.8); /* Vibrant Orange glow */ | |
| } | |
| } | |
| @keyframes slideInLeft { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-50px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| @keyframes slideInRight { | |
| from { | |
| opacity: 0; | |
| transform: translateX(50px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| /* Hero Section - Updated Gradient & Shadow */ | |
| /* Make all inner Gradio containers fully transparent */ | |
| #hero-section, | |
| #hero-section > div, | |
| #hero-section > div > div { | |
| background: transparent !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| } | |
| .hero-column > div { | |
| background: transparent !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| } | |
| .logo-container { | |
| animation: scaleIn 1s ease-out 0.3s both; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: rgba(255,255,255,0.15); | |
| border-radius: 20px; | |
| padding: 15px; | |
| backdrop-filter: blur(10px); | |
| border: 2px solid rgba(255,255,255,0.3); | |
| transition: all 0.4s ease; | |
| } | |
| .logo-container:hover { | |
| transform: scale(1.05) rotate(2deg); | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| } | |
| .hero-title { | |
| animation: fadeInUp 1s ease-out 0.5s both; | |
| } | |
| .hero-subtitle { | |
| animation: fadeInUp 1s ease-out 0.7s both; | |
| } | |
| .status-container { | |
| display: flex; | |
| gap: 15px; | |
| justify-content: flex-end; | |
| align-items: center; | |
| padding: 12px 20px; | |
| background: rgba(15,23,42,0.7); | |
| border-radius: 12px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.15); | |
| animation: fadeInRight 1s ease-out 0.9s both; | |
| transition: all 0.3s ease; | |
| } | |
| .status-container:hover { | |
| background: rgba(15,23,42,0.85); | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(0,0,0,0.3); | |
| } | |
| .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .status-icon { | |
| font-size: 20px; | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| .status-label { | |
| font-size: 10px; | |
| color: #94a3b8; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| font-weight: 600; | |
| } | |
| .status-value { | |
| font-size: 13px; | |
| font-weight: 700; | |
| } | |
| .status-divider { | |
| width: 1px; | |
| height: 30px; | |
| background: rgba(255,255,255,0.2); | |
| } | |
| /* Banner Animation */ | |
| .info-banner { | |
| animation: fadeInUp 1s ease-out 1.1s both; | |
| } | |
| /* Form Section Animations */ | |
| .form-section { | |
| animation: slideInLeft 0.8s ease-out 0.3s both; | |
| } | |
| #job-form { | |
| border: 2px solid #FF7E00; | |
| border-radius: 16px; | |
| padding: 25px; | |
| background: rgba(255,255,255,0.05); | |
| box-shadow: 0 10px 30px rgba(255,126,0,0.2); | |
| } | |
| .form-section { | |
| animation: slideInLeft 0.8s ease-out 0.3s both; | |
| border: 2px solid #FF7E00; /* Orange border */ | |
| border-radius: 16px; | |
| padding: 25px; | |
| background: rgba(255,255,255,0.05); /* optional translucent bg */ | |
| box-shadow: 0 10px 30px rgba(255, 126, 0, 0.2); /* subtle orange shadow */ | |
| } | |
| .benefits-section { | |
| animation: slideInRight 0.8s ease-out 0.3s both; | |
| } | |
| /* Input Focus Effects - Updated Border & Shadow */ | |
| textarea:focus, input:focus { | |
| outline: none !important; | |
| border: 2px solid #FF7E00 !important; /* Vibrant Orange border */ | |
| box-shadow: 0 0 0 3px rgba(255, 126, 0, 0.1), 0 0 20px rgba(255, 126, 0, 0.2) !important; /* Orange focus glow */ | |
| transition: all 0.3s ease !important; | |
| } | |
| /* Button Animations - Updated Gradient, Hover, and Shadow */ | |
| button[variant="primary"] { | |
| background: linear-gradient(135deg, #FF7E00 0%, #FF9A3C 100%) !important; /* Orange-Gold Gradient */ | |
| border: none !important; | |
| font-weight: 700 !important; | |
| transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; | |
| position: relative !important; | |
| overflow: hidden !important; | |
| } | |
| button[variant="primary"]::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| border-radius: 50%; | |
| background: rgba(255,255,255,0.3); | |
| transform: translate(-50%, -50%); | |
| transition: width 0.6s, height 0.6s; | |
| } | |
| button[variant="primary"]:hover::before { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| button[variant="primary"]:hover { | |
| transform: translateY(-4px) scale(1.02) !important; | |
| box-shadow: 0 15px 40px rgba(255, 126, 0, 0.5) !important; /* Orange shadow on hover */ | |
| } | |
| button[variant="primary"]:active { | |
| transform: translateY(-2px) scale(0.98) !important; | |
| } | |
| .refresh-btn { | |
| font-size: 12px !important; | |
| padding: 6px 14px !important; | |
| background: transparent !important; | |
| /* Updated to the requested orange border */ | |
| border: 1px solid #FF7E00 !important; | |
| color: white !important; | |
| border-radius: 8px !important; | |
| transition: all 0.3s ease !important; | |
| font-weight: 600 !important; | |
| } | |
| .refresh-btn:hover { | |
| background: linear-gradient(135deg, rgba(255, 126, 0, 0.3), rgba(255, 154, 60, 0.4)) !important; | |
| box-shadow: 0 0 15px rgba(255, 126, 0, 0.8), 0 0 25px rgba(255, 154, 60, 0.6); | |
| transition: all 0.5s ease; | |
| color: #FF7E00 !important; /* ensures icon/text matches theme */ | |
| } | |
| /* Card Animations - Updated Border Color */ | |
| .benefit-card { | |
| background: rgba(255,255,255,0.05); | |
| padding: 20px; | |
| border-radius: 12px; | |
| border-left: 4px solid #FF7E00; /* Vibrant Orange border */ | |
| transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| .benefit-card.animate { | |
| animation: fadeInUp 0.6s ease-out forwards; | |
| } | |
| .benefit-card:nth-child(1) { animation-delay: 0.1s; } | |
| .benefit-card:nth-child(2) { animation-delay: 0.2s; } | |
| .benefit-card:nth-child(3) { animation-delay: 0.3s; } | |
| .benefit-card:nth-child(4) { animation-delay: 0.4s; } | |
| .benefit-card:hover { | |
| background: rgba(255,255,255,0.1); | |
| transform: translateX(10px) translateY(-5px); | |
| box-shadow: 0 10px 30px rgba(255, 126, 0, 0.3); /* Orange shadow on hover */ | |
| border-left-width: 6px; | |
| } | |
| /* How It Works Cards */ | |
| .how-it-works-card { | |
| background: rgba(255,255,255,0.15); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| padding: 30px; | |
| border: 2px solid rgba(255,255,255,0.2); | |
| transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| opacity: 0; | |
| transform: scale(0.8); | |
| } | |
| .how-it-works-card.animate { | |
| animation: scaleIn 0.6s ease-out forwards; | |
| } | |
| .how-it-works-card:nth-child(1) { animation-delay: 0.2s; } | |
| .how-it-works-card:nth-child(2) { animation-delay: 0.4s; } | |
| .how-it-works-card:nth-child(3) { animation-delay: 0.6s; } | |
| .how-it-works-card:hover { | |
| transform: translateY(-10px) scale(1.05); | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.3); | |
| border-color: rgba(255,255,255,0.4); | |
| } | |
| .how-it-works-card .emoji { | |
| display: inline-block; | |
| transition: transform 0.3s ease; | |
| } | |
| .how-it-works-card:hover .emoji { | |
| transform: scale(1.2) rotate(10deg); | |
| } | |
| /* Success Link Animation - Updated Gradient & Glow */ | |
| #success-link-box { | |
| text-align: center; | |
| padding: 1.5rem; | |
| background: linear-gradient(135deg, #FF7E00 0%, #FF9A3C 100%); /* Orange-Gold Gradient */ | |
| border-radius: 12px; | |
| box-shadow: 0 10px 30px rgba(255, 126, 0, 0.3); /* Orange shadow */ | |
| animation: slideInUp 0.5s ease-out, glow 2s ease-in-out infinite; | |
| } | |
| @keyframes slideInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| #success-link-box a { | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| color: white !important; | |
| text-decoration: none; | |
| transition: all 0.3s ease; | |
| } | |
| #success-link-box a:hover { | |
| text-decoration: underline; | |
| transform: scale(1.05); | |
| display: inline-block; | |
| } | |
| /* Benefits Box */ | |
| #benefits-box { | |
| background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); | |
| padding: 35px; | |
| border-radius: 20px; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 25px; | |
| box-shadow: 0 25px 70px rgba(0,0,0,0.4); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| /* Benefits Box ::before - Updated Radial Gradient Color */ | |
| #benefits-box::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: radial-gradient(circle at top right, rgba(255, 126, 0, 0.1) 0%, transparent 70%); /* Orange tint */ | |
| pointer-events: none; | |
| } | |
| /* Responsive adjustments */ | |
| @media (max-width: 768px) { | |
| #hero-section { | |
| padding: 2rem 1.5rem; | |
| } | |
| .status-container { | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .status-divider { | |
| width: 100%; | |
| height: 1px; | |
| } | |
| } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="orange", secondary_hue="yellow"), css=custom_css) as demo: | |
| # Hero Section with Animated Logo and Status | |
| with gr.Group(elem_id="hero-section"): | |
| with gr.Row(): | |
| with gr.Column(scale=20, elem_classes="hero-column"): | |
| gr.HTML(""" | |
| <div style=" | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| width: 100%; | |
| height: 100%; | |
| color: white; | |
| position: relative; | |
| z-index: 1; | |
| "> | |
| <h1 class="hero-title" style=" | |
| font-size: 4rem; | |
| background: linear-gradient(135deg, #FF7E00, #FF9A3C); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-weight: 900; | |
| ">FeedHire Job Poster</h1> | |
| <p class="hero-subtitle" style=" | |
| font-size: 1.5rem; | |
| background: linear-gradient(135deg, #FF7E00, #FF9A3C); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-weight: 500; | |
| ">Post jobs instantly to LinkedIn & reach thousands of candidates 🚀</p> | |
| </div> | |
| """) | |
| with gr.Column(scale=4, min_width=280): | |
| status_display = gr.HTML(get_status_display()) | |
| refresh_status_btn = gr.Button("↺ Refresh", size="sm", elem_classes="refresh-btn") | |
| # Visibility Info Banner - Updated Background and Border | |
| gr.HTML(""" | |
| <div class="info-banner" style="background: rgba(255, 126, 0, 0.1); padding: 18px; border-radius: 14px; text-align: center; margin: 25px 0; border: 2px solid rgba(255, 126, 0, 0.3); transition: all 0.3s ease;"> | |
| <p style="margin: 0; font-size: 15px; color: #475569; font-weight: 500;"> | |
| 🌐 Your job post will be live on | |
| <a href="https://www.linkedin.com/company/109539782/" target="_blank" style="color:#FF7E00; text-decoration:none; font-weight:700; transition: all 0.3s ease;">LinkedIn</a> | |
| and visible on | |
| <a href="https://feedhire.me/" target="_blank" style="color:#FF7E00; text-decoration:none; font-weight:700; transition: all 0.3s ease;">FeedHire.me</a> | |
| within minutes ⚡ | |
| </p> | |
| </div> | |
| """) | |
| if missing_vars: | |
| gr.Warning(f"⚠️ Missing configuration: {', '.join(missing_vars)}") | |
| # Main Content: Two-Column Layout | |
| with gr.Row(equal_height=True): | |
| # LEFT COLUMN - Job Posting Form | |
| with gr.Column(scale=1, elem_classes="form-section transparent-box, ", elem_id="job-form"): | |
| gr.HTML("<h2 style='margin-bottom: 25px; font-weight: 800; text-align: Center; color: white; font-size: 1.8rem;'>📝 Create Your Job Post</h2>") | |
| name_input = gr.Textbox( | |
| label="👤 Your Name", | |
| placeholder="e.g., Arif Raafi", | |
| lines=1 | |
| ) | |
| job_post_input = gr.Textbox( | |
| label="💼 Job Post Content", | |
| placeholder="✨ Craft your perfect job post here...\n\n🎯 Role: \n📋 Responsibilities: \n📍 Location: \n💰 Salary Range: \n📧 How to Apply: ", | |
| lines=12 | |
| ) | |
| submit_btn = gr.Button("🚀 Publish to LinkedIn", variant="primary", size="lg") | |
| output = gr.Textbox(label="📊 Status", lines=2, interactive=False) | |
| success_link = gr.Markdown( | |
| "", | |
| visible=False, | |
| elem_id="success-link-box" | |
| ) | |
| # RIGHT COLUMN - Benefits & Marketing | |
| with gr.Column(scale=1, elem_id="job-form", elem_classes="benefits-section transparent-box"): | |
| gr.HTML(""" | |
| <h2 style='color: white; text-align: center; margin-bottom: 30px; font-size: 1.8rem; font-weight: 800; position: relative; z-index: 0;'> | |
| 🌟 Why Choose FeedHire? | |
| </h2> | |
| """) | |
| # The image component was missing in the previous step's final output, but present in the context. Restoring it. | |
| gr.Image( | |
| value="analytics_map.png", | |
| show_label=False, | |
| show_download_button=False, | |
| container=False, | |
| elem_classes="benefits-image", | |
| ) | |
| gr.HTML(""" | |
| <style> | |
| .benefit-card { | |
| display: flex; /* horizontal layout */ | |
| align-items: center; | |
| padding: 12px 15px; | |
| border-radius: 14px; | |
| background: rgba(255, 126, 0, 0.2); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 126, 0, 0.4); | |
| margin: 8px auto; | |
| max-width: 90%; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| font-size: 14px; | |
| } | |
| .benefit-card:hover { | |
| transform: scale(1.03); | |
| box-shadow: 0 8px 30px rgba(0,0,0,0.15); | |
| } | |
| .benefit-card .icon { | |
| font-size: 24px; | |
| margin-right: 12px; | |
| } | |
| .benefit-card .text { | |
| color: #fff; | |
| } | |
| .benefit-card .text .title { | |
| font-weight: 700; | |
| margin-right: 6px; | |
| } | |
| .benefit-card .text .desc { | |
| color: rgba(255,255,255,0.85); | |
| } | |
| </style> | |
| <div class="benefit-card animate"> | |
| <div class="icon">🌍</div> | |
| <div class="text"><span class="title">Massive Reach</span> - <span class="desc">Tap into LinkedIn's network and FeedHire's global ecosystem</span></div> | |
| </div> | |
| <div class="benefit-card animate"> | |
| <div class="icon">⚡</div> | |
| <div class="text"><span class="title">Lightning Fast</span> - <span class="desc">Post once, go live instantly on multiple platforms</span></div> | |
| </div> | |
| <div class="benefit-card animate"> | |
| <div class="icon">💯</div> | |
| <div class="text"><span class="title">100% Free</span> - <span class="desc">No hidden fees, no subscriptions—just pure visibility</span></div> | |
| </div> | |
| <div class="benefit-card animate"> | |
| <div class="icon">🎯</div> | |
| <div class="text"><span class="title">Quality Candidates</span> - <span class="desc">Attract serious applicants</span></div> | |
| </div> | |
| """) | |
| # How It Works Section - Updated Gradient | |
| gr.HTML(""" | |
| <div style="background: linear-gradient(135deg, #FF7E00 0%, #FF9A3C 100%); padding: 55px 45px; border-radius: 24px; margin: 45px 0; box-shadow: 0 25px 70px rgba(255, 126, 0, 0.4);"> | |
| <h2 style="color: white; text-align: center; margin-bottom: 45px; font-size: 2.2rem; font-weight: 900; text-shadow: 2px 2px 4px rgba(0,0,0,0.2);"> | |
| ✨ How FeedHire Works | |
| </h2> | |
| <div style="display: flex; gap: 30px; flex-wrap: wrap; justify-content: center;"> | |
| <div class="how-it-works-card animate" style="flex: 1; min-width: 280px; text-align: center;"> | |
| <div class="emoji" style="font-size: 56px; margin-bottom: 18px;">📝</div> | |
| <h3 style="color: white; margin-bottom: 14px; font-size: 22px; font-weight: 800;">Create</h3> | |
| <p style="color: rgba(255,255,255,0.95); line-height: 1.8; font-size: 15px;"> | |
| Fill out the form with your name and job details—make it compelling! | |
| </p> | |
| </div> | |
| <div class="how-it-works-card animate" style="flex: 1; min-width: 280px; text-align: center;"> | |
| <div class="emoji" style="font-size: 56px; margin-bottom: 18px;">🚀</div> | |
| <h3 style="color: white; margin-bottom: 14px; font-size: 22px; font-weight: 800;">Publish</h3> | |
| <p style="color: rgba(255,255,255,0.95); line-height: 1.8; font-size: 15px;"> | |
| Hit publish and watch it go live on LinkedIn instantly via API | |
| </p> | |
| </div> | |
| <div class="how-it-works-card animate" style="flex: 1; min-width: 280px; text-align: center;"> | |
| <div class="emoji" style="font-size: 56px; margin-bottom: 18px;">⚡</div> | |
| <h3 style="color: white; margin-bottom: 14px; font-size: 22px; font-weight: 800;">Go Live</h3> | |
| <p style="color: rgba(255,255,255,0.95); line-height: 1.8; font-size: 15px;"> | |
| Our AI refines your post before publishing it live on FeedHire.me—start attracting applicants immediately! | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| # Footer Disclaimer | |
| gr.HTML(""" | |
| <div style="background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%); border-left: 5px solid #ef4444; padding: 25px; border-radius: 12px; margin-top: 35px; transition: all 0.3s ease; animation: fadeInUp 1s ease-out 1.3s both;"> | |
| <p style="margin: 0; font-size: 14px; color: #475569; line-height: 1.7;"> | |
| <strong style="color: #ef4444; font-size: 15px;">⚠️ Important:</strong> FeedHire is not responsible for job post content. | |
| Ensure all submissions are accurate and lawful. Fake or misleading posts will be removed. | |
| </p> | |
| </div> | |
| """) | |
| # Click Actions (Unchanged) | |
| submit_btn.click( | |
| fn=post_to_linkedin, | |
| inputs=[name_input, job_post_input], | |
| outputs=[output, success_link], | |
| show_progress="full" | |
| ) | |
| def refresh_status(): | |
| return get_status_display() | |
| refresh_status_btn.click( | |
| fn=refresh_status, | |
| outputs=[status_display] | |
| ) | |
| demo.load( | |
| fn=refresh_status, | |
| outputs=[status_display] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(allowed_paths=["."]) |