Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Implementation Plan - North Pole Wishlist

## Phase 0: Git Setup
- [x] Check if the current directory is an initialized git repository.
- [x] If it is, create and checkout a new feature branch named `north-pole-wishlist` (derived from the application name).

## Phase 1: Environment & Project Structure
- [x] Initialize the project structure: create `app/`, `app/templates`, `app/static/css`, `app/static/img` directories.
- [x] Create a virtual environment `venv` and activate it (instructional step for the user/agent).
- [x] Create `requirements.txt` with dependencies: `Flask`, `Flask-SQLAlchemy`, `Flask-WTF`, `email-validator` (if needed for extensive validation, strictly strictly adherence to spec: spec doesn't ask for email, so just core libs).
- [x] Create `run.py` as the entry point.
- [x] Create `app/__init__.py` to initialize the Flask application, database extension, and configuration (Secret Key, DB URI).
- [x] Create a dummy route in `app/routes.py` and register it in `__init__.py` to verify the server runs.

## Phase 2: Database Layer
- [x] Create `app/models.py`.
- [x] Define the `CategoryEnum` class with the specified values.
- [x] Define the `GiftIdea` model using SQLAlchemy 2.0 style (`Mapped`, `mapped_column`) with fields: `id`, `name`, `description`, `category`, `created_at`.
- [x] Define the `Vote` model with fields: `id`, `gift_id`, `score` (1-5), `created_at`.
- [x] Define the `Comment` model with fields: `id`, `gift_id`, `author_name`, `content`, `created_at`.
- [x] Configure relationships between `GiftIdea`, `Vote`, and `Comment`.
- [x] Create a CLI command or update `run.py` context to initialize the database (`db.create_all()`) and test creating a sample entry.

## Phase 3: Gift Submission Feature
- [x] Create `app/forms.py` using Flask-WTF.
- [x] Define `GiftForm` with fields: `name`, `description`, `category` (SelectField). Add validators (DataRequired, Length).
- [x] Create `app/templates/base.html` with the Bootstrap 5 CDN links and a basic navigation bar structure.
- [x] Create `app/templates/submit_gift.html` extending `base.html` to render the `GiftForm`.
- [x] Implement `GET /submit` in `app/routes.py` to render the template.
- [x] Implement `POST /submit` in `app/routes.py` to validate form data and save the new `GiftIdea` to the database. Flash a success message upon completion.

## Phase 4: Feed & Filtering
- [x] Create `app/templates/index.html` extending `base.html`.
- [x] Implement `GET /` in `app/routes.py`.
- [x] Write SQLAlchemy 2.0 queries to fetch `GiftIdea` records.
- [x] Implement the "New Arrivals" sorting logic (default).
- [x] Implement the "Category" filter logic (query parameter `category`).
- [x] Implement the "Ranking" logic: "Top Rated" (Avg Score) and "Most Popular" (Vote Count).
- [x] Render the list of gifts in `index.html` using a basic Bootstrap Card component.

## Phase 5: Gift Details & Interaction
- [x] Create `app/templates/gift_details.html`.
- [x] Implement `GET /gift/<int:id>` to fetch and display a specific gift, its average score, and comments.
- [x] Update `app/forms.py` to include `VoteForm` (Integer/Radio 1-5) and `CommentForm` (Author, Content).
- [x] Implement `POST /gift/<int:id>/vote` to handle voting.
- [x] Implement `POST /gift/<int:id>/comment` to handle comments.
- [x] Add the forms to `gift_details.html` and display existing comments chronologically.

## Phase 6: The "Christmas Aesthetic" (UI/UX)
- [x] Update `app/templates/base.html` to include the Google Fonts (e.g., 'Mountains of Christmas').
- [x] Create `app/static/css/style.css` and define the color palette variables (`--santa-red`, `--pine-green`, etc.).
- [x] Apply the color palette to the Navbar, Buttons, and Body background.
- [x] Add the Hero Image (Santa on Sleigh) to `index.html`.
- [x] Style the Gift Cards to look like wrapped presents (custom borders/shadows).
- [x] Style the Vote input to use snowflakes (❄️) instead of default radio buttons or stars.

## Phase 7: Completion & Version Control
- [ ] Verify all application functionality (Submission, Browsing, Filtering, Voting, Commenting).
- [ ] Create a `README.md` file explaining the application functions, how to interact with them, the architecture, file breakdown and how to run and test it locally.
- [ ] Add all changes to the repository (`git add .`).
- [ ] Commit the changes (`git commit -m "Complete implementation of North Pole Wishlist"`).
- [ ] Push the feature branch to the remote repository, creating a branch with the same name in the remote repository, using the Gemini CLI github MCP server.
- [ ] Open a pull request for the feature branch using the Gemini CLI github MCP server, leave it open for review, don't merge it.
123 changes: 43 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,57 @@
# Spec-Driven Development w Gemini CLI
# North Pole Wishlist 🎅

This repo has some basic assets to experiment **Spec-Driven Development** using the Gemini CLI. You will act as a developer going from a raw Functional Specification to a deployed Pull Request in a single session.
A community-driven platform to share and curate the best holiday gift ideas. Built with Python (Flask) and love.

## Assets
## Features

* `.gemini/commands/`: Contains configuration files for custom commands (`techspec`, `plan`, `build`).
* `GEMINI.md`: Contains project rules and guidelines.
* `.github/workflows`: Contains CI workflow.
* **No application code**.
- **Gift Submission**: Users can share their gift ideas with descriptions and categories.
- **Vote & Rank**: Community voting system ("Snowflakes") to highlight the best gifts.
- **Comments**: Discuss and review gift ideas.
- **Festive UI**: A fully immersive Christmas-themed interface.
- **Sorting & Filtering**: Browse by category, popularity, rating, or new arrivals.

## Requirements
## Tech Stack

The `GEMINI.md` configuration and custom commands require the following extensions:
* **Google Workspace**
* **Nano Banana**
* **GitHub**
- **Backend**: Python 3, Flask, SQLAlchemy (2.0 style)
- **Frontend**: Bootstrap 5, Jinja2, Custom CSS
- **Database**: SQLite (Development)

---
## Setup & Running Locally

## Step 1: The Architect Phase (/techspec)
1. **Clone the repository**:
```bash
git clone <repository-url>
cd north-pole-wishlist
```

**Goal:** Transform a Functional Spec (Google Doc) into a Technical Spec (Google Doc).
2. **Create and activate a virtual environment**:
```bash
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```

1. **Command:**
```
/techspec "Name of your functional specs doc" "Your desired technology stack and requirements"
```
3. **Install dependencies**:
```bash
pip install -r requirements.txt
```

2. **What Happens:**
* The agent searches your Drive for the doc.
* It reads the requirements.
* It generates a **Technical Specification** including Data Models, API Routes, and Architecture based on your inputs.
* **Output:** It creates a *new* Google Doc titled "Technical Specification - Application name" and gives you the link.
4. **Run the application**:
```bash
python run.py
```
The application will start at `http://127.0.0.1:5000`. The database will be automatically initialized with a sample gift if it's empty.

---
## Project Structure

## Step 2: The Planning Phase (/plan)
- `app/`: Main application package.
- `models.py`: Database models (GiftIdea, Vote, Comment).
- `routes.py`: Route handlers and logic.
- `forms.py`: WTForms definitions.
- `templates/`: HTML templates.
- `static/`: CSS, Images, and JS.
- `run.py`: Application entry point.
- `requirements.txt`: Project dependencies.

**Goal:** Break the Technical Spec down into an atomic Implementation Plan.
## License

1. **Command:**
```
/plan "Name of your Tech spec doc"
```
*(Use the exact name of the doc generated in Step 1)*

2. **What Happens:**
* The agent reads the Tech Spec.
* It creates a local file `IMPLEMENTATION_PLAN.md`.
* It breaks the project into phases (e.g., Setup, Backend, Frontend, Polish).
* It defines the Git strategy.

---

## Step 3: The Build Phase (/build)

**Goal:** Execute the plan and write the code.

1. **Command:**
```
/build IMPLEMENTATION_PLAN.md "Name of your Tech spec doc"
```

2. **What Happens (Iterative):**
* **Execution:** The agent iterates through the plan, initializing the project structure and writing the application code.
* **Visuals:** It generates necessary visual assets (images, icons) as defined in the spec.
* **Progress:** It updates `IMPLEMENTATION_PLAN.md` as tasks are completed.

---

## Step 4: Final Delivery

**Goal:** Push the code and open a Pull Request.

1. **Action:**
The `/build` command's final phase usually covers this, or you can manually instruct the agent to finalize the project.

2. **What Happens:**
* The agent runs final checks (linting/formatting).
* It creates a `README.md` for the new application.
* It commits all changes.
* It pushes the feature branch to GitHub.
* It uses the GitHub extension to **Open a Pull Request**.

---

## Summary of Commands

| Step | Command | Input | Output |
| :--- | :--- | :--- | :--- |
| **1. Spec** | `/techspec` | Functional Doc (Drive) | Tech Spec (Drive) |
| **2. Plan** | `/plan` | Tech Spec (Drive) | `IMPLEMENTATION_PLAN.md` |
| **3. Build** | `/build` | Plan + Tech Spec | Code, Assets, App |
MIT License.
Binary file added __pycache__/run.cpython-313.pyc
Binary file not shown.
10 changes: 10 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'

db = SQLAlchemy(app)

from app import routes
Binary file added app/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added app/__pycache__/forms.cpython-313.pyc
Binary file not shown.
Binary file added app/__pycache__/models.cpython-313.pyc
Binary file not shown.
Binary file added app/__pycache__/routes.cpython-313.pyc
Binary file not shown.
19 changes: 19 additions & 0 deletions app/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, SubmitField, RadioField
from wtforms.validators import DataRequired, Length
from app.models import CategoryEnum

class GiftForm(FlaskForm):
name = StringField('Gift Name', validators=[DataRequired(), Length(max=100)])
description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)])
category = SelectField('Category', choices=[(c.value, c.value) for c in CategoryEnum], validators=[DataRequired()])
submit = SubmitField('Submit Gift Idea')

class VoteForm(FlaskForm):
score = RadioField('Rating', choices=[('5', '5'), ('4', '4'), ('3', '3'), ('2', '2'), ('1', '1')], validators=[DataRequired()])
submit = SubmitField('Vote')

class CommentForm(FlaskForm):
author_name = StringField('Name (Optional)', validators=[Length(max=100)])
content = TextAreaField('Comment', validators=[DataRequired(), Length(min=10, max=500)])
submit = SubmitField('Post Comment')
41 changes: 41 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import List, Optional
from datetime import datetime
import sqlalchemy as sa
import sqlalchemy.orm as so
from app import db
from enum import Enum

class CategoryEnum(Enum):
FOR_KIDS = "For Kids"
FOR_PARENTS = "For Parents"
STOCKING_STUFFERS = "Stocking Stuffers"
DIY_HOMEMADE = "DIY / Homemade"
TECH_GADGETS = "Tech & Gadgets"
DECORATIONS = "Decorations"

class GiftIdea(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
name: so.Mapped[str] = so.mapped_column(sa.String(100), nullable=False)
description: so.Mapped[str] = so.mapped_column(sa.String(500), nullable=False)
category: so.Mapped[str] = so.mapped_column(sa.String(50), nullable=False)
created_at: so.Mapped[datetime] = so.mapped_column(default=sa.func.now())

votes: so.Mapped[List['Vote']] = so.relationship(back_populates='gift', cascade='all, delete-orphan')
comments: so.Mapped[List['Comment']] = so.relationship(back_populates='gift', cascade='all, delete-orphan')

class Vote(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
gift_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey('gift_idea.id'), nullable=False)
score: so.Mapped[int] = so.mapped_column(nullable=False)
created_at: so.Mapped[datetime] = so.mapped_column(default=sa.func.now())

gift: so.Mapped['GiftIdea'] = so.relationship(back_populates='votes')

class Comment(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
gift_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey('gift_idea.id'), nullable=False)
author_name: so.Mapped[str] = so.mapped_column(sa.String(100), default="Anonymous Elf")
content: so.Mapped[str] = so.mapped_column(sa.String(500), nullable=False)
created_at: so.Mapped[datetime] = so.mapped_column(default=sa.func.now())

gift: so.Mapped['GiftIdea'] = so.relationship(back_populates='comments')
108 changes: 108 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from flask import render_template, url_for, flash, redirect, request, abort
from app import app, db
from app.forms import GiftForm, VoteForm, CommentForm
from app.models import GiftIdea, CategoryEnum, Vote, Comment
import sqlalchemy as sa

@app.route('/')
def home():
category_filter = request.args.get('category')
sort_option = request.args.get('sort', 'new')

query = sa.select(GiftIdea)

if category_filter:
query = query.where(GiftIdea.category == category_filter)

if sort_option == 'popular':
query = query.outerjoin(Vote).group_by(GiftIdea.id).order_by(sa.func.count(Vote.id).desc())
elif sort_option == 'top_rated':
query = query.outerjoin(Vote).group_by(GiftIdea.id).order_by(sa.func.avg(Vote.score).desc(), sa.func.count(Vote.id).desc())
else: # 'new'
query = query.order_by(GiftIdea.created_at.desc())

gifts = db.session.scalars(query).all()

for gift in gifts:
votes = gift.votes
gift.vote_count = len(votes)
if votes:
gift.avg_score = sum(v.score for v in votes) / len(votes)
else:
gift.avg_score = 0.0

return render_template('index.html',
gifts=gifts,
categories=CategoryEnum,
current_category=category_filter,
current_sort=sort_option)

@app.route('/submit', methods=['GET', 'POST'])
def submit_gift():
form = GiftForm()
if form.validate_on_submit():
gift = GiftIdea(name=form.name.data, description=form.description.data, category=form.category.data)
db.session.add(gift)
db.session.commit()
flash('Your gift idea has been submitted!', 'success')
return redirect(url_for('home'))
return render_template('submit_gift.html', title='Submit Gift', form=form)

@app.route('/gift/<int:id>')
def gift_details(id):
gift = db.session.get(GiftIdea, id)
if not gift:
abort(404)

vote_form = VoteForm()
comment_form = CommentForm()

votes = gift.votes
vote_count = len(votes)
avg_score = sum(v.score for v in votes) / vote_count if vote_count > 0 else 0.0

comments = sorted(gift.comments, key=lambda c: c.created_at, reverse=False) # Chronological order

return render_template('gift_details.html',
gift=gift,
vote_form=vote_form,
comment_form=comment_form,
avg_score=avg_score,
vote_count=vote_count,
comments=comments)

@app.route('/gift/<int:id>/vote', methods=['POST'])
def cast_vote(id):
gift = db.session.get(GiftIdea, id)
if not gift:
abort(404)

form = VoteForm()
if form.validate_on_submit():
vote = Vote(gift_id=gift.id, score=int(form.score.data))
db.session.add(vote)
db.session.commit()
flash('Vote cast successfully!', 'success')
else:
flash('Error casting vote.', 'danger')

return redirect(url_for('gift_details', id=id))

@app.route('/gift/<int:id>/comment', methods=['POST'])
def post_comment(id):
gift = db.session.get(GiftIdea, id)
if not gift:
abort(404)

form = CommentForm()
if form.validate_on_submit():
author = form.author_name.data if form.author_name.data else "Anonymous Elf"
comment = Comment(gift_id=gift.id, author_name=author, content=form.content.data)
db.session.add(comment)
db.session.commit()
flash('Comment posted!', 'success')
else:
for error in form.content.errors:
flash(f'Comment error: {error}', 'danger')

return redirect(url_for('gift_details', id=id))
Loading