This commit is contained in:
2025-09-27 20:49:58 +02:00
commit 767fb638ce
58 changed files with 6724 additions and 0 deletions

20
templates/403.html Normal file
View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}{{ _('Home') }}{% endblock %}
{% block content %}
<div class="row justify-content-center align-items-center mt-5">
<div class="col-md-8">
<div class="card shadow-lg p-4 mb-5" style="border-radius: 1.5rem;">
<div class="card-body text-center">
<h1 class="mb-3" style="font-size:2.5rem; font-weight:700; color:#2563eb;">
<i class="bi bi-people-fill me-2"></i>MiniFacebook 403
</h1>
<p class="lead mb-4" style="font-size:1.2rem;">
{{ _('This page is not accessible to you, you do not have the permissions to view it.') }}<br>
<span class="text-muted">{{ _('You can go back from the page.') }}</span>
</p>
<a href="{{ url_for('post.feed') }}" class="btn btn-success m-2 px-4 py-2"><i class="bi bi-house-door me-1"></i>{{ _('Go to Feed') }}</a>
</div>
</div>
</div>
</div>
{% endblock %}

372
templates/admin.html Normal file
View File

@@ -0,0 +1,372 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-shield-lock me-2"></i>{{ _('Admin Panel') }}</h2>
<ul class="nav nav-tabs mb-3" id="adminTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="users-tab" data-bs-toggle="tab" data-bs-target="#users" type="button" role="tab">{{ _('Users') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="posts-tab" data-bs-toggle="tab" data-bs-target="#posts" type="button" role="tab">{{ _('Posts') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="friendships-tab" data-bs-toggle="tab" data-bs-target="#friendships" type="button" role="tab">{{ _('Friendships') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="comments-tab" data-bs-toggle="tab" data-bs-target="#comments" type="button" role="tab">{{ _('Comments') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="uploads-tab" data-bs-toggle="tab" data-bs-target="#uploads" type="button" role="tab">{{ _('Uploads') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="notifications-tab" data-bs-toggle="tab" data-bs-target="#notifications" type="button" role="tab">{{ _('Notifications') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab">{{ _('Events') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="shop-items-tab" data-bs-toggle="tab" data-bs-target="#shop-items" type="button" role="tab">{{ _('Shop Orders') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="shop-orders-tab" data-bs-toggle="tab" data-bs-target="#shop-orders" type="button" role="tab">{{ _('Reward Points') }}</button>
</li>
</ul>
<div class="tab-content" id="adminTabContent">
<!-- Users Tab -->
<div class="tab-pane fade show active" id="users" role="tabpanel">
<h4><i class="bi bi-people me-2"></i>{{ _('Users') }}</h4>
<table class="table table-sm align-middle">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Email') }}</th>
<th>{{ _('Admin') }}</th>
<th>{{ _('Owner') }}</th>
<th>{{ _('Profile Pic') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{% if user.profile_pic and user.profile_pic != 'default.png' %}<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" width="32" class="rounded me-1">{% endif %} {{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{% if user.is_admin %}<i class="bi bi-check-lg text-success"></i>{% endif %}</td>
<td>{% if user.is_owner %}<i class="bi bi-star-fill text-warning"></i>{% endif %}</td>
<td>
{% if user.profile_pic and user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" width="32" class="rounded me-1">
<form action="{{ url_for('admin.admin_delete_pic', user_id=user.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" title="{{ _('Delete Picture') }}"><i class="bi bi-image"></i></button>
</form>
{% endif %}
</td>
<td>
{% if not user.is_admin %}
<form action="{{ url_for('admin.make_admin', user_id=user.id) }}" method="post" style="display:inline;">
<button class="btn btn-success btn-sm" title="{{ _('Make Admin') }}"><i class="bi bi-person-gear"></i></button>
</form>
{% elif not user.is_owner %}
<form action="{{ url_for('admin.remove_admin', user_id=user.id) }}" method="post" style="display:inline;">
<button class="btn btn-warning btn-sm" title="{{ _('Remove Admin') }}"><i class="bi bi-person-dash"></i></button>
</form>
{% endif %}
{% if not user.is_owner %}
<form action="{{ url_for('admin.admin_delete_user', user_id=user.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" title="{{ _('Delete User') }}"><i class="bi bi-person-x"></i></button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Posts Tab -->
<div class="tab-pane fade" id="posts" role="tabpanel">
<h4><i class="bi bi-file-earmark-text me-2"></i>{{ _('All Posts') }}</h4>
<table class="table table-sm align-middle">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Content') }}</th>
<th>{{ _('Visibility') }}</th>
<th>{{ _('Created') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for post in posts %}
<tr>
<td>
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" alt="{{ post.user.username }}" class="rounded me-1" width="32">{{ post.user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ post.user.username }}
{% endif %}
</td>
<td>{{ post.content|truncate(50) }}</td>
<td>
{% if post.visibility == 'public' %}
<span class="badge bg-info"><i class="bi bi-globe"></i> {{ _('Public') }}</span>
{% else %}
<span class="badge bg-secondary"><i class="bi bi-people"></i> {{ _('Friends') }}</span>
{% endif %}
</td>
<td>{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<form action="{{ url_for('admin.admin_delete_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" title="{{ _('Delete Post') }}"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Friendships Tab -->
<div class="tab-pane fade" id="friendships" role="tabpanel">
<h4><i class="bi bi-people-arrows me-2"></i>{{ _('Friendships') }}</h4>
<table class="table table-sm">
<thead>
<tr>
<th>{{ _('User 1') }}</th>
<th>{{ _('User 2') }}</th>
<th>{{ _('Status') }}</th>
</tr>
</thead>
<tbody>
{% for f in friendships %}
<tr>
<td>
{% if f.requester.profile_pic and f.requester.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ f.requester.profile_pic) }}" alt="{{ f.requester.username }}" class="rounded me-1" width="32">{{ f.requester.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ f.requester.username }}
{% endif %}
</td>
<td>
{% if f.receiver.profile_pic and f.receiver.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ f.receiver.profile_pic) }}" alt="{{ f.receiver.username }}" class="rounded me-1" width="32">{{ f.receiver.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ f.receiver.username }}
{% endif %}
</td>
<td>
{% if f.status == 'accepted' %}
<span class="badge bg-success">{{ _('Accepted') }}</span>
{% elif f.status == 'pending' %}
<span class="badge bg-warning text-dark">{{ _('Pending') }}</span>
{% else %}
<span class="badge bg-danger">{{ _('Rejected') }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Comments Tab -->
<div class="tab-pane fade" id="comments" role="tabpanel">
<h4><i class="bi bi-chat-left-text me-2"></i>{{ _('Comments') }}</h4>
<table class="table table-sm">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Post') }}</th>
<th>{{ _('Content') }}</th>
<th>{{ _('Created') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for comment in comments %}
<tr>
<td>
{% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ comment.user.profile_pic) }}" class="rounded me-2" width="32">{{ comment.user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ comment.user.username }}
{% endif %}
</td>
<td>{{ comment.post.id }}</td>
<td>{{ comment.content|truncate(50) }}</td>
<td>{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<form action="{{ url_for('post.delete_comment', comment_id=comment.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" title="{{ _('Delete Comment') }}"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Uploads Tab -->
<div class="tab-pane fade" id="uploads" role="tabpanel">
<h4><i class="bi bi-upload me-2"></i>{{ _('Uploads') }}
<form action="{{ url_for('admin.admin_delete_all_uploads') }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" title="{{ _('Delete All Uploads') }}"><i class="bi bi-trash"></i></button>
</form>
</h4>
<table class="table table-sm">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Filename') }}</th>
<th>{{ _('Type') }}</th>
<th>{{ _('Uploaded') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for upload in uploads %}
<tr>
<td>{{ upload.user.username }}</td>
<td>{{ upload.filename }}</td>
<td>{{ upload.filetype }}</td>
<td>{{ upload.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-outline-primary btn-sm" title="{{ _('Download') }}"><i class="bi bi-download"></i></a>
<form action="{{ url_for('admin.admin_delete_upload', upload_id=upload.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" title="{{ _('Delete Upload') }}"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Notifications Tab -->
<div class="tab-pane fade" id="notifications" role="tabpanel">
<h4>
<i class="bi bi-bell me-2"></i>{{ _('Notifications') }}
<form action="{{ url_for('admin.admin_delete_all_notifications') }}" method="post" onsubmit="return confirm('{{ _('Are you sure you want to delete all notifications?') }}');">
<button class="btn btn-danger mb-2 float-end"><i class="bi bi-bell-slash"></i> {{ _('Delete All Notifications') }}</button>
</form>
</h4>
<table class="table table-sm">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Message') }}</th>
<th>{{ _('Created') }}</th>
</tr>
</thead>
<tbody>
{% for notif in all_notifications %}
<tr>
<td>{{ notif.user_id }}</td>
<td>{{ notif.message }}</td>
<td>{{ notif.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Events Tab -->
<div class="tab-pane fade" id="events" role="tabpanel">
<h4>
<i class="bi bi-clock-history me-2"></i>{{ _('Recent Events') }}
<form action="{{ url_for('admin.admin_delete_all_events') }}" method="post" onsubmit="return confirm('{{ _('Are you sure you want to delete all events?') }}');">
<button class="btn btn-danger mb-2 float-end"><i class="bi bi-calendar-x"></i> {{ _('Delete All Events') }}</button>
</form>
</h4>
<table class="table table-sm">
<tbody id="events-tbody">
{% for event in events %}
<tr>
<td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ event.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- User Shop Items -->
<div class="tab-pane fade" id="shop-items" role="tabpanel">
<h4><i class="bi bi-shop me-2"></i>{{ _('Shop Orders') }}</h4>
<table class="table table-sm">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Item') }}</th>
</tr>
</thead>
<tbody>
{% for usi in user_shop_items %}
<tr>
<td>
{% if usi.user.profile_pic and usi.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ usi.user.profile_pic) }}" class="rounded me-2" width="32">{{ usi.user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ usi.user.username }}
{% endif %}
</td>
<td>{{ usi.item.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Reward Points -->
<div class="tab-pane fade" id="shop-orders" role="tabpanel">
<h4><i class="bi bi-cart-check me-2"></i>{{ _('Reward Points') }}</h4>
<table class="table table-sm">
<thead>
<tr>
<th>{{ _('User') }}</th>
<th>{{ _('Points') }}</th>
<th>{{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
{% if user.profile_pic and user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" class="rounded me-2" width="32">{{ user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ user.username }}
{% endif %}
</td>
<td>{{ user.reward_points() }}</td>
<td>
<form action="{{ url_for('admin.admin_points', user_id=user.id) }}" method="post" style="display:inline;">
<input id="points" name="points" type="number" step="1" inputmode="decimal" required>
<button class="btn btn-success btn-sm" name="action" value="add" title="{{ _('Add Points') }}">
<i class="bi bi-plus-circle"></i>
</button>
<button class="btn btn-danger btn-sm" name="action" value="remove" title="{{ _('Remove Points') }}">
<i class="bi bi-dash-circle"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script src="{{ url_for('static', filename='js/adstop.js') }}"></script>
<script>
var triggerTabList = [].slice.call(document.querySelectorAll('#adminTab button'))
triggerTabList.forEach(function (triggerEl) {
var tabTrigger = new bootstrap.Tab(triggerEl)
triggerEl.addEventListener('click', function (event) {
event.preventDefault()
tabTrigger.show()
})
})
</script>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}{{ _('Set New Password') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-key me-2"></i>{{ _('Set New Password for %(username)s', username=user.username) }}</h2>
<form method="post">
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock-fill me-1"></i>{{ _('New Password') }}</label>
<input type="password" name="new_password" class="form-control" required>
</div>
<button class="btn btn-success" type="submit"><i class="bi bi-check-circle me-1"></i>{{ _('Set Password') }}</button>
</form>
{% endblock %}

113
templates/base.html Normal file
View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="{{ get_locale() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}MiniFacebook{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/translate.js') }}"></script>
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<meta name="theme-color" content="#2563eb">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/icon-192.png') }}">
<link rel="icon" href="{{ url_for('static', filename='icons/icon-192.png') }}">
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('{{ url_for('static', filename='js/sw.js') }}');
});
}
</script>
</head>
<body class="d-flex flex-column min-vh-100 {{ theme_class }}">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="bi bi-people-fill me-2"></i>MiniFacebook
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
{% if user.is_admin %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.admin') }}"><i class="bi bi-shield-lock me-1"></i>{{ _('Admin Panel') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.reset_requests') }}"><i class="bi bi-arrow-clockwise me-1"></i>{{ _('Reset Requests') }}</a></li>
{% endif %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('post.feed') }}"><i class="bi bi-house-door me-1"></i>{{ _('Feed') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('user.users') }}"><i class="bi bi-people me-1"></i>{{ _('Users') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('friend.friends') }}"><i class="bi bi-person-check me-1"></i>{{ _('Friends') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('profil.my_posts') }}"><i class="bi bi-file-earmark-text me-1"></i>{{ _('My Posts') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('notif.notifications') }}"><i class="bi bi-bell me-1"></i>{{ _('Notifications') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('profil.profile') }}"><i class="bi bi-person-circle me-1"></i>{{ _('Profile') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('log.logout') }}"><i class="bi bi-box-arrow-right me-1"></i>{{ _('Logout') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('support.support') }}"><i class="bi bi-question-circle me-1"></i>{{ _('Support') }}</a></li>
{% else %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('post.feed') }}"><i class="bi bi-house-door me-1"></i>{{ _('Feed') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('log.login') }}"><i class="bi bi-box-arrow-in-right me-1"></i>{{ _('Login') }}</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('log.register') }}"><i class="bi bi-person-plus me-1"></i>{{ _('Register') }}</a></li>
{% endif %}
<li class="nav-item">
<button id="toggle-theme" class="btn btn-outline-secondary btn-sm ms-2" type="button">
<span id="theme-icon" class="bi"></span> <span id="theme-label">{{ _('Theme') }}</span>
</button>
</li>
<li class="nav-item dropdown ms-2">
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" id="langDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<span id="lang-label">{% if get_locale() == 'de' %}Deutsch{% else %}English{% endif %}</span>
</button>
<ul class="dropdown-menu" aria-labelledby="langDropdown">
<li><a class="dropdown-item lang-select" href="#" data-lang="de">Deutsch</a></li>
<li><a class="dropdown-item lang-select" href="#" data-lang="en">English</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="container flex-grow-1">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} mt-2">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="d-flex align-items-center mb-3">
{% if user.profile_pic and user.profile_pic != 'default.png' %}
{% if SHOPITEM_ID_GOLDRAHMEN in user.shop_items | map(attribute='item_id') | list %}
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
{% else %}
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" class="profile-pic me-2">
{% endif %}
{% endif %}
{% if user.is_authenticated %}
<h1>{{ _('Welcome, %(username)s!', username=user.username) }}</h1>
{% endif %}
</div>
{% if user.is_admin %}
<p class="text-success">{{ _('You are logged in as an admin.') }}</p>
{% endif %}
{% if SHOPITEM_ID_PREMIUM in user.shop_items | map(attribute='item_id') | list %}
<span class="badge bg-warning text-dark"><i class="bi bi-star"></i> Premium</span>
{% endif %}
{% block content %}{% endblock %}
</div>
<footer class="footer mt-auto py-3">
<div class="container text-center">
<span class="text-muted">
© 2025 MiniFacebook &middot;
<a href="https://github.com/Michatec/MiniFaceBook" target="_blank" rel="noopener" class="text-decoration-none"><i class="bi bi-github m-1"></i>GitHub</a>
&middot; <a href="{{ url_for('index') }}" class="text-decoration-none">{{ _('Home') }}</a>
&middot; <a href="{{ url_for('credit.privacy_policy') }}" class="text-decoration-none">{{ _('Privacy Policy') }}</a>
&middot; <a href="{{ url_for('credit.credits') }}" class="text-decoration-none">{{ _('Credits') }}</a>
</span>
</div>
</footer>
</body>
</html>

20
templates/credits.html Normal file
View File

@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}{{ _('Credits') }}{% endblock %}
{% block content %}
<h1>{{ _('Credits') }}</h1>
<p>{{ _('This project was developed by') }} Michatec. {{ _('Special thanks to all contributors and supporters.') }}</p>
<p>{{ _('Translators:') }}</p>
<ul>
<li>{{ _('German:') }} Michatec</li>
<li>{{ _('English:') }} Michatec</li>
</ul>
<p>{{ _('Special thanks to the open-source community for their invaluable resources and tools.') }}</p>
<p>{{ _('Design inspired by various open-source projects and communities.') }}</p>
<p>{{ _('Backend powered by Flask and SQLAlchemy.') }}</p>
<p>{{ _('Frontend built with Bootstrap and jQuery.') }}</p>
<p>{{ _('Icons by FontAwesome and other open-source resources.') }}</p>
<p>{{ _('Hosted on a secure and reliable platform.') }}</p>
<p>{{ _('If you would like to contribute, please reach out to us.') }}</p>
<a href="https://github.com/Michatec/MiniFaceBook" target="_blank">{{ _('GitHub Repository') }}</a>
<p>{{ _('Thank you for using our application!') }}</p>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}{{ _('Discord Registration') }}{% endblock %}
{% block content %}
<h2>{{ _('Complete Registration') }}</h2>
<h3>{{ _('Welcome,') }} <strong>{{ username }}</strong>!</h3>
<p>{{ _('Please set a password for your account:') }}</p>
<form method="POST">
<input type="hidden" name="username" value="{{ username }}">
<input type="hidden" name="email" value="{{ email }}">
<input type="hidden" name="discord_id" value="{{ discord_id }}">
<div class="form-group">
<label for="password">{{ _('Password') }}</label>
<input type="password" name="password" class="form-control" required minlength="8">
</div>
<div class="form-group">
<label for="confirm_password">{{ _('Confirm Password') }}</label>
<input type="password" name="confirm_password" class="form-control" required minlength="8">
</div>
<button type="submit" class="btn btn-primary">{{ _('Create Account') }}</button>
<a href="{{ url_for('log.login') }}" class="btn btn-secondary">{{ _('Cancel') }}</a>
</form>
{% endblock %}

53
templates/edit_post.html Normal file
View File

@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}{{ _('Edit Post') }}{% endblock %}
{% block content %}
<h2>{{ _('Edit Post') }}</h2>
<form action="{{ url_for('post.update_post', post_id=post.id) }}" method="post" enctype="multipart/form-data" class="mb-3">
<div class="mb-3">
<label for="content" class="form-label">{{ _('Content') }}</label>
{% if not SHOPITEM_ID_EXTRA_TYPES in current_user.shop_items | map(attribute='item_id') | list %}
<p class="form-text" style="text-align: right;">{{ _('Limit: 250') }}</p>
{% else %}
<p class="form-text" style="text-align: right;">{{ _('Limit: 500') }}</p>
{% endif %}
<textarea name="content" id="content" class="form-control" required>{{ post.content }}</textarea>
</div>
<div class="mb-3">
<label for="visibility" class="form-label">{{ _('Visibility') }}</label>
<select name="visibility" id="visibility" class="form-select">
<option value="public" {% if post.visibility == 'public' %}selected{% endif %}>🌍 {{ _('Public') }}</option>
<option value="friends" {% if post.visibility == 'friends' %}selected{% endif %}>👥 {{ _('Friends only') }}</option>
</select>
</div>
<div class="mb-3">
<label for="upload" class="form-label">{{ _('Upload Files') }}</label>
<input type="file" name="upload" id="upload" class="form-control">
{% if SHOPITEM_ID_EXTRA_UPLOAD in current_user.shop_items | map(attribute='item_id') | list %}
<input type="file" name="upload2" id="upload2" class="form-control">
{% endif %}
<small class="form-text text-muted">{{ _('You can upload images, videos, audio files, or documents.') }}</small>
</div>
<button type="submit" class="btn btn-primary">{{ _('Update Post') }}</button>
<a href="{{ url_for('post.feed') }}" class="btn btn-link">{{ _('Cancel') }}</a>
</form>
{% if post.uploads %}
<div class="mb-3">
<h4>{{ _('Current Uploads') }}</h4>
{% for upload in post.uploads %}
<div class="mb-2">
{% if upload.filetype.startswith('image') %}
<img src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" class="img-thumbnail" style="max-width: 200px;">
{% elif upload.filetype.startswith('video') %}
<video src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" controls width="320"></video>
{% elif upload.filetype.startswith('audio') %}
<audio src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" controls></audio>
{% else %}
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download>{{ upload.filename }}</a>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<h2>{{ _('No uploads found for this post.') }}</h2>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}{{ _('Edit Profile') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-pencil-square me-2"></i>{{ _('Edit Profile') }}</h2>
<form method="post">
<div class="mb-3">
<label class="form-label"><i class="bi bi-person-circle me-1"></i>{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" value="{{ user.username }}" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-envelope-at me-1"></i>{{ _('Email') }}</label>
<input type="email" name="email" class="form-control" value="{{ user.email }}" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock-fill me-1"></i>{{ _('New Password (optional)') }}</label>
<input type="password" name="password" class="form-control">
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock me-1"></i>{{ _('Confirm New Password') }}</label>
<input type="password" name="confirm_password" class="form-control">
</div>
<button class="btn btn-primary" type="submit"><i class="bi bi-save me-1"></i>{{ _('Save') }}</button>
</form>
{% endblock %}

122
templates/feed.html Normal file
View File

@@ -0,0 +1,122 @@
{% extends "base.html" %}
{% block title %}{{ _('Feed') }}{% endblock %}
{% block content %}
{% if current_user.is_authenticated %}
<form action="{{ url_for('post.create_post') }}" method="post" enctype="multipart/form-data" class="mb-4">
{% if not SHOPITEM_ID_EXTRA_TYPES in current_user.shop_items | map(attribute='item_id') | list %}
<p class="form-text" style="text-align: right;">{{ _('Limit: 250') }}</p>
{% else %}
<p class="form-text" style="text-align: right;">{{ _('Limit: 500') }}</p>
{% endif %}
<textarea name="content" class="form-control" placeholder="{{ _('What\'s on your mind?') }}" required></textarea>
<input type="file" name="file">
{% if SHOPITEM_ID_EXTRA_UPLOAD in current_user.shop_items | map(attribute='item_id') | list %}
<input type="file" name="file2">
{% endif %}
<small class="form-text text-muted">{{ _('You can upload images, videos, audio files, or documents.') }}</small>
<select name="visibility" class="form-select mt-2" style="max-width:200px;display:inline-block;">
<option value="public">🌍 {{ _('Public') }}</option>
<option value="friends">👥 {{ _('Friends only') }}</option>
</select>
<button class="btn btn-primary mt-2" type="submit"><i class="bi bi-send me-1"></i>{{ _('Post') }}</button>
</form>
{% else %}
<div class="alert alert-info" role="alert">
{{ _('Please') }} <a href="{{ url_for('log.login') }}">{{ _('log in') }}</a> {{ _('to create a post.') }}
</div>
{% endif %}
{% for post in posts %}
<div class="card mb-3">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
{% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %}
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
{% else %}
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2">
{% endif %}
{% else %}
<i class="bi bi-person-circle me-2"></i>
{% endif %}
<strong>{{ post.user.username }}</strong>
</div>
<p class="card-text">{{ post.content }}</p>
{% for upload in post.uploads %}
{% if upload.filetype.startswith('image') %}
<img src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}"
class="img-fluid mb-2"
style="max-width: 200px; cursor: pointer;"
data-bs-toggle="modal"
data-bs-target="#imgModal{{ upload.id }}">
<div class="modal fade" id="imgModal{{ upload.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-transparent border-0">
<img src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" class="w-100 rounded shadow">
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-light position-absolute top-0 end-0 m-3"><i class="bi bi-download"></i> {{ _('Download') }}</a>
</div>
</div>
</div>
{% elif upload.filetype.startswith('video') %}
<video src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" controls width="320" class="mb-2"></video>
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-sm btn-outline-secondary ms-2"><i class="bi bi-download"></i> {{ _('Download Video') }}</a>
{% elif upload.filetype.startswith('audio') %}
<audio src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" controls class="mb-2"></audio>
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-sm btn-outline-secondary ms-2"><i class="bi bi-download"></i> {{ _('Download Audio') }}</a>
{% else %}
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download><i class="bi bi-paperclip"></i> {{ upload.filename }}</a>
{% endif %}
{% endfor %}
<form action="{{ url_for('like.like_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-link" type="submit"><i class="bi bi-hand-thumbs-up"></i> {{ _('Like') }} ({{ post.likes.count() }})</button>
</form>
<form action="{{ url_for('like.unlike_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-link" type="submit"><i class="bi bi-hand-thumbs-down"></i> {{ _('Unlike') }}</button>
</form>
{% if post.user_id == current_user.id %}
<form action="{{ url_for('post.delete_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" type="submit"><i class="bi bi-trash"></i> {{ _('Delete') }}</button>
</form>
<form action="{{ url_for('post.edit_post', post_id=post.id) }}" method="get" style="display:inline;">
<button class="btn btn-secondary btn-sm" type="submit"><i class="bi bi-pencil-square"></i> {{ _('Edit') }}</button>
</form>
{% endif %}
{% if post.visibility == 'friends' %}
<span class="badge bg-secondary ms">{{ _('Friends only') }}</span>
{% else %}
<span class="badge bg-success ms">{{ _('Public') }}</span>
{% endif %}
<small class="text-muted"><i class="bi bi-clock me-1"></i>{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
<div class="mt-2">
<form action="{{ url_for('post.comment_post', post_id=post.id) }}" method="post">
<input name="comment" class="form-control" placeholder="{{ _('Comment...') }}"
{% if not current_user.is_authenticated %}disabled{% endif %} required>
</form>
{% if not current_user.is_authenticated %}
<small class="text-muted">{{ _('Please login to comment.') }}</small>
{% endif %}
{% for comment in post.comments %}
<div class="mt-1">
{% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ comment.user.profile_pic) }}" class="rounded me-2" width="32"><b>{{ comment.user.username }}</b>:
{% else %}
<i class="bi bi-person me-1"></i><b>{{ comment.user.username }}</b>:
{% endif %}
{{ comment.content }}
{% if comment.user_id == current_user.id %}
<form action="{{ url_for('post.delete_comment', comment_id=comment.id) }}" method="post" style="display:inline;">
<button class="btn btn-link btn-sm text-danger"><i class="bi bi-trash"></i> {{ _('Delete') }}</button>
</form>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
{% if not posts %}
<div class="alert alert-info" role="alert">
{{ _('No posts available.') }}
</div>
{% endif %}
<script src="{{ url_for('static', filename='js/feed.js') }}"></script>
{% endblock %}

47
templates/friends.html Normal file
View File

@@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}{{ _('Friends') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-people-fill me-2"></i>{{ _('Your Friends') }}</h2>
<ul class="list-group mb-4">
{% for friend in friends %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
{% if friend.profile_pic and friend.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' + friend.profile_pic) }}" alt="{{ friend.username }}" class="profile-pic me-2">{{ friend.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ friend.username }}
{% endif %}
</span>
<form action="{{ url_for('friend.remove_friend', user_id=friend.id) }}" method="post" style="display:inline;">
<button class="btn btn-warning btn-sm"><i class="bi bi-person-dash"></i> {{ _('Remove Friend') }}</button>
</form>
</li>
{% else %}
<li class="list-group-item">{{ _('No friends yet.') }}</li>
{% endfor %}
</ul>
<h4><i class="bi bi-person-plus me-2"></i>{{ _('Friend Requests') }}</h4>
<ul class="list-group mb-3">
{% for req in requests %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
{% if req.requester.profile_pic and req.requester.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' + req.requester.profile_pic) }}" alt="{{ req.requester.username }}" class="profile-pic me-2">{{ req.requester.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ req.requester.username }}
{% endif %}
</span>
<div>
<form action="{{ url_for('friend.accept_friend', friendship_id=req.id) }}" method="post" style="display:inline;">
<button class="btn btn-success btn-sm"><i class="bi bi-check"></i> {{ _('Accept') }}</button>
</form>
<form action="{{ url_for('friend.reject_friend', friendship_id=req.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm"><i class="bi bi-x"></i> {{ _('Reject') }}</button>
</form>
</div>
</li>
{% else %}
<li class="list-group-item">{{ _('No new requests') }}</li>
{% endfor %}
</ul>
{% endblock %}

43
templates/index.html Normal file
View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}{{ _('Home') }}{% endblock %}
{% block content %}
<div class="row justify-content-center align-items-center mt-5">
<div class="col-md-8">
<div class="card shadow-lg p-4 mb-5" style="border-radius: 1.5rem;">
<div class="card-body text-center">
<h1 class="mb-3" style="font-size:2.5rem; font-weight:700; color:#2563eb;">
<i class="bi bi-people-fill me-2"></i>MiniFacebook
</h1>
<p class="lead mb-4" style="font-size:1.2rem;">
{{ _('MiniFacebook is a minimalist social network for sharing posts, images, and messages with friends.') }}<br>
<span class="text-muted">{{ _('Fast, simple, data-saving - and with') }} <i class="bi bi-moon-stars"></i> {{ _('Dark Mode!') }}</span>
</p>
{% if not current_user.is_authenticated %}
<a href="{{ url_for('log.login') }}" class="btn btn-primary m-2 px-4 py-2"><i class="bi bi-box-arrow-in-right me-1"></i>{{ _('Login') }}</a>
<a href="{{ url_for('log.register') }}" class="btn btn-success m-2 px-4 py-2"><i class="bi bi-person-plus me-1"></i>{{ _('Register') }}</a>
<a href="{{ url_for('post.feed') }}" class="btn btn-outline-info m-2 px-4 py-2"><i class="bi bi-house-door me-1"></i>{{ _('Go to Feed') }}</a>
{% else %}
<a href="{{ url_for('post.feed') }}" class="btn btn-success m-2 px-4 py-2"><i class="bi bi-house-door me-1"></i>{{ _('Go to Feed') }}</a>
{% endif %}
</div>
</div>
<div class="card shadow-sm p-4" style="border-radius: 1.5rem; background: var(--about-bg, #f8fafc);">
<div class="card-body">
<h4 class="mb-3" style="color:#2563eb;">
<i class="bi bi-info-circle"></i> {{ _('About MiniFacebook') }}
</h4>
<ul class="list-unstyled text-start mx-auto" style="max-width:420px;">
<li class="mb-2"><i class="bi bi-check-circle text-success"></i> {{ _('Share posts, images & videos') }}</li>
<li class="mb-2"><i class="bi bi-check-circle text-success"></i> {{ _('Friendships & notifications') }}</li>
<li class="mb-2"><i class="bi bi-check-circle text-success"></i> {{ _('Modern') }} <i class="bi bi-moon-stars"></i> {{ _('Dark/Light Mode') }}</li>
<li class="mb-2"><i class="bi bi-check-circle text-success"></i> {{ _('Open Source & privacy-friendly') }}</li>
</ul>
<div class="mt-4 text-muted" style="font-size:0.95rem;">
<p><b>MiniFacebook</b> {{ _('is a private, data-saving network for you and your friends. No ads, no data sharing - just fun in sharing and communicating.') }}</p>
<p>{{ _('Developed with love,') }} <i class="bi bi-bootstrap"></i> Bootstrap, <i class="bi bi-moon-stars"></i> {{ _('Dark Mode!') }}</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

33
templates/login.html Normal file
View File

@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{ _('Login') }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow p-4 mt-4" style="border-radius:1rem;">
<h2 class="mb-4 text-center"><i class="bi bi-box-arrow-in-right me-2"></i>{{ _('Login') }}</h2>
<form method="post">
<div class="mb-3">
<label class="form-label"><i class="bi bi-person-circle me-1"></i>{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock-fill me-1"></i>{{ _('Password') }}</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary" type="submit"><i class="bi bi-box-arrow-in-right me-1"></i>{{ _('Login') }}</button>
</div>
<div class="mt-3 text-center">
<a href="{{ url_for('log.register') }}" class="btn btn-link"><i class="bi bi-person-plus"></i> {{ _('Register') }}</a>
<a href="{{ url_for('log.reset_password') }}" class="btn btn-link"><i class="bi bi-key"></i> {{ _('Forgot password?') }}</a>
</div>
</form>
<div class="mt-4 text-center">
<a href="{{ url_for('discord.login_discord') }}" class="btn btn-primary">
<i class="bi bi-discord"></i> {{ _('Login with Discord') }}
</a>
</div>
</div>
</div>
</div>
{% endblock %}

91
templates/my_posts.html Normal file
View File

@@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block title %}{{ _('My Posts') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-file-earmark-text me-2"></i>{{ _('My Posts') }}</h2>
{% for post in posts %}
<div class="card mb-3">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
{% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %}
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
{% else %}
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2">
{% endif %}
{% else %}
<i class="bi bi-person-circle me-1"></i>
{% endif %}
<strong>{{ post.user.username }}</strong>
</div>
<p class="card-text">{{ post.content }}</p>
{% for upload in post.uploads %}
{% if upload.filetype.startswith('image') %}
<img src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}"
class="img-fluid mb-2"
style="max-width: 200px; cursor: pointer;"
data-bs-toggle="modal"
data-bs-target="#imgModal{{ upload.id }}">
<div class="modal fade" id="imgModal{{ upload.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-transparent border-0">
<img src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" class="w-100 rounded shadow">
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-light position-absolute top-0 end-0 m-3"><i class="bi bi-download"></i> {{ _('Download') }}</a>
</div>
</div>
</div>
{% elif upload.filetype.startswith('video') %}
<video src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" controls width="320" class="mb-2"></video>
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-sm btn-outline-secondary ms-2"><i class="bi bi-download"></i> {{ _('Download Video') }}</a>
{% elif upload.filetype.startswith('audio') %}
<audio src="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" controls class="mb-2"></audio>
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download class="btn btn-sm btn-outline-secondary ms-2"><i class="bi bi-download"></i> {{ _('Download Audio') }}</a>
{% else %}
<a href="{{ url_for('static', filename='uploads/' ~ upload.filename) }}" download><i class="bi bi-paperclip"></i> {{ upload.filename }}</a>
{% endif %}
{% endfor %}
<form action="{{ url_for('like.like_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-link" type="submit"><i class="bi bi-hand-thumbs-up"></i> {{ _('Like') }} ({{ post.likes.count() }})</button>
</form>
<form action="{{ url_for('like.unlike_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-link" type="submit"><i class="bi bi-hand-thumbs-down"></i> {{ _('Unlike') }}</button>
</form>
<form action="{{ url_for('post.delete_post', post_id=post.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" type="submit"><i class="bi bi-trash"></i> {{ _('Delete') }}</button>
</form>
<form action="{{ url_for('post.edit_post', post_id=post.id) }}" method="get" style="display:inline;">
<button class="btn btn-secondary btn-sm" type="submit"><i class="bi bi-pencil-square"></i> {{ _('Edit') }}</button>
</form>
{% if post.visibility == 'friends' %}
<span class="badge bg-secondary ms">{{ _('Friends only') }}</span>
{% else %}
<span class="badge bg-success ms">{{ _('Public') }}</span>
{% endif %}
<small class="text-muted"><i class="bi bi-clock me-1"></i>{{ post.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
<div class="mt-2">
<form action="{{ url_for('post.comment_post', post_id=post.id) }}" method="post">
<input name="comment" class="form-control" placeholder="{{ _('Comment...') }}" required>
</form>
{% for comment in post.comments %}
<div class="mt-1">
{% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ comment.user.profile_pic) }}" class="rounded me-2" width="32"><b>{{ comment.user.username }}</b>:
{% else %}
<i class="bi bi-person me-1"></i><b>{{ comment.user.username }}</b>:
{% endif %}
{% if comment.user_id == current_user.id %}
<form action="{{ url_for('post.delete_comment', comment_id=comment.id) }}" method="post" style="display:inline;">
<button class="btn btn-link btn-sm text-danger"><i class="bi bi-trash"></i> {{ _('Delete') }}</button>
</form>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
{% if not posts %}
<div class="alert alert-info" role="alert">
{{ _('No posts available.') }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}{{ _('Notifications') }}{% endblock %}
{% block content %}
<h3>
<i class="bi bi-bell me-2"></i>
{{ _('Notifications') }}
<span class="badge bg-secondary">{{ notifications|length }}</span>
{% if notifications %}
<form action="{{ url_for('notif.delete_all_notifications') }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-sm btn-outline-danger float-end">
<i class="bi bi-trash"></i> {{ _('Delete all') }}
</button>
</form>
{% endif %}
</h3>
<ul class="list-group">
{% for notif in notifications %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<i class="bi bi-info-circle text-info me-1"></i>
{{ notif.message }}
<span class="text-muted ms-2"><i class="bi bi-clock"></i> {{ notif.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</span>
<form action="{{ url_for('notif.delete_notification', notif_id=notif.id) }}" method="post" style="display:inline;">
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i> {{ _('Delete') }}</button>
</form>
</li>
{% else %}
<li class="list-group-item">{{ _('No notifications') }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}{{ _('Privacy Policy') }}{% endblock %}
{% block content %}
<h1>{{ _('Privacy Policy') }}</h1>
<p>{{ _('Your privacy is important to us. This privacy policy explains how we collect, use, and protect your information when you use our application.') }}</p>
<p>{{ _('We collect personal information that you provide to us, such as your name, email address, and any other information you choose to share.') }}</p>
<p>{{ _('We use this information to provide and improve our services, communicate with you, and personalize your experience.') }}</p>
<p>{{ _('We do not share your personal information with third parties without your consent, except as required by law or to protect our rights.') }}</p>
<p>{{ _('We implement security measures to protect your information from unauthorized access, alteration, disclosure, or destruction.') }}</p>
<p>{{ _('You have the right to access, correct, or delete your personal information at any time. Please contact us if you wish to exercise these rights.') }}</p>
<p>{{ _('We may update this privacy policy from time to time. We will notify you of any changes by posting the new privacy policy on this page.') }}</p>
<p>{{ _('By using our application, you agree to the terms of this privacy policy. If you do not agree, please do not use our application.') }}</p>
<p>{{ _('If you have any questions or concerns about this privacy policy, please contact us.') }}</p>
<a href="https://github.com/Michatec/MiniFaceBook" target="_blank">{{ _('GitHub Repository') }}</a>
<p>{{ _('Thank you for using our application!') }}</p>
{% endblock %}

32
templates/profile.html Normal file
View File

@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}{{ _('Profile') }}{% endblock %}
{% block content %}
<p><i class="bi bi-envelope-at me-1"></i>{{ _('Email') }}: {{ user.email }}</p>
<form action="{{ url_for('user.upload_pic') }}" method="post" enctype="multipart/form-data" class="mb-3">
<input type="file" name="profile_pic" required>
<button class="btn btn-secondary btn-sm" type="submit"><i class="bi bi-upload"></i> {{ _('Upload Picture') }}</button>
</form>
{% if user.profile_pic and user.profile_pic != 'default.png' %}
<form action="{{ url_for('user.delete_pic') }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm"><i class="bi bi-trash"></i> {{ _('Delete Picture') }}</button>
</form>
{% endif %}
<a href="{{ url_for('shop') }}" class="btn btn-secondary"><i class="bi bi-shop"></i> {{ _('Shop') }}</a>
<a href="{{ url_for('profil.edit_profile') }}" class="btn btn-secondary"><i class="bi bi-pencil-square"></i> {{ _('Edit Profile') }}</a>
<form action="{{ url_for('user.delete_account') }}" method="post" style="display:inline;">
<button class="btn btn-danger"><i class="bi bi-person-x"></i> {{ _('Delete Account') }}</button>
</form>
{% if not user.discord_linked %}
<a href="{{ url_for('discord.link_discord') }}" class="btn btn-primary">
<i class="bi bi-discord"></i> {{ _('Link Discord Account') }}
</a>
{% else %}
<span class="badge bg-success"><i class="bi bi-discord"></i> {{ _('Discord Linked') }}</span>
<form action="{{ url_for('discord.unlink_discord') }}" method="post" style="display:inline;">
<button class="btn btn-warning btn-sm" type="submit">
<i class="bi bi-x-circle"></i> {{ _('Unlink Discord') }}
</button>
</form>
{% endif %}
{% endblock %}

41
templates/register.html Normal file
View File

@@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block title %}{{ _('Register') }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow p-4 mt-4" style="border-radius:1rem;">
<h2 class="mb-4 text-center"><i class="bi bi-person-plus me-2"></i>{{ _('Register') }}</h2>
<form method="post">
<div class="mb-3">
<label class="form-label"><i class="bi bi-person-circle me-1"></i>{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-envelope-at me-1"></i>{{ _('Email') }}</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock-fill me-1"></i>{{ _('Password') }}</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock me-1"></i>{{ _('Confirm Password') }}</label>
<input type="password" name="confirm_password" class="form-control" required>
</div>
<div class="d-grid gap-2">
<button class="btn btn-success" type="submit"><i class="bi bi-person-plus me-1"></i>{{ _('Register') }}</button>
</div>
<div class="mt-3 text-center">
<span>{{ _('Already have an account?') }}</span>
<a href="{{ url_for('log.login') }}" class="btn btn-link"><i class="bi bi-box-arrow-in-right"></i> {{ _('Login') }}</a>
</div>
<div class="mt-4 text-center">
<a href="{{ url_for('discord.login_discord') }}" class="btn btn-primary">
<i class="bi bi-discord"></i> {{ _('Login with Discord') }}
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}{{ _('Reset Password') }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow p-4 mt-4" style="border-radius:1rem;">
<h2 class="mb-4 text-center"><i class="bi bi-key me-2"></i>{{ _('Reset Password') }}</h2>
<form method="post">
<div class="mb-3">
<label class="form-label" for="username"><i class="bi bi-person-circle me-1"></i>{{ _('Username') }}</label>
<input type="text" name="username" id="username" class="form-control" required>
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary" type="submit"><i class="bi bi-arrow-clockwise me-1"></i>{{ _('Request Reset') }}</button>
</div>
<div class="mt-3 text-center">
<a href="{{ url_for('log.login') }}" class="btn btn-link"><i class="bi bi-box-arrow-in-right"></i> {{ _('Login') }}</a>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,90 @@
{% extends "base.html" %}
{% block title %}{{ _('Password Reset Requests') }}{% endblock %}
{% block content %}
<h2>
<i class="bi bi-arrow-clockwise me-2"></i>{{ _('Password Reset Requests') }}
<a href="{{ url_for('admin.admin_delete_all_reset_requests') }}" class="btn btn-danger btn-sm float-end"><i class="bi bi-trash"></i> {{ _('Delete All') }}</a>
</h2>
<table class="table">
<thead>
<tr>
<th><i class="bi bi-person-circle"></i> {{ _('User') }}</th>
<th><i class="bi bi-clock"></i> {{ _('Requested At') }}</th>
<th><i class="bi bi-gear"></i> {{ _('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for req in requests %}
<tr>
<td>
{% if req.user.profile_pic and req.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ req.user.profile_pic) }}" class="rounded me-2" width="32">{{ req.user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ req.user.username }}
{% endif %}
</td>
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('admin.admin_reset_password', req_id=req.id) }}" class="btn btn-success btn-sm"><i class="bi bi-key"></i> {{ _('Set Password') }}</a>
<form action="{{ url_for('admin.reject_reset_request', req_id=req.id) }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" type="submit"><i class="bi bi-x"></i> {{ _('Reject') }}</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if requests_done %}
<h3><i class="bi bi-check-circle me-2"></i>{{ _('Completed Requests') }}</h3>
<table class="table">
<thead>
<tr>
<th><i class="bi bi-person-circle"></i> {{ _('User') }}</th>
<th><i class="bi bi-check-circle"></i> {{ _('Requested At') }}</th>
</tr>
</thead>
<tbody>
{% for req in requests_done %}
<tr>
<td>
{% if req.user.profile_pic and req.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ req.user.profile_pic) }}" class="rounded me-2" width="32">{{ req.user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ req.user.username }}
{% endif %}
</td>
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if requests_rejected %}
<h3><i class="bi bi-x-circle me-2"></i>{{ _('Rejected Requests') }}</h3>
<table class="table">
<thead>
<tr>
<th><i class="bi bi-person-circle"></i> {{ _('User') }}</th>
<th><i class="bi bi-x-circle"></i> {{ _('Requested At') }}</th>
</tr>
</thead>
<tbody>
{% for req in requests_rejected %}
<tr>
<td>
{% if req.user.profile_pic and req.user.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' ~ req.user.profile_pic) }}" class="rounded me-2" width="32">{{ req.user.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ req.user.username }}
{% endif %}
</td>
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if not requests %}
<div class="alert alert-info"><i class="bi bi-info-circle"></i> {{ _('No open reset requests.') }}</div>
{% endif %}
{% endblock %}

294
templates/secret.html Normal file
View File

@@ -0,0 +1,294 @@
{% extends 'base.html' %}
{% block title %}{{ _('Secret') }}{% endblock %}
{% block content %}
<h1>Secret</h1>
<canvas width="320" height="640" id="game"></canvas>
<script>
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// generate a new tetromino sequence
// @see https://tetris.fandom.com/wiki/Random_Generator
function generateSequence() {
const sequence = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'];
while (sequence.length) {
const rand = getRandomInt(0, sequence.length - 1);
const name = sequence.splice(rand, 1)[0];
tetrominoSequence.push(name);
}
}
// get the next tetromino in the sequence
function getNextTetromino() {
if (tetrominoSequence.length === 0) {
generateSequence();
}
const name = tetrominoSequence.pop();
const matrix = tetrominos[name];
// I and O start centered, all others start in left-middle
const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);
// I starts on row 21 (-1), all others start on row 22 (-2)
const row = name === 'I' ? -1 : -2;
return {
name: name, // name of the piece (L, O, etc.)
matrix: matrix, // the current rotation matrix
row: row, // current row (starts offscreen)
col: col // current col
};
}
// rotate an NxN matrix 90deg
// @see https://codereview.stackexchange.com/a/186834
function rotate(matrix) {
const N = matrix.length - 1;
const result = matrix.map((row, i) =>
row.map((val, j) => matrix[N - j][i])
);
return result;
}
// check to see if the new matrix/row/col is valid
function isValidMove(matrix, cellRow, cellCol) {
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] && (
// outside the game bounds
cellCol + col < 0 ||
cellCol + col >= playfield[0].length ||
cellRow + row >= playfield.length ||
// collides with another piece
playfield[cellRow + row][cellCol + col])
) {
return false;
}
}
}
return true;
}
// place the tetromino on the playfield
function placeTetromino() {
for (let row = 0; row < tetromino.matrix.length; row++) {
for (let col = 0; col < tetromino.matrix[row].length; col++) {
if (tetromino.matrix[row][col]) {
// game over if piece has any part offscreen
if (tetromino.row + row < 0) {
return showGameOver();
}
playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
}
}
}
// check for line clears starting from the bottom and working our way up
for (let row = playfield.length - 1; row >= 0; ) {
if (playfield[row].every(cell => !!cell)) {
// drop every row above this one
for (let r = row; r >= 0; r--) {
for (let c = 0; c < playfield[r].length; c++) {
playfield[r][c] = playfield[r-1][c];
}
}
}
else {
row--;
}
}
tetromino = getNextTetromino();
}
// show the game over screen
function showGameOver() {
cancelAnimationFrame(rAF);
gameOver = true;
context.fillStyle = 'black';
context.globalAlpha = 0.75;
context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);
context.globalAlpha = 1;
context.fillStyle = 'white';
context.font = '36px monospace';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);
}
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 32;
const tetrominoSequence = [];
// keep track of what is in every cell of the game using a 2d array
// tetris playfield is 10x20, with a few rows offscreen
const playfield = [];
// populate the empty state
for (let row = -2; row < 20; row++) {
playfield[row] = [];
for (let col = 0; col < 10; col++) {
playfield[row][col] = 0;
}
}
// how to draw each tetromino
// @see https://tetris.fandom.com/wiki/SRS
const tetrominos = {
'I': [
[0,0,0,0],
[1,1,1,1],
[0,0,0,0],
[0,0,0,0]
],
'J': [
[1,0,0],
[1,1,1],
[0,0,0],
],
'L': [
[0,0,1],
[1,1,1],
[0,0,0],
],
'O': [
[1,1],
[1,1],
],
'S': [
[0,1,1],
[1,1,0],
[0,0,0],
],
'Z': [
[1,1,0],
[0,1,1],
[0,0,0],
],
'T': [
[0,1,0],
[1,1,1],
[0,0,0],
]
};
// color of each tetromino
const colors = {
'I': 'cyan',
'O': 'yellow',
'T': 'purple',
'S': 'green',
'Z': 'red',
'J': 'blue',
'L': 'orange'
};
let count = 0;
let tetromino = getNextTetromino();
let rAF = null; // keep track of the animation frame so we can cancel it
let gameOver = false;
// game loop
function loop() {
rAF = requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// draw the playfield
for (let row = 0; row < 20; row++) {
for (let col = 0; col < 10; col++) {
if (playfield[row][col]) {
const name = playfield[row][col];
context.fillStyle = colors[name];
// drawing 1 px smaller than the grid creates a grid effect
context.fillRect(col * grid, row * grid, grid-1, grid-1);
}
}
}
// draw the active tetromino
if (tetromino) {
// tetromino falls every 35 frames
if (++count > 35) {
tetromino.row++;
count = 0;
// place piece if it runs into anything
if (!isValidMove(tetromino.matrix, tetromino.row, tetromino.col)) {
tetromino.row--;
placeTetromino();
}
}
context.fillStyle = colors[tetromino.name];
for (let row = 0; row < tetromino.matrix.length; row++) {
for (let col = 0; col < tetromino.matrix[row].length; col++) {
if (tetromino.matrix[row][col]) {
// drawing 1 px smaller than the grid creates a grid effect
context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);
}
}
}
}
}
// listen to keyboard events to move the active tetromino
document.addEventListener('keydown', function(e) {
if (gameOver) return;
// left and right arrow keys (move)
if (e.which === 37 || e.which === 39) {
const col = e.which === 37
? tetromino.col - 1
: tetromino.col + 1;
if (isValidMove(tetromino.matrix, tetromino.row, col)) {
tetromino.col = col;
}
}
// up arrow key (rotate)
if (e.which === 38) {
const matrix = rotate(tetromino.matrix);
if (isValidMove(matrix, tetromino.row, tetromino.col)) {
tetromino.matrix = matrix;
}
}
// down arrow key (drop)
if(e.which === 40) {
const row = tetromino.row + 1;
if (!isValidMove(tetromino.matrix, row, tetromino.col)) {
tetromino.row = row - 1;
placeTetromino();
return;
}
tetromino.row = row;
}
});
// start the game
rAF = requestAnimationFrame(loop);
</script>
{% endblock %}

24
templates/setup.html Normal file
View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin Setup') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-person-gear me-2"></i>{{ _('Admin Account Setup') }}</h2>
<form method="post">
<div class="mb-3">
<label class="form-label"><i class="bi bi-person-circle me-1"></i>{{ _('Username') }}</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-envelope-at me-1"></i>{{ _('Email') }}</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock-fill me-1"></i>{{ _('Password') }}</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-lock me-1"></i>{{ _('Confirm Password') }}</label>
<input type="password" name="confirm_password" class="form-control" required>
</div>
<button class="btn btn-primary" type="submit"><i class="bi bi-person-gear me-1"></i>{{ _('Create Admin') }}</button>
</form>
{% endblock %}

33
templates/shop.html Normal file
View File

@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}{{ _('Shop') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-shop me-2"></i>{{ _('Shop') }}</h2>
<p>{{ _('Deine Reward-Punkte:') }} <b>{{ current_user.reward_points() }}</b></p>
{% if message %}
<div class="alert alert-info">{{ message }}</div>
{% endif %}
<div class="row">
{% for item in items %}
<div class="col-md-4 mb-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi {{ item.icon }} display-4 mb-2"></i>
<h5>{{ item.name }}</h5>
<p>{{ item.description }}</p>
<p><b>{{ item.price }}</b> {{ _('Points') }}</p>
{% if item.id in owned_ids %}
<button class="btn btn-secondary" disabled>{{ _('Bought') }}</button>
{% else %}
<form method="post">
<input type="hidden" name="item_id" value="{{ item.id }}">
<button class="btn btn-success" {% if current_user.reward_points() < item.price %}disabled{% endif %}>
<i class="bi bi-cart"></i> {{ _('Buy') }}
</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

49
templates/support.html Normal file
View File

@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block title %}{{ _('Support') }}{% endblock %}
{% block content %}
<div class="container">
<p>{{ _('If you have any questions or need assistance, please contact us at:') }}</p>
<p><strong>{{ _('Github:') }}</strong> <a href="https://github.com/Michatec/MiniFaceBook/issues" target="_blank">https://github.com/Michatec/MiniFaceBook/issues</a></p>
</div>
{% if user.is_owner %}
<form action="{{ url_for('admin.wipe_server') }}" method="post" style="display:inline;">
<button class="btn btn-danger btn-sm" type="submit">
<i class="bi bi-x-circle"></i> {{ _('Wipe Server') }}
</button>
</form>
{% endif %}
<h1><i class="bi bi-question-circle me-2"></i>{{ _('Support') }}</h1>
<form method="POST" class="mb-4">
<input type="text" name="title" placeholder="{{ _('Title') }}" required class="form-control mb-2">
<textarea name="description" placeholder="{{ _('Describe your issue...') }}" required class="form-control mb-2"></textarea>
<button class="btn btn-primary">{{ _('Create Ticket') }}</button>
</form>
<table class="table">
<thead>
<tr>
<th>{{ _('Title') }}</th>
<th>{{ _('Status') }}</th>
<th>{{ _('Created') }}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for ticket in support_requests %}
<tr>
<td>{{ ticket.title }}</td>
<td>
{% if ticket.status == 'open' %}
<span class="badge bg-success">{{ _('Open') }}</span>
{% else %}
<span class="badge bg-secondary">{{ _('Closed') }}</span>
{% endif %}
</td>
<td>{{ ticket.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('support.support_thread', request_id=ticket.id) }}" class="btn btn-sm btn-info">{{ _('View') }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}{{ ticket.title }}{% endblock %}
{% block content %}
<h2>{{ ticket.title }}</h2>
<p><strong>{{ _('Status') }}:</strong>
{% if ticket.status == 'open' %}
<span class="badge bg-success">{{ _('Open') }}</span>
{% else %}
<span class="badge bg-secondary">{{ _('Closed') }}</span>
{% endif %}
</p>
<div class="mb-3">
{% for comment in comments %}
<div class="border rounded p-2 mb-2">
<strong>{{ comment.user.username }}</strong>
<span class="text-muted">{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<p>{{ comment.message }}</p>
</div>
{% endfor %}
</div>
{% if ticket.status == 'open' %}
<form method="POST">
<textarea name="message" class="form-control mb-2" placeholder="{{ _('Write a message...') }}" required></textarea>
<button class="btn btn-primary">{{ _('Send') }}</button>
</form>
<form method="POST" action="{{ url_for('support.support_close', request_id=ticket.id) }}">
<button class="btn btn-warning mt-2">{{ _('Close Ticket') }}</button>
</form>
{% else %}
<div class="alert alert-info">{{ _('This ticket is closed.') }}</div>
{% endif %}
{% if user.is_admin %}
<form method="POST" action="{{ url_for('support.support_delete', request_id=ticket.id) }}" style="display:inline;">
<button class="btn btn-danger btn-sm" onclick="return confirm('Delete this ticket?')">
<i class="bi bi-trash"></i> {{ _('Delete') }}
</button>
</form>
{% endif %}
<a href="{{ url_for('support.support') }}" class="btn btn-secondary mt-3">{{ _('Back to Support') }}</a>
{% endblock %}

46
templates/users.html Normal file
View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}{{ _('Users') }}{% endblock %}
{% block content %}
<h2><i class="bi bi-people me-2"></i>{{ _('All Users') }}</h2>
<ul class="list-group">
{% for user_item in users %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
{% if user_item.profile_pic and user_item.profile_pic != 'default.png' %}
<img src="{{ url_for('static', filename='profile_pics/' + user_item.profile_pic) }}" alt="{{ user_item.username }}" class="profile-pic me-2">{{ user_item.username }}
{% else %}
<i class="bi bi-person-circle me-1"></i>{{ user_item.username }}
{% endif %}
</span>
<div>
{% if user_item.id != user.id %}
{% set friendship = (user.friendships_sent | selectattr('receiver_id', 'equalto', user_item.id) | list) %}
{% set reverse_friendship = (user.friendships_received | selectattr('requester_id', 'equalto', user_item.id) | list) %}
{% if friendship and friendship[0].status == 'pending' %}
<span class="badge bg-warning"><i class="bi bi-hourglass-split me-1"></i>{{ _('Request sent') }}</span>
{% elif reverse_friendship and reverse_friendship[0].status == 'pending' %}
<span class="badge bg-info"><i class="bi bi-envelope-open me-1"></i>{{ _('Request received') }}</span>
{% elif (friendship and friendship[0].status == 'accepted') or (reverse_friendship and reverse_friendship[0].status == 'accepted') %}
<span class="badge bg-success"><i class="bi bi-person-check me-1"></i>{{ _('Friend') }}</span>
{% else %}
<form action="{{ url_for('friend.add_friend', user_id=user_item.id) }}" method="post" style="display:inline;">
<button class="btn btn-sm btn-primary"><i class="bi bi-person-plus"></i> {{ _('Add Friend') }}</button>
</form>
{% endif %}
{% endif %}
{% if user.is_admin %}
{% if not user_item.is_admin %}
<form action="{{ url_for('admin.make_admin', user_id=user_item.id) }}" method="post" style="display:inline;">
<button class="btn btn-sm btn-success"><i class="bi bi-person-gear"></i> {{ _('Make Admin') }}</button>
</form>
{% elif not user_item.is_owner %}
<form action="{{ url_for('admin.remove_admin', user_id=user_item.id) }}" method="post" style="display:inline;">
<button class="btn btn-sm btn-warning"><i class="bi bi-person-dash"></i> {{ _('Remove Admin') }}</button>
</form>
{% endif %}
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% endblock %}