Understand how the web works; build a clean, responsive Personal Portfolio.
week1-portfolio/
ββ index.html
ββ styles.css
ββ script.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Portfolio</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header class="container">
<nav class="nav">
<a href="#about">About</a>
<a href="#skills">Skills</a>
<a href="#projects">Projects</a>
<a href="#contact">Contact</a>
</nav>
<h1>Hello, I'm <span class="accent">Your Name</span></h1>
<p>Aspiring developer | Tech Enthusiast | Lifelong Learner</p>
<button id="themeBtn">Toggle Theme</button>
</header>
<main class="container">
<section id="about">
<h2>About Me</h2>
<p>I am a fresher learning web and Python. This is my first portfolio.</p>
</section>
<section id="skills">
<h2>Skills</h2>
<ul class="tags">
<li>HTML</li><li>CSS</li><li>JavaScript</li><li>Python</li>
</ul>
</section>
<section id="projects">
<h2>Projects</h2>
<article class="card">
<h3>Portfolio Website</h3>
<p>A responsive personal website built with HTML/CSS/JS.</p>
<a href="#" target="_blank">View</a>
</article>
</section>
<section id="contact">
<h2>Contact</h2>
<form id="contactForm">
<label>
Name
<input name="name" required />
</label>
<label>
Email
<input name="email" type="email" required />
</label>
<label>
Message
<textarea name="message" rows="4" required></textarea>
</label>
<button type="submit">Send</button>
<p id="formMsg" class="muted"></p>
</form>
</section>
</main>
<footer class="container muted">Β© 2025 Your Name</footer>
<script src="script.js"></script>
</body>
</html>
styles.css
:root { --bg:#0f172a; --fg:#e2e8f0; --accent:#38bdf8; --card:#111827; }
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:var(--bg); color:var(--fg); }
.container { width:min(1000px, 92%); margin-inline:auto; padding:1.5rem 0; }
.nav { display:flex; gap:1rem; margin-bottom:1rem; flex-wrap:wrap; }
.nav a { color:var(--fg); opacity:.8; text-decoration:none; }
h1 { font-size:2.2rem; margin:.2rem 0 1rem; }
h2 { margin-top:2rem; }
.accent { color:var(--accent); }
.tags { display:flex; gap:.5rem; padding:0; list-style:none; flex-wrap:wrap; }
.tags li { background:#1f2937; padding:.4rem .7rem; border-radius:999px; }
.card { background:var(--card); padding:1rem; border-radius:14px; box-shadow:0 4px 20px rgba(0,0,0,.2); }
label { display:block; margin:.7rem 0; }
input, textarea { width:100%; padding:.7rem; border:1px solid #334155; border-radius:10px; background:#0b1220; color:var(--fg); }
button { padding:.7rem 1rem; border-radius:10px; background:var(--accent); color:#031018; border:0; cursor:pointer; }
.muted { color:#94a3b8; font-size:.9rem; }
@media (max-width:600px){ h1 { font-size:1.6rem; } }
.light { --bg:#f8fafc; --fg:#0b1220; --card:#ffffff; }
script.js
const themeBtn = document.getElementById("themeBtn");
themeBtn.addEventListener("click", () =>
document.body.classList.toggle("light")
);
const form = document.getElementById("contactForm");
const msg = document.getElementById("formMsg");
form.addEventListener("submit", (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
// Simple front-end validation
if (!data.name || !data.email || !data.message) {
msg.textContent = "Please fill all fields.";
return;
}
msg.textContent = "Thanks! Your message was not really sent (demo).";
form.reset();
});
How to open: doubleβclick index.html
(or use VSCode βGo Liveβ extension).
Move from static β dynamic pages, forms, and templates.
week2-flask-todo/
ββ app.py
ββ requirements.txt
ββ templates/
β ββ base.html
β ββ index.html
ββ static/
ββ styles.css
requirements.txt
Flask==3.0.3
app.py
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
tasks = [] # in-memory list (Week 3 we'll persist to DB)
@app.route("/")
def home():
return render_template("index.html", tasks=tasks)
@app.route("/add", methods=["POST"])
def add():
title = request.form.get("title", "").strip()
if title:
tasks.append({"title": title, "done": False})
return redirect(url_for("home"))
@app.route("/toggle/<int:index>")
def toggle(index):
if 0 <= index < len(tasks):
tasks[index]["done"] = not tasks[index]["done"]
return redirect(url_for("home"))
@app.route("/delete/<int:index>")
def delete(index):
if 0 <= index < len(tasks):
tasks.pop(index)
return redirect(url_for("home"))
if __name__ == "__main__":
app.run(debug=True)
templates/base.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Flask To-Do{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<header class="container">
<h1>Flask ToβDo</h1>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="container muted">Built in Month 2</footer>
</body>
</html>
templates/index.html
{% extends "base.html" %}
{% block title %}My Tasks{% endblock %}
{% block content %}
<form method="post" action="{{ url_for('add') }}" class="row">
<input name="title" placeholder="New task..." required />
<button>Add</button>
</form>
<ul class="list">
{% for t in tasks %}
<li class="item">
<span class="{{ 'done' if t.done }}">{{ t.title }}</span>
<a href="{{ url_for('toggle', index=loop.index0) }}">Toggle</a>
<a href="{{ url_for('delete', index=loop.index0) }}">Delete</a>
</li>
{% else %}
<li class="muted">No tasks yet.</li>
{% endfor %}
</ul>
{% endblock %}
static/styles.css
/* Reuse aesthetics from Week1 quickly */
:root { --bg:#0f172a; --fg:#e2e8f0; --accent:#38bdf8; }
* { box-sizing: border-box; } body { margin:0; font-family: system-ui; background:var(--bg); color:var(--fg); }
.container { width:min(900px, 92%); margin-inline:auto; padding:1.2rem 0; }
.row { display:flex; gap:.5rem; }
input { flex:1; padding:.6rem; border-radius:10px; border:1px solid #334155; background:#0b1220; color:var(--fg); }
button { padding:.6rem 1rem; border-radius:10px; background:var(--accent); border:0; color:#031018; cursor:pointer; }
.list { list-style:none; padding:0; }
.item { display:flex; gap:.8rem; align-items:center; padding:.6rem 0; border-bottom:1px solid #1f2937; }
.item a { color:var(--accent); text-decoration:none; }
.done { text-decoration: line-through; opacity:.7; }
.muted { color:#94a3b8; }
Run
# in week2-flask-todo/
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python app.py
# Open http://127.0.0.1:5000
Persist data with SQLite. Learn schema, parameterized queries, CRUD.
week3-notes-sqlite/
ββ app.py
ββ db.py
ββ schema.sql
ββ requirements.txt
ββ templates/
β ββ base.html
β ββ index.html
ββ static/
ββ styles.css
requirements.txt
Flask==3.0.3
schema.sql
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
db.py
import sqlite3
from pathlib import Path
DB_PATH = Path("notes.db")
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # dict-like
return conn
def init_db():
with get_conn() as conn, open("schema.sql", "r", encoding="utf-8") as f:
conn.executescript(f.read())
def add_note(title: str, content: str):
with get_conn() as conn:
conn.execute(
"INSERT INTO notes (title, content) VALUES (?, ?)",
(title, content)
)
def list_notes():
with get_conn() as conn:
return conn.execute(
"SELECT id, title, content, created_at FROM notes ORDER BY id DESC"
).fetchall()
def delete_note(note_id: int):
with get_conn() as conn:
conn.execute("DELETE FROM notes WHERE id = ?", (note_id,))
def search_notes(q: str):
with get_conn() as conn:
return conn.execute(
"SELECT id, title, content, created_at FROM notes WHERE title LIKE ? OR content LIKE ? ORDER BY id DESC",
(f"%{q}%", f"%{q}%")
).fetchall()
app.py
from flask import Flask, render_template, request, redirect, url_for
import db
app = Flask(__name__)
@app.before_first_request
def setup():
db.init_db()
@app.get("/")
def home():
q = request.args.get("q", "").strip()
notes = db.search_notes(q) if q else db.list_notes()
return render_template("index.html", notes=notes, q=q)
@app.post("/add")
def add():
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
if title and content:
db.add_note(title, content)
return redirect(url_for("home"))
@app.get("/delete/<int:note_id>")
def delete(note_id: int):
db.delete_note(note_id)
return redirect(url_for("home"))
if __name__ == "__main__":
app.run(debug=True)
templates/base.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}Notes{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<header class="container">
<h1>SQLite Notes</h1>
<form method="get" action="{{ url_for('home') }}" class="row">
<input name="q" value="{{ q or '' }}" placeholder="Search notes..." />
<button>Search</button>
</form>
</header>
<main class="container">
{% block content %}{% endblock %}
</main>
<footer class="container muted">SQLite + Flask | Parameterized queries prevent SQL injection β
</footer>
</body>
</html>
templates/index.html
{% extends "base.html" %}
{% block content %}
<form method="post" action="{{ url_for('add') }}" class="card">
<h2>New Note</h2>
<label>Title <input name="title" required /></label>
<label>Content <textarea name="content" rows="4" required></textarea></label>
<button>Add</button>
</form>
<section class="grid">
{% for n in notes %}
<article class="card">
<h3>{{ n.title }}</h3>
<p>{{ n.content }}</p>
<small class="muted">{{ n.created_at }}</small><br />
<a href="{{ url_for('delete', note_id=n.id) }}">Delete</a>
</article>
{% else %}
<p class="muted">No notes yet.</p>
{% endfor %}
</section>
{% endblock %}
static/styles.css
:root { --bg:#0f172a; --fg:#e2e8f0; --accent:#38bdf8; --card:#111827; }
* { box-sizing:border-box; } body { margin:0; font-family:system-ui; background:var(--bg); color:var(--fg); }
.container { width:min(900px, 92%); margin-inline:auto; padding:1.2rem 0; }
.row { display:flex; gap:.5rem; }
.card { background:var(--card); padding:1rem; border-radius:14px; margin:.7rem 0; }
.grid { display:grid; grid-template-columns: repeat(auto-fill,minmax(250px,1fr)); gap:1rem; }
label { display:block; margin:.5rem 0; }
input, textarea { width:100%; padding:.6rem; border-radius:10px; border:1px solid #334155; background:#0b1220; color:var(--fg); }
button { padding:.6rem 1rem; border-radius:10px; background:var(--accent); color:#031018; border:0; cursor:pointer; }
a { color:var(--accent); text-decoration:none; }
.muted { color:#94a3b8; font-size:.9rem; }
Run
# in week3-notes-sqlite/
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python app.py
# Open http://127.0.0.1:5000
Why parameterized queries?
We used ?
placeholders in SQL (db.py
). This prevents SQL injection by separating code and data.
Package the Flask+SQLite app in Docker. Optional: run with Docker Compose (named volume for DB).
Weβll containerize Week 3 project.
week3-notes-sqlite/
Dockerfile
.dockerignore
gunicorn.conf.py
Dockerfile
# 1) Base
FROM python:3.12-slim
# 2) Workdir & env
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
# 3) System deps (optional but useful)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates && rm -rf /var/lib/apt/lists/*
# 4) Copy deps first for better caching
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt gunicorn
# 5) Copy app code
COPY . /app
# 6) Expose port & command (use gunicorn)
EXPOSE 5000
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app:app"]
gunicorn.conf.py
bind = "0.0.0.0:5000"
workers = 2
timeout = 60
.dockerignore
.venv
__pycache__/
*.pyc
.git
.idea
.vscode
notes.db
Build & run
# from week3-notes-sqlite/
docker build -t notes-app:1 .
docker run -p 5000:5000 -v $PWD/notes.db:/app/notes.db notes-app:1
# Windows PowerShell volume: -v ${PWD}\notes.db:/app/notes.db
# Open http://localhost:5000
Add a new file in the same folder:
docker-compose.yml
version: "3.9"
services:
web:
build: .
image: notes-app:compose
ports:
- "5000:5000"
volumes:
- notes_data:/app/notes.db
restart: unless-stopped
volumes:
notes_data:
Run with Compose
docker compose up --build
# Open http://localhost:5000
Why mount only the DB? So code is baked into the image (immutable), while data persists in a volume. Clean separation = fewer βit works on my machineβ issues.
week1-portfolio
folder (screenshot + short demo video).week2-flask-todo
repo link + screenshot.week3-notes-sqlite
repo link + demo.docker run
or docker compose up
proof (screenshot of terminal + browser)./api/notes
).alpine
/multiβstage build to slim images.flake8
or ruff
lint on push.Objective: Learn how the web works and build a static personal portfolio.
Assignments:
Create a webpage with:
<img>
) and a link (<a>
).Style the webpage with CSS:
Add simple JavaScript:
Mini Project: π Personal Portfolio Website
Objective: Move from static β dynamic web apps.
Assignments:
/about
that shows a custom HTML page.{{ name }}
).Mini Project: π To-Do List App
Objective: Store and manage data with a database.
Assignments:
notes(id, title, content)
.sqlite3
module).Mini Project: π Notes App with Database
Objective: Package and run apps in Docker.
Assignments:
hello-world
.nginx
) and access it via browser.Mini Project: π Dockerized Notes App
Submission Format:
Each project must have a short README.md
with:
Objective: Learn how the web works and build a personal portfolio site.
Level | Criteria |
---|---|
Beginner (β ) | Writes simple HTML with headings and paragraphs but struggles with structure, CSS, or linking files. |
Intermediate (β β ) | Builds a multi-section webpage with HTML + CSS (header, body, footer). Uses CSS selectors, classes, and styles. Completes Portfolio Page assignment. |
Pro (β β β ) | Adds interactivity with JavaScript (form validation, button actions). Designs a polished, responsive page (flexbox/grid). Includes creativity (e.g., animations, hover effects). |
Objective: Move from static β dynamic content with a backend.
Level | Criteria |
---|---|
Beginner (β ) | Runs a basic Flask server with βHello Worldβ route but struggles with templates or routing. |
Intermediate (β β ) | Creates multiple Flask routes, uses HTML templates (Jinja2), and handles forms. Completes To-Do List App. |
Pro (β β β ) | Builds a clean, structured Flask app. Adds extra features (mark tasks done, delete tasks). Organizes code using blueprints or templates. |
Objective: Learn persistent storage with a database.
Level | Criteria |
---|---|
Beginner (β ) | Understands SQL basics, can create a table and insert rows manually. |
Intermediate (β β ) | Connects Flask app to SQLite. Implements CRUD (Create, Read, Update, Delete). Completes Notes App with DB. |
Pro (β β β ) | Adds search, sorting, or authentication to the app. Uses parameterized queries to prevent SQL injection. Shows awareness of good database practices. |
Objective: Package and run applications in Docker.
Level | Criteria |
---|---|
Beginner (β ) | Runs simple Docker images (hello-world , nginx ). Struggles with custom Dockerfiles. |
Intermediate (β β ) | Writes a Dockerfile for Flask app, builds and runs the container. Understands basic commands (docker run , ps , logs , stop ). |
Pro (β β β ) | Dockerizes Flask + SQLite app with Docker Compose (app + DB). Optimizes Dockerfile (small image, .dockerignore). Explains container vs VM concepts clearly. |
π Suggested Weightage: