diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..5e14aae5 --- /dev/null +++ b/IMPLEMENTATION_PLAN.md @@ -0,0 +1,80 @@ +# 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: Environment & Project Structure +- [x] Set up a Python virtual environment and activate it. +- [x] Install required packages: `Flask`, `Flask-SQLAlchemy`, `Flask-WTF`, `email_validator` (if needed for WTForms). +- [x] Create `requirements.txt` with frozen dependencies. +- [x] Create project structure: + ``` + / + ├── app.py + ├── config.py + ├── models.py + ├── forms.py + ├── static/ + │ ├── css/ + │ │ └── style.css + │ └── images/ + └── templates/ + ├── base.html + ├── index.html + ├── submit.html + └── detail.html + ``` +- [x] Create `config.py` with secret key and database URI configuration. +- [x] Create a basic `app.py` to verify Flask runs. + +## Phase 2: Database & Models +- [x] Configure `Flask-SQLAlchemy` in `app.py` using modern SQLAlchemy 2.0 style. +- [x] In `models.py`, implement the `Gift` model with fields: `id`, `title`, `description`, `category`, `created_at`. +- [x] In `models.py`, implement the `Vote` model with fields: `id`, `gift_id`, `score`. +- [x] In `models.py`, implement the `Comment` model with 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: Forms & Backend Logic +- [x] In `forms.py`, create `GiftForm` using Flask-WTF (Title, Description, Category select). +- [x] In `forms.py`, create `CommentForm` (Author Name, Content). +- [x] In `forms.py`, create `VoteForm` (Score integer/radio). +- [x] In `app.py`, implement the `POST /gift/new` route to handle gift submission and validation. +- [x] In `app.py`, implement the `POST /gift//vote` route to save votes. +- [x] In `app.py`, implement the `POST /gift//comment` route to save comments. +- [x] In `app.py`, implement the `GET /` route with query parameters for sorting (Ranking/Recency) and filtering (Category). Use `sqlalchemy.select` with `func.avg` for ranking logic. + +## Phase 4: Frontend - Base & Home +- [x] Create `templates/base.html` including Bootstrap 5 (via CDN) and a custom stylesheet link. +- [x] In `static/css/style.css`, define the "Christmas" theme variables: + - Primary: `#D42426` (Santa Red) + - Success: `#165B33` (Pine Green) + - Background: `#F8F8FF` (Snow White) +- [x] Generate a "Santa flying on a sleigh" hero image using Nano Banana (or use a placeholder) and save to `static/images/hero.png`. +- [x] Implement `templates/index.html`: + - Display the Hero image. + - Add Filter and Sort controls (dropdowns/links). + - Iterate through gifts and display them as Bootstrap Cards. + - Show average rating and comment count on cards. + +## Phase 5: Frontend - Details & Interactions +- [x] Implement `templates/submit.html`: Render `GiftForm` with festive styling. +- [x] Implement `templates/detail.html`: + - Display full gift details. + - Show list of comments. + - Render `CommentForm` for new comments. + - Render `VoteForm` or a custom interactive star/snowflake rating widget. +- [x] Integrate "Snowflake" icons (Bootstrap Icons `bi-snow2`) for ratings in both the list and detail views. + +## Phase 6: Final Polish +- [x] Add Flash message support in `base.html` for feedback (e.g., "Gift submitted!", "Vote cast!"). +- [x] Ensure all forms have validation error display. +- [x] Verify responsive layout on mobile/desktop. + +## Phase 7: Completion & Version Control +- [ ] Verify application functionality (Submit gift, Vote, Comment, Sort/Filter). +- [ ] 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. +- [ ] Open a pull request for the feature branch using the Gemini CLI github MCP server, leave it open for review, don't merge it. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8488f8d9 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# North Pole Wishlist + +North Pole Wishlist is a festive community platform where users can discover, share, and rate the best holiday gift ideas. It's designed with a "Santa's Workshop" theme to bring holiday cheer to your gift planning. + +## Features + +* **Share Gift Ideas**: Submit new gift suggestions with titles, descriptions, and categories. +* **Discover Gifts**: Browse a feed of community-submitted gifts. +* **Vote & Rate**: Rate gifts using a 1-5 "Snowflake" scale. +* **Discuss**: Comment on gift ideas to share reviews or ask questions. +* **Sort & Filter**: Find the perfect gift by filtering categories (e.g., "For Kids", "Tech") or sorting by popularity/recency. + +## Tech Stack + +* **Backend**: Python 3, Flask +* **Database**: SQLite with SQLAlchemy 2.0 ORM +* **Frontend**: Bootstrap 5, Jinja2 Templates, Custom CSS +* **Forms**: Flask-WTF + +## Installation & 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. **Initialize the database**: + ```bash + python init_db.py + ``` + +5. **Run the application**: + ```bash + python app.py + ``` + The app will be available at `http://127.0.0.1:5000`. + +## Project Structure + +* `app.py`: Main application entry point and route definitions. +* `models.py`: Database models (Gift, Vote, Comment). +* `forms.py`: WTForms definitions for validation. +* `config.py`: Configuration settings. +* `templates/`: HTML templates (Jinja2). +* `static/`: CSS, Images, and JavaScript. + +## License + +Merry Christmas! 🎄 diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 00000000..8aba0e59 Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 00000000..3827a45b Binary files /dev/null and b/__pycache__/config.cpython-313.pyc differ diff --git a/__pycache__/forms.cpython-313.pyc b/__pycache__/forms.cpython-313.pyc new file mode 100644 index 00000000..82a55b68 Binary files /dev/null and b/__pycache__/forms.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc new file mode 100644 index 00000000..cae3fe56 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..1487974f --- /dev/null +++ b/app.py @@ -0,0 +1,144 @@ +from flask import Flask, render_template, redirect, url_for, flash, request +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy import desc, func +import sqlalchemy as sa +from config import Config +from forms import GiftForm, CommentForm, VoteForm + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + + db.init_app(app) + + with app.app_context(): + import models + from models import Gift, Vote, Comment + db.create_all() + + @app.route('/') + def index(): + from models import Gift, Vote + + category_filter = request.args.get('category') + sort_by = request.args.get('sort', 'recency') + + # Base query: Join Gift with Vote to calculate average score + # Using left outer join to include gifts with no votes + stmt = ( + sa.select( + Gift, + func.avg(Vote.score).label('avg_score'), + func.count(Vote.id).label('vote_count') + ) + .outerjoin(Vote) + .group_by(Gift.id) + ) + + if category_filter: + stmt = stmt.where(Gift.category == category_filter) + + if sort_by == 'ranking': + # Sort by average score descending, then by number of votes + stmt = stmt.order_by(desc('avg_score'), desc('vote_count')) + else: + # Default: Recency (Newest first) + stmt = stmt.order_by(desc(Gift.created_at)) + + results = db.session.execute(stmt).all() + + # Determine available categories for filter dropdown + categories = ['For Kids', 'For Parents', 'Stocking Stuffers', 'DIY', 'Tech', 'Decorations'] + + return render_template('index.html', gifts=results, categories=categories, current_filter=category_filter, current_sort=sort_by) + + @app.route('/submit', methods=['GET', 'POST']) + def submit_gift(): + from models import 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() + flash('Gift idea submitted successfully!', 'success') + return redirect(url_for('index')) + return render_template('submit.html', form=form) + + @app.route('/gift/', methods=['GET']) + def gift_detail(id): + from models import Gift, Comment, Vote + + gift = db.session.get(Gift, id) + if not gift: + flash('Gift not found.', 'danger') + return redirect(url_for('index')) + + # Calculate score manually for detail view + avg_score = db.session.scalar( + sa.select(func.avg(Vote.score)).where(Vote.gift_id == id) + ) + vote_count = db.session.scalar( + sa.select(func.count(Vote.id)).where(Vote.gift_id == id) + ) + + comments = db.session.scalars( + sa.select(Comment).where(Comment.gift_id == id).order_by(desc(Comment.created_at)) + ).all() + + vote_form = VoteForm() + comment_form = CommentForm() + + return render_template('detail.html', + gift=gift, + avg_score=avg_score, + vote_count=vote_count, + comments=comments, + vote_form=vote_form, + comment_form=comment_form) + + @app.route('/gift//vote', methods=['POST']) + def vote_gift(id): + from models import Vote + form = VoteForm() + if form.validate_on_submit(): + vote = Vote(gift_id=id, score=int(form.score.data)) + db.session.add(vote) + db.session.commit() + flash('Vote cast successfully!', 'success') + else: + flash('Invalid vote.', 'danger') + return redirect(url_for('gift_detail', id=id)) + + @app.route('/gift//comment', methods=['POST']) + def comment_gift(id): + from models import Comment + form = CommentForm() + if form.validate_on_submit(): + author = form.author_name.data if form.author_name.data else "Secret Santa" + comment = Comment( + gift_id=id, + author_name=author, + content=form.content.data + ) + db.session.add(comment) + db.session.commit() + flash('Comment added!', 'success') + else: + flash('Comment too short or invalid.', 'danger') + return redirect(url_for('gift_detail', id=id)) + + return app + +if __name__ == '__main__': + app = create_app() + app.run(debug=True) diff --git a/config.py b/config.py new file mode 100644 index 00000000..1ea82a12 --- /dev/null +++ b/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:///north_pole.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/flask.log b/flask.log new file mode 100644 index 00000000..a95d11ef --- /dev/null +++ b/flask.log @@ -0,0 +1,9 @@ + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 103-519-779 +127.0.0.1 - - [09/Dec/2025 10:11:00] "GET / HTTP/1.1" 200 - diff --git a/flask_db_init.log b/flask_db_init.log new file mode 100644 index 00000000..2ba4f40a --- /dev/null +++ b/flask_db_init.log @@ -0,0 +1,8 @@ + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 103-519-779 diff --git a/flask_test.log b/flask_test.log new file mode 100644 index 00000000..dfe1fb44 --- /dev/null +++ b/flask_test.log @@ -0,0 +1,78 @@ + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 103-519-779 +Traceback (most recent call last): + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context + self.dialect.do_execute( + ^ + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py", line 951, in do_execute + cursor.execute(statement, parameters) + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ +sqlite3.OperationalError: no such table: gift + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 1536, in __call__ + return self.wsgi_app(environ, start_response) + ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 1514, in wsgi_app + response = self.handle_exception(e) + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 1511, in wsgi_app + response = self.full_dispatch_request() + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 919, in full_dispatch_request + rv = self.handle_user_exception(e) + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 917, in full_dispatch_request + rv = self.dispatch_request() + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^ + File "/Users/galloro/cli-spec-driven-dev/app.py", line 54, in index + results = db.session.execute(stmt).all() + ~~~~~~~~~~~~~~~~~~^^^^^^ + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/scoping.py", line 765, in execute + return self._proxied.execute( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2351, in execute + return self._execute_internal( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2249, in _execute_internal + result: Result[Any] = compile_state_cls.orm_execute_statement( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/orm/context.py", line 306, in orm_execute_statement + result = conn.execute( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1419, in execute + return meth( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/sql/elements.py", line 526, in _execute_on_connection + return connection._execute_clauseelement( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1641, in _execute_clauseelement + ret = self._execute_context( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context + return self._exec_single_context( + + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context + self._handle_dbapi_exception( + ^ + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 2355, in _handle_dbapi_exception + raise sqlalchemy_exception.with_traceback(exc_info[2]) from e + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context + self.dialect.do_execute( + ^ + File "/Users/galloro/cli-spec-driven-dev/venv/lib/python3.13/site-packages/sqlalchemy/engine/default.py", line 951, in do_execute + cursor.execute(statement, parameters) + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ +sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: gift +[SQL: SELECT gift.id, gift.title, gift.description, gift.category, gift.created_at, avg(vote.score) AS avg_score, count(vote.id) AS vote_count +FROM gift LEFT OUTER JOIN vote ON gift.id = vote.gift_id GROUP BY gift.id ORDER BY gift.created_at DESC] +(Background on this error at: https://sqlalche.me/e/20/e3q8) +127.0.0.1 - - [09/Dec/2025 10:24:44] "HEAD / HTTP/1.1" 500 - +127.0.0.1 - - [09/Dec/2025 10:24:44] "HEAD /submit HTTP/1.1" 200 - diff --git a/flask_test_2.log b/flask_test_2.log new file mode 100644 index 00000000..5f9a31a4 --- /dev/null +++ b/flask_test_2.log @@ -0,0 +1,10 @@ + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 103-519-779 +127.0.0.1 - - [09/Dec/2025 10:25:46] "HEAD / HTTP/1.1" 200 - +127.0.0.1 - - [09/Dec/2025 10:25:46] "HEAD /submit HTTP/1.1" 200 - diff --git a/forms.py b/forms.py new file mode 100644 index 00000000..536e29c8 --- /dev/null +++ b/forms.py @@ -0,0 +1,24 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SelectField, RadioField +from wtforms.validators import DataRequired, Length, Optional + +CATEGORIES = [ + ('For Kids', 'For Kids'), + ('For Parents', 'For Parents'), + ('Stocking Stuffers', 'Stocking Stuffers'), + ('DIY', 'DIY'), + ('Tech', 'Tech'), + ('Decorations', 'Decorations') +] + +class GiftForm(FlaskForm): + title = StringField('Gift Name', validators=[DataRequired(), Length(max=100)]) + description = TextAreaField('Description', validators=[DataRequired(), Length(max=500)]) + category = SelectField('Category', choices=CATEGORIES, validators=[DataRequired()]) + +class CommentForm(FlaskForm): + author_name = StringField('Your Name', validators=[Optional(), Length(max=50)]) + content = TextAreaField('Comment', validators=[DataRequired(), Length(min=10, max=500)]) + +class VoteForm(FlaskForm): + score = RadioField('Rating', choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5')], validators=[DataRequired()]) diff --git a/init_db.py b/init_db.py new file mode 100644 index 00000000..469f9b31 --- /dev/null +++ b/init_db.py @@ -0,0 +1,9 @@ +from app import create_app, db +from models import Gift, Vote, Comment + +app = create_app() + +with app.app_context(): + print("Creating database tables...") + db.create_all() + print("Tables created.") diff --git a/instance/north_pole.db b/instance/north_pole.db new file mode 100644 index 00000000..cb0660c1 Binary files /dev/null and b/instance/north_pole.db differ diff --git a/models.py b/models.py new file mode 100644 index 00000000..79af80b1 --- /dev/null +++ b/models.py @@ -0,0 +1,46 @@ +from datetime import datetime, timezone +import sqlalchemy as sa +import sqlalchemy.orm as so +from app import db + +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=lambda: datetime.now(timezone.utc) + ) + + # Relationships + 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") + + def __repr__(self): + return f'' + +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) + + # Relationship + gift: so.Mapped["Gift"] = so.relationship(back_populates="votes") + + def __repr__(self): + return f'' + +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(50), default="Secret Santa") + content: so.Mapped[str] = so.mapped_column(sa.String(500), nullable=False) + created_at: so.Mapped[datetime] = so.mapped_column( + default=lambda: datetime.now(timezone.utc) + ) + + # Relationship + gift: so.Mapped["Gift"] = so.relationship(back_populates="comments") + + def __repr__(self): + return f'' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..db4656ad --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +blinker==1.9.0 +click==8.3.1 +dnspython==2.8.0 +email-validator==2.3.0 +Flask==3.1.2 +Flask-SQLAlchemy==3.1.1 +Flask-WTF==1.2.2 +idna==3.11 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +SQLAlchemy==2.0.44 +typing_extensions==4.15.0 +Werkzeug==3.1.4 +WTForms==3.2.1 diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 00000000..e253a833 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,89 @@ +:root { + --santa-red: #D42426; + --pine-green: #165B33; + --snow-white: #F8F8FF; + --gold: #FFD700; +} + +body { + background-color: var(--snow-white); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +h1, h2, h3, h4, h5, h6, .navbar-brand { + font-family: 'Mountains of Christmas', cursive; + font-weight: 700; +} + +.bg-primary { + background-color: var(--santa-red) !important; +} + +.btn-primary { + background-color: var(--santa-red); + border-color: var(--santa-red); +} + +.btn-primary:hover { + background-color: #b31b1d; + border-color: #b31b1d; +} + +.btn-success { + background-color: var(--pine-green); + border-color: var(--pine-green); +} + +.btn-success:hover { + background-color: #0e3d22; + border-color: #0e3d22; +} + +.text-primary { + color: var(--santa-red) !important; +} + +.text-success { + color: var(--pine-green) !important; +} + +.card { + border: none; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + transition: transform 0.2s; +} + +.card:hover { + transform: translateY(-5px); +} + +.hero-section { + position: relative; + text-align: center; + color: white; + margin-bottom: 2rem; + overflow: hidden; + border-radius: 0 0 15px 15px; +} + +.hero-image { + width: 100%; + height: 400px; + object-fit: cover; +} + +.hero-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-shadow: 2px 2px 4px rgba(0,0,0,0.7); +} + +.snowflake-rating { + color: #ADD8E6; /* Light Blue for empty snowflakes */ +} + +.snowflake-rating .bi-snow2.filled { + color: var(--santa-red); +} diff --git a/static/images/hero.png b/static/images/hero.png new file mode 100644 index 00000000..5f39998d Binary files /dev/null and b/static/images/hero.png differ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..bc63214c --- /dev/null +++ b/templates/base.html @@ -0,0 +1,60 @@ + + + + + + {% block title %}North Pole Wishlist{% endblock %} + + + + + + + + + + + + +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ +
+
+

© 2025 North Pole Wishlist. Made with by Santa's Elves.

+
+
+ + + + diff --git a/templates/detail.html b/templates/detail.html new file mode 100644 index 00000000..9771f660 --- /dev/null +++ b/templates/detail.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} + +{% block title %}{{ gift.title }} - North Pole Wishlist{% endblock %} + +{% block content %} +
+
+ +
+
+
+ {{ gift.category }} + Submitted on {{ gift.created_at.strftime('%B %d, %Y') }} +
+

{{ gift.title }}

+

{{ gift.description }}

+ +
+ +
+
+ + {% if avg_score %} + {{ "%.1f"|format(avg_score) }} + {% else %} + - + {% endif %} + + / 5 +
+
+ {% for i in range(1, 6) %} + {% if avg_score and avg_score >= i %} + + {% elif avg_score and avg_score >= i - 0.5 %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+ {{ vote_count }} votes +
+
+
+ + + +
+ + +
+
+

Comments

+
+
+ +
+ {{ comment_form.hidden_tag() }} +
+ {{ comment_form.author_name.label(class="form-label") }} + {{ comment_form.author_name(class="form-control", placeholder="Your Name (optional)") }} +
+
+ {{ comment_form.content.label(class="form-label") }} + {{ comment_form.content(class="form-control", rows=3, placeholder="Share your thoughts...") }} + {% if comment_form.content.errors %} +
+ {% for error in comment_form.content.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + +
+ {% for comment in comments %} +
+
+
{{ comment.author_name }}
+ {{ comment.created_at.strftime('%b %d, %H:%M') }} +
+

{{ comment.content }}

+
+ {% else %} +
+

No comments yet. Be the first to share your thoughts!

+
+ {% endfor %} +
+
+
+
+ +
+ +
+
+
Santa's Tip
+

Make sure to check if this gift is available at local stores to support your community!

+
+
+ Back to List +
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..2c7b61c8 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% block content %} +
+ Santa Flying +
+

North Pole Wishlist

+

Discover and share the best holiday gifts!

+
+
+ +
+
+

Gift Ideas

+
+
+
+ + +
+
+
+ +
+ {% for gift in gifts %} +
+
+
+
+ {{ gift.Gift.category }} + {{ gift.Gift.created_at.strftime('%b %d') }} +
+
{{ gift.Gift.title }}
+

{{ gift.Gift.description }}

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

No gift ideas found yet!

+

Be the first to submit a wish.

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

Submit a Gift Idea

+
+
+
+ {{ form.hidden_tag() }} + +
+ {{ form.title.label(class="form-label") }} + {{ form.title(class="form-control", placeholder="e.g. Retro Popcorn Maker") }} + {% if form.title.errors %} +
+ {% for error in form.title.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ {{ form.category.label(class="form-label") }} + {{ form.category(class="form-select") }} + {% if form.category.errors %} +
+ {% for error in form.category.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ {{ form.description.label(class="form-label") }} + {{ form.description(class="form-control", rows=5, placeholder="Describe why this gift is awesome...") }} + {% if form.description.errors %} +
+ {% for error in form.description.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + Cancel +
+
+
+
+
+
+{% endblock %}