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 @@ - Tisha Patience Malongo — Software Engineer - + Tisha Patience Malongo — Software & AI Engineer + - - - + - + -
+
-
-
+
+
+ + +
+ + AVAILABLE FOR ROLES & COLLABORATIONS +
-
+
-
-

- Software & AI Engineer -

- -

- Tisha Patience - Malongo. +
+

+ Tisha Patience + Malongo.

-
- | +
+ |
-

- Junior software and AI engineer based in Entebbe, Uganda. I build web applications with Python and Django, and I'm currently studying Software and AI Engineering at Turing College. Passionate about clean code, practical AI, and building tools that solve real problems. Open to junior roles and internships. +

+ Junior software and AI engineer based in Entebbe, Uganda. I build web applications with Python and Django, integrate LLMs using the OpenAI API, and write clean, tested code with tools like pytest, mypy, and Ruff. Currently studying Software and AI Engineering at Turing College. Open to junior roles and internships.

- - -
-
+ +
+ +
+
+ + +
Tisha Patience Malongo + class="photo-img w-full h-full object-cover object-top" /> +
+ + +
+ +
+
AI Engineer
+
Full-Stack Dev
+
+
+ + +
+ +
+
Open to Roles
+
Junior Software Eng
+
@@ -152,87 +188,265 @@

+
+ +
+

+ + How I got here +

+

+ A continuous progression. +

+

+ From computer science fundamentals to building end-to-end Python web systems and exploring modern AI engineering. +

+
+ +
+ + +
+
+ 01 +

Foundations

+

+ Computer science core, algorithms, problem solving, discrete math concepts and modern web basics. +

+
+
+

CS Fundamentals • Problem Solving • Git

+
+
+ + +
+
+ 02 +

Full-stack Python

+

+ Deep dive into backend web engineering with Django, APIs, authentication, CRUD systems and clean application structure. +

+
+
+

Python • Django • SQL • Git

+
+
+ + +
+
+ 03 +

Software engineering

+

+ Focused on testing, OOP, maintainable architecture, CI, linting and writing code that is easier to change. +

+
+
+

pytest • Ruff • uv • CI

+
+
+ + +
+
+
+ 04 + + ✦ Active Focus + +
+

AI engineering

+

+ Now learning how to build reliable AI applications around LLMs, APIs, prompts, validation and evaluation. +

+
+
+

OpenAI • LLMs • Prompting • Evaluation

+
+
+ +
+
+

+ -
-
+
+
+ +
+

+ + My Toolkit +

+

+ Skills I use to build. +

+

+ My stack is growing from backend Python & full-stack development into AI application engineering. +

+
-
-

What I work with

-

Skills

+ +
+ + + + +
-
- {% for category, skills in skills_by_category.items %} -
-

{{ category }}

-
- {% for skill in skills %} - - {{ skill.name }} - - {% endfor %} +
+ + +
+ {% for category, skills in skills_by_category.items %} +
+
+
+

{{ category }}

+
+
+ {% for skill in skills %} +
+ {{ skill.name }} +
+ {% endfor %} +
+ {% empty %} +

+ No skills yet — add them via admin panel. +

+ {% endfor %}
- {% empty %} -

- No skills yet — add them via the admin panel. -

- {% endfor %} + + +
+
+ + Active Focus +
+

Currently Sharpening

+
+
+ 01 + LLM application design +
+
+ 02 + Prompt engineering & evaluation +
+
+ 03 + AI testing & reliability +
+
+ 04 + Production-ready Python +
+
+
+

+ Building test-driven tools and evaluating generative agent patterns with Python and modern APIs. +

+
+
+
-
-
- -
-

What I've built

-

Projects

+
+
+ +
+
+

+ + Selected Work +

+

+ Things I've actually built. +

+

+ Projects are where I turn learning into working software — from Django backends to AI-powered tools. +

+
+ {% if featured_projects %} -
+
{% for project in featured_projects %} -
+
- {% if project.image %} -
+ +
+ {% if project.image %} {{ project.title }} + {% else %} +
+ + {{ project.title|slice:":1" }} + +
+ {% endif %} +
+ {{ forloop.counter|stringformat:"02d" }} +
- {% endif %}
-

+

{{ project.title }}

- -

+

{{ project.description }}

{% for tech in project.get_tech_list %} - + {{ tech }} {% endfor %}
-
- {% if project.github_url %} - - GitHub ↗ - - {% endif %} - {% if project.live_url %} - - Live demo ↗ - - {% endif %} +
+
+ {% if project.github_url %} + + ⌥ Code + + {% endif %} + {% if project.live_url %} + + ↗ Live demo + + {% endif %} +
@@ -240,24 +454,34 @@

-

Other projects

+
+

+ More Experiments & CLI Tools +

{% for project in other_projects %} {% if not project.featured %} -
-
- {{ project.title }} - {{ project.tech_stack }} +
+
+ +
+
+ {{ project.title }} + tool +
+

{{ project.description|truncatechars:80 }}

+
-
+
{% if project.github_url %} - GitHub ↗ + GitHub ↗ {% endif %} {% if project.live_url %} - Live ↗ + Live ↗ {% endif %}
@@ -269,7 +493,7 @@

+

No projects yet.

Add your first project →
@@ -278,76 +502,148 @@

+ +
+
+
+
+
+

+ What I'm building next +

+

+ AI that helps people learn. +

+

+ I'm exploring ideas around accessible learning and AI-assisted education, including a project inspired by the experience of students with dyslexia — focusing on tailored text restructuring, comprehension assistance, and adaptive cognitive pacing. +

+
+ + ✦ LLM Context Adaptation + + + 📖 Dyslexia-friendly Typography & Spacing + +
+
+ +
+
+
+ NOW + +
+

AI Engineering

+

Learning • Building • Testing

+
+
+
+
+
+
+ -
-
+
+
-
-

Say hello

-

Contact

+
+

+ + Let's talk +

+

+ Have an opportunity or an idea? +

+

+ I'm open to junior software roles, internships, collaborations and conversations about building useful technology. +

-
+
-
-

- Got a project, opportunity, or just want to connect? I'd love to hear from you. -

-
-
- - tishapatiencemalongo@gmail.com + +
+ +
+
+ Email + tishapatiencemalongo@gmail.com
-
-
+ +
+ {% for message in messages %} -
+
{{ message }}
{% endfor %}
{% csrf_token %} -
-
+
+
+ {{ contact_form.name }} - {% if contact_form.name.errors %} -

{{ contact_form.name.errors.0 }}

- {% endif %} + {% if contact_form.name.errors %}

{{ contact_form.name.errors.0 }}

{% endif %}
-
+
+ {{ contact_form.email }} - {% if contact_form.email.errors %} -

{{ contact_form.email.errors.0 }}

- {% endif %} + {% if contact_form.email.errors %}

{{ contact_form.email.errors.0 }}

{% endif %}
-
+
+ {{ contact_form.subject }} - {% if contact_form.subject.errors %} -

{{ contact_form.subject.errors.0 }}

- {% endif %} + {% if contact_form.subject.errors %}

{{ contact_form.subject.errors.0 }}

{% endif %}
-
+
+ {{ contact_form.message }} - {% if contact_form.message.errors %} -

{{ contact_form.message.errors.0 }}

- {% endif %} + {% if contact_form.message.errors %}

{{ contact_form.message.errors.0 }}

{% endif %}
@@ -358,19 +654,74 @@

Contact

-