diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..fe7de8e4 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,78 @@ +# 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 +- [x] Set up a Python virtual environment (`python3 -m venv venv`) and activate it. +- [x] Create a `requirements.txt` file including `Flask`, `Flask-SQLAlchemy`, and any other necessary libraries. +- [x] Install the dependencies (`pip install -r requirements.txt`). +- [x] Create the basic project structure: + - `app.py` (Main application file) + - `models.py` (Database models) + - `templates/` (HTML templates) + - `static/css/` (Stylesheets) + - `static/img/` (Images) +- [x] Create a minimal `app.py` to verify the Flask setup works (Hello World). + +## Phase 2: Database & Models +- [x] Configure SQLite database connection in `app.py`. +- [x] In `models.py`, define the `Gift` model using SQLAlchemy 2.0 style (Mapped, mapped_column). + - Fields: `id`, `title`, `description`, `category`, `created_at`. +- [x] In `models.py`, define the `Vote` model. + - Fields: `id`, `gift_id`, `score`, `created_at`. +- [x] In `models.py`, define the `Comment` model. + - Fields: `id`, `gift_id`, `author_name`, `content`, `created_at`. +- [x] Create a database initialization script or function to create tables (`db.create_all()`). + +## Phase 3: Core Backend Logic & Routes +- [x] Implement the Home route (`GET /`): + - Query all gifts from the database. + - Implement filtering by category (via query parameter). + - Implement sorting (Ranking: avg score, Popularity: vote count, Recency: created_at). +- [x] Implement the Submit Gift route (`GET /gifts/new` & `POST /gifts/new`): + - GET: Render the submission form. + - POST: Validate input (lengths, required fields). Create new `Gift` record. +- [x] Implement the Gift Detail route (`GET /gifts/`): + - Query specific gift by ID. + - Calculate average rating and fetch related comments. +- [x] Implement the Voting route (`POST /gifts//vote`): + - Validate score (1-5). Create `Vote` record. Redirect or return updated score. +- [x] Implement the Commenting route (`POST /gifts//comment`): + - Validate content length. Default author name if empty. Create `Comment` record. + +## Phase 4: Frontend Implementation +- [x] Create `templates/base.html`: + - Include Bootstrap 5 via CDN. + - Link custom CSS (`static/css/style.css`). + - Define the layout (Navigation, Content Block, Footer). +- [x] Create `static/css/style.css`: + - Implement the "Christmas Aesthetic" (Deep Red #8B0000, Forest Green #228B22). + - Style fonts and general layout. +- [x] Create `templates/index.html` (Home): + - Extend `base.html`. + - Hero section with Santa image placeholder. + - Grid of Gift Cards showing Title, Category, Rating. +- [x] Create `templates/submit_gift.html`: + - Form with fields: Title, Category (Select), Description. +- [x] Create `templates/gift_detail.html`: + - Detailed view of the gift. + - Section for Voting (Snowflake icons). + - Section for Comments list and "Add Comment" form. + +## Phase 5: Refinement & Assets +- [x] Add the "Santa flying on sleigh" hero image to `static/img/` (or use a placeholder/generated one). +- [x] Add the "Snowflake" icon for ratings (SVG or image). +- [x] Ensure all forms have proper HTML5 validation attributes (`required`, `maxlength`). +- [x] Test all routes and flows manually to ensure stability. + +## Phase 6: Completion & Version Control +- [ ] Verify application functionality (Submit a gift, vote on it, comment on it, check filters). +- [ ] Create a `README.md` file: + - Explain features (Gift submission, Voting, Comments). + - Instructions to run locally (`python app.py`). + - Tech stack details. +- [ ] 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8d6e89ab --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# North Pole Wishlist + +The **North Pole Wishlist** is a community-driven platform designed to help users discover, share, and curate the best gift ideas for the holiday season. It features a festive Christmas theme and allows elves (users) to submit ideas, vote on them ("Naughty or Nice"), and discuss the best presents. + +## Features + +- **Browse Gifts**: View a curated list of gift ideas, filterable by category (Kids, Parents, Tech, etc.) and sortable by popularity or rating. +- **Submit Ideas**: Share your own gift suggestions with the community. +- **Naughty or Nice Voting**: Rate gifts on a scale of 1 to 5 Snowflakes. +- **Elf Discussion**: Leave comments on gift ideas to share reviews or ask questions. +- **Festive UI**: A fully themed interface with Santa, snowflakes, and holiday colors. + +## Tech Stack + +- **Backend**: Python 3, Flask +- **Database**: SQLite (Development), SQLAlchemy 2.0 ORM +- **Frontend**: HTML5, Jinja2 Templates, Bootstrap 5, Custom CSS +- **Icons**: FontAwesome + +## Local Development Setup + +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. **Run the application**: + ```bash + python app.py + ``` + +5. **Open in Browser**: + Visit `http://127.0.0.1:5000` to start exploring the wishlist! + +## Project Structure + +- `app.py`: Main Flask application entry point and route definitions. +- `models.py`: SQLAlchemy database models (Gift, Vote, Comment). +- `templates/`: Jinja2 HTML templates. +- `static/`: CSS styles and images. +- `instance/`: Contains the SQLite database (`northpole.db`). + +## License + +Copyright 2025 North Pole Workshop. Made with ❤️ and ❄️. diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 00000000..9d47e443 Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc new file mode 100644 index 00000000..7395128e Binary files /dev/null and b/__pycache__/models.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 00000000..42294c83 --- /dev/null +++ b/app.py @@ -0,0 +1,129 @@ +import os +from flask import Flask, render_template, request, redirect, url_for, abort +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy import select, func, desc + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +def create_app(): + app = Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///northpole.db') + app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key') + + db.init_app(app) + + with app.app_context(): + import models + db.create_all() + + from models import Gift, Vote, Comment + + @app.route('/') + def index(): + category = request.args.get('category') + sort_by = request.args.get('sort', 'newest') + + stmt = select(Gift) + + if category: + stmt = stmt.where(Gift.category == category) + + if sort_by == 'popular': + # Sort by vote count + stmt = stmt.outerjoin(Gift.votes).group_by(Gift.id).order_by(func.count(Vote.id).desc()) + elif sort_by == 'ranking': + # Sort by average score + stmt = stmt.outerjoin(Gift.votes).group_by(Gift.id).order_by(func.avg(Vote.score).desc()) + else: # newest + stmt = stmt.order_by(Gift.created_at.desc()) + + gifts = db.session.scalars(stmt).all() + + # Calculate average scores for display (could be optimized) + gift_stats = {} + for gift in gifts: + avg_score = db.session.scalar(select(func.avg(Vote.score)).where(Vote.gift_id == gift.id)) + vote_count = db.session.scalar(select(func.count(Vote.id)).where(Vote.gift_id == gift.id)) + gift_stats[gift.id] = { + 'avg_score': round(avg_score, 1) if avg_score else 0, + 'vote_count': vote_count + } + + return render_template('index.html', gifts=gifts, stats=gift_stats, current_category=category, current_sort=sort_by) + + @app.route('/gifts/new', methods=['GET', 'POST']) + def submit_gift(): + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + category = request.form.get('category') + + if not title or len(title) > 100: + # Basic validation, ideally flash error + return "Invalid Title", 400 + if not description or len(description) > 500: + return "Invalid Description", 400 + if not category: + return "Category required", 400 + + new_gift = Gift(title=title, description=description, category=category) + db.session.add(new_gift) + db.session.commit() + + return redirect(url_for('index')) + + return render_template('submit_gift.html') + + @app.route('/gifts/') + def gift_detail(gift_id): + gift = db.get_or_404(Gift, gift_id) + + avg_score = db.session.scalar(select(func.avg(Vote.score)).where(Vote.gift_id == gift_id)) + vote_count = db.session.scalar(select(func.count(Vote.id)).where(Vote.gift_id == gift_id)) + + # Get comments ordered by newest + comments = db.session.scalars(select(Comment).where(Comment.gift_id == gift_id).order_by(Comment.created_at.desc())).all() + + stats = { + 'avg_score': round(avg_score, 1) if avg_score else 0, + 'vote_count': vote_count + } + + return render_template('gift_detail.html', gift=gift, stats=stats, comments=comments) + + @app.route('/gifts//vote', methods=['POST']) + def vote_gift(gift_id): + score = request.form.get('score', type=int) + if not score or score < 1 or score > 5: + return "Invalid Score", 400 + + vote = Vote(gift_id=gift_id, score=score) + db.session.add(vote) + db.session.commit() + + return redirect(url_for('gift_detail', gift_id=gift_id)) + + @app.route('/gifts//comment', methods=['POST']) + def comment_gift(gift_id): + author = request.form.get('author_name') or "Secret Santa" + content = request.form.get('content') + + if not content or len(content) < 10 or len(content) > 500: + return "Content length must be between 10 and 500 characters", 400 + + comment = Comment(gift_id=gift_id, author_name=author, content=content) + db.session.add(comment) + db.session.commit() + + return redirect(url_for('gift_detail', gift_id=gift_id)) + + return app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/instance/northpole.db b/instance/northpole.db new file mode 100644 index 00000000..0579424b Binary files /dev/null and b/instance/northpole.db differ diff --git a/models.py b/models.py new file mode 100644 index 00000000..48c93a20 --- /dev/null +++ b/models.py @@ -0,0 +1,37 @@ +from datetime import datetime +from sqlalchemy import String, Integer, ForeignKey, DateTime +from sqlalchemy.orm import Mapped, mapped_column, relationship +from app import db + +class Gift(db.Model): + __tablename__ = 'gift_idea' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str] = mapped_column(String(500), nullable=False) + category: Mapped[str] = mapped_column(String(50), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + votes: Mapped[list["Vote"]] = relationship(back_populates="gift") + comments: Mapped[list["Comment"]] = relationship(back_populates="gift") + +class Vote(db.Model): + __tablename__ = 'vote' + + id: Mapped[int] = mapped_column(primary_key=True) + gift_id: Mapped[int] = mapped_column(ForeignKey('gift_idea.id'), nullable=False) + score: Mapped[int] = mapped_column(Integer, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + gift: Mapped["Gift"] = relationship(back_populates="votes") + +class Comment(db.Model): + __tablename__ = 'comment' + + id: Mapped[int] = mapped_column(primary_key=True) + gift_id: Mapped[int] = mapped_column(ForeignKey('gift_idea.id'), nullable=False) + author_name: Mapped[str] = mapped_column(String(50), nullable=False, default="Secret Santa") + content: Mapped[str] = mapped_column(String(500), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + gift: Mapped["Gift"] = relationship(back_populates="comments") diff --git a/nanobanana-output/a_magical_highquality_banner_ill.png b/nanobanana-output/a_magical_highquality_banner_ill.png new file mode 100644 index 00000000..9409c7ee Binary files /dev/null and b/nanobanana-output/a_magical_highquality_banner_ill.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c23f1fd8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask +Flask-SQLAlchemy +SQLAlchemy diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 00000000..fe5ff511 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,88 @@ +:root { + --christmas-red: #8B0000; + --christmas-green: #228B22; + --snow-white: #F8F9FA; + --gold: #FFD700; +} + +body { + background-color: var(--snow-white); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.bg-christmas-red { + background-color: var(--christmas-red) !important; +} + +.bg-christmas-green { + background-color: var(--christmas-green) !important; +} + +.text-christmas-red { + color: var(--christmas-red) !important; +} + +.text-christmas-green { + color: var(--christmas-green) !important; +} + +.btn-christmas-red { + background-color: var(--christmas-red); + border-color: var(--christmas-red); +} + +.btn-christmas-red:hover { + background-color: #660000; + border-color: #660000; + color: white; +} + +.font-christmas { + font-family: 'Mountains of Christmas', cursive; + font-weight: 700; +} + +.card { + border: none; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + transition: transform 0.2s; +} + +.card:hover { + transform: translateY(-5px); +} + +.snowflake-rating { + color: var(--gold); +} + +.hero-section { + background-color: #e9ecef; + padding: 2rem; + border-radius: 0.5rem; + margin-bottom: 2rem; + background-size: cover; + background-position: center; + position: relative; + color: white; + text-shadow: 2px 2px 4px rgba(0,0,0,0.7); +} + +/* Fallback if image not loaded, though we'll try to use one */ +.hero-overlay { + background-color: rgba(0, 0, 0, 0.4); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 0.5rem; + z-index: 1; +} + +.hero-content { + position: relative; + z-index: 2; +} diff --git a/static/img/santa_hero.png b/static/img/santa_hero.png new file mode 100644 index 00000000..9409c7ee Binary files /dev/null and b/static/img/santa_hero.png differ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..a92f856d --- /dev/null +++ b/templates/base.html @@ -0,0 +1,47 @@ + + + + + + {% block title %}North Pole Wishlist{% endblock %} + + + + + + + + + + + +
+ {% block content %}{% endblock %} +
+ +
+
+

© 2025 North Pole Workshop. Made with by Elves.

+
+
+ + + + diff --git a/templates/gift_detail.html b/templates/gift_detail.html new file mode 100644 index 00000000..c9481efd --- /dev/null +++ b/templates/gift_detail.html @@ -0,0 +1,103 @@ +{% extends 'base.html' %} + +{% block title %}{{ gift.title }} - North Pole Wishlist{% endblock %} + +{% block content %} +
+
+ + +
+
+
+

{{ gift.title }}

+ {{ gift.category }} +
+ +

{{ gift.description }}

+ +
+ +
+
+
{{ stats.avg_score }}
+ Average Score +
+
+
{{ stats.vote_count }}
+ Total Votes +
+
+
+
+ +
+
+

Elf Discussion

+
+
+ {% if comments %} + {% for comment in comments %} +
+
+ {{ comment.author_name }} + {{ comment.created_at.strftime('%Y-%m-%d %H:%M') }} +
+

{{ comment.content }}

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

No comments yet. Be the first to say something!

+ {% endif %} + +

Leave a Comment

+
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+
+

Cast Your Vote

+
+
+

Is this gift Naughty or Nice?

+
+
+ + + + + + + + + + + + + + +
+ +
+
+
+
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..30feb28b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,66 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+

The North Pole Wishlist

+

Discover, share, and rate the best holiday gifts from around the world!

+ Submit a Gift Idea +
+
+ +
+ +
+ +
+
+ +
+ {% for gift in gifts %} +
+
+
+
+
{{ gift.title }}
+ {{ gift.category }} +
+

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

+
+
+ {{ stats[gift.id]['avg_score'] }} ({{ stats[gift.id]['vote_count'] }} votes) +
+ View Details +
+
+ +
+
+ {% else %} +
+

No gifts found under this chimney yet!

+

Be the first to add one.

+
+ {% endfor %} +
+{% endblock %} diff --git a/templates/submit_gift.html b/templates/submit_gift.html new file mode 100644 index 00000000..f40761fd --- /dev/null +++ b/templates/submit_gift.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} + +{% block title %}Submit Gift Idea - North Pole Wishlist{% endblock %} + +{% block content %} +
+
+
+
+

Submit a New Gift Idea

+
+
+
+
+ + +
Keep it short and sweet (max 100 chars).
+
+ +
+ + +
+ +
+ + +
Max 500 characters.
+
+ +
+ + Cancel +
+
+
+
+
+
+{% endblock %}