This commit is contained in:
hex
2025-04-29 13:54:11 -07:00
commit 0c38bf894c
32 changed files with 1431 additions and 0 deletions

81
README.md Normal file
View File

@@ -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.

22
app.py Normal file
View File

@@ -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)

21
app/__init__.py Normal file
View File

@@ -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

Binary file not shown.

Binary file not shown.

5
app/admin/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('admin', __name__)
from app.admin import routes

Binary file not shown.

Binary file not shown.

256
app/admin/routes.py Normal file
View File

@@ -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/<int:id>/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/<int:id>/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/<int:id>/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')

5
app/main/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes

Binary file not shown.

Binary file not shown.

38
app/main/routes.py Normal file
View File

@@ -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/<int:id>')
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)

34
app/models.py Normal file
View File

@@ -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'<Category {self.name}>'
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'<Item {self.title}>'
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'<ContactInfo {self.email}>'

268
app/static/css/style.css Normal file
View File

@@ -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;
}
}

View File

@@ -0,0 +1,16 @@
{% extends "admin/base.html" %}
{% block title %}Add Category - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Add New Category</h2>
<form method="post" action="{{ url_for('admin.add_category') }}">
<div class="form-group">
<label for="name">Category Name</label>
<input type="text" id="name" name="name" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Add Category</button>
<a href="{{ url_for('admin.categories') }}" class="btn" style="margin-left: 0.5rem;">Cancel</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,52 @@
{% extends "admin/base.html" %}
{% block title %}Add Item - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Add New Item</h2>
<form method="post" action="{{ url_for('admin.add_item') }}" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Item Title</label>
<input type="text" id="title" name="title" class="form-control" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" class="form-control" rows="4" required></textarea>
</div>
<div class="form-group">
<label for="price">Price ($)</label>
<input type="number" id="price" name="price" class="form-control" step="0.01" min="0" required>
</div>
<div class="form-group">
<label for="category_id">Category</label>
<select id="category_id" name="category_id" class="form-control" required>
<option value="">Select a category</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="status">Status</label>
<select id="status" name="status" class="form-control" required>
<option value="For Sale">For Sale</option>
<option value="On Hold">On Hold</option>
<option value="Sold">Sold</option>
</select>
</div>
<div class="form-group">
<label for="image">Item Image</label>
<input type="file" id="image" name="image" class="form-control">
<small>Optional. Accepted formats: JPG, PNG, GIF</small>
</div>
<button type="submit" class="btn btn-primary">Add Item</button>
<a href="{{ url_for('admin.items') }}" class="btn" style="margin-left: 0.5rem;">Cancel</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin - Digital Garage Sale{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="admin-header">
<div class="container">
<h1 class="admin-title">Digital Garage Sale Admin</h1>
{% if session.get('admin_authenticated') %}
<nav class="nav">
<ul class="nav-list">
<li class="nav-item"><a href="{{ url_for('main.index') }}">View Site</a></li>
<li class="nav-item"><a href="{{ url_for('admin.dashboard') }}">Dashboard</a></li>
<li class="nav-item"><a href="{{ url_for('admin.categories') }}">Categories</a></li>
<li class="nav-item"><a href="{{ url_for('admin.items') }}">Items</a></li>
<li class="nav-item"><a href="{{ url_for('admin.edit_contact') }}">Contact Info</a></li>
</ul>
</nav>
{% endif %}
</div>
</header>
<main class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<div class="container">
<p>&copy; 2025 Digital Garage Sale Admin</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Garage Sale Catalog</title>
<style>
body {
font-family: 'Times New Roman', serif;
line-height: 1.6;
margin: 0;
padding: 0;
color: #000;
}
.container {
width: 90%;
margin: 0 auto;
padding: 1rem;
}
h1, h2, h3 {
margin-top: 0;
}
.header {
text-align: center;
margin-bottom: 2rem;
border-bottom: 1px solid #000;
padding-bottom: 1rem;
}
.category {
margin-bottom: 2rem;
page-break-inside: avoid;
}
.category-title {
border-bottom: 1px solid #000;
padding-bottom: 0.5rem;
}
.items-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.item {
border: 1px solid #000;
padding: 1rem;
margin-bottom: 1rem;
page-break-inside: avoid;
}
.item-title {
margin-top: 0;
margin-bottom: 0.5rem;
}
.item-price {
font-weight: bold;
margin-bottom: 0.5rem;
}
.item-status {
font-style: italic;
margin-bottom: 0.5rem;
}
.footer {
margin-top: 2rem;
text-align: center;
border-top: 1px solid #000;
padding-top: 1rem;
}
.page-break {
page-break-after: always;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Digital Garage Sale Catalog</h1>
<p>Printed on {{ now.strftime('%B %d, %Y') }}</p>
</div>
<div class="contact-info">
<h2>Contact Information</h2>
<p><strong>Email:</strong> {{ contact_info.email }}</p>
{% if contact_info.signal %}
<p><strong>Signal:</strong> {{ contact_info.signal }}</p>
{% endif %}
{% if contact_info.donation_link %}
<p><strong>Donations:</strong> {{ contact_info.donation_link }}</p>
{% endif %}
</div>
<div class="page-break"></div>
<h2>Items For Sale</h2>
{% for category in categories %}
<div class="category">
<h3 class="category-title">{{ category.name }}</h3>
<div class="items-grid">
{% for item in items if item.category_id == category.id %}
<div class="item">
<h4 class="item-title">{{ item.title }}</h4>
<p class="item-price">${{ "%.2f"|format(item.price) }}</p>
<p class="item-status">Status: {{ item.status }}</p>
<p>{{ item.description }}</p>
</div>
{% endfor %}
</div>
</div>
{% if not loop.last %}
<div class="page-break"></div>
{% endif %}
{% endfor %}
<div class="footer">
<p>All items are sold as-is. Please contact for more information.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,38 @@
{% extends "admin/base.html" %}
{% block title %}Manage Categories - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Manage Categories</h2>
<a href="{{ url_for('admin.add_category') }}" class="btn btn-primary" style="margin-bottom: 1rem;">Add New Category</a>
<table style="width: 100%; border-collapse: collapse; margin-top: 1rem;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">ID</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Name</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Items Count</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Actions</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td style="border: 1px solid #ddd; padding: 0.5rem;">{{ category.id }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">{{ category.name }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">{{ category.items.count() }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">
<form method="post" action="{{ url_for('admin.delete_category', id=category.id) }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this category? All items in this category will also be deleted.');">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="4" style="border: 1px solid #ddd; padding: 0.5rem; text-align: center;">No categories found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "admin/base.html" %}
{% block title %}Admin Dashboard - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Admin Dashboard</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-top: 2rem;">
<div style="border: 1px solid #ddd; padding: 1rem; text-align: center;">
<h3>Manage Categories</h3>
<p>Add, edit, or remove item categories</p>
<a href="{{ url_for('admin.categories') }}" class="btn btn-primary">Categories</a>
</div>
<div style="border: 1px solid #ddd; padding: 1rem; text-align: center;">
<h3>Manage Items</h3>
<p>Add, edit, or remove items for sale</p>
<a href="{{ url_for('admin.items') }}" class="btn btn-primary">Items</a>
</div>
<div style="border: 1px solid #ddd; padding: 1rem; text-align: center;">
<h3>Information</h3>
<p>Update information and contact details</p>
<a href="{{ url_for('admin.edit_contact') }}" class="btn btn-primary">Edit Information</a>
</div>
<div style="border: 1px solid #ddd; padding: 1rem; text-align: center;">
<h3>Generate Catalog</h3>
<p>Create a PDF catalog of all items</p>
<a href="{{ url_for('admin.generate_catalog') }}" class="btn btn-primary">Generate PDF</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% extends "admin/base.html" %}
{% block title %}Edit Information - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Edit Information</h2>
<form method="post" action="{{ url_for('admin.edit_contact') }}">
<div class="form-group">
<label for="information">Information Text</label>
<textarea id="information" name="information" class="form-control" rows="6">{{ contact_info.information }}</textarea>
<small>Provide general information about your garage sale that will be displayed at the top of the page.</small>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" class="form-control" value="{{ contact_info.email }}" required>
</div>
<div class="form-group">
<label for="signal">Signal (Username or Phone Number)</label>
<input type="text" id="signal" name="signal" class="form-control" value="{{ contact_info.signal }}">
<small>Optional. Leave blank if you don't want to display Signal contact info.</small>
</div>
<div class="form-group">
<label for="donation_link">Donation Link</label>
<input type="url" id="donation_link" name="donation_link" class="form-control" value="{{ contact_info.donation_link }}">
<small>Optional. Full URL to your donation page.</small>
</div>
<button type="submit" class="btn btn-primary">Update Information</button>
<a href="{{ url_for('admin.dashboard') }}" class="btn" style="margin-left: 0.5rem;">Cancel</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends "admin/base.html" %}
{% block title %}Edit Item - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Edit Item</h2>
<form method="post" action="{{ url_for('admin.edit_item', id=item.id) }}" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Item Title</label>
<input type="text" id="title" name="title" class="form-control" value="{{ item.title }}" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" class="form-control" rows="4" required>{{ item.description }}</textarea>
</div>
<div class="form-group">
<label for="price">Price ($)</label>
<input type="number" id="price" name="price" class="form-control" step="0.01" min="0" value="{{ item.price }}" required>
</div>
<div class="form-group">
<label for="category_id">Category</label>
<select id="category_id" name="category_id" class="form-control" required>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == item.category_id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="status">Status</label>
<select id="status" name="status" class="form-control" required>
<option value="For Sale" {% if item.status == 'For Sale' %}selected{% endif %}>For Sale</option>
<option value="On Hold" {% if item.status == 'On Hold' %}selected{% endif %}>On Hold</option>
<option value="Sold" {% if item.status == 'Sold' %}selected{% endif %}>Sold</option>
</select>
</div>
<div class="form-group">
{% if item.image_filename %}
<p>Current image:</p>
<img src="{{ url_for('static', filename='uploads/' + item.image_filename) }}" alt="{{ item.title }}" style="max-width: 200px; max-height: 200px; margin-bottom: 1rem;">
{% endif %}
<label for="image">Change Item Image</label>
<input type="file" id="image" name="image" class="form-control">
<small>Optional. Leave empty to keep current image. Accepted formats: JPG, PNG, GIF</small>
</div>
<button type="submit" class="btn btn-primary">Update Item</button>
<a href="{{ url_for('admin.items') }}" class="btn" style="margin-left: 0.5rem;">Cancel</a>
</form>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "admin/base.html" %}
{% block title %}Manage Items - Digital Garage Sale{% endblock %}
{% block content %}
<h2>Manage Items</h2>
<a href="{{ url_for('admin.add_item') }}" class="btn btn-primary" style="margin-bottom: 1rem;">Add New Item</a>
<table style="width: 100%; border-collapse: collapse; margin-top: 1rem;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">ID</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Image</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Title</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Price</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Category</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Status</th>
<th style="border: 1px solid #ddd; padding: 0.5rem; text-align: left;">Actions</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td style="border: 1px solid #ddd; padding: 0.5rem;">{{ item.id }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">
{% if item.image_filename %}
<img src="{{ url_for('static', filename='uploads/' + item.image_filename) }}" alt="{{ item.title }}" style="width: 50px; height: 50px; object-fit: cover;">
{% else %}
No Image
{% endif %}
</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">{{ item.title }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">${{ "%.2f"|format(item.price) }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">{{ item.category.name }}</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">
<span class="item-status status-{{ item.status|lower|replace(' ', '-') }}">{{ item.status }}</span>
</td>
<td style="border: 1px solid #ddd; padding: 0.5rem;">
<a href="{{ url_for('admin.edit_item', id=item.id) }}" class="btn">Edit</a>
<form method="post" action="{{ url_for('admin.delete_item', id=item.id) }}" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this item?');">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" style="border: 1px solid #ddd; padding: 0.5rem; text-align: center;">No items found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "admin/base.html" %}
{% block title %}Admin Login - Digital Garage Sale{% endblock %}
{% block content %}
<div style="max-width: 400px; margin: 0 auto;">
<h2>Admin Login</h2>
<form method="post" action="{{ url_for('admin.login') }}">
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<p style="margin-top: 20px;">
<a href="{{ url_for('main.index') }}">&larr; Back to site</a>
</p>
</div>
{% endblock %}

46
app/templates/base.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Digital Garage Sale{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="site-header">
<div class="container">
<h1 class="site-title">Hex's Garage Sale Catalog</h1>
<nav class="nav">
<ul class="nav-list">
<li class="nav-item"><a href="{{ url_for('main.index') }}">Home</a></li>
{% if session.get('admin_authenticated') %}
<li class="nav-item"><a href="{{ url_for('admin.dashboard') }}">Admin</a></li>
{% else %}
<li class="nav-item"><a href="{{ url_for('admin.login') }}">Admin Login</a></li>
{% endif %}
</ul>
</nav>
</div>
</header>
<main class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<div class="container">
<p>Uncopyrighted software, free for public use! <3</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<section class="information-section">
<h2>Information</h2>
{% if contact_info.information %}
<p>{{ contact_info.information }}</p>
{% endif %}
<p><strong>Email:</strong> {{ contact_info.email }}</p>
{% if contact_info.signal %}
<p><strong>Signal:</strong> {{ contact_info.signal }}</p>
{% endif %}
{% if contact_info.donation_link %}
<p><strong>Donations:</strong> <a href="{{ contact_info.donation_link }}" target="_blank">Support us</a></p>
{% endif %}
</section>
<h2>{{ category.name }}</h2>
<p><a href="{{ url_for('main.index') }}">&larr; Back to all categories</a></p>
<div class="items-grid">
{% for item in items %}
<div class="item-card">
{% if item.image_filename %}
<img src="{{ url_for('static', filename='uploads/' + item.image_filename) }}" alt="{{ item.title }}" class="item-image">
{% else %}
<div class="item-image" style="background-color: #eee; display: flex; align-items: center; justify-content: center;">
<span>No Image</span>
</div>
{% endif %}
<h3 class="item-title">{{ item.title }}</h3>
<p class="item-price">${{ "%.2f"|format(item.price) }}</p>
<p class="item-status status-{{ item.status|lower|replace(' ', '-') }}">{{ item.status }}</p>
<p>{{ item.description|truncate(100) }}</p>
</div>
{% else %}
<p>No items in this category</p>
{% endfor %}
</div>
{% endblock %}

54
app/templates/index.html Normal file
View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<section class="information-section">
<h2>Information</h2>
{% if contact_info.information %}
<p>{{ contact_info.information }}</p>
{% endif %}
<p><strong>Email:</strong> {{ contact_info.email }}</p>
{% if contact_info.signal %}
<p><strong>Signal:</strong> {{ contact_info.signal }}</p>
{% endif %}
{% if contact_info.donation_link %}
<p><strong>Donations:</strong> <a href="{{ contact_info.donation_link }}" target="_blank">Support us</a></p>
{% endif %}
</section>
<section class="category-list">
<h2 class="category-title">Item Categories</h2>
<ul>
{% for category in categories %}
<li><a href="{{ url_for('main.category', id=category.id) }}">{{ category.name }}</a></li>
{% else %}
<li>No categories available</li>
{% endfor %}
</ul>
</section>
<h2>Items For Sale</h2>
<div class="items-grid">
{% for item in items %}
<div class="item-card">
{% if item.image_filename %}
<img src="{{ url_for('static', filename='uploads/' + item.image_filename) }}" alt="{{ item.title }}" class="item-image">
{% else %}
<div class="item-image" style="background-color: #eee; display: flex; align-items: center; justify-content: center;">
<span>No Image</span>
</div>
{% endif %}
<h3 class="item-title">{{ item.title }}</h3>
<p class="item-price">${{ "%.2f"|format(item.price) }}</p>
<p class="item-status status-{{ item.status|lower|replace(' ', '-') }}">{{ item.status }}</p>
<p>{{ item.description|truncate(100) }}</p>
<p>Category: {{ item.category.name }}</p>
</div>
{% else %}
<p>No items available</p>
{% endfor %}
</div>
{% endblock %}

12
config.py Normal file
View File

@@ -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

33
init_db.py Normal file
View File

@@ -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!")

8
requirements.txt Normal file
View File

@@ -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

35
update_database.py Normal file
View File

@@ -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()