commit 0c38bf894c91e6e1ef2c2e1d8827ad04aec67498 Author: hex Date: Tue Apr 29 13:54:11 2025 -0700 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..d590ad6 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Digital Garage Sale Website + +A minimalistic, printable-style website for hosting a digital garage sale. Built with Flask and Python. + +## Features + +- List items for sale with descriptions and photos +- Categorize items +- Track item status (For Sale, On Hold, Sold) +- Display contact info (email and Signal) +- Provide donation link +- Password-protected admin portal +- Print catalog feature for physical distribution + +## Installation + +1. Clone this repository +2. Create a virtual environment and activate it: +``` +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` +3. Install dependencies: +``` +pip install -r requirements.txt +``` +4. Install wkhtmltopdf for PDF generation: +``` +# Ubuntu/Debian +sudo apt-get install wkhtmltopdf + +# Fedora/CentOS/RHEL +sudo dnf install wkhtmltopdf + +# macOS with Homebrew +brew install wkhtmltopdf + +# Windows +# Download the installer from https://wkhtmltopdf.org/downloads.html +``` + +## Configuration + +You can modify the `config.py` file to change: +- The admin password (default: admin123) +- Secret key +- Database path +- Max upload size + +For production, set these environment variables: +- `SECRET_KEY`: A secure random string +- `ADMIN_PASSWORD`: A strong password for admin access +- `DATABASE_URL`: Optional database URL (defaults to SQLite) + +## Usage + +1. Initialize the database: +``` +python init_db.py +``` + +2. Run the application: +``` +python app.py +``` + +3. Visit http://127.0.0.1:5000 in your browser +4. Access the admin portal at http://127.0.0.1:5000/admin (password: admin123 by default) + +## Admin Features + +- Add/remove categories +- Add/remove items +- Edit item descriptions and photos +- Update item status +- Generate printable PDF catalog +- Update contact information + +## License + +This project is open source and available under the MIT License. diff --git a/app.py b/app.py new file mode 100644 index 0000000..1661569 --- /dev/null +++ b/app.py @@ -0,0 +1,22 @@ +import os +from datetime import datetime +from flask import Flask +from app import create_app, db +from app.models import Category, Item, ContactInfo + +app = create_app() + +@app.shell_context_processor +def make_shell_context(): + return {'db': db, 'Category': Category, 'Item': Item, 'ContactInfo': ContactInfo} + +@app.context_processor +def inject_now(): + return {'now': datetime.utcnow()} + +# Create upload folder when app starts instead of using before_first_request +with app.app_context(): + os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..c839773 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,21 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from config import Config + +db = SQLAlchemy() + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + + db.init_app(app) + + from app.main import bp as main_bp + app.register_blueprint(main_bp) + + from app.admin import bp as admin_bp + app.register_blueprint(admin_bp, url_prefix='/admin') + + return app + +from app import models diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..924325f Binary files /dev/null and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..ab2c42f Binary files /dev/null and b/app/__pycache__/models.cpython-313.pyc differ diff --git a/app/admin/__init__.py b/app/admin/__init__.py new file mode 100644 index 0000000..23ed694 --- /dev/null +++ b/app/admin/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('admin', __name__) + +from app.admin import routes diff --git a/app/admin/__pycache__/__init__.cpython-313.pyc b/app/admin/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b5db4a7 Binary files /dev/null and b/app/admin/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/admin/__pycache__/routes.cpython-313.pyc b/app/admin/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000..b277e28 Binary files /dev/null and b/app/admin/__pycache__/routes.cpython-313.pyc differ diff --git a/app/admin/routes.py b/app/admin/routes.py new file mode 100644 index 0000000..c962241 --- /dev/null +++ b/app/admin/routes.py @@ -0,0 +1,256 @@ +import os +import uuid +from functools import wraps +from datetime import datetime +from flask import render_template, redirect, url_for, request, flash, session, current_app, send_file +from werkzeug.utils import secure_filename +from app.admin import bp +from app.models import Category, Item, ContactInfo +from app import db +import pdfkit + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'admin_authenticated' not in session: + return redirect(url_for('admin.login')) + return f(*args, **kwargs) + return decorated_function + +def allowed_file(filename): + ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +@bp.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + password = request.form.get('password') + if password == current_app.config['ADMIN_PASSWORD']: + session['admin_authenticated'] = True + return redirect(url_for('admin.dashboard')) + else: + flash('Invalid password', 'danger') + return render_template('admin/login.html', title='Admin Login') + +@bp.route('/') +@admin_required +def dashboard(): + return render_template('admin/dashboard.html', title='Admin Dashboard') + +@bp.route('/categories') +@admin_required +def categories(): + categories = Category.query.all() + return render_template('admin/categories.html', + title='Manage Categories', + categories=categories) + +@bp.route('/category/add', methods=['GET', 'POST']) +@admin_required +def add_category(): + if request.method == 'POST': + name = request.form.get('name') + if name: + # Check if category already exists + existing = Category.query.filter_by(name=name).first() + if existing: + flash(f'Category "{name}" already exists', 'warning') + else: + category = Category(name=name) + db.session.add(category) + db.session.commit() + flash(f'Category "{name}" added successfully', 'success') + return redirect(url_for('admin.categories')) + else: + flash('Category name is required', 'danger') + return render_template('admin/add_category.html', title='Add Category') + +@bp.route('/category//delete', methods=['POST']) +@admin_required +def delete_category(id): + category = Category.query.get_or_404(id) + if category: + db.session.delete(category) + db.session.commit() + flash(f'Category "{category.name}" deleted successfully', 'success') + return redirect(url_for('admin.categories')) + +@bp.route('/items') +@admin_required +def items(): + items = Item.query.order_by(Item.created_at.desc()).all() + return render_template('admin/items.html', + title='Manage Items', + items=items) + +@bp.route('/item/add', methods=['GET', 'POST']) +@admin_required +def add_item(): + categories = Category.query.all() + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + price = request.form.get('price') + category_id = request.form.get('category_id') + status = request.form.get('status', 'For Sale') + + if title and description and price and category_id: + try: + price = float(price) + item = Item( + title=title, + description=description, + price=price, + category_id=category_id, + status=status + ) + + # Handle image upload + if 'image' in request.files: + file = request.files['image'] + if file and file.filename and allowed_file(file.filename): + # Create a unique filename + filename = secure_filename(file.filename) + unique_filename = f"{uuid.uuid4().hex}_{filename}" + file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)) + item.image_filename = unique_filename + + db.session.add(item) + db.session.commit() + flash(f'Item "{title}" added successfully', 'success') + return redirect(url_for('admin.items')) + except ValueError: + flash('Price must be a number', 'danger') + else: + flash('All fields are required', 'danger') + + return render_template('admin/add_item.html', + title='Add Item', + categories=categories) + +@bp.route('/item//edit', methods=['GET', 'POST']) +@admin_required +def edit_item(id): + item = Item.query.get_or_404(id) + categories = Category.query.all() + + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + price = request.form.get('price') + category_id = request.form.get('category_id') + status = request.form.get('status') + + if title and description and price and category_id and status: + try: + price = float(price) + item.title = title + item.description = description + item.price = price + item.category_id = category_id + item.status = status + item.updated_at = datetime.utcnow() + + # Handle image upload + if 'image' in request.files: + file = request.files['image'] + if file and file.filename and allowed_file(file.filename): + # Delete old image if it exists + if item.image_filename: + old_image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], item.image_filename) + if os.path.exists(old_image_path): + os.remove(old_image_path) + + # Create a unique filename + filename = secure_filename(file.filename) + unique_filename = f"{uuid.uuid4().hex}_{filename}" + file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)) + item.image_filename = unique_filename + + db.session.commit() + flash(f'Item "{title}" updated successfully', 'success') + return redirect(url_for('admin.items')) + except ValueError: + flash('Price must be a number', 'danger') + else: + flash('All fields are required', 'danger') + + return render_template('admin/edit_item.html', + title='Edit Item', + item=item, + categories=categories) + +@bp.route('/item//delete', methods=['POST']) +@admin_required +def delete_item(id): + item = Item.query.get_or_404(id) + if item: + # Delete image file if exists + if item.image_filename: + image_path = os.path.join(current_app.config['UPLOAD_FOLDER'], item.image_filename) + if os.path.exists(image_path): + os.remove(image_path) + + db.session.delete(item) + db.session.commit() + flash(f'Item "{item.title}" deleted successfully', 'success') + return redirect(url_for('admin.items')) + +@bp.route('/information', methods=['GET', 'POST']) +@admin_required +def edit_contact(): + contact_info = ContactInfo.query.first() + + # Create default contact info if it doesn't exist + if not contact_info: + contact_info = ContactInfo( + information="Welcome to our Digital Garage Sale! This is a place to find unique pre-owned items at great prices.", + email="example@example.com", + signal="Signal Username or Number", + donation_link="https://example.com/donate" + ) + db.session.add(contact_info) + db.session.commit() + + if request.method == 'POST': + information = request.form.get('information') + email = request.form.get('email') + signal = request.form.get('signal') + donation_link = request.form.get('donation_link') + + if email: + contact_info.information = information + contact_info.email = email + contact_info.signal = signal + contact_info.donation_link = donation_link + + db.session.commit() + flash('Contact information updated successfully', 'success') + return redirect(url_for('admin.dashboard')) + else: + flash('Email is required', 'danger') + + return render_template('admin/edit_contact.html', + title='Edit Contact Information', + contact_info=contact_info) + +@bp.route('/catalog/generate') +@admin_required +def generate_catalog(): + items = Item.query.order_by(Item.created_at.desc()).all() + categories = Category.query.all() + contact_info = ContactInfo.query.first() + + # Create HTML to convert to PDF + html = render_template('admin/catalog_pdf.html', + items=items, + categories=categories, + contact_info=contact_info) + + # Generate PDF + pdf_path = os.path.join(current_app.root_path, 'static', 'catalog.pdf') + pdfkit.from_string(html, pdf_path) + + # Return PDF for download + return send_file(pdf_path, as_attachment=True, download_name='garage_sale_catalog.pdf') diff --git a/app/main/__init__.py b/app/main/__init__.py new file mode 100644 index 0000000..3b580b0 --- /dev/null +++ b/app/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from app.main import routes diff --git a/app/main/__pycache__/__init__.cpython-313.pyc b/app/main/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..586877e Binary files /dev/null and b/app/main/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/main/__pycache__/routes.cpython-313.pyc b/app/main/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000..e45a406 Binary files /dev/null and b/app/main/__pycache__/routes.cpython-313.pyc differ diff --git a/app/main/routes.py b/app/main/routes.py new file mode 100644 index 0000000..65159af --- /dev/null +++ b/app/main/routes.py @@ -0,0 +1,38 @@ +from flask import render_template, current_app, flash, redirect, url_for +from app.main import bp +from app.models import Item, Category, ContactInfo +from app import db + +@bp.route('/') +@bp.route('/index') +def index(): + categories = Category.query.all() + items = Item.query.order_by(Item.created_at.desc()).all() + contact_info = ContactInfo.query.first() + + # Create default contact info if it doesn't exist + if not contact_info: + contact_info = ContactInfo( + email="example@example.com", + signal="Signal Username or Number", + donation_link="https://example.com/donate" + ) + db.session.add(contact_info) + db.session.commit() + + return render_template('index.html', + title='Digital Garage Sale', + categories=categories, + items=items, + contact_info=contact_info) + +@bp.route('/category/') +def category(id): + category = Category.query.get_or_404(id) + items = Item.query.filter_by(category_id=id).order_by(Item.created_at.desc()).all() + contact_info = ContactInfo.query.first() + return render_template('category.html', + title=f'Category: {category.name}', + category=category, + items=items, + contact_info=contact_info) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..de2d29b --- /dev/null +++ b/app/models.py @@ -0,0 +1,34 @@ +from datetime import datetime +from app import db + +class Category(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), unique=True, nullable=False) + items = db.relationship('Item', backref='category', lazy='dynamic', cascade='all, delete-orphan') + + def __repr__(self): + return f'' + +class Item(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text, nullable=False) + price = db.Column(db.Float, nullable=False) + image_filename = db.Column(db.String(100), nullable=True) + status = db.Column(db.String(20), default='For Sale') # 'For Sale', 'On Hold', 'Sold' + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) + + def __repr__(self): + return f'' + +class ContactInfo(db.Model): + id = db.Column(db.Integer, primary_key=True) + information = db.Column(db.Text, nullable=True) + email = db.Column(db.String(100), nullable=False) + signal = db.Column(db.String(100), nullable=True) + donation_link = db.Column(db.String(255), nullable=True) + + def __repr__(self): + return f'' diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000..d4ed874 --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,268 @@ +/* Base Styles */ +body { + font-family: 'Courier New', monospace; + line-height: 1.6; + margin: 0; + padding: 0; + background-color: #fafafa; + color: #333; +} + +.container { + width: 90%; + max-width: 1200px; + margin: 0 auto; + padding: 1rem; +} + +/* Typography */ +h1, h2, h3, h4 { + font-family: 'Times New Roman', serif; + margin-top: 0; +} + +a { + color: #333; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +a:hover { + border-bottom: 1px solid #333; +} + +/* Header */ +.site-header { + border-bottom: 1px solid #ddd; + padding: 1rem 0; + margin-bottom: 2rem; +} + +.site-title { + font-size: 2rem; + margin: 0; + font-weight: normal; +} + +/* Navigation */ +.nav { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; +} + +.nav-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; +} + +.nav-item { + margin-right: 1.5rem; +} + +/* Categories */ +.category-list { + border: 1px solid #ddd; + padding: 1rem; + margin-bottom: 2rem; +} + +.category-title { + margin-top: 0; + border-bottom: 1px solid #ddd; + padding-bottom: 0.5rem; +} + +/* Items Grid */ +.items-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 2rem; +} + +.item-card { + border: 1px solid #ddd; + padding: 1rem; + transition: transform 0.3s ease; +} + +.item-card:hover { + transform: translateY(-5px); +} + +.item-image { + width: 100%; + height: 200px; + object-fit: cover; + margin-bottom: 1rem; +} + +.item-title { + margin-top: 0; + margin-bottom: 0.5rem; +} + +.item-price { + font-weight: bold; + margin-bottom: 0.5rem; +} + +.item-status { + display: inline-block; + padding: 0.25rem 0.5rem; + background-color: #eee; + margin-bottom: 0.5rem; +} + +.status-for-sale { + background-color: #d4edda; +} + +.status-on-hold { + background-color: #fff3cd; +} + +.status-sold { + background-color: #f8d7da; +} + +/* Footer */ +.site-footer { + border-top: 1px solid #ddd; + margin-top: 2rem; + padding: 1rem 0; + text-align: center; +} + +/* Information Section */ +.information-section { + margin-bottom: 2rem; + padding: 1rem; + border: 1px solid #ddd; + background-color: #fafafa; +} + +.information-section h2 { + border-bottom: 1px solid #ddd; + padding-bottom: 0.5rem; + margin-top: 0; +} + +/* Contact Info - Keeping for backward compatibility */ +.contact-info { + margin-top: 2rem; + padding: 1rem; + border: 1px solid #ddd; +} + +/* Admin Styles */ +.admin-header { + background-color: #333; + color: white; + padding: 1rem; + margin-bottom: 2rem; +} + +.admin-title { + margin: 0; + color: white; +} + +.btn { + display: inline-block; + padding: 0.5rem 1rem; + background-color: #333; + color: white; + border: none; + cursor: pointer; + text-decoration: none; + font-size: 1rem; + transition: background-color 0.3s ease; +} + +.btn:hover { + background-color: #555; +} + +.btn-primary { + background-color: #007bff; +} + +.btn-primary:hover { + background-color: #0069d9; +} + +.btn-danger { + background-color: #dc3545; +} + +.btn-danger:hover { + background-color: #c82333; +} + +/* Forms */ +.form-group { + margin-bottom: 1rem; +} + +.form-control { + display: block; + width: 100%; + padding: 0.5rem; + font-size: 1rem; + border: 1px solid #ddd; +} + +/* Flash Messages */ +.alert { + padding: 1rem; + margin-bottom: 1rem; + border: 1px solid transparent; +} + +.alert-success { + background-color: #d4edda; + border-color: #c3e6cb; + color: #155724; +} + +.alert-danger { + background-color: #f8d7da; + border-color: #f5c6cb; + color: #721c24; +} + +.alert-warning { + background-color: #fff3cd; + border-color: #ffeeba; + color: #856404; +} + +/* Print styles */ +@media print { + body { + font-size: 12pt; + line-height: 1.4; + } + + .site-header, .nav, .admin-header, .btn { + display: none; + } + + .container { + width: 100%; + max-width: none; + padding: 0; + } + + .item-card { + page-break-inside: avoid; + border: 1px solid #000; + } + + .item-image { + max-height: 150px; + } +} diff --git a/app/templates/admin/add_category.html b/app/templates/admin/add_category.html new file mode 100644 index 0000000..1f43102 --- /dev/null +++ b/app/templates/admin/add_category.html @@ -0,0 +1,16 @@ +{% extends "admin/base.html" %} + +{% block title %}Add Category - Digital Garage Sale{% endblock %} + +{% block content %} +

Add New Category

+ +
+
+ + +
+ + Cancel +
+{% endblock %} diff --git a/app/templates/admin/add_item.html b/app/templates/admin/add_item.html new file mode 100644 index 0000000..fc82b06 --- /dev/null +++ b/app/templates/admin/add_item.html @@ -0,0 +1,52 @@ +{% extends "admin/base.html" %} + +{% block title %}Add Item - Digital Garage Sale{% endblock %} + +{% block content %} +

Add New Item

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + Optional. Accepted formats: JPG, PNG, GIF +
+ + + Cancel +
+{% endblock %} diff --git a/app/templates/admin/base.html b/app/templates/admin/base.html new file mode 100644 index 0000000..3172be4 --- /dev/null +++ b/app/templates/admin/base.html @@ -0,0 +1,47 @@ + + + + + + {% block title %}Admin - Digital Garage Sale{% endblock %} + + + +
+
+

Digital Garage Sale Admin

+ {% if session.get('admin_authenticated') %} + + {% endif %} +
+
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ +
+
+

© 2025 Digital Garage Sale Admin

+
+
+ + diff --git a/app/templates/admin/catalog_pdf.html b/app/templates/admin/catalog_pdf.html new file mode 100644 index 0000000..123dbdc --- /dev/null +++ b/app/templates/admin/catalog_pdf.html @@ -0,0 +1,118 @@ + + + + + + Garage Sale Catalog + + + +
+
+

Digital Garage Sale Catalog

+

Printed on {{ now.strftime('%B %d, %Y') }}

+
+ +
+

Contact Information

+

Email: {{ contact_info.email }}

+ {% if contact_info.signal %} +

Signal: {{ contact_info.signal }}

+ {% endif %} + {% if contact_info.donation_link %} +

Donations: {{ contact_info.donation_link }}

+ {% endif %} +
+ +
+ +

Items For Sale

+ + {% for category in categories %} +
+

{{ category.name }}

+
+ {% for item in items if item.category_id == category.id %} +
+

{{ item.title }}

+

${{ "%.2f"|format(item.price) }}

+

Status: {{ item.status }}

+

{{ item.description }}

+
+ {% endfor %} +
+
+ + {% if not loop.last %} +
+ {% endif %} + {% endfor %} + + +
+ + diff --git a/app/templates/admin/categories.html b/app/templates/admin/categories.html new file mode 100644 index 0000000..8514afc --- /dev/null +++ b/app/templates/admin/categories.html @@ -0,0 +1,38 @@ +{% extends "admin/base.html" %} + +{% block title %}Manage Categories - Digital Garage Sale{% endblock %} + +{% block content %} +

Manage Categories

+ + Add New Category + + + + + + + + + + + + {% for category in categories %} + + + + + + + {% else %} + + + + {% endfor %} + +
IDNameItems CountActions
{{ category.id }}{{ category.name }}{{ category.items.count() }} +
+ +
+
No categories found
+{% endblock %} diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html new file mode 100644 index 0000000..4977d20 --- /dev/null +++ b/app/templates/admin/dashboard.html @@ -0,0 +1,33 @@ +{% extends "admin/base.html" %} + +{% block title %}Admin Dashboard - Digital Garage Sale{% endblock %} + +{% block content %} +

Admin Dashboard

+ +
+
+

Manage Categories

+

Add, edit, or remove item categories

+ Categories +
+ +
+

Manage Items

+

Add, edit, or remove items for sale

+ Items +
+ +
+

Information

+

Update information and contact details

+ Edit Information +
+ +
+

Generate Catalog

+

Create a PDF catalog of all items

+ Generate PDF +
+
+{% endblock %} diff --git a/app/templates/admin/edit_contact.html b/app/templates/admin/edit_contact.html new file mode 100644 index 0000000..70bbc78 --- /dev/null +++ b/app/templates/admin/edit_contact.html @@ -0,0 +1,35 @@ +{% extends "admin/base.html" %} + +{% block title %}Edit Information - Digital Garage Sale{% endblock %} + +{% block content %} +

Edit Information

+ +
+
+ + + Provide general information about your garage sale that will be displayed at the top of the page. +
+ +
+ + +
+ +
+ + + Optional. Leave blank if you don't want to display Signal contact info. +
+ +
+ + + Optional. Full URL to your donation page. +
+ + + Cancel +
+{% endblock %} diff --git a/app/templates/admin/edit_item.html b/app/templates/admin/edit_item.html new file mode 100644 index 0000000..09d2fff --- /dev/null +++ b/app/templates/admin/edit_item.html @@ -0,0 +1,56 @@ +{% extends "admin/base.html" %} + +{% block title %}Edit Item - Digital Garage Sale{% endblock %} + +{% block content %} +

Edit Item

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ {% if item.image_filename %} +

Current image:

+ {{ item.title }} + {% endif %} + + + + Optional. Leave empty to keep current image. Accepted formats: JPG, PNG, GIF +
+ + + Cancel +
+{% endblock %} diff --git a/app/templates/admin/items.html b/app/templates/admin/items.html new file mode 100644 index 0000000..b54ad03 --- /dev/null +++ b/app/templates/admin/items.html @@ -0,0 +1,53 @@ +{% extends "admin/base.html" %} + +{% block title %}Manage Items - Digital Garage Sale{% endblock %} + +{% block content %} +

Manage Items

+ + Add New Item + + + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + + {% else %} + + + + {% endfor %} + +
IDImageTitlePriceCategoryStatusActions
{{ item.id }} + {% if item.image_filename %} + {{ item.title }} + {% else %} + No Image + {% endif %} + {{ item.title }}${{ "%.2f"|format(item.price) }}{{ item.category.name }} + {{ item.status }} + + Edit +
+ +
+
No items found
+{% endblock %} diff --git a/app/templates/admin/login.html b/app/templates/admin/login.html new file mode 100644 index 0000000..f879adb --- /dev/null +++ b/app/templates/admin/login.html @@ -0,0 +1,21 @@ +{% extends "admin/base.html" %} + +{% block title %}Admin Login - Digital Garage Sale{% endblock %} + +{% block content %} +
+

Admin Login

+ +
+
+ + +
+ +
+ +

+ ← Back to site +

+
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..de4ace6 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,46 @@ + + + + + + {% block title %}Digital Garage Sale{% endblock %} + + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ +
+
+

Uncopyrighted software, free for public use! <3

+
+
+ + diff --git a/app/templates/category.html b/app/templates/category.html new file mode 100644 index 0000000..b317523 --- /dev/null +++ b/app/templates/category.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+

Information

+ {% if contact_info.information %} +

{{ contact_info.information }}

+ {% endif %} +

Email: {{ contact_info.email }}

+ {% if contact_info.signal %} +

Signal: {{ contact_info.signal }}

+ {% endif %} + {% if contact_info.donation_link %} +

Donations: Support us

+ {% endif %} +
+ +

{{ category.name }}

+

← Back to all categories

+ +
+ {% for item in items %} +
+ {% if item.image_filename %} + {{ item.title }} + {% else %} +
+ No Image +
+ {% endif %} +

{{ item.title }}

+

${{ "%.2f"|format(item.price) }}

+

{{ item.status }}

+

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

+
+ {% else %} +

No items in this category

+ {% endfor %} +
+ + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..cc73376 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+

Information

+ {% if contact_info.information %} +

{{ contact_info.information }}

+ {% endif %} +

Email: {{ contact_info.email }}

+ {% if contact_info.signal %} +

Signal: {{ contact_info.signal }}

+ {% endif %} + {% if contact_info.donation_link %} +

Donations: Support us

+ {% endif %} +
+ +
+

Item Categories

+
    + {% for category in categories %} +
  • {{ category.name }}
  • + {% else %} +
  • No categories available
  • + {% endfor %} +
+
+ +

Items For Sale

+
+ {% for item in items %} +
+ {% if item.image_filename %} + {{ item.title }} + {% else %} +
+ No Image +
+ {% endif %} +

{{ item.title }}

+

${{ "%.2f"|format(item.price) }}

+

{{ item.status }}

+

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

+

Category: {{ item.category.name }}

+
+ {% else %} +

No items available

+ {% endfor %} +
+ + +{% endblock %} diff --git a/config.py b/config.py new file mode 100644 index 0000000..da6b273 --- /dev/null +++ b/config.py @@ -0,0 +1,12 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'garage_sale.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + UPLOAD_FOLDER = os.path.join(basedir, 'app/static/uploads') + ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD') or 'admin123' # Change in production! + MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max upload size diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..de9e984 --- /dev/null +++ b/init_db.py @@ -0,0 +1,33 @@ +from app import create_app, db +from app.models import Category, Item, ContactInfo + +app = create_app() + +with app.app_context(): + db.drop_all() # Be careful! This will delete all existing data + db.create_all() + + # Add some default categories + categories = [ + Category(name='Furniture'), + Category(name='Electronics'), + Category(name='Clothing'), + Category(name='Books'), + Category(name='Kitchen'), + Category(name='Toys'), + Category(name='Miscellaneous') + ] + + db.session.add_all(categories) + + # Add default contact info + contact_info = ContactInfo( + email="your-email@example.com", + signal="Your Signal username or number", + donation_link="https://example.com/donate" + ) + + db.session.add(contact_info) + db.session.commit() + + print("Database initialized successfully!") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..309c017 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask>=2.3.0 +Flask-SQLAlchemy>=3.0.0 +Flask-WTF>=1.1.0 +Flask-Login>=0.6.0 +Pillow>=9.0.0 +Werkzeug>=2.3.0 +WTForms>=3.0.0 +pdfkit>=1.0.0 diff --git a/update_database.py b/update_database.py new file mode 100644 index 0000000..e8ce7e6 --- /dev/null +++ b/update_database.py @@ -0,0 +1,35 @@ +""" +This script updates the database to add the 'information' field to the ContactInfo model. +Run this script once to update your existing database. +""" + +from app import create_app, db +from app.models import ContactInfo +from sqlalchemy import text + +app = create_app() + +def add_information_field(): + """Add the information field to the ContactInfo table if it doesn't exist.""" + with app.app_context(): + # Check if the column already exists + result = db.session.execute(text("PRAGMA table_info(contact_info)")).fetchall() + columns = [row[1] for row in result] + + if 'information' not in columns: + print("Adding 'information' column to ContactInfo table...") + db.session.execute(text("ALTER TABLE contact_info ADD COLUMN information TEXT")) + + # Add default information to existing contact info + contact_info = ContactInfo.query.first() + if contact_info: + contact_info.information = "Welcome to our Digital Garage Sale! This is a place to find unique pre-owned items at great prices." + db.session.commit() + print("Added default information text to existing contact info.") + + print("Database update complete!") + else: + print("The 'information' column already exists in the ContactInfo table.") + +if __name__ == "__main__": + add_information_field()