From 0c38bf894c91e6e1ef2c2e1d8827ad04aec67498 Mon Sep 17 00:00:00 2001 From: hex Date: Tue, 29 Apr 2025 13:54:11 -0700 Subject: [PATCH] init --- README.md | 81 ++++++ app.py | 22 ++ app/__init__.py | 21 ++ app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 930 bytes app/__pycache__/models.cpython-313.pyc | Bin 0 -> 3422 bytes app/admin/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 305 bytes app/admin/__pycache__/routes.cpython-313.pyc | Bin 0 -> 14591 bytes app/admin/routes.py | 256 +++++++++++++++++ app/main/__init__.py | 5 + app/main/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 302 bytes app/main/__pycache__/routes.cpython-313.pyc | Bin 0 -> 2411 bytes app/main/routes.py | 38 +++ app/models.py | 34 +++ app/static/css/style.css | 268 ++++++++++++++++++ app/templates/admin/add_category.html | 16 ++ app/templates/admin/add_item.html | 52 ++++ app/templates/admin/base.html | 47 +++ app/templates/admin/catalog_pdf.html | 118 ++++++++ app/templates/admin/categories.html | 38 +++ app/templates/admin/dashboard.html | 33 +++ app/templates/admin/edit_contact.html | 35 +++ app/templates/admin/edit_item.html | 56 ++++ app/templates/admin/items.html | 53 ++++ app/templates/admin/login.html | 21 ++ app/templates/base.html | 46 +++ app/templates/category.html | 44 +++ app/templates/index.html | 54 ++++ config.py | 12 + init_db.py | 33 +++ requirements.txt | 8 + update_database.py | 35 +++ 32 files changed, 1431 insertions(+) create mode 100644 README.md create mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-313.pyc create mode 100644 app/__pycache__/models.cpython-313.pyc create mode 100644 app/admin/__init__.py create mode 100644 app/admin/__pycache__/__init__.cpython-313.pyc create mode 100644 app/admin/__pycache__/routes.cpython-313.pyc create mode 100644 app/admin/routes.py create mode 100644 app/main/__init__.py create mode 100644 app/main/__pycache__/__init__.cpython-313.pyc create mode 100644 app/main/__pycache__/routes.cpython-313.pyc create mode 100644 app/main/routes.py create mode 100644 app/models.py create mode 100644 app/static/css/style.css create mode 100644 app/templates/admin/add_category.html create mode 100644 app/templates/admin/add_item.html create mode 100644 app/templates/admin/base.html create mode 100644 app/templates/admin/catalog_pdf.html create mode 100644 app/templates/admin/categories.html create mode 100644 app/templates/admin/dashboard.html create mode 100644 app/templates/admin/edit_contact.html create mode 100644 app/templates/admin/edit_item.html create mode 100644 app/templates/admin/items.html create mode 100644 app/templates/admin/login.html create mode 100644 app/templates/base.html create mode 100644 app/templates/category.html create mode 100644 app/templates/index.html create mode 100644 config.py create mode 100644 init_db.py create mode 100644 requirements.txt create mode 100644 update_database.py 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 0000000000000000000000000000000000000000..924325f9fe67b8cabae3b1ea1e60575563d7f6a6 GIT binary patch literal 930 zcmZ`&&ubGw6rS0g-E7j<+M-rV521=ShjuRpEfhg29yU_6A*U9GNix}P-5>F7D%Mkq zhaS9mu;9Po%|FAVLO~gfRI~?gLd3Ig@?#5v1N-)ynfJc==G(XZsi^|NQ7+Hyyr+bG z*OT!uMxeie;D}U7l~#yHffhA3Du!o*IlyLxc@|hjvTZJs@~8~#ZR;kjvU##gA%_)K z#qsixFB0D_<$mg{5>?jyP^6C$6snIN+zlH$GTO(Py`IFaz;B?YY*>g)eEMH89FeVT zZ)>cSlt*hZtmP>6JvDtuF)|+-kPEVwwH^IBEhp}&a%d+ zX8e<#$87ZLKiOv0tg_Vs+ci?MHb7qroiOU{=e8@FQ4njF_8{bKkgecZsQ}sy9zNA&5IF zuTzHSF*P})2}vY zv)rL~;C*8AAKCnywPUvQgV~3bgUT!Om|Zw!*D+zVdFz;6J!MO03%B1nA9UU7$;7#? z*GFP)v>hc)8KxVhg0fn=DTnr+A7Gw73B|BGLEjeiZv(m`VNwSz6J~X|jNvVTIgKx3 zP|g|>?}hRXEZ|f(Xu62umq{t@k+~kZ(IZ#BS<^4JyT-}F;%_oZr~fPvIyd0>3noL& Ar~m)} literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-313.pyc b/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab2c42f1687c77429809d4396176caed8c188967 GIT binary patch literal 3422 zcmc(h-%k`*6vt+Vkox zI3l5qciO>&NAkA2X%+aW-*&7|5~V81F9q5K8jynfIU1BgG?c22hMCv}yG$80w=BTi z!6!TmWT={!X)bY-Dsj(~wzgul)W z*d3cqHJsXM_fanD?jQGeuE!r?{CT)30fHc~3&AAXsEOVq>|*Rona0M9G>N4QL!ol4 z%US&zHExikWyTaWrCPGql9H*MA}yw@k(ivx#L@=r!#tVEF)@*#BtsJkRS|`u%{uBFfHgxCS(gHjASihg5QJCM-QOchX%D~8(h!q1@j%ZR27(PnrND8Eu4FB zu5_kcbt->xDG-`$pJ<;NC=5*x73s_ur87^Dl>-;@-Alph$*U7ri#?^@r$dX^lyZ>d zFTD_WPt;ikSJ9*E~jB z{l*@A6>0zxz(aT!50NclLv^@j;OMKZwx_y(!_yA_{Xn<+UHosuRT%xjtC0dX1CI$bugY(mA8eQw-Z?#p}0IO>akQyBXlp+9KbmOwzVF@krgmC;*fPPH5P?A z|Ez!NtND)>!^alIV~#2j1O3q^dIVY`W3+F`g|f!%MVp5^v4?9n20a17gJ|;!`69 zby}Tw&*-IWIoOuJv{YMPh)zd~7w0a|UM`Vx?U{TZ(#;t=_F$~kP!7gmTtlNn!MWU{ zT&eEy-KRpi;gfv-QYbt*G%++q3%TjseC^EFr5oi?d%kC>rmoO5-BfIy>zM6W5Pu5) z7%tab$oFp3{;c`e*e|hi&F9ctiKch9x8(XE^nGX{{abVQvuwHcdcF?{d)&E8-1jEJ zR+6sI;7j=5=P8hz2yaeRP^qV)Qm^e{H^Oei89>sqs%_7J`VA8* zC_oigQ1DLCnj4=TFKVU3i{TTC;)#tY*z=E3fXN{XsIY&Uwo?xW4qe#nH9@hm#zKmr z+kFX0Xm0K-*-v2-MS~}4-UhRSWGiAoX)5K~(;KC^MaG+CST}QbK`4jXK!){u3-RfA zab!+?q?X)|^#!Y3-YgBf{9fzi+52Z#IVi3k6#15^ PBZcM%%`Z48+A;kNqS%_5 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b5db4a725a597edb93337a22c63fdbff12658077 GIT binary patch literal 305 zcmXv`u}Z{15Zz6(M^1@gWue7sm*TQm*olR9IS?VGC(C$QbE`=<%qEJRr9U9}3x1E? zf`tfzosb`JbLxYcc{A_L>!WCmIGpcqE-&2t7?ytof224ePb9?879 zci{bc(08go3sQz`LW&?}UxN@+J3cNu*`TUh%OI~*?QSE~T`SFLJhJSn1apJFGuu#z z8Y(G-^)4GbhNekYAIO~gLi!=mru-I=FLfn(p>>YHPrxi7muIN2<)Sq_SA}Y!oECsk z$fdI`n_U%(j-^lhm+jzWt4qw^sgZD{x HaNTJ?C!J8N literal 0 HcmV?d00001 diff --git a/app/admin/__pycache__/routes.cpython-313.pyc b/app/admin/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b277e281575ff4dded9fb60053fa8052bd3aaf91 GIT binary patch literal 14591 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..586877eb016bdf10395a7c704332680a54d2f72f GIT binary patch literal 302 zcmXv`u}Z{15Zz6(M^qwMSzHtBQd|}bJF(Ep92`PQ50>$==2nwzm`x7tEG=#P2*1Z} z!9oPVPRI|qL47bYZ|1#uLlkWjhwN}~ed*@MxcnpdBSY%QGs#I#=Om*@W3r?NB=?ry zk@xFC->LpQ$QZJiltIG2Mj@eg>$K`+i>m2tP=jjRX@a`%q*){r%dRUhx9B^s4TWf+ zmO@zXsyZ|PtO1V literal 0 HcmV?d00001 diff --git a/app/main/__pycache__/routes.cpython-313.pyc b/app/main/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e45a406d42a884a9bc8cad2b4af4e539798cef90 GIT binary patch literal 2411 zcmbVN&2Jk;6rcUF*XwoM#D#p6CY#2TxN5Vdq(MnPKnkeo21Uh|#UYi<#=CKru6M)R z4Qeb@MR1@wr5q}_A|a8UI3po(;Sb2l1zW3BMWPpOiBfT9X6>;6(9d0TI~4pY{%>vdHhI63%vjbEyk$t*w_U1cH2V{5fHi_V&zl+?vozf zjb79#aG9D9n67KB2qh>A>+EySmVEXs)swTyXJ$3QaQZMPF;w^|!4 zSQSUNm7-^YqgPGE1!ZBax@1CaXvOzey%|+KG+#Ab2g3=2OE9atWrr~@iVstf!*ZRH zZ8>*pQiQ3A6~AiNGVfUx%g1}%&_S=5N~OqcgfV>bs^tTLc#eY%Zh$-$yn-u_j4Fx}-cUc&L0xgHrdn}b19Wv(_sY6q-U9a% zOgK+9S_ivOi4Jzb!v|9_HC22wjtcIYZ+a7}>-boNV4&c22^qXcxY|I^W2kTFo8hmA z8@*#e?^wO}^$l%TKCz{3Y7KcLkVn4fe@K6qZYYz1GFexq>hjwS`AQ&Psms?k-rvm( zH!|5ElWk&W}-PXwIAmPr2F!72_=$S(x$Z4v)S|D z^T%R#SCmMlX zRm<^bEaQ5)t&GHvI)ZWge+otsQ3^QBAptl}ZjO5f?&aVF62blDY0*uZZR`DvlH-ABQ7FUvi-|d2zDEt;Dir`w_W-1kg-h7UH-H zMa=IB!fzmu52dK<8m8?yp&t{mZ0}in# eNtNE?ad+<=VtF!oM;fj(Bm1n_nxaHjl)*ovug-e_ literal 0 HcmV?d00001 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()