add user management
This commit is contained in:
124
app.py
124
app.py
@@ -23,12 +23,21 @@ class User(UserMixin, db.Model):
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(120), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
can_add_recipes = db.Column(db.Boolean, default=False)
|
||||
recipes = db.relationship('Recipe', backref='author', lazy=True)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
class Recipe(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
ingredients = db.Column(db.Text, nullable=False)
|
||||
instructions = db.Column(db.Text, nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
@@ -54,19 +63,25 @@ def recipe_detail(recipe_id):
|
||||
recipe = Recipe.query.get_or_404(recipe_id)
|
||||
return render_template('recipe_detail.html', recipe=recipe)
|
||||
|
||||
@app.route('/admin', methods=['GET'])
|
||||
@app.route('/admin')
|
||||
@login_required
|
||||
def admin():
|
||||
if not current_user.is_admin:
|
||||
if not current_user.is_admin and not current_user.can_add_recipes:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Admin sees all recipes, others see only their own
|
||||
if current_user.is_admin:
|
||||
recipes = Recipe.query.all()
|
||||
else:
|
||||
recipes = Recipe.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
return render_template('admin.html', recipes=recipes)
|
||||
|
||||
@app.route('/admin/recipe/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_recipe():
|
||||
if not current_user.is_admin:
|
||||
if not current_user.is_admin and not current_user.can_add_recipes:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@@ -74,23 +89,29 @@ def add_recipe():
|
||||
recipe = Recipe(
|
||||
title=request.form['title'],
|
||||
ingredients=request.form['ingredients'],
|
||||
instructions=request.form['instructions']
|
||||
instructions=request.form['instructions'],
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(recipe)
|
||||
db.session.commit()
|
||||
flash('Recipe added successfully!')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
return render_template('recipe_form.html')
|
||||
return render_template('add_recipe.html')
|
||||
|
||||
@app.route('/admin/recipe/edit/<int:recipe_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_recipe(recipe_id):
|
||||
if not current_user.is_admin:
|
||||
if not current_user.is_admin and not current_user.can_add_recipes:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
recipe = Recipe.query.get_or_404(recipe_id)
|
||||
|
||||
# Only allow editing if admin or if user created the recipe
|
||||
if not current_user.is_admin and recipe.user_id != current_user.id:
|
||||
flash('You can only edit your own recipes.')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
if request.method == 'POST':
|
||||
recipe.title = request.form['title']
|
||||
recipe.ingredients = request.form['ingredients']
|
||||
@@ -98,22 +119,97 @@ def edit_recipe(recipe_id):
|
||||
db.session.commit()
|
||||
flash('Recipe updated successfully!')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
return render_template('recipe_form.html', recipe=recipe)
|
||||
return render_template('edit_recipe.html', recipe=recipe)
|
||||
|
||||
@app.route('/admin/recipe/delete/<int:recipe_id>')
|
||||
@login_required
|
||||
def delete_recipe(recipe_id):
|
||||
if not current_user.is_admin:
|
||||
if not current_user.is_admin and not current_user.can_add_recipes:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
recipe = Recipe.query.get_or_404(recipe_id)
|
||||
|
||||
# Only allow deletion if admin or if user created the recipe
|
||||
if not current_user.is_admin and recipe.user_id != current_user.id:
|
||||
flash('You can only delete your own recipes.')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
db.session.delete(recipe)
|
||||
db.session.commit()
|
||||
flash('Recipe deleted successfully!')
|
||||
return redirect(url_for('admin'))
|
||||
|
||||
@app.route('/users')
|
||||
@login_required
|
||||
def list_users():
|
||||
if not current_user.is_admin:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
users = User.query.all()
|
||||
return render_template('users.html', users=users)
|
||||
|
||||
@app.route('/users/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_user():
|
||||
if not current_user.is_admin:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
can_add_recipes = 'can_add_recipes' in request.form
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('Username already exists.')
|
||||
return redirect(url_for('add_user'))
|
||||
|
||||
user = User(username=username, can_add_recipes=can_add_recipes)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash('User added successfully.')
|
||||
return redirect(url_for('list_users'))
|
||||
|
||||
return render_template('add_user.html')
|
||||
|
||||
@app.route('/users/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_user(id):
|
||||
if not current_user.is_admin:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
user = User.query.get_or_404(id)
|
||||
if request.method == 'POST':
|
||||
user.username = request.form.get('username')
|
||||
if request.form.get('password'):
|
||||
user.set_password(request.form.get('password'))
|
||||
user.can_add_recipes = 'can_add_recipes' in request.form
|
||||
db.session.commit()
|
||||
flash('User updated successfully.')
|
||||
return redirect(url_for('list_users'))
|
||||
|
||||
return render_template('edit_user.html', user=user)
|
||||
|
||||
@app.route('/users/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_user(id):
|
||||
if not current_user.is_admin:
|
||||
flash('Access denied.')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
user = User.query.get_or_404(id)
|
||||
if user.is_admin:
|
||||
flash('Cannot delete admin user.')
|
||||
return redirect(url_for('list_users'))
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash('User deleted successfully.')
|
||||
return redirect(url_for('list_users'))
|
||||
|
||||
# HTMX Endpoints
|
||||
@app.route('/recipes/search')
|
||||
def search_recipes():
|
||||
@@ -127,7 +223,7 @@ def search_recipes():
|
||||
@app.route('/recipe/<int:recipe_id>/delete', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_recipe_htmx(recipe_id):
|
||||
if not current_user.is_admin:
|
||||
if not current_user.is_admin and not current_user.can_add_recipes:
|
||||
return 'Unauthorized', 403
|
||||
|
||||
recipe = Recipe.query.get_or_404(recipe_id)
|
||||
@@ -143,9 +239,11 @@ def health_check():
|
||||
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']):
|
||||
if user and user.check_password(request.form['password']):
|
||||
login_user(user)
|
||||
if user.is_admin or user.can_add_recipes:
|
||||
return redirect(url_for('admin'))
|
||||
return redirect(url_for('index'))
|
||||
flash('Invalid username or password')
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
49
init_db.py
49
init_db.py
@@ -1,29 +1,52 @@
|
||||
from app import app, db, User
|
||||
from app import app, db, User, Recipe
|
||||
from werkzeug.security import generate_password_hash
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
def init_db():
|
||||
with app.app_context():
|
||||
# Create tables
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
|
||||
# Check if admin user already exists
|
||||
admin_username = os.getenv('ADMIN_USERNAME', 'admin')
|
||||
admin = User.query.filter_by(username=admin_username).first()
|
||||
if not admin:
|
||||
# Create admin user
|
||||
admin = User(
|
||||
username=admin_username,
|
||||
password_hash=generate_password_hash(os.getenv('ADMIN_PASSWORD', 'change-this-password')),
|
||||
is_admin=True
|
||||
username=os.getenv('ADMIN_USERNAME', 'admin'),
|
||||
is_admin=True,
|
||||
can_add_recipes=True
|
||||
)
|
||||
admin.set_password(os.getenv('ADMIN_PASSWORD', 'admin'))
|
||||
db.session.add(admin)
|
||||
|
||||
# Create a recipe manager user
|
||||
manager = User(
|
||||
username='recipe_manager',
|
||||
can_add_recipes=True
|
||||
)
|
||||
manager.set_password('manager123')
|
||||
db.session.add(manager)
|
||||
|
||||
# Add some sample recipes
|
||||
recipes = [
|
||||
{
|
||||
'title': 'Chocolate Chip Cookies',
|
||||
'ingredients': '2 1/4 cups all-purpose flour\n1 cup butter\n3/4 cup sugar\n2 eggs\n2 cups chocolate chips',
|
||||
'instructions': '1. Preheat oven to 375°F\n2. Mix ingredients\n3. Drop spoonfuls onto baking sheet\n4. Bake for 10 minutes'
|
||||
},
|
||||
{
|
||||
'title': 'Classic Pancakes',
|
||||
'ingredients': '1 1/2 cups all-purpose flour\n3 1/2 teaspoons baking powder\n1 teaspoon salt\n1 tablespoon sugar',
|
||||
'instructions': '1. Mix dry ingredients\n2. Add wet ingredients\n3. Cook on griddle'
|
||||
}
|
||||
]
|
||||
|
||||
for recipe_data in recipes:
|
||||
recipe = Recipe(**recipe_data)
|
||||
db.session.add(recipe)
|
||||
|
||||
db.session.commit()
|
||||
print("Admin user created successfully!")
|
||||
else:
|
||||
print("Admin user already exists.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
|
||||
83
templates/add_user.html
Normal file
83
templates/add_user.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Add New User</h1>
|
||||
<form method="POST" class="form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="can_add_recipes">
|
||||
Can add and edit recipes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('list_users') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn">Add User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
max-width: 500px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: var(--background-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--secondary-color-dark);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1,16 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Recipe Management</h2>
|
||||
<a href="{{ url_for('add_recipe') }}" class="btn btn-success">Add New Recipe</a>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="admin-header">
|
||||
<h1>{% if current_user.is_admin %}Recipe Management{% else %}My Recipes{% endif %}</h1>
|
||||
<div class="admin-actions">
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('list_users') }}" class="btn">Manage Users</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('add_recipe') }}" class="btn">Add New Recipe</a>
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-secondary">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<div class="recipes-list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Created By</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -18,13 +26,82 @@
|
||||
{% for recipe in recipes %}
|
||||
<tr>
|
||||
<td>{{ recipe.title }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('edit_recipe', recipe_id=recipe.id) }}" class="btn btn-primary btn-sm">Edit</a>
|
||||
<a href="{{ url_for('delete_recipe', recipe_id=recipe.id) }}" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete this recipe?')">Delete</a>
|
||||
<td>{{ recipe.author.username if recipe.author else 'Unknown' }}</td>
|
||||
<td class="actions">
|
||||
{% if current_user.is_admin or recipe.user_id == current_user.id %}
|
||||
<a href="{{ url_for('edit_recipe', recipe_id=recipe.id) }}" class="btn btn-small">Edit</a>
|
||||
<a href="{{ url_for('delete_recipe', recipe_id=recipe.id) }}" class="btn btn-small btn-danger" onclick="return confirm('Are you sure you want to delete this recipe?')">Delete</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.admin-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.recipes-list {
|
||||
background: var(--background-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: var(--error-color-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--secondary-color-dark);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a class="nav-link" href="{{ url_for('index') }}">Home</a>
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
<a class="nav-link" href="{{ url_for('admin') }}">Admin</a>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.is_admin or current_user.can_add_recipes %}
|
||||
<a class="nav-link" href="{{ url_for('admin') }}">{% if current_user.is_admin %}Recipe Management{% else %}My Recipes{% endif %}</a>
|
||||
{% endif %}
|
||||
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{{ url_for('login') }}">Login</a>
|
||||
|
||||
83
templates/edit_user.html
Normal file
83
templates/edit_user.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Edit User: {{ user.username }}</h1>
|
||||
<form method="POST" class="form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" value="{{ user.username }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">New Password (leave blank to keep current)</label>
|
||||
<input type="password" id="password" name="password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="can_add_recipes" {% if user.can_add_recipes %}checked{% endif %}>
|
||||
Can add and edit recipes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('list_users') }}" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
max-width: 500px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: var(--background-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--secondary-color-dark);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
95
templates/users.html
Normal file
95
templates/users.html
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>User Management</h1>
|
||||
<div class="action-bar">
|
||||
<a href="{{ url_for('add_user') }}" class="btn">Add New User</a>
|
||||
</div>
|
||||
|
||||
<div class="users-list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Permissions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
Admin
|
||||
{% elif user.can_add_recipes %}
|
||||
Recipe Manager
|
||||
{% else %}
|
||||
Viewer
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{% if not user.is_admin %}
|
||||
<a href="{{ url_for('edit_user', id=user.id) }}" class="btn btn-small">Edit</a>
|
||||
<form action="{{ url_for('delete_user', id=user.id) }}" method="POST" class="inline-form">
|
||||
<button type="submit" class="btn btn-small btn-danger" onclick="return confirm('Are you sure you want to delete this user?')">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.users-list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--background-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--error-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: var(--error-color-dark);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user