diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml new file mode 100644 index 0000000..18d083b --- /dev/null +++ b/.github/workflows/auto_release.yml @@ -0,0 +1,52 @@ +name: Auto Release on Push + +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate version tag + id: tag + run: | + LATEST="" + while IFS= read -r TAG; do + if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + LATEST="$TAG" + break + fi + done < <(git for-each-ref --sort=-version:refname --format='%(refname:strip=2)' refs/tags) + if [ -z "$LATEST" ]; then + VERSION="v0.1.0" + TITLE="v0.1.0 — First production release" + else + IFS=. read -r MAJOR MINOR PATCH <<< "${LATEST#v}" + VERSION="v$MAJOR.$MINOR.$((PATCH + 1))" + TITLE="$VERSION" + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "title=$TITLE" >> "$GITHUB_OUTPUT" + echo "Tagging as $VERSION" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.version }} + name: ${{ steps.tag.outputs.title }} + body: | + Automated release from push to main. + Commit: ${{ github.sha }} + Message: ${{ github.event.head_commit.message }} + draft: false + prerelease: false \ No newline at end of file diff --git a/core/views.py b/core/views.py index ce11e6d..4311a0b 100644 --- a/core/views.py +++ b/core/views.py @@ -13,7 +13,13 @@ def home(request): skills_by_category = {} for skill in Skill.objects.all(): - cat_label = skill.get_category_display() + # Django generates this method dynamically, so access it defensively for + # static type checkers that cannot see generated model methods. + get_category_display = getattr(skill, 'get_category_display', None) + cat_label = ( + get_category_display() if callable(get_category_display) + else str(skill.category) + ) if cat_label not in skills_by_category: skills_by_category[cat_label] = [] skills_by_category[cat_label].append(skill) diff --git a/static/css/style.css b/static/css/style.css index a66d503..21960a3 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,236 +1,215 @@ -/* ── Theme transition ── */ -*, *::before, *::after { - transition: background-color 0.3s ease, border-color 0.3s ease, color 0.2s ease, box-shadow 0.3s ease, opacity 0.3s ease; -} - -/* ── Decorative page background ── */ -html { - background-color: #0A0A0A; +/* ── CSS Variables ── */ +:root { + --bg: #0A0A0A; + --surface: #141414; + --surface-hover: #1c1c1c; + --border: #2A2A2A; + --text-primary: #F0EDE8; + --text-secondary: #999490; + --text-muted: #66625e; + --brand: #FF5C00; + --brand-hover: #ff7526; } html.light { - background-color: #F5F3EF; + --bg: #F9F8F6; + --surface: #FFFFFF; + --surface-hover: #F2EFEA; + --border: #E2DDD6; + --text-primary: #191816; + --text-secondary: #5E5A55; + --text-muted: #8F8A83; + --brand: #E64E00; + --brand-hover: #d14600; +} + +/* ── Base ── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; } body { - position: relative; - background-color: transparent; - min-height: 100vh; -} - -.page-bg { - position: fixed; - inset: 0; - z-index: 0; - pointer-events: none; - overflow: hidden; -} - -.page-bg-glow { - position: absolute; - inset: 0; - background-color: #0A0A0A; - background-image: - radial-gradient(ellipse 90% 55% at 15% -5%, rgba(255, 92, 0, 0.18) 0%, transparent 55%), - radial-gradient(ellipse 65% 45% at 85% 105%, rgba(255, 92, 0, 0.12) 0%, transparent 50%), - radial-gradient(ellipse 45% 35% at 60% 45%, rgba(255, 120, 40, 0.08) 0%, transparent 60%); -} - -.page-bg-pattern { - position: absolute; - inset: 0; - background-image: - linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); - background-size: 64px 64px; - mask-image: radial-gradient(ellipse 90% 80% at 50% 30%, black 20%, transparent 75%); -} - -html.light body { - background-color: transparent; -} - -html.light .page-bg-glow { - background-color: #F5F3EF; - background-image: - linear-gradient(165deg, rgba(250, 247, 242, 0.9) 0%, rgba(240, 232, 220, 0.9) 45%, rgba(248, 244, 238, 0.9) 100%), - radial-gradient(ellipse 80% 55% at 5% -5%, rgba(255, 92, 0, 0.22) 0%, transparent 55%), - radial-gradient(ellipse 60% 45% at 95% 95%, rgba(255, 140, 60, 0.16) 0%, transparent 52%), - radial-gradient(ellipse 40% 30% at 55% 50%, rgba(255, 180, 120, 0.12) 0%, transparent 65%); -} - -html.light .page-bg-pattern { - background-image: radial-gradient(circle, rgba(255, 92, 0, 0.18) 1px, transparent 1px); - background-size: 24px 24px; - mask-image: none; - opacity: 0.7; -} - -/* ── Light mode ── */ -html.light body { color: #1A1A1A !important; } -html.light nav { background-color: rgba(250, 247, 242, 0.82) !important; backdrop-filter: blur(12px); } -html.light .bg-ink { background-color: transparent !important; } -html.light .bg-surface { background-color: rgba(236, 234, 229, 0.85) !important; backdrop-filter: blur(8px); } -html.light .border-border { border-color: #D5D0C8 !important; } -html.light .text-text-primary { color: #1A1A1A !important; } -html.light .text-text-secondary { color: #555047 !important; } -html.light .text-text-muted { color: #999490 !important; } -html.light .project-card { - background-color: rgba(236, 234, 229, 0.88) !important; - border-color: #D5D0C8 !important; - backdrop-filter: blur(8px); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(255, 92, 0, 0.04); -} -html.light .skill-tag { - background-color: rgba(236, 234, 229, 0.75); - backdrop-filter: blur(4px); - border-color: #D5D0C8 !important; - color: #555047 !important; -} -html.light .skill-tag:hover { border-color: #FF5C00 !important; color: #FF5C00 !important; } -html.light .form-input { background: #ECEAE5; border-color: #D5D0C8; color: #1A1A1A; } -html.light .form-input::placeholder { color: #999490; } -html.light footer { border-color: #D5D0C8 !important; } -html.light .border-t { border-color: #D5D0C8 !important; } -html.light ::-webkit-scrollbar-track { background: #EDE9E2; } -html.light ::-webkit-scrollbar-thumb { background: #C8C0B4; } -html.light ::-webkit-scrollbar-thumb:hover { background: #FF5C00; } -html.light ::selection { background: rgba(255, 92, 0, 0.18); color: #1A1A1A; } - -/* ── Theme toggle switch ── */ -.theme-toggle { - position: relative; - width: 3.25rem; - height: 1.75rem; - padding: 0; - border: 1px solid #2A2A2A; - border-radius: 9999px; - background: #141414; - cursor: pointer; - flex-shrink: 0; - transition: border-color 0.25s ease, background-color 0.25s ease, box-shadow 0.25s ease; + background-color: var(--bg); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; + overflow-x: hidden; } -.theme-toggle:hover { - border-color: rgba(255, 92, 0, 0.45); - box-shadow: 0 0 0 3px rgba(255, 92, 0, 0.08); +/* ── Nav blur ── */ +.nav-blur { + background: color-mix(in srgb, var(--bg) 90%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); } -.theme-toggle:focus-visible { - outline: none; - border-color: #FF5C00; - box-shadow: 0 0 0 3px rgba(255, 92, 0, 0.2); +/* ── Nav links ── */ +.nav-link { + color: var(--text-secondary); + transition: color 0.2s; } +.nav-link:hover { color: var(--text-primary); } -.theme-toggle-track { - position: relative; - display: block; - width: 100%; - height: 100%; +/* ── Scroll reveal ── */ +.reveal { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.6s ease, transform 0.6s ease; } - -.theme-toggle-thumb { - position: absolute; - top: 2px; - left: 2px; - width: 1.25rem; - height: 1.25rem; - border-radius: 50%; - background: linear-gradient(145deg, #FF5C00, #E05200); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35); - transition: transform 0.3s cubic-bezier(0.34, 1.2, 0.64, 1), background 0.3s ease; +.reveal.visible { + opacity: 1; + transform: translateY(0); } -.theme-toggle-icon { - position: absolute; - top: 50%; - width: 0.75rem; - height: 0.75rem; - transform: translateY(-50%); - transition: opacity 0.25s ease, color 0.25s ease; +/* ── Orbital animations ── */ +@keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes spin-slow-reverse { + from { transform: rotate(360deg); } + to { transform: rotate(0deg); } +} +@keyframes float-gentle { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-8px); } +} +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } } -.theme-toggle-sun { - left: 0.45rem; - color: #999490; - opacity: 0.55; +.animate-spin-slow { animation: spin-slow 35s linear infinite; } +.animate-spin-slow-reverse { animation: spin-slow-reverse 45s linear infinite; } +.animate-float { animation: float-gentle 5s ease-in-out infinite; } + +/* ── Typed cursor ── */ +.typed-cursor { + color: #FF5C00; + animation: blink 1s step-end infinite; } +html.light .typed-cursor { color: #E64E00; } -.theme-toggle-moon { - right: 0.45rem; - color: #F0EDE8; - opacity: 0.9; +/* ── Stat cards ── */ +.stat-card { + transition: border-color 0.2s, transform 0.2s; +} +.stat-card:hover { + border-color: var(--brand) !important; + transform: translateY(-2px); } -html.light .theme-toggle { - background: #ECEAE5; - border-color: #D5D0C8; +/* ── Journey cards ── */ +.journey-card { + transition: background-color 0.2s; } +.journey-card:hover { background-color: var(--surface-hover) !important; } -html.light .theme-toggle:hover { - border-color: rgba(255, 92, 0, 0.35); - box-shadow: 0 0 0 3px rgba(255, 92, 0, 0.06); +/* ── Skill filter buttons ── */ +.skill-filter-btn { + background: var(--surface); + border-color: var(--border); + color: var(--text-secondary); + transition: all 0.2s; +} +.skill-filter-btn:hover { + border-color: var(--brand); + color: var(--text-primary); +} +.skill-filter-btn.active { + background: var(--brand); + border-color: var(--brand); + color: #0A0A0A; } -html.light .theme-toggle-thumb { - transform: translateX(1.5rem); - background: linear-gradient(145deg, #FFF8F0, #F5EDE4); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12); +/* ── Skill items ── */ +.skill-item:hover { + border-color: var(--brand) !important; } -html.light .theme-toggle-sun { - color: #FF5C00; - opacity: 1; +/* ── Project cards ── */ +.project-card:hover { + border-color: rgba(255, 92, 0, 0.4) !important; + transform: translateY(-3px); + box-shadow: 0 12px 40px rgba(255, 92, 0, 0.08); } -html.light .theme-toggle-moon { - color: #999490; - opacity: 0.4; +/* ── Contact links ── */ +.contact-link:hover { + border-color: rgba(255, 92, 0, 0.4) !important; + background-color: var(--surface-hover) !important; } -/* ── Nav glass effect ── */ -nav { - background-color: rgba(10, 10, 10, 0.82) !important; - backdrop-filter: blur(12px); +/* ── Footer icons ── */ +.footer-icon:hover { + color: var(--brand) !important; + border-color: rgba(255, 92, 0, 0.4) !important; } /* ── Form inputs ── */ .form-input { display: block; width: 100%; - background: #141414; - border: 1px solid #2A2A2A; - color: #F0EDE8; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text-primary); font-family: 'Inter', system-ui, sans-serif; font-size: 0.875rem; - padding: 0.6rem 0.875rem; + padding: 0.625rem 0.875rem; outline: none; - border-radius: 6px; + border-radius: 0.5rem; transition: border-color 0.2s ease; } -.form-input::placeholder { color: #555250; } -.form-input:focus { border-color: #FF5C00; } -.form-textarea { resize: vertical; min-height: 120px; } - -/* ── Scroll reveal ── */ -.reveal { - opacity: 0; - transform: translateY(16px); - transition: opacity 0.5s ease, transform 0.5s ease; -} -.reveal.visible { opacity: 1; transform: translateY(0); } - -/* ── Project card ── */ -.project-card { position: relative; } - -/* ── Skill tag ── */ -.skill-tag { cursor: default; } +.form-input::placeholder { color: var(--text-muted); } +.form-input:focus { border-color: var(--brand); } +.form-textarea { resize: vertical; min-height: 110px; } /* ── Scrollbar ── */ ::-webkit-scrollbar { width: 5px; } -::-webkit-scrollbar-track { background: #0A0A0A; } -::-webkit-scrollbar-thumb { background: #2A2A2A; } -::-webkit-scrollbar-thumb:hover { background: #FF5C00; } +::-webkit-scrollbar-track { background: var(--bg); } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 9999px; } +::-webkit-scrollbar-thumb:hover { background: var(--brand); } /* ── Selection ── */ -::selection { background: rgba(255,92,0,0.25); color: #F0EDE8; } \ No newline at end of file +::selection { background: rgba(255, 92, 0, 0.25); } + +/* ── Photo grayscale effect ── */ +.photo-img { + filter: grayscale(100%); + transition: filter 0.4s ease; +} +.photo-ring:hover .photo-img { + filter: grayscale(0%); +} + +/* ── Floating tag animations ── */ +@keyframes float-gentle { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-8px); } +} +@keyframes float-delayed { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-8px); } +} +.animate-float { animation: float-gentle 4s ease-in-out infinite; } +.animate-float-delayed { animation: float-delayed 4s ease-in-out 2s infinite; } + +/* ── Theme toggle pill ── */ +.theme-toggle-pill { cursor: pointer; } +.theme-sun, .theme-moon { + transition: background 0.2s, opacity 0.2s; + opacity: 0.5; +} +/* Dark mode: sun is active */ +html:not(.light) .theme-sun { + background: var(--surface-hover, #1c1c1c); + opacity: 1; +} +/* Light mode: moon is active */ +html.light .theme-moon { + background: var(--surface-hover, #F2EFEA); + opacity: 1; +} +html.light .theme-sun { opacity: 0.4; } +html:not(.light) .theme-moon { opacity: 0.4; } \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 35cfdc9..7e8845e 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,147 +1,62 @@ -/* ── Theme toggle ── */ -(function () { - const html = document.documentElement; - const toggle = document.getElementById('theme-toggle'); - if (!toggle) return; - - function setTheme(isLight) { - html.classList.toggle('light', isLight); - localStorage.setItem('theme', isLight ? 'light' : 'dark'); - toggle.setAttribute('aria-pressed', String(isLight)); - toggle.setAttribute('aria-label', isLight ? 'Switch to dark mode' : 'Switch to light mode'); - } - - setTheme(localStorage.getItem('theme') === 'light'); - - toggle.addEventListener('click', () => { - setTheme(!html.classList.contains('light')); - }); -})(); - - /* ── Typed hero text ── */ (function () { const phrases = [ - 'building with Python & Django.', - 'learning something new every day.', - 'turning ideas into working software.', - 'open to junior roles & opportunities.', + 'I build with Python and Django.', + 'I am learning to engineer with LLMs.', + 'I care about clean, tested software.', + 'I build tools for real problems.', ]; - const el = document.getElementById('typed-text'); if (!el) return; - - let phraseIndex = 0; - let charIndex = 0; - let deleting = false; - - function type() { - const current = phrases[phraseIndex]; - - if (!deleting) { - el.textContent = current.slice(0, charIndex + 1); - charIndex++; - if (charIndex === current.length) { - deleting = true; - setTimeout(type, 2200); - return; - } + let pi = 0, ci = 0, del = false; + function tick() { + const cur = phrases[pi]; + if (!del) { + el.textContent = cur.slice(0, ++ci); + if (ci === cur.length) { del = true; setTimeout(tick, 2000); return; } } else { - el.textContent = current.slice(0, charIndex - 1); - charIndex--; - if (charIndex === 0) { - deleting = false; - phraseIndex = (phraseIndex + 1) % phrases.length; - } + el.textContent = cur.slice(0, --ci); + if (ci === 0) { del = false; pi = (pi + 1) % phrases.length; } } - setTimeout(type, deleting ? 45 : 70); + setTimeout(tick, del ? 40 : 65); } - - setTimeout(type, 900); + setTimeout(tick, 800); })(); - /* ── Scroll reveal ── */ (function () { const targets = document.querySelectorAll('.reveal'); - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.classList.add('visible'); - } - }); - }, - { threshold: 0.12 } + (entries) => entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); }), + { threshold: 0.1 } ); - - targets.forEach((el) => observer.observe(el)); + targets.forEach(el => observer.observe(el)); })(); - -/* ── Skill bar animations (triggered when in view) ── */ +/* ── Active nav on scroll ── */ (function () { - const bars = document.querySelectorAll('.skill-bar'); - if (!bars.length) return; - - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const target = entry.target; - const width = target.getAttribute('data-width'); - setTimeout(() => { - target.style.width = width + '%'; - }, 150); - observer.unobserve(target); - } - }); - }, - { threshold: 0.3 } - ); - - bars.forEach((bar) => observer.observe(bar)); -})(); - - -/* ── Active nav link highlight on scroll ── */ -(function () { - const sections = document.querySelectorAll('section[id]'); - const navLinks = document.querySelectorAll('nav a[href^="#"]'); - + const sections = document.querySelectorAll('section[id], div[id]'); + const navLinks = document.querySelectorAll('.nav-link'); function onScroll() { let current = ''; - sections.forEach((section) => { - const top = section.offsetTop - 80; - if (window.scrollY >= top) { - current = section.getAttribute('id'); - } + sections.forEach(s => { + if (window.scrollY >= s.offsetTop - 100) current = s.getAttribute('id'); }); - - navLinks.forEach((link) => { - link.classList.remove('text-brand'); - link.classList.add('text-text-secondary'); - if (link.getAttribute('href') === '#' + current) { - link.classList.add('text-brand'); - link.classList.remove('text-text-secondary'); - } + navLinks.forEach(link => { + link.style.color = link.getAttribute('href') === '#' + current + ? 'var(--text-primary)' + : 'var(--text-secondary)'; }); } - window.addEventListener('scroll', onScroll, { passive: true }); })(); - -/* ── Smooth scroll for anchor links ── */ -document.querySelectorAll('a[href^="#"]').forEach((anchor) => { +/* ── Smooth scroll ── */ +document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { const href = this.getAttribute('href'); if (href === '#') return; const target = document.querySelector(href); - if (target) { - e.preventDefault(); - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } + if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); -}); +}); \ No newline at end of file diff --git a/templates/core/home.html b/templates/core/home.html index b5ba616..a52d6f1 100644 --- a/templates/core/home.html +++ b/templates/core/home.html @@ -4,8 +4,8 @@
-