init
This commit is contained in:
81
README.md
Normal file
81
README.md
Normal 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
22
app.py
Normal 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
21
app/__init__.py
Normal 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
|
||||
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/models.cpython-313.pyc
Normal file
BIN
app/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
5
app/admin/__init__.py
Normal file
5
app/admin/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('admin', __name__)
|
||||
|
||||
from app.admin import routes
|
||||
BIN
app/admin/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/admin/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/admin/__pycache__/routes.cpython-313.pyc
Normal file
BIN
app/admin/__pycache__/routes.cpython-313.pyc
Normal file
Binary file not shown.
256
app/admin/routes.py
Normal file
256
app/admin/routes.py
Normal 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
5
app/main/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
from app.main import routes
|
||||
BIN
app/main/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/main/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/main/__pycache__/routes.cpython-313.pyc
Normal file
BIN
app/main/__pycache__/routes.cpython-313.pyc
Normal file
Binary file not shown.
38
app/main/routes.py
Normal file
38
app/main/routes.py
Normal 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
34
app/models.py
Normal 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
268
app/static/css/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
16
app/templates/admin/add_category.html
Normal file
16
app/templates/admin/add_category.html
Normal 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 %}
|
||||
52
app/templates/admin/add_item.html
Normal file
52
app/templates/admin/add_item.html
Normal 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 %}
|
||||
47
app/templates/admin/base.html
Normal file
47
app/templates/admin/base.html
Normal 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>© 2025 Digital Garage Sale Admin</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
118
app/templates/admin/catalog_pdf.html
Normal file
118
app/templates/admin/catalog_pdf.html
Normal 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>
|
||||
38
app/templates/admin/categories.html
Normal file
38
app/templates/admin/categories.html
Normal 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 %}
|
||||
33
app/templates/admin/dashboard.html
Normal file
33
app/templates/admin/dashboard.html
Normal 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 %}
|
||||
35
app/templates/admin/edit_contact.html
Normal file
35
app/templates/admin/edit_contact.html
Normal 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 %}
|
||||
56
app/templates/admin/edit_item.html
Normal file
56
app/templates/admin/edit_item.html
Normal 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 %}
|
||||
53
app/templates/admin/items.html
Normal file
53
app/templates/admin/items.html
Normal 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 %}
|
||||
21
app/templates/admin/login.html
Normal file
21
app/templates/admin/login.html
Normal 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') }}">← Back to site</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
46
app/templates/base.html
Normal file
46
app/templates/base.html
Normal 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>
|
||||
44
app/templates/category.html
Normal file
44
app/templates/category.html
Normal 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') }}">← 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
54
app/templates/index.html
Normal 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
12
config.py
Normal 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
33
init_db.py
Normal 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
8
requirements.txt
Normal 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
35
update_database.py
Normal 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()
|
||||
Reference in New Issue
Block a user