DEV Community

Cover image for Week 9: Finally, I understand the Web.
Om Kolhapure
Om Kolhapure

Posted on

Week 9: Finally, I understand the Web.

Eight weeks of Python. Then Week 9 started and there wasn't a single .py file in sight.

That's not a mistake — it's the plan. Understanding how the web actually works has been on the roadmap since Month 1, and Week 9 was the week to finally build it: HTML, CSS, and JavaScript, from a bare page to a dynamic to-do list talking to a real API. New repo, new language, same daily habit.

Here's how it went.


Day 57 — HTML Structure: A Real Portfolio Page

The project for the week: a personal portfolio page. Day 57 was just the skeleton — semantic HTML, no styling at all.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My Portfolio</title>
</head>
<body>
    <header>
        <h1>Om Kolhapure</h1>
        <nav>
            <ul>
                <li><a href="#home">Home</a></li>
                <li><a href="#projects">Projects</a></li>
                <li><a href="#contact">Contact</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <section id="home">
            <h2>Education</h2>
            <p>
                I am currently pursuing my <strong>Bachelor's in Computer Science and Engineering</strong>
                from Walchand Institute of Technology. In my first year, I scored 9.61 CGPA.
            </p>

            <h2>My Interests</h2>
            <p>
                I chose Computer Science because I was always curious about how computers work,
                how different apps function, and so on. Now I am interested in Artificial Intelligence,
                Machine Learning, and Cloud Computing.
            </p>
        </section>

        <section id="projects">
            <h2>My Projects</h2>
            <p>
                I have done many projects; all are present on my 
                <a href="https://github.com/Omk4314/progress-on-python.git">GitHub</a>.
                My most real-world project was a 
                <a href="https://github.com/Omk4314/progress-on-python/tree/main/url-shortner">URL Shortener</a>.
                It takes a long URL from the user and makes it short so that when the user clicks on it,
                they are redirected to the same page as the long URL.
            </p>
            <p>
                The main problem I faced was assigning different codes to different long URLs and keeping track of them.
            </p>
        </section>

        <section id="contact">
            <h2>Contact Me</h2>
            <p>LinkedIn: <a href="https://www.linkedin.com/in/om-kolhapure-0572603b6">linkedin.com/in/om-kolhapure</a></p>
        </section>
    </main>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The word that kept coming up in the lesson was semantic. <header>, <nav>, <main>, <section> — these aren't just containers, they tell a screen reader (or any tool parsing the page) what each part of the page actually is. A <div> says nothing. A <nav> says "this is navigation."

The <ul> inside <nav> for the menu links felt odd at first — a navigation menu is a list of links. Once that clicked, semantic HTML stopped feeling like extra rules and started feeling like describing the page honestly.

And there it is — my Month 1–2 Python work getting linked from inside this very page. The URL shortener project earned its place as the portfolio's showcase piece.


Day 58 — CSS Fundamentals: Making It Actually Look Like Something

Day 58 took the bare HTML and gave it real styling — flexbox, responsive breakpoints, and a design system built from scratch:

* {
    box-sizing: border-box;
}
body {
    margin: 0;
    font-family:sans-serif;
    line-height: 1.6;
}
ul {
    list-style: none;
    padding: 0;
    margin: 0;
}
a {
    text-decoration: none;
    color: inherit;
}
header {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 20px;
}
nav ul {
    display: flex;
    gap: 20px;
}
nav a {
    display: inline-block;
    padding: 0.5rem 1rem;
}
nav a:hover {
    background-color: red;
}
main {
    max-width: 700px;
    margin:0 auto;
    padding: 2rem;
}
section {
    padding: 2rem 0;
    border-bottom: 20px;
}
header {
    flex-direction: column;
    text-align: center;
    gap: 1rem;
}
@media (min-width: 600px) {
    header {
        flex-direction: row;
        /* How do you push nav to the right? */
    }
}
main {
    padding: 1rem;
}

@media (min-width: 600px) {
    main {
        padding: 2rem;
    }
}
#contact a {
    color:#0077b5;
}
#contact a:hover {
    text-decoration: underline;
}
strong {
    color: #2c3e50;
}
h2 {
    border-bottom: 2px solid #333;
    padding-bottom: 0.5rem;
}
Enter fullscreen mode Exit fullscreen mode

Two things clicked hard on this day.

box-sizing: border-box on the universal selector — a rule I'd read about but didn't understand the point of until I removed it once by accident and watched every element's padding blow past its intended width. With border-box, padding and border get counted inside the element's declared width instead of added on top. Every layout tutorial assumes this rule exists. Now I know why.

@media (min-width: 600px) — my first real media query. Mobile-first thinking: define the small-screen layout as the default (header stacked in a column), then override it for wider screens (flex-direction: row). That inversion — style for mobile first, then add rules for bigger screens — was the opposite of how I assumed CSS worked.

I left a comment in the CSS mid-lesson that I never deleted: /* How do you push nav to the right? */. That's an honest artifact of learning in public — a question I hadn't answered yet, sitting right there in the file, later solved with flexbox's justify-content: space-between once I understood the property better.


Day 59 — JavaScript Basics: A Dark Mode Toggle

Day 59 added the first sliver of interactivity — a dark mode button that actually works:

const toggleBtn = document.getElementById("theme-toggle");
toggleBtn.addEventListener("click", function() {
    document.body.classList.toggle("dark-mode");
    if (document.body.classList.contains("dark-mode")) {
        toggleBtn.innerText = "Light Mode";
    }
    else {
        toggleBtn.innerText = "Dark Mode";
    }
});
Enter fullscreen mode Exit fullscreen mode

Paired with dark-mode-specific CSS rules:

body.dark-mode {
    background-color: #1a1a1a;
    color: #f0f0f0;
}

.dark-mode a {
    color: #4dabf7;
}

.dark-mode strong {
    color: #a5d8ff;
}

.dark-mode h2 {
    border-bottom-color: #555;
}

.dark-mode nav a:hover {
    background-color: #333;
}
Enter fullscreen mode Exit fullscreen mode

Three ideas landed at once here:

  • document.getElementById() — JavaScript reaching into the page to grab a specific element by its id. This is the JS equivalent of Python's input() — the moment the program starts responding to the actual page instead of just running blind.
  • classList.toggle("dark-mode") — adds the class if it's missing, removes it if it's present. One line does what would otherwise be an if/else on its own.
  • CSS reacting to JS. The .dark-mode class doesn't do anything by itself — it's just a hook. All the actual dark-mode styling lives in CSS, keyed off a class that JavaScript flips on and off. Separation of concerns, made concrete: JavaScript handles behaviour, CSS handles appearance, and they meet at exactly one point — a class name.

Coming from Python's if/else, writing toggleBtn.addEventListener("click", function() {...}) felt inside-out at first — you're not calling the function, you're handing it to the browser and saying "run this whenever a click happens." Event-driven thinking is a different rhythm than the top-to-bottom scripts Month 1 and 2 were full of.


Day 60 — DOM Manipulation: A Real To-Do List

Day 60 was the biggest jump of the week — a fully dynamic to-do list: add tasks, mark them complete, delete them, move completed tasks to their own section, all without a single page reload.

<section aria-labelledby="add-task-heading">
    <h2 id="add-task-heading">Add a New Task</h2>
    <form action="#" method="post">
        <label for="task-input">What do you need to do?</label>
        <input 
            type="text" 
            id="task-input" 
            name="task" 
            placeholder="Enter your task here..." 
            required
            minlength="1"
            maxlength="200"
        >
        <button type="submit">Add Task</button>
    </form>
</section>
Enter fullscreen mode Exit fullscreen mode

And the JavaScript driving all of it:

document.addEventListener('DOMContentLoaded', function () {
    const taskForm = document.querySelector('form');
    const taskInput = document.getElementById('task-input');
    const taskList = document.querySelector('ul');
    const completedSection = document.querySelector('section[aria-labelledby="completed-heading"]');

    let taskCounter = 3;

    function createTaskElement(taskText) {
        taskCounter++;
        const li = document.createElement('li');
        li.setAttribute('data-task-id', taskCounter);

        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = 'task-' + taskCounter;
        checkbox.name = 'task-' + taskCounter;

        const label = document.createElement('label');
        label.setAttribute('for', 'task-' + taskCounter);
        label.textContent = taskText;

        const deleteBtn = document.createElement('button');
        deleteBtn.type = 'button';
        deleteBtn.setAttribute('aria-label', 'Delete task: ' + taskText);
        deleteBtn.textContent = 'Delete';

        li.appendChild(checkbox);
        li.appendChild(label);
        li.appendChild(deleteBtn);

        deleteBtn.addEventListener('click', function () {
            deleteTask(li);
        });

        checkbox.addEventListener('change', function () {
            handleTaskToggle(checkbox, label, li);
        });

        return li;
    }

    function addTask(event) {
        event.preventDefault();
        const taskText = taskInput.value.trim();
        if (taskText === '') {
            return;
        }
        const newTask = createTaskElement(taskText);
        taskList.appendChild(newTask);
        taskInput.value = '';
        taskInput.focus();
    }

    function deleteTask(taskElement) {
        const isCompleted = taskElement.querySelector('input[type="checkbox"]').checked;
        if (isCompleted) {
            const completedList = document.getElementById('completed-task-list');
            if (completedList) {
                completedList.removeChild(taskElement);
                checkEmptyCompleted();
            }
        } else {
            taskList.removeChild(taskElement);
        }
    }

    function handleTaskToggle(checkbox, label, taskElement) {
        if (checkbox.checked) {
            label.style.textDecoration = 'line-through';
            label.style.opacity = '0.6';
            moveToCompleted(taskElement);
        } else {
            label.style.textDecoration = 'none';
            label.style.opacity = '1';
            taskList.appendChild(taskElement);
            checkEmptyCompleted();
        }
    }

    function moveToCompleted(taskElement) {
        let completedList = document.getElementById('completed-task-list');
        if (!completedList) {
            completedList = document.createElement('ul');
            completedList.id = 'completed-task-list';
            completedSection.appendChild(completedList);
            const emptyMsg = completedSection.querySelector('p');
            if (emptyMsg) {
                emptyMsg.remove();
            }
        }
        completedList.appendChild(taskElement);
    }

    function checkEmptyCompleted() {
        const completedList = document.getElementById('completed-task-list');
        if (completedList && completedList.children.length === 0) {
            const emptyMsg = document.createElement('p');
            emptyMsg.textContent = 'No completed tasks yet.';
            completedSection.appendChild(emptyMsg);
            completedList.remove();
        }
    }

    function setupExistingTasks() {
        const existingTasks = taskList.querySelectorAll('li');
        existingTasks.forEach(function (taskElement) {
            const checkbox = taskElement.querySelector('input[type="checkbox"]');
            const label = taskElement.querySelector('label');
            const deleteBtn = taskElement.querySelector('button');

            deleteBtn.addEventListener('click', function () {
                deleteTask(taskElement);
            });
            checkbox.addEventListener('change', function () {
                handleTaskToggle(checkbox, label, taskElement);
            });

            if (checkbox.checked) {
                label.style.textDecoration = 'line-through';
                label.style.opacity = '0.6';
                moveToCompleted(taskElement);
            }
        });
    }

    taskForm.addEventListener('submit', addTask);
    setupExistingTasks();
});
Enter fullscreen mode Exit fullscreen mode

The pattern that made everything else make sense: document.createElement() builds pieces, .appendChild() assembles them, event listeners get attached before the element ever touches the page. createTaskElement() builds an entire <li> — checkbox, label, delete button — completely in memory first, wires up its own delete and toggle behaviour, and only then gets appended to the visible list.

event.preventDefault() in addTask() was a small but essential line — without it, submitting the form does what HTML forms do by default: reload the page and lose everything. That one line is the difference between a static form and an actual single-page interaction.

setupExistingTasks() deserves a callout too — it attaches the same event listeners to the three tasks that were already hardcoded in the HTML, so the "starter" tasks behave identically to ones added dynamically. Nice touch for consistency.


Day 61 — Fetch API: Talking to a Real Server

Day 61 replaced the in-memory to-do list with one backed by a real API — jsonplaceholder.typicode.com, a free fake REST API for testing exactly this kind of thing.

const API_BASE = 'https://jsonplaceholder.typicode.com';
const MAX_RETRIES = 3;
const BASE_RETRY_DELAY = 1000;
const USER_ID = 1;

let activeAbortController = null;

async function apiRequest(endpoint, options = {}) {
    const {
        method = 'GET',
        body = null,
        retries = MAX_RETRIES,
        ...fetchOptions
    } = options;

    if (activeAbortController) {
        activeAbortController.abort();
    }

    const abortController = new AbortController();
    activeAbortController = abortController;

    const url = `${API_BASE}${endpoint}`;
    const config = {
        method,
        headers: {
            'Content-Type': 'application/json',
            ...fetchOptions.headers
        },
        signal: abortController.signal,
        ...fetchOptions
    };

    if (body) {
        config.body = JSON.stringify(body);
    }

    let lastError;

    for (let attempt = 0; attempt <= retries; attempt++) {
        try {
            const response = await fetch(url, config);

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }

            const contentType = response.headers.get('content-type');
            const data = (contentType && contentType.includes('application/json'))
                ? await response.json()
                : null;

            if (activeAbortController === abortController) {
                activeAbortController = null;
            }

            return data;

        } catch (error) {
            lastError = error;

            if (error.name === 'AbortError') {
                throw error;
            }

            if (error.message && error.message.startsWith('HTTP 4')) {
                throw error;
            }

            if (attempt < retries) {
                const delay = BASE_RETRY_DELAY * Math.pow(2, attempt);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }

    if (activeAbortController === abortController) {
        activeAbortController = null;
    }

    throw lastError;
}

async function getTodos() {
    return apiRequest(`/todos?userId=${USER_ID}&_limit=5`, {
        preventRace: true
    });
}

async function addTodo(title) {
    return apiRequest('/todos', {
        method: 'POST',
        body: {
            title,
            completed: false,
            userId: USER_ID
        },
        preventRace: false
    });
}

async function deleteTodo(id) {
    return apiRequest(`/todos/${id}`, {
        method: 'DELETE',
        preventRace: false
    });
}

async function updateTodo(id, updates) {
    return apiRequest(`/todos/${id}`, {
        method: 'PATCH',
        body: updates,
        preventRace: false
    });
}
Enter fullscreen mode Exit fullscreen mode

This file is doing more than a basic fetch tutorial ever shows, and each extra piece exists for a real reason:

  • async/await — reading top to bottom like normal code, even though fetch() is asynchronous under the hood. This is JavaScript's answer to the same problem Python's try/except solves for unreliable operations, just with a different shape.
  • Retry with exponential backoffBASE_RETRY_DELAY * Math.pow(2, attempt) means each retry waits longer than the last (1s, 2s, 4s...). A flaky network shouldn't mean an instant failure.
  • AbortController — cancelling an in-flight request if a new one starts before the old one finishes. This stops a slow, stale request from overwriting fresher data — a real bug that's easy to introduce and easy to miss until it happens.
  • Distinguishing 4xx errors from network errorsif (error.message && error.message.startsWith('HTTP 4')) skips retrying on client errors like "404 Not Found," because retrying a request that's wrong by definition just wastes time. Only genuinely flaky failures get retried.

The rest of newScript.js reuses the exact same DOM manipulation functions from Day 60 — moveToCompleted(), checkEmptyCompleted() — but now every checkbox toggle and delete button calls the API first and only updates the DOM after a successful response, with setLoading() disabling the form while a request is in flight so nothing gets submitted twice.

Watching real data load from a live URL into the same to-do list interface from Day 60 was the moment the whole week's arc paid off — static HTML, to styled HTML, to interactive HTML, to an HTML page that talks to the internet.


Day 62 — Weather Dashboard: Still In Progress

The plan for Day 62 is a weather dashboard pulling live data from the OpenWeatherMap API, showing current conditions plus a 5-day forecast — the same Fetch API skills from Day 61, applied to a second, independent API. That project is still being built as this post goes out, and it'll get proper coverage once it's finished and pushed.


📁 What I Built This Week

Project File Concepts Used
Portfolio (structure) portfolio.html Semantic HTML, accessibility basics
Portfolio (styled) css-portfolio/styles.css, styledPortfolio.html Flexbox, media queries, box-sizing
Dark Mode Toggle css-portfolio/script.js DOM selection, classList.toggle(), event listeners
To-Do List (local) Todo-list/todo.html, style.css, script.js Dynamic DOM creation, form handling, event.preventDefault()
To-Do List (API-backed) Todo-list/newScript.js fetch, async/await, retry logic, AbortController

All the code is on GitHub — brand new repo, same daily habit:
👉 github.com/Omk4314/Progress-on-HTML-CSS-JS


What Actually Clicked This Week

  • Semantic HTML describes the page, it doesn't just structure it. <nav> and <section> communicate meaning that a stack of <div>s never could.
  • box-sizing: border-box should probably always be on. I understood why the moment I saw a layout break without it.
  • CSS and JavaScript meet at a class name. The dark mode toggle taught me that JS shouldn't be reaching in to change styles directly — it should flip a class, and let CSS own the actual appearance.
  • document.createElement() + appendChild() is how dynamic pages are actually built. Nothing on the page is fixed — it's all just JavaScript assembling pieces and dropping them in.
  • Fetch code that's actually reliable is more than just fetch(). Retry logic, abort controllers, and distinguishing error types are the difference between a demo and something that survives a flaky connection.
  • This felt like starting over, and that was fine. Eight weeks of Python confidence didn't transfer directly to CSS flexbox — but the habit of building daily, reading errors carefully, and testing as I go transferred completely.

What I Want to Learn Next

Week 10 is already planned around finishing the weather dashboard and going further into the front end:

  • Finish the weather dashboard — real API, current + forecast data, proper error states
  • CSS Grid — flexbox got me through this week, but grid is the tool for genuinely two-dimensional layouts
  • More async patternsPromise.all(), handling multiple simultaneous requests
  • Maybe a small framework preview — enough to see what problems React or similar tools are actually solving

Nine weeks of daily practice, and this week was proof that the habit is the real skill — not any single language. Same discipline, brand new syntax, and the terminal-fear-is-gone feeling from Month 1 showed up again by Wednesday.

If you've made the jump from a backend language into web basics, I'd love to hear what clicked fastest for you — and what didn't.

See you in Week 10. 🌐


Week 9 complete. Different language, same daily commit.

Top comments (0)