diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..a94e4d3c --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,76 @@ +# 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`. + +## Phase 1: Project Initialization & Structure +- [x] Create a virtual environment and activate it. +- [x] Create a `requirements.txt` file with dependencies: `Flask`, `Flask-SQLAlchemy`, `Flask-WTF`, `email_validator`. +- [x] Install dependencies. +- [x] Create the project directory structure: + ``` + north_pole_wishlist/ + ├── static/ + │ ├── css/ + │ └── images/ + ├── templates/ + ├── models.py + ├── forms.py + ├── routes.py + ├── config.py + └── app.py + ``` +- [x] Create `config.py` with basic Flask configuration and SQLite database URI. +- [x] Create a basic `app.py` to verify the setup and serve a "Hello World" route. + +## Phase 2: Database Models (SQLAlchemy 2.0) +- [x] Implement the `Gift` model in `models.py` using `so.Mapped` and `so.mapped_column`. +- [x] Implement the `Vote` model in `models.py` with a relationship to `Gift`. +- [x] Implement the `Comment` model in `models.py` with a relationship to `Gift`. +- [x] Configure the database extension in `app.py` and import models. +- [x] Create a script or use `flask shell` to initialize the SQLite database tables. + +## Phase 3: Forms & Backend Logic +- [x] Create `forms.py` using WTForms. + - [x] `GiftForm`: title (max 100), description (max 500), category (SelectField). + - [x] `VoteForm`: score (integer 1-5). + - [x] `CommentForm`: author_name (optional), content (max 500). +- [x] Create `routes.py` and register a blueprint or attach to app. +- [x] Implement the `GET /gift/new` and `POST /gift/new` logic to add gifts to the database. +- [x] Implement `POST /gift//vote` logic. +- [x] Implement `POST /gift//comment` logic. + +## Phase 4: Views & Templates (Basic) +- [x] Create `templates/base.html` with Bootstrap 5 CDN links and a navigation bar. +- [x] Create `templates/index.html` to display a simple list of gifts (fetching all gifts for now). +- [x] Create `templates/create_gift.html` to render the `GiftForm`. +- [x] Create `templates/gift_detail.html` to show gift details, comments, and the voting form. +- [x] Update `routes.py` to render these templates instead of returning strings. + +## Phase 5: Theming & Visuals +- [x] Create `static/css/style.css` and define the Christmas color palette: + - Red: `#D42426`, Green: `#165B33`, White: `#F8F9FA`, Gold: `#FFD700`. +- [x] Update `base.html` to include `style.css` and Google Fonts (*Mountains of Christmas*, *Merryweather*). +- [x] Generate the "Santa Claus flying on his sleigh" hero image using Nano Banana and save to `static/images/hero.png`. +- [x] Update `index.html` to include the Hero image section. +- [x] Style the "Snowflake" rating system in `gift_detail.html` (using icons). +- [x] Create a custom 404 page `templates/404.html` with the "Lost in the Snow" theme. + +## Phase 6: Advanced Logic (Sorting & Filtering) +- [x] Update `routes.py` for the Home route (`/`) to accept `category` and `sort_by` query parameters. +- [x] Implement SQLAlchemy 2.0 queries for filtering by category. +- [x] Implement sorting logic: + - [x] Recency (default): `gift.created_at` desc. + - [x] Top Rated: Average vote score. + - [x] Most Popular: Total vote count. +- [x] Update `index.html` to include UI controls for filtering (dropdown/links) and sorting. + +## Phase 7: Completion & Version Control +- [ ] Verify application functionality (Create gift, Vote, Comment, Filter, Sort). +- [ ] Run a final lint/formatting check. +- [ ] Create a `README.md` file explaining the architecture, how to run the app, and the features. +- [ ] 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. +- [ ] Open a pull request for the feature branch. diff --git a/north_pole_wishlist/README.md b/north_pole_wishlist/README.md new file mode 100644 index 00000000..7ce75737 --- /dev/null +++ b/north_pole_wishlist/README.md @@ -0,0 +1,69 @@ +# North Pole Wishlist + +North Pole Wishlist is a community-driven web application designed to help users discover, share, and curate the best holiday gift ideas. + +## Features + +- **Share Gift Ideas**: Users can submit their own gift suggestions with a title, description, and category. +- **Voting System**: "Naughty or Nice" voting allows users to rate gifts from 1 to 5 snowflakes. +- **Comments**: Community members can leave comments and reviews on gift ideas. +- **Filtering & Sorting**: Discover gifts by category (e.g., For Kids, Tech, Decorations) and sort by Newest, Top Rated, or Most Popular. +- **Festive Theme**: A fully custom Christmas-themed UI built with Bootstrap 5 and custom CSS. + +## Architecture + +The application follows the MVC pattern and is built with: +- **Backend**: Python, Flask, SQLAlchemy 2.0 (ORM) +- **Frontend**: Jinja2 Templates, Bootstrap 5, Custom CSS +- **Database**: SQLite (default) or PostgreSQL + +## Project Structure + +``` +north_pole_wishlist/ +├── app.py # Application entry point +├── config.py # Configuration settings +├── forms.py # WTForms definitions +├── models.py # SQLAlchemy database models +├── routes.py # Route definitions and views +├── static/ # CSS, Images, JS +└── templates/ # HTML Templates +``` + +## How to Run Locally + +1. **Clone the repository**: + ```bash + git clone + cd north-pole-wishlist + ``` + +2. **Create and activate a virtual environment**: + ```bash + python3 -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` + +3. **Install dependencies**: + ```bash + pip install -r requirements.txt + ``` + +4. **Initialize the Database**: + ```bash + python3 -c "from app import app, db; app.app_context().push(); db.create_all()" + ``` + +5. **Run the application**: + ```bash + python3 app.py + ``` + +6. **Open in Browser**: + Visit `http://127.0.0.1:5000` + +## Credits + +- Hero Image generated by Nano Banana. +- Fonts from Google Fonts (Mountains of Christmas, Merryweather). +- Icons by Bootstrap Icons. diff --git a/north_pole_wishlist/__pycache__/app.cpython-313.pyc b/north_pole_wishlist/__pycache__/app.cpython-313.pyc new file mode 100644 index 00000000..13961683 Binary files /dev/null and b/north_pole_wishlist/__pycache__/app.cpython-313.pyc differ diff --git a/north_pole_wishlist/__pycache__/config.cpython-313.pyc b/north_pole_wishlist/__pycache__/config.cpython-313.pyc new file mode 100644 index 00000000..bc0c17cf Binary files /dev/null and b/north_pole_wishlist/__pycache__/config.cpython-313.pyc differ diff --git a/north_pole_wishlist/__pycache__/forms.cpython-313.pyc b/north_pole_wishlist/__pycache__/forms.cpython-313.pyc new file mode 100644 index 00000000..77cc7e47 Binary files /dev/null and b/north_pole_wishlist/__pycache__/forms.cpython-313.pyc differ diff --git a/north_pole_wishlist/__pycache__/models.cpython-313.pyc b/north_pole_wishlist/__pycache__/models.cpython-313.pyc new file mode 100644 index 00000000..f9013fda Binary files /dev/null and b/north_pole_wishlist/__pycache__/models.cpython-313.pyc differ diff --git a/north_pole_wishlist/__pycache__/routes.cpython-313.pyc b/north_pole_wishlist/__pycache__/routes.cpython-313.pyc new file mode 100644 index 00000000..5729d4c4 Binary files /dev/null and b/north_pole_wishlist/__pycache__/routes.cpython-313.pyc differ diff --git a/north_pole_wishlist/app.py b/north_pole_wishlist/app.py new file mode 100644 index 00000000..9a609fef --- /dev/null +++ b/north_pole_wishlist/app.py @@ -0,0 +1,17 @@ +from flask import Flask, render_template +from config import Config +from models import db +from routes import routes + +app = Flask(__name__) +app.config.from_object(Config) + +db.init_app(app) +app.register_blueprint(routes) + +@app.errorhandler(404) +def page_not_found(e): + return render_template('404.html'), 404 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/north_pole_wishlist/config.py b/north_pole_wishlist/config.py new file mode 100644 index 00000000..6523bf41 --- /dev/null +++ b/north_pole_wishlist/config.py @@ -0,0 +1,6 @@ +import os + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess-santa-secret' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/north_pole_wishlist/forms.py b/north_pole_wishlist/forms.py new file mode 100644 index 00000000..53ea18f6 --- /dev/null +++ b/north_pole_wishlist/forms.py @@ -0,0 +1,25 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SelectField, IntegerField, SubmitField +from wtforms.validators import DataRequired, Length, NumberRange, Optional + +class GiftForm(FlaskForm): + title = StringField('Gift Name', validators=[DataRequired(), Length(max=100)]) + description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)]) + category = SelectField('Category', choices=[ + ('For Kids', 'For Kids'), + ('For Parents', 'For Parents'), + ('Stocking Stuffers', 'Stocking Stuffers'), + ('DIY / Homemade', 'DIY / Homemade'), + ('Tech & Gadgets', 'Tech & Gadgets'), + ('Decorations', 'Decorations') + ], validators=[DataRequired()]) + submit = SubmitField('Submit Gift') + +class VoteForm(FlaskForm): + score = IntegerField('Score (1-5)', validators=[DataRequired(), NumberRange(min=1, max=5)]) + submit = SubmitField('Vote') + +class CommentForm(FlaskForm): + author_name = StringField('Your Name (Optional)', validators=[Optional(), Length(max=100)]) + content = TextAreaField('Comment', validators=[DataRequired(), Length(max=500)]) + submit = SubmitField('Post Comment') diff --git a/north_pole_wishlist/instance/app.db b/north_pole_wishlist/instance/app.db new file mode 100644 index 00000000..eba78828 Binary files /dev/null and b/north_pole_wishlist/instance/app.db differ diff --git a/north_pole_wishlist/models.py b/north_pole_wishlist/models.py new file mode 100644 index 00000000..d365263c --- /dev/null +++ b/north_pole_wishlist/models.py @@ -0,0 +1,33 @@ +from datetime import datetime +from typing import List +import sqlalchemy as sa +import sqlalchemy.orm as so +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class Gift(db.Model): + id: so.Mapped[int] = so.mapped_column(primary_key=True) + title: 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=datetime.utcnow) + + 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.id"), nullable=False) + score: so.Mapped[int] = so.mapped_column(nullable=False) # 1-5 + + gift: so.Mapped["Gift"] = 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.id"), nullable=False) + author_name: so.Mapped[str] = so.mapped_column(sa.String(100), nullable=True, default="Secret Santa") + content: so.Mapped[str] = so.mapped_column(sa.String(500), nullable=False) + created_at: so.Mapped[datetime] = so.mapped_column(default=datetime.utcnow) + + gift: so.Mapped["Gift"] = so.relationship(back_populates="comments") diff --git a/north_pole_wishlist/requirements.txt b/north_pole_wishlist/requirements.txt new file mode 100644 index 00000000..43922dc6 --- /dev/null +++ b/north_pole_wishlist/requirements.txt @@ -0,0 +1,4 @@ +Flask +Flask-SQLAlchemy +Flask-WTF +email_validator diff --git a/north_pole_wishlist/routes.py b/north_pole_wishlist/routes.py new file mode 100644 index 00000000..3c509be0 --- /dev/null +++ b/north_pole_wishlist/routes.py @@ -0,0 +1,88 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +import sqlalchemy as sa +from models import db, Gift, Vote, Comment +from forms import GiftForm, VoteForm, CommentForm + +routes = Blueprint('routes', __name__) + +@routes.route('/', methods=['GET']) +def index(): + category = request.args.get('category') + sort_by = request.args.get('sort_by', 'recency') + + stmt = sa.select(Gift) + + if category: + stmt = stmt.where(Gift.category == category) + + if sort_by == 'top_rated': + # Subquery to calculate average score + subquery = sa.select( + Vote.gift_id, + sa.func.avg(Vote.score).label('avg_score') + ).group_by(Vote.gift_id).subquery() + + # Join with subquery and order by avg_score + stmt = stmt.outerjoin(subquery, Gift.id == subquery.c.gift_id).order_by(subquery.c.avg_score.desc().nullslast()) + + elif sort_by == 'most_popular': + # Subquery to count votes + subquery = sa.select( + Vote.gift_id, + sa.func.count(Vote.id).label('vote_count') + ).group_by(Vote.gift_id).subquery() + + # Join with subquery and order by vote_count + stmt = stmt.outerjoin(subquery, Gift.id == subquery.c.gift_id).order_by(subquery.c.vote_count.desc().nullslast()) + else: + # Default: Recency + stmt = stmt.order_by(Gift.created_at.desc()) + + gifts = db.session.scalars(stmt).all() + return render_template('index.html', gifts=gifts, current_category=category, current_sort=sort_by) + +@routes.route('/gift/new', methods=['GET', 'POST']) +def new_gift(): + form = GiftForm() + if form.validate_on_submit(): + gift = Gift( + title=form.title.data, + description=form.description.data, + category=form.category.data + ) + db.session.add(gift) + db.session.commit() + return redirect(url_for('routes.index')) + return render_template('create_gift.html', form=form) + +@routes.route('/gift/', methods=['GET']) +def gift_detail(gift_id): + gift = db.session.get(Gift, gift_id) + if not gift: + return "Gift not found", 404 + vote_form = VoteForm() + comment_form = CommentForm() + return render_template('gift_detail.html', gift=gift, vote_form=vote_form, comment_form=comment_form) + +@routes.route('/gift//vote', methods=['POST']) +def vote_gift(gift_id): + form = VoteForm() + if form.validate_on_submit(): + vote = Vote(gift_id=gift_id, score=form.score.data) + db.session.add(vote) + db.session.commit() + return redirect(url_for('routes.gift_detail', gift_id=gift_id)) + +@routes.route('/gift//comment', methods=['POST']) +def comment_gift(gift_id): + form = CommentForm() + if form.validate_on_submit(): + author = form.author_name.data if form.author_name.data else "Secret Santa" + comment = Comment( + gift_id=gift_id, + author_name=author, + content=form.content.data + ) + db.session.add(comment) + db.session.commit() + return redirect(url_for('routes.gift_detail', gift_id=gift_id)) diff --git a/north_pole_wishlist/static/css/style.css b/north_pole_wishlist/static/css/style.css new file mode 100644 index 00000000..64d94b97 --- /dev/null +++ b/north_pole_wishlist/static/css/style.css @@ -0,0 +1,47 @@ +:root { + --christmas-red: #D42426; + --christmas-green: #165B33; + --christmas-white: #F8F9FA; + --christmas-gold: #FFD700; +} + +body { + background-color: var(--christmas-white); + font-family: 'Merryweather', serif; +} + +h1, h2, h3, h4, h5, h6, .navbar-brand { + font-family: 'Mountains of Christmas', cursive; + color: var(--christmas-red); +} + +.navbar { + background-color: var(--christmas-green) !important; +} + +.navbar-brand, .nav-link { + color: var(--christmas-white) !important; +} + +.btn-primary { + background-color: var(--christmas-red); + border-color: var(--christmas-red); +} + +.btn-primary:hover { + background-color: #a81c1e; + border-color: #a81c1e; +} + +.card { + border: 1px solid var(--christmas-gold); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.card-title { + color: var(--christmas-green); +} + +.snowflake-icon { + color: #a0d3e8; /* Light blue for snowflakes */ +} diff --git a/north_pole_wishlist/static/images/hero.png b/north_pole_wishlist/static/images/hero.png new file mode 100644 index 00000000..77243d32 Binary files /dev/null and b/north_pole_wishlist/static/images/hero.png differ diff --git a/north_pole_wishlist/templates/404.html b/north_pole_wishlist/templates/404.html new file mode 100644 index 00000000..99c97f6e --- /dev/null +++ b/north_pole_wishlist/templates/404.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

404

+

Lost in the Snow?

+

It seems you've wandered off the path to Santa's Workshop.

+

The page you are looking for might have been buried under a snowdrift.

+ Return Home +
+
+{% endblock %} diff --git a/north_pole_wishlist/templates/base.html b/north_pole_wishlist/templates/base.html new file mode 100644 index 00000000..4456e7da --- /dev/null +++ b/north_pole_wishlist/templates/base.html @@ -0,0 +1,52 @@ + + + + + + {% block title %}North Pole Wishlist{% endblock %} + + + + + + + {% block styles %}{% endblock %} + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + + + diff --git a/north_pole_wishlist/templates/create_gift.html b/north_pole_wishlist/templates/create_gift.html new file mode 100644 index 00000000..ca1159f3 --- /dev/null +++ b/north_pole_wishlist/templates/create_gift.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+

Submit a Gift Idea

+
+
+
+ {{ form.hidden_tag() }} +
+ {{ form.title.label(class="form-label") }} + {{ form.title(class="form-control") }} + {% for error in form.title.errors %} +
{{ error }}
+ {% endfor %} +
+
+ {{ form.category.label(class="form-label") }} + {{ form.category(class="form-select") }} + {% for error in form.category.errors %} +
{{ error }}
+ {% endfor %} +
+
+ {{ form.description.label(class="form-label") }} + {{ form.description(class="form-control", rows="5") }} + {% for error in form.description.errors %} +
{{ error }}
+ {% endfor %} +
+
+ {{ form.submit(class="btn btn-primary") }} +
+
+
+
+
+
+{% endblock %} diff --git a/north_pole_wishlist/templates/gift_detail.html b/north_pole_wishlist/templates/gift_detail.html new file mode 100644 index 00000000..26d8e848 --- /dev/null +++ b/north_pole_wishlist/templates/gift_detail.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+

{{ gift.title }}

+
{{ gift.category }}
+

{{ gift.description }}

+

Submitted on {{ gift.created_at.strftime('%Y-%m-%d') }}

+
+ +
+ +

Comments

+ {% for comment in gift.comments %} +
+
+
{{ comment.author_name }} says:
+

{{ comment.content }}

+

{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}

+
+
+ {% else %} +

No comments yet.

+ {% endfor %} + +
+
Add a Comment
+
+
+ {{ comment_form.hidden_tag() }} +
+ {{ comment_form.author_name.label(class="form-label") }} + {{ comment_form.author_name(class="form-control") }} +
+
+ {{ comment_form.content.label(class="form-label") }} + {{ comment_form.content(class="form-control", rows="3") }} +
+ {{ comment_form.submit(class="btn btn-primary") }} +
+
+
+
+ +
+
+
+

Current Rating

+

+ {% if gift.votes %} + {{ (gift.votes|map(attribute='score')|sum / gift.votes|length)|round(1) }} + {% else %} + - + {% endif %} +

+

Snowflakes

+

({{ gift.votes|length }} votes)

+
+
+
+
+{% endblock %} diff --git a/north_pole_wishlist/templates/index.html b/north_pole_wishlist/templates/index.html new file mode 100644 index 00000000..d4978761 --- /dev/null +++ b/north_pole_wishlist/templates/index.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+ Santa Claus flying on his sleigh +

North Pole Wishlist

+

Discover and share the best holiday gift ideas!

+ Submit a Gift Idea +
+
+ +
+
+
+ + + {% if current_category or current_sort != 'recency' %} + Clear + {% endif %} +
+
+
+ +
+ {% for gift in gifts %} +
+
+
+
{{ gift.title }}
+
{{ gift.category }}
+

{{ gift.description|truncate(100) }}

+
+ +
+
+ {% else %} +
+

No gifts submitted yet. Be the first!

+
+ {% endfor %} +
+{% endblock %}