
Merge branch 'main' of https://huggingface.co/spaces/Manyue-DataScientist/manyue-portfolio
09c2ff2
import gradio as gr | |
import base64 | |
import os | |
import json | |
# --- Helper Functions --- | |
def load_json(filename): | |
"""Load JSON data from content folder""" | |
try: | |
with open(f"content/{filename}.json", "r", encoding="utf-8") as f: | |
return json.load(f) | |
except Exception as e: | |
print(f"Error loading {filename}.json: {e}") | |
return {} | |
def file_to_data_uri(filepath, mime_type="application/pdf"): | |
"""Convert file to data URI""" | |
try: | |
with open(filepath, "rb") as f: | |
data = f.read() | |
b64 = base64.b64encode(data).decode("utf-8") | |
return f"data:{mime_type};base64,{b64}" | |
except Exception as e: | |
print(f"Error converting file to data URI: {e}") | |
return None | |
def image_to_data_uri(filepath, mime_type="image/jpeg"): | |
"""Convert image to data URI""" | |
try: | |
with open(filepath, "rb") as f: | |
data = f.read() | |
b64 = base64.b64encode(data).decode("utf-8") | |
return f"data:{mime_type};base64,{b64}" | |
except Exception as e: | |
print(f"Error converting image to data URI: {e}") | |
return None | |
# --- Navigation Functions --- | |
def show_data_analytics(): | |
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) | |
def show_machine_learning(): | |
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) | |
def show_computer_vision(): | |
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) | |
def go_home(): | |
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False) | |
def toggle_resume(is_visible): | |
"""Toggle the visibility of the resume section.""" | |
new_state = not is_visible | |
new_label = "Hide Resume" if new_state else "View Resume" | |
return new_state, gr.update(visible=new_state), gr.update(value=new_label) | |
# --- Icons (SVG) --- | |
data_analytics_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path><path d="M8 10h.01"></path><path d="M12 10h.01"></path><path d="M16 10h.01"></path></svg>""" | |
machine_learning_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>""" | |
computer_vision_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>""" | |
home_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>""" | |
linkedin_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>""" | |
github_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>""" | |
mail_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>""" | |
link_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>""" | |
document_icon = """<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>""" | |
# Dictionary for icon access | |
icons = { | |
"data_analytics_icon": data_analytics_icon, | |
"machine_learning_icon": machine_learning_icon, | |
"computer_vision_icon": computer_vision_icon, | |
"home_icon": home_icon, | |
"linkedin_icon": linkedin_icon, | |
"github_icon": github_icon, | |
"mail_icon": mail_icon, | |
"link_icon": link_icon, | |
"document_icon": document_icon | |
} | |
# --- Helper functions for generating HTML --- | |
def generate_profile_html(): | |
"""Generate HTML for the profile section""" | |
try: | |
profile_img_uri = image_to_data_uri(profile_data.get("photo", "data/My_photo.jpeg")) | |
# Skills HTML | |
skills_html = "" | |
for skill in profile_data.get("skills", []): | |
skills_html += f'<div class="skill-pill">{skill}</div>\n' | |
# Social links HTML | |
social_links_html = "" | |
for link in profile_data.get("social_links", []): | |
icon = icons.get(link.get("icon", ""), "") | |
id_attr = f' id="{link["id"]}"' if "id" in link else "" | |
social_links_html += f''' | |
<a href="{link["url"]}" target="_blank" class="social-button social-{link["name"].lower()}" aria-label="{link.get("aria_label", link["name"])}"{ id_attr }> | |
{ icon } | |
</a> | |
''' | |
return f''' | |
<div class="profile-container"> | |
<div class="profile-pic"> | |
<img src="{profile_img_uri}" alt="{profile_data.get("name", "Profile")}" /> | |
</div> | |
<div class="name-text">{profile_data.get("name", "")}</div> | |
</div> | |
<h2>{profile_data.get("title", "")}</h2> | |
<div class="about-text"> | |
{profile_data.get("about", "")} | |
</div> | |
<div class="skills-container"> | |
{skills_html} | |
</div> | |
<div class="social-links"> | |
{social_links_html} | |
</div> | |
''' | |
except Exception as e: | |
# Fallback if error occurs | |
print(f"Error generating profile HTML: {e}") | |
return f''' | |
<div class="profile-container"> | |
<div class="profile-pic"> | |
<img src="/api/placeholder/400/400" alt="Profile" /> | |
</div> | |
<div class="name-text">{profile_data.get("name", "Name")}</div> | |
</div> | |
<h2>{profile_data.get("title", "Title")}</h2> | |
<div class="about-text"> | |
{profile_data.get("about", "About text")}</div> | |
<div class="skills-container"> | |
{', '.join(profile_data.get("skills", ["Skills"]))} | |
</div> | |
''' | |
def generate_resume_html(): | |
"""Generate HTML for the collapsible resume section""" | |
return ''' | |
<div class="resume-section glass-container"> | |
<button class="resume-toggle-button" onclick="toggleResume()">View Resume</button> | |
<div id="resume-content" class="resume-content" style="display: none;"> | |
<p>Download my resume or view it below:</p> | |
<a id="resume-download-link" class="resume-download-button" target="_blank">Download Resume</a> | |
<div class="resume-preview"> | |
<iframe id="resume-iframe" frameborder="0" class="resume-iframe"></iframe> | |
</div> | |
</div> | |
</div> | |
<script> | |
function toggleResume() { | |
const resumeContent = document.getElementById('resume-content'); | |
const isHidden = resumeContent.style.display === 'none'; | |
resumeContent.style.display = isHidden ? 'block' : 'none'; | |
if (isHidden) { | |
const resumeIframe = document.getElementById('resume-iframe'); | |
const resumeDownloadLink = document.getElementById('resume-download-link'); | |
fetch('/file/data/resume.pdf') | |
.then(response => response.blob()) | |
.then(blob => { | |
const url = URL.createObjectURL(blob); | |
resumeIframe.src = url; | |
resumeDownloadLink.href = url; | |
}) | |
.catch(err => console.error('Error loading resume:', err)); | |
} | |
} | |
</script> | |
''' | |
def generate_cards_html(): | |
"""Generate HTML for the specialization cards""" | |
cards_html = "" | |
for card in sections_data.get("cards", []): | |
icon = icons.get(card.get("icon", ""), "") | |
cards_html += f''' | |
<div class="card-container {card.get("class", "")}"> | |
<div class="card-inner"> | |
<div class="card-content"> | |
{icon} | |
<span>{card.get("title", "")}</span> | |
</div> | |
<div class="card-description"> | |
{card.get("description", "")} | |
</div> | |
</div> | |
</div> | |
''' | |
return cards_html | |
def generate_contact_html(): | |
"""Generate HTML for the contact section""" | |
contact = profile_data.get("contact", {}) | |
footer = profile_data.get("footer", {}) | |
return f''' | |
<!-- Contact section --> | |
<div id="contact_section"> | |
<h2>{contact.get("heading", "Contact Me")}</h2> | |
<div class="contact-container"> | |
<p>{contact.get("text", "")}</p> | |
<a href="mailto:{contact.get("email", "")}" class="hire-me-button">{contact.get("button_text", "Contact")}</a> | |
</div> | |
</div> | |
<!-- Footer --> | |
<div class="footer"> | |
<p>{footer.get("copyright", "")}</p> | |
<p>{footer.get("credits", "")}</p> | |
</div> | |
''' | |
def generate_skills_html(skills_data, section_class): | |
"""Generate HTML for skills in a section""" | |
skills_html = "" | |
for skill_category in skills_data: | |
items_html = "" | |
for item in skill_category.get("items", []): | |
items_html += f"<li>{item}</li>\n" | |
skills_html += f''' | |
<div class="skill-category"> | |
<h4>{skill_category.get("category", "")}</h4> | |
<ul> | |
{items_html} | |
</ul> | |
</div> | |
''' | |
return skills_html | |
def generate_projects_html(projects_data, section_class): | |
"""Generate HTML for projects in a section""" | |
projects_html = "" | |
for project in projects_data: | |
projects_html += f''' | |
<div class="project-card"> | |
<div class="project-title"> | |
<span class="project-title-text">{project.get("title", "")}</span> | |
<a href="{project.get("url", "#")}" target="_blank" class="project-link"> | |
{icons.get("link_icon", "")} | |
<span>View Project</span> | |
</a> | |
</div> | |
<div class="project-description"> | |
{project.get("description", "")} | |
<span class="tech-stack"><strong>Tech Stack:</strong> {project.get("tech_stack", "")}</span> | |
</div> | |
</div> | |
''' | |
return projects_html | |
def generate_section_html(section_data, section_class): | |
"""Generate HTML for a complete section""" | |
skills_html = generate_skills_html(section_data.get("skills", []), section_class) | |
projects_html = generate_projects_html(section_data.get("projects", []), section_class) | |
return f''' | |
<h1 class="section-heading">{section_data.get("heading", "")}</h1> | |
<div class="section-intro"> | |
{section_data.get("intro", "")} | |
</div> | |
<h3 class="section-subheading {section_class}">Skills</h3> | |
<div class="skills-list"> | |
{skills_html} | |
</div> | |
<h3 class="section-subheading {section_class}">Projects</h3> | |
{projects_html} | |
''' | |
# --- CSS --- | |
portfolio_css = """ | |
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Montserrat:wght@700;800&display=swap'); | |
:root { | |
--primary-da: #8a2be2; | |
--secondary-da: #2575fc; | |
--primary-ml: #00b4db; | |
--secondary-ml: #0083b0; | |
--primary-cv: #ff4d7e; | |
--secondary-cv: #fd3e58; | |
--dark-bg: #0f1118; | |
--card-bg: #1a1d29; | |
--text-primary: #ffffff; | |
--text-secondary: #e0e0e0; | |
--text-muted: #a0a0a0; | |
--shadow-sm: 0 4px 6px rgba(0, 0, 0, 0.1); | |
--shadow-md: 0 8px 16px rgba(0, 0, 0, 0.2); | |
--shadow-lg: 0 12px 24px rgba(0, 0, 0, 0.2); | |
--border-radius-sm: 8px; | |
--border-radius-md: 12px; | |
--border-radius-lg: 20px; | |
--transition-fast: 0.2s ease; | |
--transition-med: 0.3s ease; | |
--transition-slow: 0.5s ease; | |
} | |
body { | |
font-family: 'Poppins', sans-serif; | |
background: var(--dark-bg); | |
background-image: | |
radial-gradient(circle at 25% 25%, rgba(53, 53, 113, 0.05) 0%, transparent 50%), | |
radial-gradient(circle at 75% 75%, rgba(113, 53, 53, 0.05) 0%, transparent 50%); | |
color: var(--text-primary); | |
margin: 0; | |
padding: 0; | |
overflow-x: hidden; | |
line-height: 1.6; | |
letter-spacing: 0.5px; | |
font-weight: 400; | |
} | |
.gr-container { | |
max-width: 1200px; | |
margin: 0 auto; | |
padding: 20px; | |
} | |
/* Scrollbar styling */ | |
::-webkit-scrollbar { | |
width: 8px; | |
height: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: rgba(255, 255, 255, 0.05); | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb { | |
background: rgba(255, 255, 255, 0.2); | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: rgba(255, 255, 255, 0.3); | |
} | |
/* Landing section */ | |
.landing-section { | |
text-align: center; | |
margin-bottom: 60px; | |
padding: 40px 20px; | |
position: relative; | |
} | |
.landing-section:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 500px; | |
background: linear-gradient(180deg, rgba(0,0,0,0.7) 0%, transparent 100%); | |
z-index: -1; | |
} | |
.landing-section h1, .landing-section h2 { | |
color: var(--text-primary) !important; | |
margin-top: 0; | |
} | |
.landing-section h1 { | |
font-family: 'Montserrat', sans-serif; | |
font-size: 3.2rem; | |
font-weight: 800; | |
margin-bottom: 0.5rem; | |
background: linear-gradient(90deg, var(--primary-da), var(--primary-ml), var(--primary-cv)); | |
-webkit-background-clip: text; | |
background-clip: text; | |
color: transparent !important; | |
letter-spacing: -0.5px; | |
} | |
.landing-section h2 { | |
font-size: 2rem; | |
font-weight: 600; | |
margin-bottom: 1.5rem; | |
} | |
.profile-container { | |
margin: 30px auto; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
flex-direction: column; | |
} | |
.profile-pic { | |
width: 180px; | |
height: 180px; | |
border-radius: 50%; | |
object-fit: cover; | |
border: 4px solid rgba(255, 255, 255, 0.2); | |
box-shadow: var(--shadow-md); | |
margin-bottom: 20px; | |
position: relative; | |
background: linear-gradient(45deg, var(--primary-da), var(--primary-ml), var(--primary-cv)); | |
padding: 4px; | |
} | |
.profile-pic img { | |
border-radius: 50%; | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
} | |
.name-text { | |
font-size: 2.6rem; | |
font-weight: 700; | |
margin-top: 10px; | |
margin-bottom: 10px; | |
} | |
@keyframes float { | |
0% { transform: translateY(0px) } | |
50% { transform: translateY(-10px) } | |
100% { transform: translateY(0px) } | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.05); } | |
100% { transform: scale(1); } | |
} | |
.about-text { | |
max-width: 800px; | |
margin: 0 auto 40px; | |
font-size: 1.25rem; | |
line-height: 1.6; | |
color: var(--text-secondary); | |
} | |
.skills-container { | |
margin-top: 20px; | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 10px; | |
margin-bottom: 40px; | |
} | |
.skill-pill { | |
background: rgba(255, 255, 255, 0.1); | |
padding: 8px 16px; | |
border-radius: 30px; | |
font-size: 0.9rem; | |
font-weight: 500; | |
color: var(--text-secondary); | |
} | |
.social-links { | |
display: flex; | |
justify-content: center; | |
gap: 20px; | |
margin: 30px 0; | |
} | |
.social-button { | |
background: rgba(255, 255, 255, 0.1); | |
border: none; | |
border-radius: 50%; | |
width: 50px; | |
height: 50px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transition: all var(--transition-med); | |
color: var(--text-primary); | |
font-size: 1.2rem; | |
box-shadow: var(--shadow-sm); | |
} | |
.social-button:hover { | |
transform: translateY(-5px); | |
background: rgba(255, 255, 255, 0.2); | |
box-shadow: var(--shadow-md); | |
} | |
.social-linkedin:hover { background: #0077b5; } | |
.social-github:hover { background: #333; } | |
.social-email:hover { background: #ea4335; } | |
.social-button svg { | |
width: 24px; | |
height: 24px; | |
} | |
/* Card styling */ | |
.cards-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
gap: 30px; | |
margin: 40px 0; | |
} | |
.card-container { | |
position: relative; /* Important for button positioning */ | |
margin-bottom: 20px; | |
height: 100%; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
.card-container.da:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 6px; | |
background: linear-gradient(90deg, var(--primary-da), var(--secondary-da)); | |
z-index: 5; | |
border-radius: var(--border-radius-md) var(--border-radius-md) 0 0; | |
} | |
.card-container.ml:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 6px; | |
background: linear-gradient(90deg, var(--primary-ml), var(--secondary-ml)); | |
z-index: 5; | |
transition: all var(--transition-med); | |
border-radius: var(--border-radius-md) var(--border-radius-md) 0 0; | |
} | |
.card-container.cv:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
height: 6px; | |
background: linear-gradient(90deg, var(--primary-cv), var(--secondary-cv)); | |
z-index: 5; | |
border-radius: var(--border-radius-md) var(--border-radius-md) 0 0; | |
} | |
.card-content { | |
padding: 30px; | |
min-height: 200px; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
font-size: 26px; | |
font-weight: 700; | |
position: relative; | |
z-index: 2; | |
transition: all var(--transition-med); | |
} | |
.card-content svg { | |
width: 60px; | |
height: 60px; | |
margin-bottom: 20px; | |
opacity: 0.9; | |
transition: all var(--transition-med); | |
} | |
.card-inner { | |
transition: transform var(--transition-med), box-shadow var(--transition-med), background-color var(--transition-med); | |
text-align: center; | |
border-radius: var(--border-radius-md); | |
background: var(--card-bg); | |
overflow: hidden; | |
box-shadow: var(--shadow-md); | |
height: 100%; | |
cursor: pointer; /* Indicates the card is clickable */ | |
position: relative; /* Ensure child elements are positioned relative to the card */ | |
} | |
.card-inner:hover { | |
transform: translateY(-10px) scale(1.05); /* Adds a slight zoom effect */ | |
box-shadow: var(--shadow-lg); /* Increases shadow for emphasis */ | |
background: rgba(255, 255, 255, 0.1); /* Subtle background change */ | |
border: 2px solid var(--primary-da); /* Optional: Add a border to emphasize hover */ | |
} | |
.card-inner:hover .card-content svg { | |
transform: scale(1.1); /* Slightly enlarges the icon */ | |
opacity: 1; | |
} | |
.card-inner:hover .card-description { | |
color: var(--text-primary); /* Optional: changes text color for emphasis */ | |
} | |
/* Add a subtle glow effect */ | |
.card-inner:hover:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
border-radius: var(--border-radius-md); | |
box-shadow: 0 0 15px rgba(255, 255, 255, 0.3); | |
z-index: -1; | |
} | |
.card-description { | |
padding: 0 20px 20px; | |
color: var(--text-secondary); | |
font-size: 1.1rem; | |
line-height: 1.5; | |
} | |
/* Card button styling - crucial for making cards clickable */ | |
.card-button { | |
position: absolute !important; | |
top: 0 !important; | |
left: 0 !important; | |
width: 100% !important; | |
height: 100% !important; | |
opacity: 0 !important; | |
z-index: 10 !important; | |
cursor: pointer !important; | |
margin: 0 !important; | |
padding: 0 !important; | |
border: none !important; | |
transform: scale(1.05) !important; | |
transition: transform 0.2s ease !important; | |
background: none !important; | |
} | |
.click-to-view { | |
margin-top: 15px; | |
padding: 8px 15px; | |
border-radius: 20px; | |
display: inline-block; | |
font-weight: 600; | |
font-size: 0.9rem; | |
transition: all var(--transition-med); | |
background: rgba(255, 255, 255, 0.1); | |
box-shadow: var(--shadow-sm); | |
margin-left: auto; | |
margin-right: auto; | |
color: var(--text-primary); | |
text-align: center; | |
} | |
/* Add glow effect */ | |
.click-to-view:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 0 15px rgba(255, 255, 255, 0.5), var(--shadow-md); | |
background: rgba(255, 255, 255, 0.2); | |
color: var(--text-primary); | |
filter: brightness(1.2); | |
} | |
/* Add animation for subtle pulsing effect */ | |
@keyframes glow-pulse { | |
0% { | |
box-shadow: 0 0 5px rgba(255, 255, 255, 0.2); | |
} | |
50% { | |
box-shadow: 0 0 15px rgba(255, 255, 255, 0.4); | |
} | |
100% { | |
box-shadow: 0 0 5px rgba(255, 255, 255, 0.2); | |
} | |
} | |
.click-to-view { | |
animation: glow-pulse 3s infinite; | |
} | |
.card-description p { | |
margin-bottom: 15px; | |
margin-top: 0; | |
} | |
.card-description { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: space-between; | |
height: 100%; | |
padding: 0 20px 20px; | |
color: var(--text-secondary); | |
font-size: 1.1rem; | |
line-height: 1.5; | |
} | |
/* Different colors for each card type */ | |
.da-click { | |
color: var(--primary-da); | |
border: 1px solid var(--primary-da); | |
} | |
.ml-click { | |
color: var(--primary-ml); | |
border: 1px solid var(--primary-ml); | |
} | |
.cv-click { | |
color: var(--primary-cv); | |
border: 1px solid var(--primary-cv); | |
} | |
/* Hover effects */ | |
.click-to-view:hover { | |
transform: translateY(-2px); | |
box-shadow: var(--shadow-md); | |
background: rgba(255, 255, 255, 0.15); | |
} | |
.card-inner:hover .click-to-view { | |
transform: translateY(-2px); | |
box-shadow: var(--shadow-md); | |
} | |
.card-inner:hover .da-click { | |
background-color: rgba(138, 43, 226, 0.1); | |
} | |
.card-inner:hover .ml-click { | |
background-color: rgba(0, 180, 219, 0.1); | |
} | |
.card-inner:hover .cv-click { | |
background-color: rgba(255, 77, 126, 0.1); | |
} | |
.experience-timeline { | |
margin: 80px 0 60px; | |
padding: 20px; | |
position: relative; | |
} | |
.experience-timeline h2 { | |
text-align: center; | |
margin-bottom: 60px; | |
position: relative; | |
display: inline-block; | |
left: 50%; | |
transform: translateX(-50%); | |
font-family: 'Montserrat', sans-serif; | |
font-size: 2.5rem; | |
font-weight: 700; | |
background: linear-gradient(90deg, var(--primary-da), var(--primary-ml), var(--primary-cv)); | |
-webkit-background-clip: text; | |
background-clip: text; | |
color: transparent; | |
} | |
.experience-timeline h2:after { | |
content: ''; | |
position: absolute; | |
bottom: -10px; | |
left: 0; | |
width: 100%; | |
height: 3px; | |
border-radius: 3px; | |
background: linear-gradient(90deg, var(--primary-da), var(--primary-ml), var(--primary-cv)); | |
} | |
/* Horizontal Timeline Styling */ | |
.timeline-container { | |
display: flex; | |
justify-content: space-between; | |
align-items: flex-start; | |
position: relative; | |
margin: 40px auto; | |
max-width: 100%; | |
overflow-x: auto; | |
padding: 20px 0; | |
} | |
.timeline-track { | |
position: absolute; | |
top: 50%; | |
left: 0; | |
right: 0; | |
height: 4px; | |
background: linear-gradient(to right, var(--primary-da), var(--primary-ml), var(--primary-cv)); | |
border-radius: 2px; | |
z-index: 1; | |
} | |
.timeline-node { | |
position: relative; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
width: 150px; | |
margin: 0 20px; | |
z-index: 2; | |
} | |
.timeline-dot { | |
width: 20px; | |
height: 20px; | |
background: var(--primary-da); | |
border-radius: 50%; | |
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
margin-bottom: 10px; | |
transition: transform 0.3s ease; | |
} | |
.timeline-year { | |
font-size: 1rem; | |
font-weight: 600; | |
margin-bottom: 10px; | |
color: var(--text-primary); | |
} | |
.timeline-content { | |
background: var(--card-bg); | |
border-radius: var(--border-radius-md); | |
box-shadow: var(--shadow-sm); | |
padding: 15px; | |
text-align: center; | |
width: 100%; | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
} | |
.timeline-node:hover .timeline-dot { | |
transform: scale(1.3); | |
} | |
.timeline-node:hover .timeline-content { | |
transform: translateY(-10px); | |
box-shadow: var(--shadow-md); | |
} | |
/* Responsive adjustments for smaller screens */ | |
@media (max-width: 768px) { | |
.timeline-container { | |
flex-direction: column; | |
align-items: center; | |
} | |
.timeline-track { | |
display: none; | |
} | |
.timeline-node { | |
margin: 20px 0; | |
} | |
} | |
/* Section styling */ | |
.section-container { | |
padding: 60px 20px; | |
position: relative; | |
} | |
.section-container:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 300px; | |
background: radial-gradient(ellipse at top, rgba(255,255,255,0.05) 0%, transparent 70%); | |
z-index: 0; | |
} | |
.da-section h1.section-heading { | |
color: var(--primary-da); | |
position: relative; | |
display: inline-block; | |
} | |
.ml-section h1.section-heading { | |
color: var(--primary-ml); | |
position: relative; | |
display: inline-block; | |
} | |
.cv-section h1.section-heading { | |
color: var(--primary-cv); | |
position: relative; | |
display: inline-block; | |
} | |
.section-heading:after { | |
content: ''; | |
position: absolute; | |
bottom: -10px; | |
left: 0; | |
width: 100%; | |
height: 3px; | |
border-radius: 3px; | |
} | |
.da-section .section-heading:after { background: var(--primary-da); } | |
.ml-section .section-heading:after { background: var(--primary-ml); } | |
.cv-section .section-heading:after { background: var(--primary-cv); } | |
/* Subheadings color-coded */ | |
.section-subheading.da { color: var(--primary-da); } | |
.section-subheading.ml { color: var(--primary-ml); } | |
.section-subheading.cv { color: var(--primary-cv); } | |
/* Back buttons */ | |
.back-button { | |
border: none; | |
border-radius: var(--border-radius-lg); | |
padding: 10px 20px; | |
font-size: 0.95rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: transform var(--transition-fast), box-shadow var(--transition-fast); | |
margin-bottom: 30px; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.back-button:hover { | |
transform: translateY(-3px); | |
box-shadow: var(--shadow-md); | |
} | |
.back-button-da { | |
background: linear-gradient(45deg, var(--primary-da), var(--secondary-da)); | |
color: #fff; | |
} | |
.back-button-ml { | |
background: linear-gradient(45deg, var(--primary-ml), var(--secondary-ml)); | |
color: #fff; | |
} | |
.back-button-cv { | |
background: linear-gradient(45deg, var(--primary-cv), var(--secondary-cv)); | |
color: #fff; | |
} | |
.back-button svg { | |
width: 20px; | |
height: 20px; | |
} | |
/* Contact form */ | |
.contact-container { | |
background: var(--card-bg); | |
border-radius: var(--border-radius-md); | |
padding: 30px; | |
max-width: 600px; | |
margin: 0 auto; | |
box-shadow: var(--shadow-md); | |
} | |
.hire-me-button { | |
background: linear-gradient(45deg, var(--primary-da), var(--primary-cv)); | |
color: white; | |
border: none; | |
border-radius: var(--border-radius-lg); | |
padding: 12px 25px; | |
font-size: 1rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all var(--transition-med); | |
box-shadow: var(--shadow-sm); | |
display: inline-block; | |
text-decoration: none; | |
} | |
.hire-me-button:hover { | |
transform: translateY(-3px); | |
box-shadow: var(--shadow-md); | |
filter: brightness(1.1); | |
} | |
/* Project cards */ | |
.project-card { | |
background: var(--card-bg); | |
border-radius: var(--border-radius-md); | |
padding: 25px; | |
margin-bottom: 20px; | |
box-shadow: var(--shadow-sm); | |
transition: all var(--transition-med); | |
border-left: 4px solid transparent; | |
} | |
.da-section .project-card { border-left-color: var(--primary-da); } | |
.ml-section .project-card { border-left-color: var(--primary-ml); } | |
.cv-section .project-card { border-left-color: var(--primary-cv); } | |
.project-card:hover { | |
transform: translateX(5px); | |
box-shadow: var(--shadow-md); | |
} | |
.project-title { | |
font-size: 1.3rem; | |
font-weight: 600; | |
margin-bottom: 10px; | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
} | |
.project-title-text { | |
flex: 1; | |
} | |
.project-link { | |
color: var(--text-secondary); | |
transition: all var(--transition-med); | |
text-decoration: none; | |
display: inline-flex; | |
align-items: center; | |
margin-left: 10px; | |
} | |
.project-link svg { | |
width: 16px; | |
height: 16px; | |
} | |
.da-section .project-title-text { color: var(--primary-da); } | |
.ml-section .project-title-text { color: var(--primary-ml); } | |
.cv-section .project-title-text { color: var(--primary-cv); } | |
.da-section .project-link:hover { color: var(--primary-da); } | |
.ml-section .project-link:hover { color: var(--primary-ml); } | |
.cv-section .project-link:hover { color: var(--primary-cv); } | |
.project-description { | |
color: var(--text-secondary); | |
line-height: 1.5; | |
} | |
.tech-stack { | |
display: block; | |
margin-top: 10px; | |
font-style: italic; | |
color: var(--text-muted); | |
} | |
/* Skills list */ | |
.skills-list { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); | |
gap: 15px; | |
margin-top: 20px; | |
margin-bottom: 40px; /* Added margin to create space between skills and projects */ | |
} | |
.skill-category { | |
background: rgba(255, 255, 255, 0.05); | |
border-radius: var(--border-radius-sm); | |
padding: 15px; | |
transition: all var(--transition-med); | |
} | |
.skill-category:hover { | |
background: rgba(255, 255, 255, 0.08); | |
transform: translateY(-3px); | |
} | |
.skill-category h4 { | |
margin-top: 0; | |
margin-bottom: 10px; | |
font-size: 1.1rem; | |
} | |
.da-section .skill-category h4 { color: var(--primary-da); } | |
.ml-section .skill-category h4 { color: var(--primary-ml); } | |
.cv-section .skill-category h4 { color: var(--primary-cv); } | |
.skill-category ul { | |
margin: 0; | |
padding-left: 20px; | |
color: var(--text-secondary); | |
} | |
.skill-category li { | |
margin-bottom: 5px; | |
} | |
/* Section intro text */ | |
.section-intro { | |
max-width: 800px; | |
margin-bottom: 30px; | |
line-height: 1.6; | |
color: var(--text-secondary); | |
font-size: 1.1rem; | |
} | |
/* Footer */ | |
.footer { | |
text-align: center; | |
padding: 40px 20px; | |
margin-top: 60px; | |
color: var(--text-muted); | |
font-size: 0.9rem; | |
} | |
/* Animations for scroll */ | |
.animate-on-scroll { | |
opacity: 0; | |
transform: translateY(20px); | |
transition: opacity 0.6s ease, transform 0.6s ease; | |
} | |
.animate-on-scroll.show { | |
animation: fadeInUp 0.6s ease forwards; | |
} | |
@keyframes fadeInUp { | |
0% { | |
opacity: 0; | |
transform: translateY(20px); | |
} | |
100% { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
/* Loading states */ | |
.loading-spinner { | |
border: 4px solid rgba(255, 255, 255, 0.2); | |
border-top: 4px solid var(--primary-da); | |
border-radius: 50%; | |
width: 40px; | |
height: 40px; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
/* Glassmorphism effects */ | |
.glass-container { | |
background: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
border-radius: var(--border-radius-md); | |
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); | |
} | |
/* Subtle background patterns */ | |
body { | |
background-image: url('https://www.transparenttextures.com/patterns/noise.png'); | |
} | |
/* Gradient refinements */ | |
button { | |
background: linear-gradient(45deg, var(--primary-da), var(--primary-ml)); | |
} | |
/* Shadow depth variations */ | |
.card-container:hover { | |
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); | |
} | |
/* ARIA attributes */ | |
button { | |
aria-label: "Interactive button"; | |
} | |
/* Keyboard navigation */ | |
button:focus { | |
outline: 2px solid var(--primary-da); | |
outline-offset: 2px; | |
} | |
/* Mobile-first refinements */ | |
@media (max-width: 768px) { | |
.cards-grid { | |
grid-template-columns: 1fr; | |
} | |
} | |
/* Touch-friendly targets */ | |
button { | |
padding: 12px 20px; | |
} | |
/* Responsive typography */ | |
h1 { | |
font-size: calc(2rem + 1vw); | |
} | |
/* Parallax scrolling effects */ | |
.section-container:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 300px; | |
background: linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent); | |
transform: translateY(-50%); | |
will-change: transform; | |
transition: transform 0.3s ease; | |
} | |
body.onscroll .section-container:before { | |
transform: translateY(0); | |
} | |
/* Mouse-follow glow effect */ | |
.interactive:hover { | |
box-shadow: 0 0 15px rgba(255, 255, 255, 0.5); | |
} | |
/* Staggered animation sequences */ | |
@keyframes staggeredFadeIn { | |
0% { | |
opacity: 0; | |
transform: translateY(20px); | |
} | |
100% { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
.list-item { | |
opacity: 0; | |
animation: staggeredFadeIn 0.6s ease forwards; | |
} | |
.list-item:nth-child(1) { | |
animation-delay: 0.2s; | |
} | |
.list-item:nth-child(2) { | |
animation-delay: 0.4s; | |
} | |
.list-item:nth-child(3) { | |
animation-delay: 0.6s; | |
} | |
/* Interactive particles background */ | |
.particles-bg { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: url('https://www.transparenttextures.com/patterns/cubes.png'); | |
opacity: 0.1; | |
} | |
/* Smooth page section transitions */ | |
html { | |
scroll-behavior: smooth; | |
} | |
/* Custom cursor styles */ | |
body { | |
cursor: url('https://example.com/custom-cursor.png'), auto; | |
} | |
button:hover { | |
cursor: pointer; | |
} | |
/* Resume Section */ | |
.resume-section { | |
margin: 40px auto; | |
padding: 20px; | |
text-align: center; | |
max-width: 800px; | |
box-shadow: var(--shadow-md); | |
} | |
.resume-toggle-button { | |
background: linear-gradient(45deg, var(--primary-da), var(--primary-cv)); | |
color: white; | |
border: none; | |
border-radius: var(--border-radius-lg); | |
padding: 10px 20px; | |
font-size: 1rem; | |
font-weight: 600; | |
cursor: pointer; | |
transition: all var(--transition-med); | |
box-shadow: var(--shadow-sm); | |
width: auto; /* Adjust width to fit the text */ | |
min-width: 150px; /* Optional: Set a minimum width for consistency */ | |
} | |
.resume-toggle-button:hover { | |
transform: translateY(-3px); | |
box-shadow: var(--shadow-md); | |
filter: brightness(1.1); | |
} | |
.resume-content { | |
margin-top: 20px; | |
text-align: center; | |
} | |
.resume-download-button { | |
display: inline-block; | |
margin: 20px 0; | |
padding: 10px 20px; | |
font-size: 1rem; | |
font-weight: 600; | |
color: white; | |
background: linear-gradient(45deg, var(--primary-da), var(--primary-cv)); | |
border: none; | |
border-radius: var(--border-radius-lg); | |
text-decoration: none; | |
transition: all var(--transition-med); | |
box-shadow: var(--shadow-sm); | |
} | |
.resume-download-button:hover { | |
transform: translateY(-3px); | |
box-shadow: var(--shadow-md); | |
filter: brightness(1.1); | |
} | |
.resume-preview { | |
margin-top: 20px; | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
border-radius: var(--border-radius-md); | |
overflow: hidden; | |
box-shadow: var(--shadow-sm); | |
} | |
.resume-iframe { | |
width: 100%; | |
height: 500px; | |
border: none; | |
} | |
/* Performance optimizations */ | |
img { | |
loading: lazy; | |
} | |
/* Micro-interactions */ | |
button:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); | |
transition: transform 0.3s ease, box-shadow 0.3s ease; | |
} | |
button:active { | |
transform: scale(0.95); | |
transition: transform 0.1s ease; | |
} | |
/* Scroll to Top Button */ | |
.scroll-to-top { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
background: linear-gradient(45deg, var(--primary-da), var(--primary-cv)); | |
color: white; | |
border: none; | |
border-radius: 50%; | |
width: 50px; | |
height: 50px; | |
font-size: 1.5rem; | |
font-weight: bold; | |
cursor: pointer; | |
box-shadow: var(--shadow-md); | |
display: none; /* Initially hidden */ | |
align-items: center; | |
justify-content: center; | |
transition: all var(--transition-med); | |
z-index: 1000; | |
} | |
.scroll-to-top:hover { | |
transform: translateY(-3px); | |
box-shadow: var(--shadow-lg); | |
filter: brightness(1.1); | |
} | |
""" | |
# Load all content from JSON files | |
try: | |
profile_data = load_json("profile") | |
sections_data = load_json("sections") | |
data_analytics_data = load_json("data_analytics") | |
machine_learning_data = load_json("machine_learning") | |
computer_vision_data = load_json("computer_vision") | |
except Exception as e: | |
print(f"Error loading content: {e}") | |
# Default values in case of error | |
profile_data = {} | |
sections_data = {"cards": []} | |
data_analytics_data = {} | |
machine_learning_data = {} | |
computer_vision_data = {} | |
# --- Portfolio Layout --- | |
with gr.Blocks(title=f"{profile_data.get('name', 'Portfolio')}", css=portfolio_css) as demo: | |
# Create sections | |
# Data Analytics Section (initially hidden) | |
with gr.Row(visible=False, elem_classes="section-container da-section") as da_section: | |
with gr.Column(): | |
# Back button | |
back_from_da = gr.Button("← Back to Home", elem_classes="back-button back-button-da") | |
gr.HTML(generate_section_html(data_analytics_data, "da")) | |
# Machine Learning Section (initially hidden) | |
with gr.Row(visible=False, elem_classes="section-container ml-section") as ml_section: | |
with gr.Column(): | |
# Back button | |
back_from_ml = gr.Button("← Back to Home", elem_classes="back-button back-button-ml") | |
gr.HTML(generate_section_html(machine_learning_data, "ml")) | |
# Computer Vision Section (initially hidden) | |
with gr.Row(visible=False, elem_classes="section-container cv-section") as cv_section: | |
with gr.Column(): | |
# Back button | |
back_from_cv = gr.Button("← Back to Home", elem_classes="back-button back-button-cv") | |
gr.HTML(generate_section_html(computer_vision_data, "cv")) | |
with gr.Row(visible=True, elem_classes="landing-section") as landing_section: | |
with gr.Column(): | |
# Profile section with picture | |
gr.HTML(generate_profile_html()) | |
# Resume Section (moved above "My Specializations") | |
resume_state = gr.State(value=False) | |
with gr.Group(visible=False) as resume_container: | |
resume_pdf = file_to_data_uri("data/resume.pdf") | |
gr.HTML(f"""<iframe src="{resume_pdf}" width="100%" height="600px" style="border:none;"></iframe>""") | |
resume_toggle_btn = gr.Button("View Resume") | |
resume_toggle_btn.click(fn=toggle_resume, inputs=[resume_state], outputs=[resume_state, resume_container, resume_toggle_btn]) | |
# Specializations heading | |
gr.HTML("<h2>My Specializations</h2>") | |
# Cards Grid with proper structure | |
with gr.Row(elem_classes="cards-grid"): | |
with gr.Column(): | |
# Data Analytics Card | |
gr.HTML('<div class="card-container da">') | |
da_button = gr.Button("Data Analytics", elem_classes="card-button") | |
gr.HTML(f''' | |
<div class="card-inner"> | |
<div class="card-content"> | |
{icons.get(sections_data.get("cards", [])[0].get("icon", ""), "")} | |
<span>{sections_data.get("cards", [])[0].get("title", "Data Analytics")}</span> | |
</div> | |
<div class="card-description"> | |
{sections_data.get("cards", [])[0].get("description", "")} | |
<div class="click-to-view da-click">Click to view</div> | |
</div> | |
</div> | |
''') | |
with gr.Column(): | |
# Machine Learning Card | |
gr.HTML('<div class="card-container ml">') | |
ml_button = gr.Button("Machine Learning", elem_classes="card-button") | |
gr.HTML(f''' | |
<div class="card-inner"> | |
<div class="card-content"> | |
{icons.get(sections_data.get("cards", [])[1].get("icon", ""), "")} | |
<span>{sections_data.get("cards", [])[1].get("title", "Machine Learning")}</span> | |
</div> | |
<div class="card-description"> | |
{sections_data.get("cards", [])[1].get("description", "")} | |
<div class="click-to-view ml-click">Click to view</div> | |
</div> | |
</div> | |
''') | |
with gr.Column(): | |
# Computer Vision Card | |
gr.HTML('<div class="card-container cv">') | |
cv_button = gr.Button("Computer Vision", elem_classes="card-button") | |
gr.HTML(f''' | |
<div class="card-inner"> | |
<div class="card-content"> | |
{icons.get(sections_data.get("cards", [])[2].get("icon", ""), "")} | |
<span>{sections_data.get("cards", [])[2].get("title", "Computer Vision")}</span> | |
</div> | |
<div class="card-description"> | |
{sections_data.get("cards", [])[2].get("description", "")} | |
<div class="click-to-view cv-click">Click to view</div> | |
</div> | |
</div> | |
''') | |
gr.HTML(f''' | |
<div class="experience-timeline"> | |
<h2>My Journey</h2> | |
<div class="timeline-container"> | |
<div class="timeline-track"></div> | |
<div class="timeline-node"> | |
<div class="timeline-dot"></div> | |
<div class="timeline-year">2018-2021</div> | |
<div class="timeline-content"> | |
<div class="timeline-title">Bachelor's in Commerce (89%)</div> | |
<div class="timeline-details"> | |
<p class="timeline-location">SRM University Chennai</p> | |
<p>Graduated with honors focusing on Business and Finance.</p> | |
</div> | |
</div> | |
</div> | |
<div class="timeline-node"> | |
<div class="timeline-dot"></div> | |
<div class="timeline-year">2021-2023</div> | |
<div class="timeline-content"> | |
<div class="timeline-title">Junior Software Engineer at Cognizant</div> | |
<div class="timeline-details"> | |
<p class="timeline-location">Chennai India</p> | |
<p>Developed and maintained enterprise-grade Java applications for the Insurance domain</p> | |
</div> | |
</div> | |
</div> | |
<div class="timeline-node"> | |
<div class="timeline-dot"></div> | |
<div class="timeline-year">2023-2024</div> | |
<div class="timeline-content"> | |
<div class="timeline-title">Post-Graduation in AI/ML (97%)</div> | |
<div class="timeline-details"> | |
<p class="timeline-location">George Brown college,Canada</p> | |
<p>Advanced studies in AI/ML and data science.</p> | |
</div> | |
</div> | |
</div> | |
<div class="timeline-node"> | |
<div class="timeline-dot"></div> | |
<div class="timeline-year">2025</div> | |
<div class="timeline-content"> | |
<div class="timeline-title">Seeking ML Engineer/Data Scientist Role</div> | |
<div class="timeline-details"> | |
<p>Ready for opportunities in ML and Data Science in Canada.</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
''') | |
gr.HTML(""" | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Get all timeline nodes | |
const timelineNodes = document.querySelectorAll('.timeline-node'); | |
// Add click event listener to each node | |
timelineNodes.forEach(node => { | |
node.addEventListener('click', function() { | |
// Check if this node is already active | |
const isActive = this.classList.contains('active'); | |
// Remove active class from all nodes | |
timelineNodes.forEach(n => n.classList.remove('active')); | |
// If node wasn't active before, make it active | |
if (!isActive) { | |
this.classList.add('active'); | |
} | |
}); | |
}); | |
// Make the first node active by default | |
if (timelineNodes.length > 0) { | |
timelineNodes[0].classList.add('active'); | |
} | |
}); | |
</script> | |
""") | |
# Contact section | |
gr.HTML(generate_contact_html()) | |
# Add a Gradio File component to serve the resume file | |
gr.File(value="data/resume.pdf", label="Resume", interactive=False, visible=False) | |
# Set up click events for navigation | |
da_button.click(show_data_analytics, inputs=None, outputs=[landing_section, da_section, ml_section, cv_section]) | |
ml_button.click(show_machine_learning, inputs=None, outputs=[landing_section, da_section, ml_section, cv_section]) | |
cv_button.click(show_computer_vision, inputs=None, outputs=[landing_section, da_section, ml_section, cv_section]) | |
back_from_da.click(go_home, inputs=None, outputs=[landing_section, da_section, ml_section, cv_section]) | |
back_from_ml.click(go_home, inputs=None, outputs=[landing_section, da_section, ml_section, cv_section]) | |
back_from_cv.click(go_home, inputs=None, outputs=[landing_section, da_section, ml_section, cv_section]) | |
# Scroll to Top Button | |
gr.HTML(""" | |
<button id="scrollToTop" class="scroll-to-top" aria-label="Scroll to Top"> | |
↑ | |
</button> | |
<script> | |
document.addEventListener('DOMContentLoaded', function () { | |
const scrollToTopButton = document.getElementById('scrollToTop'); | |
// Show or hide the button based on scroll position | |
window.addEventListener('scroll', function () { | |
if (window.scrollY > 300) { | |
scrollToTopButton.style.display = 'flex'; // Use 'flex' for proper alignment | |
} else { | |
scrollToTopButton.style.display = 'none'; | |
} | |
}); | |
// Scroll to top when the button is clicked | |
scrollToTopButton.addEventListener('click', function () { | |
window.scrollTo({ | |
top: 0, | |
behavior: 'smooth' | |
}); | |
}); | |
}); | |
</script> | |
""") | |
# Launch the app | |
if __name__ == "__main__": | |
demo.launch() |