first commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
venv
|
||||
.env
|
||||
env
|
||||
*.db
|
||||
__pycache__
|
||||
instance
|
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Używamy lekkiego obrazu Pythona
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Ustawiamy katalog roboczy
|
||||
WORKDIR /app
|
||||
|
||||
# Kopiujemy wymagania
|
||||
COPY requirements.txt requirements.txt
|
||||
|
||||
# Instalujemy zależności
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Kopiujemy resztę aplikacji
|
||||
COPY . .
|
||||
|
||||
# Kopiujemy entrypoint i ustawiamy uprawnienia
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Otwieramy port
|
||||
EXPOSE 8000
|
||||
|
||||
# Ustawiamy entrypoint
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
281
app.py
Normal file
281
app.py
Normal file
@ -0,0 +1,281 @@
|
||||
import os
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||
from flask_socketio import SocketIO, emit, join_room
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from config import Config
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
SYSTEM_PASSWORD = app.config.get('SYSTEM_PASSWORD', 'changeme')
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
socketio = SocketIO(app)
|
||||
login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(150), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(150), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
|
||||
class ShoppingList(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(150), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
is_temporary = db.Column(db.Boolean, default=False)
|
||||
share_token = db.Column(db.String(64), unique=True, nullable=True)
|
||||
expires_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
class Item(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id'))
|
||||
name = db.Column(db.String(150), nullable=False)
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
added_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
purchased = db.Column(db.Boolean, default=False)
|
||||
purchased_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
@app.before_request
|
||||
def require_system_password():
|
||||
if 'authorized' not in request.cookies and request.endpoint != 'system_auth' and not request.endpoint.startswith('static') and not request.endpoint.startswith('login'):
|
||||
return redirect(url_for('system_auth'))
|
||||
|
||||
@app.route('/system-auth', methods=['GET', 'POST'])
|
||||
def system_auth():
|
||||
if request.method == 'POST':
|
||||
if request.form['password'] == SYSTEM_PASSWORD:
|
||||
db.create_all()
|
||||
if not User.query.filter_by(is_admin=True).first():
|
||||
admin_user = User(username='admin', password_hash=generate_password_hash('admin123'), is_admin=True)
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
flash('Utworzono konto administratora: login=admin, hasło=admin123')
|
||||
resp = redirect(url_for('index_guest'))
|
||||
resp.set_cookie('authorized', 'true')
|
||||
return resp
|
||||
flash('Nieprawidłowe hasło do systemu')
|
||||
return render_template('system_auth.html')
|
||||
|
||||
@app.route('/')
|
||||
def index_guest():
|
||||
lists = ShoppingList.query.all()
|
||||
return render_template('index.html', lists=lists)
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
user = User.query.filter_by(username=request.form['username']).first()
|
||||
if user and check_password_hash(user.password_hash, request.form['password']):
|
||||
login_user(user)
|
||||
flash('Zalogowano pomyślnie')
|
||||
return redirect(url_for('dashboard'))
|
||||
flash('Nieprawidłowy login lub hasło')
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/dashboard')
|
||||
@login_required
|
||||
def dashboard():
|
||||
lists = ShoppingList.query.filter_by(owner_id=current_user.id).all()
|
||||
return render_template('dashboard.html', lists=lists)
|
||||
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_list():
|
||||
title = request.form.get('title')
|
||||
is_temporary = 'temporary' in request.form
|
||||
token = secrets.token_hex(16)
|
||||
expires_at = datetime.utcnow() + timedelta(days=7) if is_temporary else None
|
||||
new_list = ShoppingList(title=title, owner_id=current_user.id, is_temporary=is_temporary, share_token=token, expires_at=expires_at)
|
||||
db.session.add(new_list)
|
||||
db.session.commit()
|
||||
return redirect(url_for('view_list', list_id=new_list.id))
|
||||
|
||||
@app.route('/list/<int:list_id>')
|
||||
@login_required
|
||||
def view_list(list_id):
|
||||
shopping_list = ShoppingList.query.get_or_404(list_id)
|
||||
items = Item.query.filter_by(list_id=list_id).all()
|
||||
return render_template('list.html', list=shopping_list, items=items)
|
||||
|
||||
@app.route('/share/<token>')
|
||||
def share_list(token):
|
||||
shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404()
|
||||
items = Item.query.filter_by(list_id=shopping_list.id).all()
|
||||
return render_template('list_guest.html', list=shopping_list, items=items)
|
||||
|
||||
@app.route('/copy/<int:list_id>')
|
||||
@login_required
|
||||
def copy_list(list_id):
|
||||
original = ShoppingList.query.get_or_404(list_id)
|
||||
token = secrets.token_hex(16)
|
||||
new_list = ShoppingList(title=original.title + ' (Kopia)', owner_id=current_user.id, share_token=token)
|
||||
db.session.add(new_list)
|
||||
db.session.commit()
|
||||
original_items = Item.query.filter_by(list_id=original.id).all()
|
||||
for item in original_items:
|
||||
copy_item = Item(list_id=new_list.id, name=item.name)
|
||||
db.session.add(copy_item)
|
||||
db.session.commit()
|
||||
return redirect(url_for('view_list', list_id=new_list.id))
|
||||
|
||||
@app.route('/admin')
|
||||
@login_required
|
||||
def admin_panel():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
user_count = User.query.count()
|
||||
list_count = ShoppingList.query.count()
|
||||
item_count = Item.query.count()
|
||||
all_lists = ShoppingList.query.all()
|
||||
return render_template('admin/admin_panel.html', user_count=user_count, list_count=list_count, item_count=item_count, all_lists=all_lists)
|
||||
|
||||
@app.route('/admin/delete_list/<int:list_id>')
|
||||
@login_required
|
||||
def delete_list(list_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
list_to_delete = ShoppingList.query.get_or_404(list_id)
|
||||
# Usuń wszystkie powiązane produkty
|
||||
Item.query.filter_by(list_id=list_to_delete.id).delete()
|
||||
db.session.delete(list_to_delete)
|
||||
db.session.commit()
|
||||
flash(f'Usunięto listę: {list_to_delete.title}')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/delete_all_lists')
|
||||
@login_required
|
||||
def delete_all_lists():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
Item.query.delete()
|
||||
ShoppingList.query.delete()
|
||||
db.session.commit()
|
||||
flash('Usunięto wszystkie listy')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/add_user', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_user():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = generate_password_hash(request.form['password'])
|
||||
new_user = User(username=username, password_hash=password)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
flash('Dodano nowego użytkownika')
|
||||
return redirect(url_for('admin_panel'))
|
||||
return render_template('admin/add_user.html')
|
||||
|
||||
@app.route('/admin/users')
|
||||
@login_required
|
||||
def list_users():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
users = User.query.all()
|
||||
user_count = User.query.count()
|
||||
list_count = ShoppingList.query.count()
|
||||
item_count = Item.query.count()
|
||||
activity_log = ["Utworzono listę: Zakupy weekendowe", "Dodano produkt: Mleko"]
|
||||
return render_template('admin/list_users.html', users=users, user_count=user_count, list_count=list_count, item_count=item_count, activity_log=activity_log)
|
||||
|
||||
@app.route('/admin/reset_password/<int:user_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def reset_password(user_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
user = User.query.get_or_404(user_id)
|
||||
if request.method == 'POST':
|
||||
new_password = generate_password_hash(request.form['password'])
|
||||
user.password_hash = new_password
|
||||
db.session.commit()
|
||||
flash('Hasło zresetowane')
|
||||
return redirect(url_for('list_users'))
|
||||
return render_template('admin/reset_password.html', user=user)
|
||||
|
||||
@app.route('/admin/delete_user/<int:user_id>')
|
||||
@login_required
|
||||
def delete_user(user_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
user = User.query.get_or_404(user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash('Użytkownik usunięty')
|
||||
return redirect(url_for('list_users'))
|
||||
|
||||
@socketio.on('delete_item')
|
||||
def handle_delete_item(data):
|
||||
item = Item.query.get(data['item_id'])
|
||||
if item:
|
||||
db.session.delete(item)
|
||||
db.session.commit()
|
||||
emit('item_deleted', {'item_id': item.id}, to=str(item.list_id))
|
||||
|
||||
@socketio.on('edit_item')
|
||||
def handle_edit_item(data):
|
||||
item = Item.query.get(data['item_id'])
|
||||
new_name = data['new_name']
|
||||
if item and new_name.strip():
|
||||
item.name = new_name
|
||||
db.session.commit()
|
||||
emit('item_edited', {'item_id': item.id, 'new_name': item.name}, to=str(item.list_id))
|
||||
|
||||
|
||||
@socketio.on('join_list')
|
||||
def handle_join(data):
|
||||
room = str(data['room'])
|
||||
username = data.get('username', 'Gość')
|
||||
join_room(room)
|
||||
emit('user_joined', {'username': username}, to=room)
|
||||
|
||||
@socketio.on('add_item')
|
||||
def handle_add_item(data):
|
||||
list_id = data['list_id']
|
||||
name = data['name']
|
||||
new_item = Item(
|
||||
list_id=list_id,
|
||||
name=name,
|
||||
added_by=current_user.id if current_user.is_authenticated else None
|
||||
)
|
||||
db.session.add(new_item)
|
||||
db.session.commit()
|
||||
emit('item_added', {
|
||||
'id': new_item.id,
|
||||
'name': new_item.name,
|
||||
'added_by': current_user.username if current_user.is_authenticated else 'Gość'
|
||||
}, to=str(list_id), include_self=True)
|
||||
|
||||
@socketio.on('check_item')
|
||||
def handle_check_item(data):
|
||||
item = Item.query.get(data['item_id'])
|
||||
if item:
|
||||
item.purchased = True
|
||||
item.purchased_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
emit('item_checked', {'item_id': item.id}, to=str(item.list_id))
|
||||
|
||||
@app.cli.command('create_db')
|
||||
def create_db():
|
||||
db.create_all()
|
||||
print('Database created.')
|
||||
|
||||
if __name__ == '__main__':
|
||||
socketio.run(app, debug=True)
|
9
config.py
Normal file
9
config.py
Normal file
@ -0,0 +1,9 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'D8pceNZ8q%YR7^7F&9wAC2')
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///shopping.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SYSTEM_PASSWORD = os.environ.get('SYSTEM_PASSWORD', 'admin')
|
||||
DEFAULT_ADMIN_USERNAME = os.environ.get('DEFAULT_ADMIN_USERNAME', 'admin')
|
||||
DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123')
|
0
docker-compose.yml
Normal file
0
docker-compose.yml
Normal file
3
entrypoint.sh
Normal file
3
entrypoint.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
flask db upgrade 2>/dev/null || flask create_db
|
||||
exec python app.py
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
Flask
|
||||
Flask-SQLAlchemy
|
||||
Flask-Login
|
||||
Flask-SocketIO
|
||||
eventlet
|
||||
Werkzeug
|
109
static/js/live.js
Normal file
109
static/js/live.js
Normal file
@ -0,0 +1,109 @@
|
||||
const socket = io();
|
||||
|
||||
function setupList(listId, username) {
|
||||
socket.emit('join_list', { room: listId, username: username });
|
||||
|
||||
const newItemInput = document.getElementById('newItem');
|
||||
if (newItemInput) {
|
||||
newItemInput.focus();
|
||||
newItemInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addItem(listId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('user_joined', data => showToast(`${data.username} dołączył do listy`));
|
||||
|
||||
socket.on('item_added', data => {
|
||||
showToast(`${data.added_by} dodał: ${data.name}`);
|
||||
const list = document.getElementById('items');
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item bg-dark text-white d-flex justify-content-between align-items-center';
|
||||
li.id = `item-${data.id}`;
|
||||
li.innerHTML = `
|
||||
<div>
|
||||
<input type="checkbox" onchange="checkItem(${data.id})">
|
||||
<span id="name-${data.id}">${data.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-warning" onclick="editItem(${data.id}, '${data.name}')">Edytuj</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteItem(${data.id})">Usuń</button>
|
||||
</div>
|
||||
`;
|
||||
list.appendChild(li);
|
||||
});
|
||||
|
||||
socket.on('item_checked', data => {
|
||||
const checkbox = document.querySelector(`#item-${data.item_id} input[type='checkbox']`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('item_deleted', data => {
|
||||
const li = document.getElementById(`item-${data.item_id}`);
|
||||
if (li) {
|
||||
li.remove();
|
||||
}
|
||||
showToast('Usunięto produkt');
|
||||
});
|
||||
|
||||
socket.on('item_edited', data => {
|
||||
const nameSpan = document.getElementById(`name-${data.item_id}`);
|
||||
if (nameSpan) {
|
||||
nameSpan.innerText = data.new_name;
|
||||
}
|
||||
showToast(`Zmieniono nazwę na: ${data.new_name}`);
|
||||
});
|
||||
}
|
||||
|
||||
function addItem(listId) {
|
||||
const name = document.getElementById('newItem').value;
|
||||
if (name.trim() === '') return;
|
||||
socket.emit('add_item', { list_id: listId, name: name });
|
||||
document.getElementById('newItem').value = '';
|
||||
document.getElementById('newItem').focus();
|
||||
}
|
||||
|
||||
function checkItem(id) {
|
||||
socket.emit('check_item', { item_id: id });
|
||||
}
|
||||
|
||||
function deleteItem(id) {
|
||||
if (confirm('Na pewno usunąć produkt?')) {
|
||||
socket.emit('delete_item', { item_id: id });
|
||||
}
|
||||
}
|
||||
|
||||
function editItem(id, oldName) {
|
||||
const newName = prompt('Podaj nową nazwę:', oldName);
|
||||
if (newName && newName.trim() !== '') {
|
||||
socket.emit('edit_item', { item_id: id, new_name: newName });
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink(link) {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: 'Lista zakupów',
|
||||
text: 'Udostępniam Ci moją listę zakupów:',
|
||||
url: link
|
||||
}).catch((err) => console.log('Udostępnianie anulowane', err));
|
||||
} else {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
showToast('Link skopiowany do schowka!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-bg-primary border-0 show';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div></div>`;
|
||||
toastContainer.appendChild(toast);
|
||||
setTimeout(() => { toast.remove(); }, 4000);
|
||||
}
|
24
templates/admin/add_user.html
Normal file
24
templates/admin/add_user.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Dodaj użytkownika{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">➕ Dodaj nowego użytkownika</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="username" placeholder="Nazwa użytkownika" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password" placeholder="Hasło" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">✅ Dodaj użytkownika</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
50
templates/admin/admin_panel.html
Normal file
50
templates/admin/admin_panel.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Panel administratora{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">⚙️ Panel administratora</h2>
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do strony głównej</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<p><strong>👤 Liczba użytkowników:</strong> {{ user_count }}</p>
|
||||
<p><strong>📝 Liczba list:</strong> {{ list_count }}</p>
|
||||
<p><strong>🛒 Liczba produktów:</strong> {{ item_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<a href="/admin/users" class="btn btn-outline-light">👥 Lista użytkowników</a>
|
||||
<a href="/admin/add_user" class="btn btn-success">➕ Dodaj użytkownika</a>
|
||||
<a href="/admin/delete_all_lists" class="btn btn-danger">🗑️ Usuń wszystkie listy</a>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-4">📄 Wszystkie listy zakupowe</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Właściciel (ID)</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in all_lists %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold">{{ l.title }}</td>
|
||||
<td>{{ l.owner_id }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('delete_list', list_id=l.id) }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
32
templates/admin/list_users.html
Normal file
32
templates/admin/list_users.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Lista użytkowników{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">👥 Lista użytkowników</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Login</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td class="fw-bold">{{ user.username }}</td>
|
||||
<td>
|
||||
<a href="/admin/reset_password/{{ user.id }}" class="btn btn-sm btn-outline-warning me-1">🔑 Resetuj</a>
|
||||
<a href="/admin/delete_user/{{ user.id }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
21
templates/admin/reset_password.html
Normal file
21
templates/admin/reset_password.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Resetuj hasło{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🔑 Resetuj hasło: {{ user.username }}</h2>
|
||||
<a href="/admin/users" class="btn btn-outline-secondary">← Powrót do listy użytkowników</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password" placeholder="Nowe hasło" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">💾 Zapisz nowe hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
30
templates/base.html
Normal file
30
templates/base.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Live Lista Zakupów{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/live.js') }}"></script>
|
||||
</head>
|
||||
<body class="bg-dark text-white">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">Live Lista Zakupów</a>
|
||||
<div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-outline-light">🚪 Wyloguj</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" class="btn btn-outline-light">🔑 Zaloguj się</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
44
templates/dashboard.html
Normal file
44
templates/dashboard.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Twoje listy{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">📝 Twoje listy zakupowe</h2>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-light">👀 Tryb gościa</a>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-success">➕ Utwórz nową listę</a>
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-secondary">🚪 Wyloguj</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if lists %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Data utworzenia</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in lists %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold">{{ l.title }}</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="{{ url_for('copy_list', list_id=l.id) }}" class="btn btn-sm btn-outline-secondary">📋 Kopiuj</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Nie masz jeszcze żadnych list. Kliknij „Utwórz nową listę”, aby dodać pierwszą!</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
45
templates/index.html
Normal file
45
templates/index.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Twoje listy zakupów{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Twoje listy zakupów</h2>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post" class="row g-2 align-items-center">
|
||||
<div class="col-sm-5">
|
||||
<input type="text" name="title" placeholder="Nazwa listy" required class="form-control">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="temporary" class="form-check-input" id="tempCheck">
|
||||
<label for="tempCheck" class="form-check-label">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-success">➕ Utwórz nową listę</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if lists %}
|
||||
<ul class="list-group">
|
||||
{% for l in lists %}
|
||||
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap">
|
||||
<span class="fw-bold">{{ l.title }}</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-secondary">📋 Kopiuj</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając z formularza powyżej!</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
47
templates/list.html
Normal file
47
templates/list.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ list.title }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<h2 class="mb-2">{{ list.title }}</h2>
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body d-flex flex-wrap justify-content-between align-items-center">
|
||||
<div class="mb-2 mb-md-0">
|
||||
<strong>Udostępnij link:</strong>
|
||||
<span class="badge bg-secondary">{{ request.url_root }}share/{{ list.share_token }}</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-sm" onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')">
|
||||
📋 Skopiuj lub udostępnij
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
{% for item in items %}
|
||||
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<input type="checkbox" onchange="checkItem({{ item.id }})" {% if item.purchased %}checked{% endif %}>
|
||||
<span id="name-{{ item.id }}">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<button class="btn btn-sm btn-outline-warning me-1" onclick="editItem({{ item.id }}, '{{ item.name }}')">✏️ Edytuj</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem({{ item.id }})">🗑️ Usuń</button>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="input-group mb-2">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
|
||||
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
</script>
|
||||
{% endblock %}
|
20
templates/list_guest.html
Normal file
20
templates/list_guest.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ list.title }} (Gość){% endblock %}
|
||||
{% block content %}
|
||||
<h2>{{ list.title }} (Gość)</h2>
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
{% for item in items %}
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<input type="checkbox" onchange="checkItem({{ item.id }})" {% if item.purchased %}checked{% endif %}> {{ item.name }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<input id="newItem" class="form-control mb-2" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-primary">Dodaj</button>
|
||||
|
||||
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
<a href="/" class="btn btn-secondary mt-3">Powrót</a>
|
||||
{% endblock %}
|
24
templates/login.html
Normal file
24
templates/login.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Logowanie{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🔒 Logowanie</h2>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="username" placeholder="Login" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password" placeholder="Hasło" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">🔑 Zaloguj</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
21
templates/system_auth.html
Normal file
21
templates/system_auth.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Hasło do systemu{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🔑 Hasło do systemu</h2>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<input type="password" name="password" placeholder="Hasło" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">🔓 Wejdź</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
Reference in New Issue
Block a user