mirror of
https://github.com/Michatec/MiniFaceBook.git
synced 2026-05-31 02:12:40 +02:00
Compare commits
28 Commits
1.1
...
5febf7e64d
| Author | SHA1 | Date | |
|---|---|---|---|
| 5febf7e64d | |||
| 7f8948bba9 | |||
| 2b93ae73ff | |||
| b376311cff | |||
| 72c9ff6601 | |||
| d8ba367b0d | |||
| a5766c510d | |||
| b3e9fd9819 | |||
| 7811396791 | |||
| 1ac918496d | |||
| 803d3ca360 | |||
| c7b18d76ef | |||
| 1c05248829 | |||
| 3332a9ca7c | |||
| df8ee7703d | |||
| 0e9024949b | |||
| 2cff51e779 | |||
| af8b69989c | |||
| 1fd5cddd3c | |||
| 1429e50b2b | |||
| 2ef98ce897 | |||
| 858c98412f | |||
| 77e46d03c7 | |||
| 986a1a2a25 | |||
| 0e03aa9c33 | |||
| 1b5976a190 | |||
| 0fd32dc2b8 | |||
| 1a94d52d61 |
@@ -0,0 +1,3 @@
|
|||||||
|
# You can change this file, to your preferred Settings.
|
||||||
|
|
||||||
|
PORT=YOUR PORT
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
+2
-4
@@ -8,14 +8,12 @@ migrations
|
|||||||
*.log
|
*.log
|
||||||
*.db
|
*.db
|
||||||
*.env
|
*.env
|
||||||
*.DS_Store
|
|
||||||
.vscode
|
.vscode
|
||||||
routes/__pycache__
|
routes/__pycache__
|
||||||
tools
|
|
||||||
*.pot
|
*.pot
|
||||||
*.mo
|
*.mo
|
||||||
routes/oauth.py
|
routes/oauth.py
|
||||||
static/profile_pics
|
static/profile_pics
|
||||||
static/uploads
|
static/uploads
|
||||||
commands.txt
|
venv/
|
||||||
py-to-exemfc.json
|
.env
|
||||||
@@ -7,10 +7,12 @@ MiniFacebook is a minimalist social network built with [Flask](https://flask.pal
|
|||||||
- Share posts, images, videos, and documents
|
- Share posts, images, videos, and documents
|
||||||
- Friend requests and friends list
|
- Friend requests and friends list
|
||||||
- Activity notifications
|
- Activity notifications
|
||||||
|
- Realtime notifications
|
||||||
- Shop for premium features (e.g., gold frames, extra uploads)
|
- Shop for premium features (e.g., gold frames, extra uploads)
|
||||||
- Admin panel with user management
|
- Admin panel with user management
|
||||||
- Multilingual (German/English)
|
- Multilingual (German/English)
|
||||||
- Dark and light mode
|
- Dark and light mode
|
||||||
|
- Gravatar Addon
|
||||||
- Discord login and linking
|
- Discord login and linking
|
||||||
- Support ticket system
|
- Support ticket system
|
||||||
- Password reset via email
|
- Password reset via email
|
||||||
@@ -31,18 +33,27 @@ MiniFacebook is a minimalist social network built with [Flask](https://flask.pal
|
|||||||
pip install -r requirments.txt
|
pip install -r requirments.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start**
|
3. **Setup the .env file**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mv .env_example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
And you can change this file, to your preferred Settings.
|
||||||
|
⚠️ You need to set up a PORT
|
||||||
|
|
||||||
|
4. **Start**
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
4. **Optional:**
|
5. **Optional:**
|
||||||
|
|
||||||
Go to routes/example oauth.py
|
Go to routes/example_oauth.py
|
||||||
|
|
||||||
Paste Your Client ID and Client Secret from the Discord Dev portal.
|
Paste Your Client ID and Client Secret from the Discord Dev portal.
|
||||||
|
|
||||||
And rename it oauth.py
|
And rename it to oauth.py
|
||||||
|
|
||||||
|
|
||||||
## Help to translate
|
## Help to translate
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, request, render_template, redirect, url_for, flash, abort, jsonify
|
from flask import Flask, request, render_template, redirect, url_for, flash, abort, jsonify, current_app, session, has_request_context
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_login import LoginManager, login_required, current_user
|
from flask_login import LoginManager, login_required, current_user
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
@@ -15,13 +15,19 @@ from routes.user import user_bp
|
|||||||
from routes.friends import friends_bp
|
from routes.friends import friends_bp
|
||||||
from routes.notifications import noti_bp
|
from routes.notifications import noti_bp
|
||||||
from routes.credits import credits_bp
|
from routes.credits import credits_bp
|
||||||
from models import db, User, Reward, Event, UserShopItem, ShopItem, SHOPITEM_ID_PREMIUM, SHOPITEM_ID_GOLDRAHMEN, SHOPITEM_ID_EXTRA_TYPES, SHOPITEM_ID_EXTRA_UPLOAD
|
from models import db, User, Reward, UserShopItem, ShopItem, SHOPITEM_ID_PREMIUM, SHOPITEM_ID_GOLDRAHMEN, SHOPITEM_ID_EXTRA_TYPES, SHOPITEM_ID_EXTRA_UPLOAD
|
||||||
try:
|
try:
|
||||||
from routes.oauth import oauth
|
from routes.oauth import oauth
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import os
|
import os, sys
|
||||||
|
|
||||||
|
logger = logging.getLogger('waitress')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||||
|
|
||||||
__mapper_args__ = {"confirm_deleted_rows": False}
|
__mapper_args__ = {"confirm_deleted_rows": False}
|
||||||
|
|
||||||
@@ -66,40 +72,12 @@ app.register_blueprint(friends_bp)
|
|||||||
app.register_blueprint(noti_bp)
|
app.register_blueprint(noti_bp)
|
||||||
app.register_blueprint(credits_bp)
|
app.register_blueprint(credits_bp)
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
if db.session.query(ShopItem).count() == 0:
|
|
||||||
db.session.add(ShopItem(
|
|
||||||
name="Premium Account",
|
|
||||||
description="Exclusive features and content.",
|
|
||||||
price=100,
|
|
||||||
icon="bi-star"
|
|
||||||
))
|
|
||||||
db.session.add(ShopItem(
|
|
||||||
name="Gold Profile Frame",
|
|
||||||
description="Adds a golden profile frame to your profile.",
|
|
||||||
price=50,
|
|
||||||
icon="bi-person-bounding-box"
|
|
||||||
))
|
|
||||||
db.session.add(ShopItem(
|
|
||||||
name="Extra Upload Slot",
|
|
||||||
description="Become able to upload more files.",
|
|
||||||
price=130,
|
|
||||||
icon="bi-cloud-upload"
|
|
||||||
))
|
|
||||||
db.session.add(ShopItem(
|
|
||||||
name="More Types",
|
|
||||||
description="More types for your posts. Limit: 500 types per post.",
|
|
||||||
price=80,
|
|
||||||
icon="bi-megaphone"
|
|
||||||
))
|
|
||||||
db.session.commit()
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_locale():
|
def get_locale():
|
||||||
|
if has_request_context():
|
||||||
lang = request.cookies.get('lang')
|
lang = request.cookies.get('lang')
|
||||||
if lang in ['de', 'en']:
|
if lang in ['de', 'en']:
|
||||||
return lang
|
return lang
|
||||||
|
return None
|
||||||
|
|
||||||
babel.init_app(app, locale_selector=get_locale)
|
babel.init_app(app, locale_selector=get_locale)
|
||||||
|
|
||||||
@@ -110,6 +88,7 @@ def needs_admin_setup():
|
|||||||
def inject_discord_available():
|
def inject_discord_available():
|
||||||
try:
|
try:
|
||||||
from routes.oauth import discord
|
from routes.oauth import discord
|
||||||
|
return dict(discord=discord)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return dict(discord=None)
|
return dict(discord=None)
|
||||||
|
|
||||||
@@ -184,38 +163,51 @@ def setup():
|
|||||||
return redirect(url_for('log.login'))
|
return redirect(url_for('log.login'))
|
||||||
return render_template('setup.html')
|
return render_template('setup.html')
|
||||||
|
|
||||||
@app.route('/api/events')
|
|
||||||
@login_required
|
|
||||||
def api_events():
|
|
||||||
if not current_user.is_admin:
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
events = db.session.query(Event).order_by(Event.timestamp.desc()).limit(20).all()
|
|
||||||
return jsonify([
|
|
||||||
{"timestamp": e.timestamp.strftime('%Y-%m-%d %H:%M'), "message": e.message}
|
|
||||||
for e in events
|
|
||||||
])
|
|
||||||
|
|
||||||
@app.route('/shop', methods=['GET', 'POST'])
|
@app.route('/shop', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def shop():
|
def shop():
|
||||||
items = db.session.query(ShopItem).all()
|
if 'shop_csrf_token' not in session:
|
||||||
message = None
|
session['shop_csrf_token'] = os.urandom(24).hex()
|
||||||
|
csrf_token_value = session['shop_csrf_token']
|
||||||
|
|
||||||
|
items = db.session.query(ShopItem).order_by(ShopItem.price.asc()).all()
|
||||||
owned_ids = [usi.item_id for usi in current_user.shop_items]
|
owned_ids = [usi.item_id for usi in current_user.shop_items]
|
||||||
|
user_points = current_user.reward_points()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
if request.form.get('csrf_token') != session.get('shop_csrf_token'):
|
||||||
|
logger.warning(f"CSRF token mismatch for user {current_user.id} in shop")
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
try:
|
||||||
item_id = int(request.form['item_id'])
|
item_id = int(request.form['item_id'])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
flash(_('Invalid item selected.'), 'danger')
|
||||||
|
return redirect(url_for('shop'))
|
||||||
|
|
||||||
item = db.session.get(ShopItem, item_id)
|
item = db.session.get(ShopItem, item_id)
|
||||||
if item_id in owned_ids:
|
|
||||||
message = _("Already purchased!")
|
if not item:
|
||||||
elif item and current_user.reward_points() >= item.price:
|
flash(_('Item not found.'), 'danger')
|
||||||
|
elif item_id in owned_ids:
|
||||||
|
flash(_('Already purchased!'), 'warning')
|
||||||
|
elif user_points < item.price:
|
||||||
|
flash(_('Not enough points! You need %(needed)d more.', needed=item.price - user_points), 'danger')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
db.session.add(Reward(user_id=current_user.id, type=f'buy_{item.name}', points=-item.price))
|
db.session.add(Reward(user_id=current_user.id, type=f'buy_{item.name}', points=-item.price))
|
||||||
db.session.add(UserShopItem(user_id=current_user.id, item_id=item.id))
|
db.session.add(UserShopItem(user_id=current_user.id, item_id=item.id))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
message = _(f"Purchased: {item.name}")
|
session['shop_csrf_token'] = os.urandom(24).hex()
|
||||||
owned_ids.append(item_id)
|
flash(_('Successfully purchased: %(item_name)s', item_name=item.name), 'success')
|
||||||
else:
|
return redirect(url_for('shop'))
|
||||||
message = _("Not enough points!")
|
except Exception as e:
|
||||||
return render_template('shop.html', items=items, message=message, owned_ids=owned_ids)
|
db.session.rollback()
|
||||||
|
logger.error(f"Shop purchase failed for user {current_user.id}: {e}")
|
||||||
|
flash(_('Purchase failed. Please try again.'), 'danger')
|
||||||
|
|
||||||
|
return render_template('shop.html', items=items, owned_ids=owned_ids,
|
||||||
|
user_points=user_points, csrf_token_value=csrf_token_value)
|
||||||
|
|
||||||
@app.errorhandler(403)
|
@app.errorhandler(403)
|
||||||
def forbidden(error):
|
def forbidden(error):
|
||||||
@@ -228,13 +220,85 @@ def not_found(error):
|
|||||||
return redirect(url_for('post.feed'))
|
return redirect(url_for('post.feed'))
|
||||||
return render_template('index.html'), 200
|
return render_template('index.html'), 200
|
||||||
|
|
||||||
@app.route('/secret')
|
def print_error(message):
|
||||||
@login_required
|
print(f"\033[91m[ERROR] {message}\033[0m")
|
||||||
def secret():
|
|
||||||
return render_template('secret.html')
|
def print_loading(message):
|
||||||
|
colors = ["\033[91m", "\033[93m", "\033[92m", "\033[96m", "\033[94m", "\033[95m"]
|
||||||
|
color = colors[hash(message) % len(colors)]
|
||||||
|
print(f"{color}◆ {message}...\033[0m")
|
||||||
|
|
||||||
|
|
||||||
|
def print_success(message):
|
||||||
|
colors = ["\033[92m", "\033[96m", "\033[94m", "\033[95m"]
|
||||||
|
color = colors[hash(message) % len(colors)]
|
||||||
|
print(f"{color}✓ {message}\033[0m")
|
||||||
|
|
||||||
|
def print_rainbow_separator():
|
||||||
|
rainbow = "\033[91m▆\033[93m▆\033[92m▆\033[96m▆\033[94m▆\033[95m▆\033[0m"
|
||||||
|
print(f" {rainbow * 12}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
print_loading("Starting MiniFaceBook...")
|
||||||
|
print_rainbow_separator()
|
||||||
|
print_loading("Initializing database")
|
||||||
try:
|
try:
|
||||||
serve(app, host="0.0.0.0", port=80, threads=12)
|
with app.app_context():
|
||||||
|
if db.session.query(ShopItem).count() == 0:
|
||||||
|
shop_items = [
|
||||||
|
{
|
||||||
|
'name': _('Premium Account'),
|
||||||
|
'description': _('Exclusive features and content.'),
|
||||||
|
'price': 100,
|
||||||
|
'icon': 'bi-star'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': _('Gold Profile Frame'),
|
||||||
|
'description': _('Adds a golden profile frame to your profile.'),
|
||||||
|
'price': 50,
|
||||||
|
'icon': 'bi-person-bounding-box'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': _('Extra Upload Slot'),
|
||||||
|
'description': _('Become able to upload more files.'),
|
||||||
|
'price': 130,
|
||||||
|
'icon': 'bi-cloud-upload'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': _('More Types'),
|
||||||
|
'description': _('More types for your posts. Limit: 500 types per post.'),
|
||||||
|
'price': 80,
|
||||||
|
'icon': 'bi-megaphone'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for item_data in shop_items:
|
||||||
|
item = ShopItem(**item_data)
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
print_success("Database initialized successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Database initialization failed: {e}")
|
||||||
|
print_error("Please check your database configuration and ensure the database is accessible.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print_loading("Loading environment variables")
|
||||||
|
try:
|
||||||
|
load_dotenv()
|
||||||
|
port = os.environ.get('PORT')
|
||||||
|
print_success("Environment variables loaded successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Failed to load environment variables: {e}")
|
||||||
|
print_error("Please set the environment variables!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print_loading(f"Using port {port}")
|
||||||
|
print_rainbow_separator()
|
||||||
|
print_loading("Starting server with Waitress")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print_success(f"Server started successfully with Waitress at the port {port}.")
|
||||||
|
serve(app, host="0.0.0.0", port=port, threads=12, connection_limit=1000)
|
||||||
except:
|
except:
|
||||||
app.run(debug=True, host="0.0.0.0", port=80)
|
print_error(f"Failed to start with Waitress, falling back to Flask's built-in server at port {port}. This is not recommended for production use.")
|
||||||
|
app.run(debug=True, host="0.0.0.0", port=port)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from hashlib import md5
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
@@ -39,6 +40,10 @@ class User(UserMixin, db.Model):
|
|||||||
def reward_points(self):
|
def reward_points(self):
|
||||||
return sum(r.points for r in self.rewards)
|
return sum(r.points for r in self.rewards)
|
||||||
|
|
||||||
|
def avatar(self):
|
||||||
|
digest = md5(self.email.lower().encode('utf-8')).hexdigest()
|
||||||
|
return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s=120'
|
||||||
|
|
||||||
class Friendship(db.Model):
|
class Friendship(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
requester_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
requester_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
@@ -115,6 +120,8 @@ class UserShopItem(db.Model):
|
|||||||
bought_at = db.Column(db.DateTime, default=datetime.now)
|
bought_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
item = db.relationship('ShopItem')
|
item = db.relationship('ShopItem')
|
||||||
|
|
||||||
|
__table_args__ = (db.UniqueConstraint('user_id', 'item_id', name='unique_user_item_purchase'),)
|
||||||
|
|
||||||
class SupportRequest(db.Model):
|
class SupportRequest(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ flask_babel
|
|||||||
waitress
|
waitress
|
||||||
authlib
|
authlib
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
requests
|
||||||
|
dotenv
|
||||||
+12
-12
@@ -113,7 +113,7 @@ def admin_delete_post(post_id):
|
|||||||
db.session.add(notification)
|
db.session.add(notification)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Post and associated files deleted.'), 'success')
|
flash(_('Post and associated files deleted.'), 'success')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#posts')
|
||||||
|
|
||||||
@admin_bp.route('/delete_user/<int:user_id>', methods=['POST'])
|
@admin_bp.route('/delete_user/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -121,7 +121,7 @@ def admin_delete_user(user_id):
|
|||||||
user = db.session.get(User, user_id)
|
user = db.session.get(User, user_id)
|
||||||
if user.is_owner:
|
if user.is_owner:
|
||||||
flash(_('Cannot delete the owner account.'), 'danger')
|
flash(_('Cannot delete the owner account.'), 'danger')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#users')
|
||||||
if user and not user.is_admin:
|
if user and not user.is_admin:
|
||||||
event = Event(message=f"Admin {current_user.username} hat {user.username} gelöscht.")
|
event = Event(message=f"Admin {current_user.username} hat {user.username} gelöscht.")
|
||||||
db.session.add(event)
|
db.session.add(event)
|
||||||
@@ -158,7 +158,7 @@ def admin_delete_user(user_id):
|
|||||||
flash(_('User deleted.'), 'success')
|
flash(_('User deleted.'), 'success')
|
||||||
else:
|
else:
|
||||||
flash(_('Cannot delete admin or user not found.'), 'danger')
|
flash(_('Cannot delete admin or user not found.'), 'danger')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#users')
|
||||||
|
|
||||||
@admin_bp.route('/delete_pic/<int:user_id>', methods=['POST'])
|
@admin_bp.route('/delete_pic/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -174,7 +174,7 @@ def admin_delete_pic(user_id):
|
|||||||
user.profile_pic = "default.png"
|
user.profile_pic = "default.png"
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_(f'Profile picture of {user.username} deleted.'), 'success')
|
flash(_(f'Profile picture of {user.username} deleted.'), 'success')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#users')
|
||||||
|
|
||||||
@admin_bp.route('/delete_all_notifications', methods=['POST'])
|
@admin_bp.route('/delete_all_notifications', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -184,7 +184,7 @@ def admin_delete_all_notifications():
|
|||||||
db.session.query(Notification).delete()
|
db.session.query(Notification).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('All notifications have been deleted.'), 'success')
|
flash(_('All notifications have been deleted.'), 'success')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#notifications')
|
||||||
|
|
||||||
@admin_bp.route('/delete_all_events', methods=['POST'])
|
@admin_bp.route('/delete_all_events', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -194,7 +194,7 @@ def admin_delete_all_events():
|
|||||||
db.session.query(Event).delete()
|
db.session.query(Event).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('All events have been deleted.'), 'success')
|
flash(_('All events have been deleted.'), 'success')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#events')
|
||||||
|
|
||||||
@admin_bp.route('/delete_upload/<int:upload_id>', methods=['POST'])
|
@admin_bp.route('/delete_upload/<int:upload_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -212,7 +212,7 @@ def admin_delete_upload(upload_id):
|
|||||||
db.session.delete(upload)
|
db.session.delete(upload)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Upload deleted.'), 'success')
|
flash(_('Upload deleted.'), 'success')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#uploads')
|
||||||
|
|
||||||
@admin_bp.route('/delete_all_uploads', methods=['POST'])
|
@admin_bp.route('/delete_all_uploads', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -230,7 +230,7 @@ def admin_delete_all_uploads():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
flash(_('All uploads have been deleted.'), 'success')
|
flash(_('All uploads have been deleted.'), 'success')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#uploads')
|
||||||
|
|
||||||
@admin_bp.route('/admin/points/<int:user_id>', methods=['POST'])
|
@admin_bp.route('/admin/points/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -242,7 +242,7 @@ def admin_points(user_id):
|
|||||||
points = int(request.form['points'])
|
points = int(request.form['points'])
|
||||||
except:
|
except:
|
||||||
flash(_('No Points entered!'))
|
flash(_('No Points entered!'))
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#shop-orders')
|
||||||
cuser = db.session.get(User, current_user.id)
|
cuser = db.session.get(User, current_user.id)
|
||||||
if not cuser.is_owner:
|
if not cuser.is_owner:
|
||||||
abort(403)
|
abort(403)
|
||||||
@@ -258,7 +258,7 @@ def admin_points(user_id):
|
|||||||
flash(_('Points removed!'), 'success')
|
flash(_('Points removed!'), 'success')
|
||||||
else:
|
else:
|
||||||
flash(_("The user has not enough points to take!"), 'danger')
|
flash(_("The user has not enough points to take!"), 'danger')
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#shop-orders')
|
||||||
|
|
||||||
@admin_bp.route('/make_admin/<int:user_id>', methods=['POST'])
|
@admin_bp.route('/make_admin/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -270,7 +270,7 @@ def make_admin(user_id):
|
|||||||
user.is_admin = True
|
user.is_admin = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_(f"{user.username} is now an admin."), "success")
|
flash(_(f"{user.username} is now an admin."), "success")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#users')
|
||||||
|
|
||||||
@admin_bp.route('/remove_admin/<int:user_id>', methods=['POST'])
|
@admin_bp.route('/remove_admin/<int:user_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -284,7 +284,7 @@ def remove_admin(user_id):
|
|||||||
flash(_(f"Admin rights of {user.username} removed."), "info")
|
flash(_(f"Admin rights of {user.username} removed."), "info")
|
||||||
else:
|
else:
|
||||||
flash(_("Owner cannot be removed!"), "danger")
|
flash(_("Owner cannot be removed!"), "danger")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin')+'#users')
|
||||||
|
|
||||||
@admin_bp.route('/wipe_server', methods=['POST'])
|
@admin_bp.route('/wipe_server', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
+31
-2
@@ -1,6 +1,6 @@
|
|||||||
from flask import Blueprint, redirect, url_for, flash, render_template
|
from flask import Blueprint, jsonify, redirect, request, url_for, flash, render_template, abort
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from models import db, Notification
|
from models import User, db, Notification, Event
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -31,3 +31,32 @@ def notifications():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
notifications = db.session.query(Notification).filter_by(user_id=current_user.id).order_by(Notification.created_at.desc()).all()
|
notifications = db.session.query(Notification).filter_by(user_id=current_user.id).order_by(Notification.created_at.desc()).all()
|
||||||
return render_template('notifications.html', notifications=notifications)
|
return render_template('notifications.html', notifications=notifications)
|
||||||
|
|
||||||
|
@noti_bp.route('/api/notifications')
|
||||||
|
@login_required
|
||||||
|
def notifications_api():
|
||||||
|
expire_time = datetime.now() - timedelta(days=3)
|
||||||
|
db.session.query(Notification).filter(Notification.created_at < expire_time).delete()
|
||||||
|
db.session.commit()
|
||||||
|
notifications = db.session.query(Notification).filter_by(user_id=current_user.id).order_by(Notification.created_at.desc()).all()
|
||||||
|
return jsonify(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'name': User.query.get(n.user_id).username,
|
||||||
|
'data': n.message,
|
||||||
|
'created_at': n.created_at
|
||||||
|
} for n in notifications
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@noti_bp.route('/api/events')
|
||||||
|
@login_required
|
||||||
|
def api_events():
|
||||||
|
if not current_user.is_admin:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
events = db.session.query(Event).order_by(Event.timestamp.desc()).limit(20).all()
|
||||||
|
return jsonify([
|
||||||
|
{"timestamp": e.timestamp.strftime('%Y-%m-%d %H:%M'), "message": e.message}
|
||||||
|
for e in events
|
||||||
|
])
|
||||||
@@ -102,3 +102,12 @@ def delete_account():
|
|||||||
def users():
|
def users():
|
||||||
all_users = db.session.query(User).filter(User.id != current_user.id).all()
|
all_users = db.session.query(User).filter(User.id != current_user.id).all()
|
||||||
return render_template('users.html', users=all_users)
|
return render_template('users.html', users=all_users)
|
||||||
|
|
||||||
|
@user_bp.route('/use_gravatar', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def gravatar():
|
||||||
|
avatar_url = current_user.avatar()
|
||||||
|
current_user.profile_pic = avatar_url
|
||||||
|
db.session.commit()
|
||||||
|
flash(_('Added Gravatar profile picture.'), 'success')
|
||||||
|
return redirect(url_for('profil.profile'))
|
||||||
+89
-13
@@ -5,6 +5,16 @@ body {
|
|||||||
cursor: url("/static/icons/custom-cursor.png"), auto;
|
cursor: url("/static/icons/custom-cursor.png"), auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body *::selection {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
input::selection, textarea::selection {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
body input, body textarea {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
background: black;
|
background: black;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
@@ -14,12 +24,31 @@ canvas {
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2563eb !important;
|
color: #2563eb !important;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Navbar in dark mode */
|
||||||
|
body.dark-mode .navbar {
|
||||||
|
background: #1f2937 !important;
|
||||||
|
border-color: #374151 !important;
|
||||||
|
}
|
||||||
|
body.dark-mode .navbar .nav-link {
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
body.dark-mode .navbar .nav-link:hover {
|
||||||
|
color: #60a5fa !important;
|
||||||
|
}
|
||||||
|
body.dark-mode .navbar-toggler {
|
||||||
|
border-color: #4b5563 !important;
|
||||||
|
}
|
||||||
|
body.dark-mode .navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(229,231,235,0.9)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E") !important;
|
||||||
|
}
|
||||||
.profile-pic {
|
.profile-pic {
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -120,29 +149,65 @@ canvas {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Light mode scrollbar */
|
||||||
|
body.light-mode ::-webkit-scrollbar {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
body.light-mode ::-webkit-scrollbar-thumb {
|
||||||
|
background: #9ca3af;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Light Mode (default) */
|
/* Light Mode (default) */
|
||||||
body, .card, .navbar, .list-group-item, .table, .form-control, .form-select {
|
body, .card, .navbar, .list-group-item, .table, .form-control, .form-select {
|
||||||
transition: background 0.3s, color 0.3s;
|
transition: background 0.3s, color 0.3s;
|
||||||
}
|
}
|
||||||
body.light-mode {
|
body.light-mode {
|
||||||
background: linear-gradient(135deg, #0f5b86 0%, #0245aabb 100%);
|
background: linear-gradient(135deg, #f0f2f5 0%, #e8ecf0 100%);
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
body.light-mode .card,
|
body.light-mode .card,
|
||||||
body.light-mode .navbar,
|
|
||||||
body.light-mode .list-group-item,
|
body.light-mode .list-group-item,
|
||||||
body.light-mode .table {
|
body.light-mode .table {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
body.light-mode .navbar {
|
||||||
|
background: #f8fafc !important;
|
||||||
|
border-color: #e5e7eb !important;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
body.light-mode .navbar .nav-link {
|
||||||
|
color: #374151 !important;
|
||||||
|
}
|
||||||
|
body.light-mode .navbar .nav-link:hover {
|
||||||
|
color: #2563eb !important;
|
||||||
|
}
|
||||||
body.light-mode .form-control,
|
body.light-mode .form-control,
|
||||||
body.light-mode .form-select {
|
body.light-mode .form-select {
|
||||||
background: #f8fafc;
|
background: #fff;
|
||||||
color: #222;
|
color: #222;
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
body.light-mode .form-control:focus,
|
||||||
|
body.light-mode .form-select:focus {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 2px #2563eb22;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-mode li button {
|
body.light-mode li button {
|
||||||
color: #0011ff !important;
|
color: #2563eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links in light mode */
|
||||||
|
body.light-mode a,
|
||||||
|
body.light-mode a:visited {
|
||||||
|
color: #2563eb !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
body.light-mode a:hover {
|
||||||
|
color: #1d4ed8 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode */
|
/* Dark Mode */
|
||||||
@@ -155,9 +220,6 @@ body.dark-mode li button {
|
|||||||
color: #0099f1 !important;
|
color: #0099f1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-mode p {
|
|
||||||
color: #000000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .card,
|
body.dark-mode .card,
|
||||||
body.dark-mode .navbar,
|
body.dark-mode .navbar,
|
||||||
@@ -177,9 +239,14 @@ body.dark-mode .form-control:focus,
|
|||||||
body.dark-mode .form-select:focus {
|
body.dark-mode .form-select:focus {
|
||||||
border-color: #2563eb !important;
|
border-color: #2563eb !important;
|
||||||
box-shadow: 0 0 0 2px #2563eb55 !important;
|
box-shadow: 0 0 0 2px #2563eb55 !important;
|
||||||
background: #23272f !important;
|
background: #1f2937 !important;
|
||||||
color: #e5e7eb !important;
|
color: #e5e7eb !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.light-mode .form-control:focus,
|
||||||
|
body.light-mode .form-select:focus {
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
body.dark-mode .btn,
|
body.dark-mode .btn,
|
||||||
body.dark-mode .btn-primary,
|
body.dark-mode .btn-primary,
|
||||||
body.dark-mode .btn-success,
|
body.dark-mode .btn-success,
|
||||||
@@ -226,10 +293,10 @@ body.dark-mode .profile-pic {
|
|||||||
body.dark-mode .table th,
|
body.dark-mode .table th,
|
||||||
body.dark-mode .table td {
|
body.dark-mode .table td {
|
||||||
color: #e5e7eb !important;
|
color: #e5e7eb !important;
|
||||||
background: #23272f !important;
|
background: #1f2937 !important;
|
||||||
}
|
}
|
||||||
body.dark-mode .list-group-item {
|
body.dark-mode .list-group-item {
|
||||||
background: #23272f !important;
|
background: #1f2937 !important;
|
||||||
color: #e5e7eb !important;
|
color: #e5e7eb !important;
|
||||||
box-shadow: 0 1px 4px rgba(37,99,235,0.08) !important;
|
box-shadow: 0 1px 4px rgba(37,99,235,0.08) !important;
|
||||||
}
|
}
|
||||||
@@ -271,17 +338,26 @@ body.dark-mode .navbar-toggler-icon {
|
|||||||
border-top: 1px solid #e5e7eb;
|
border-top: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.light-mode .footer {
|
||||||
|
background: #f8fafc !important;
|
||||||
|
border-top: 1px solid #e5e7eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
body.dark-mode .footer {
|
body.dark-mode .footer {
|
||||||
background: #181a1b !important;
|
background: #1f2937 !important;
|
||||||
border-top: 1px solid #23272f !important;
|
border-top: 1px solid #374151 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer .text-muted, .footer a {
|
.footer .text-muted, .footer a {
|
||||||
color: #6c757d !important;
|
color: #6c757d !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.light-mode .footer .text-muted, body.light-mode .footer a {
|
||||||
|
color: #6b7280 !important;
|
||||||
|
}
|
||||||
|
|
||||||
body.dark-mode .footer .text-muted, body.dark-mode .footer a {
|
body.dark-mode .footer .text-muted, body.dark-mode .footer a {
|
||||||
color: #b0b8c1 !important;
|
color: #9ca3af !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
|
|||||||
+5
-2
@@ -12,5 +12,8 @@ function reloadEvents() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(reloadEvents, 10000);
|
$(function() {
|
||||||
window.onload = reloadEvents;
|
setInterval(function() {
|
||||||
|
reloadEvents();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
function reload_feed() {
|
|
||||||
setTimeout(function() {
|
|
||||||
window.location.reload();
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
+46
-15
@@ -51,13 +51,26 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<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>
|
||||||
|
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
||||||
|
{% if user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ user.username }}
|
||||||
|
</td>
|
||||||
<td>{{ user.email }}</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_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.is_owner %}<i class="bi bi-star-fill text-warning"></i>{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
{% 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">
|
{% if user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
<form action="{{ url_for('admin.admin_delete_pic', user_id=user.id) }}" method="post" style="display:inline;">
|
<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>
|
<button class="btn btn-danger btn-sm" title="{{ _('Delete Picture') }}"><i class="bi bi-image"></i></button>
|
||||||
</form>
|
</form>
|
||||||
@@ -103,10 +116,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if post.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ post.user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ post.user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ post.user.username }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ post.content|truncate(50) }}</td>
|
<td>{{ post.content|truncate(50) }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -144,17 +160,23 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if f.requester.profile_pic and f.requester.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if f.requester.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ f.requester.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ f.requester.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ f.requester.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ f.requester.username }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if f.receiver.profile_pic and f.receiver.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if f.receiver.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ f.receiver.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ f.receiver.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ f.receiver.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ f.receiver.username }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if f.status == 'accepted' %}
|
{% if f.status == 'accepted' %}
|
||||||
@@ -189,10 +211,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if comment.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ comment.user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ comment.user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ comment.user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ comment.user.username }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ comment.post.id }}</td>
|
<td>{{ comment.post.id }}</td>
|
||||||
<td>{{ comment.content|truncate(50) }}</td>
|
<td>{{ comment.content|truncate(50) }}</td>
|
||||||
@@ -307,10 +332,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if usi.user.profile_pic and usi.user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if usi.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ usi.user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ usi.user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ usi.user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ usi.user.username }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ usi.item.name }}</td>
|
<td>{{ usi.item.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -335,10 +363,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{{ user.username }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user.reward_points() }}</td>
|
<td>{{ user.reward_points() }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -358,7 +389,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ url_for('static', filename='js/adstop.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/adtab.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
var triggerTabList = [].slice.call(document.querySelectorAll('#adminTab button'))
|
var triggerTabList = [].slice.call(document.querySelectorAll('#adminTab button'))
|
||||||
triggerTabList.forEach(function (triggerEl) {
|
triggerTabList.forEach(function (triggerEl) {
|
||||||
|
|||||||
+36
-2
@@ -8,6 +8,7 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.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') }}">
|
<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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||||
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/translate.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/translate.js') }}"></script>
|
||||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column min-vh-100 {{ theme_class }}">
|
<body class="d-flex flex-column min-vh-100 {{ theme_class }}">
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav class="navbar navbar-expand-lg bg-light" id="main-navbar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{{ url_for('index') }}">
|
<a class="navbar-brand" href="{{ url_for('index') }}">
|
||||||
<i class="bi bi-people-fill me-2"></i>MiniFacebook
|
<i class="bi bi-people-fill me-2"></i>MiniFacebook
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
<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('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('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('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('notif.notifications') }}"><span class="badge bg-secondary" id="message_count">0</span><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('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('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>
|
<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>
|
||||||
@@ -80,12 +81,20 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
||||||
|
{% if user.profile_pic.startswith('http') %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in user.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ user.profile_pic }}" class="profile-pic me-2" style="border-color: gold;" alt="Profile Picture">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ user.profile_pic }}" class="profile-pic me-2" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
{% if SHOPITEM_ID_GOLDRAHMEN in user.shop_items | map(attribute='item_id') | list %}
|
{% 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;">
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" class="profile-pic me-2">
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user.profile_pic) }}" class="profile-pic me-2">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<h1>{{ _('Welcome, %(username)s!', username=user.username) }}</h1>
|
<h1>{{ _('Welcome, %(username)s!', username=user.username) }}</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -93,9 +102,15 @@
|
|||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<p class="text-success">{{ _('You are logged in as an admin.') }}</p>
|
<p class="text-success">{{ _('You are logged in as an admin.') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<span class="badge bg-secondary" id="check_notify">{{ _('You don’t have any notifications.') }}</span>
|
||||||
|
{% endif %}
|
||||||
{% if SHOPITEM_ID_PREMIUM in user.shop_items | map(attribute='item_id') | list %}
|
{% 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>
|
<span class="badge bg-warning text-dark"><i class="bi bi-star"></i> Premium</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<hr>
|
||||||
|
{% endif %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer mt-auto py-3">
|
<footer class="footer mt-auto py-3">
|
||||||
@@ -109,5 +124,24 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
<script>
|
||||||
|
function set_message_count(n) {
|
||||||
|
$('#message_count').text(n);
|
||||||
|
$('#check_notify').text('{{ _("You have ") }}' + n + '{{ _(" new notification/s") }}');
|
||||||
|
}
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
$(function() {
|
||||||
|
setInterval(function() {
|
||||||
|
$.ajax("{{ url_for('notif.notifications_api') }}").done(
|
||||||
|
function(notifications) {
|
||||||
|
for (var i = 0; i < notifications.length; i++) {
|
||||||
|
set_message_count(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+14
-3
@@ -30,11 +30,19 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
|
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
|
||||||
|
{% if post.user.profile_pic.startswith('http') %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ post.user.profile_pic }}" class="profile-pic me-2" style="border-color: gold;" alt="Profile Picture">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ post.user.profile_pic }}" class="profile-pic me-2" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
{% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %}
|
{% 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;">
|
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2">
|
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-2"></i>
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -97,9 +105,13 @@
|
|||||||
{% for comment in post.comments %}
|
{% for comment in post.comments %}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %}
|
{% 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>:
|
{% if comment.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ comment.user.profile_pic }}" class="rounded me-2" width="32" alt="Profile Picture"><b>{{ comment.user.username }}</b>:
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person me-1"></i><b>{{ comment.user.username }}</b>:
|
<img src="{{ url_for('static', filename='profile_pics/' ~ comment.user.profile_pic) }}" class="rounded me-2" width="32" alt="Profile Picture"><b>{{ comment.user.username }}</b>:
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-person-circle me-2"></i><b>{{ comment.user.username }}</b>:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ comment.content }}
|
{{ comment.content }}
|
||||||
{% if comment.user_id == current_user.id %}
|
{% if comment.user_id == current_user.id %}
|
||||||
@@ -118,5 +130,4 @@
|
|||||||
{{ _('No posts available.') }}
|
{{ _('No posts available.') }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script src="{{ url_for('static', filename='js/feed.js') }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+30
-4
@@ -7,10 +7,23 @@
|
|||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span>
|
<span>
|
||||||
{% if friend.profile_pic and friend.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if friend.profile_pic.startswith('http') %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in friend.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ friend.profile_pic }}" class="profile-pic me-2" style="border-color: gold;" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ friend.username }}
|
<img src="{{ friend.profile_pic }}" class="profile-pic me-2" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in friend.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ friend.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ friend.profile_pic) }}" class="profile-pic me-2">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ friend.username }}
|
||||||
</span>
|
</span>
|
||||||
<form action="{{ url_for('friend.remove_friend', user_id=friend.id) }}" method="post" style="display:inline;">
|
<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>
|
<button class="btn btn-warning btn-sm"><i class="bi bi-person-dash"></i> {{ _('Remove Friend') }}</button>
|
||||||
@@ -26,10 +39,23 @@
|
|||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span>
|
<span>
|
||||||
{% if req.requester.profile_pic and req.requester.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if req.requester.profile_pic.startswith('http') %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in req.requester.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ req.requester.profile_pic }}" class="profile-pic me-2" style="border-color: gold;" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ req.requester.username }}
|
<img src="{{ req.requester.profile_pic }}" class="profile-pic me-2" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in req.requester.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ req.requester.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ req.requester.profile_pic) }}" class="profile-pic me-2">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ req.requester.username }}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<form action="{{ url_for('friend.accept_friend', friendship_id=req.id) }}" method="post" style="display:inline;">
|
<form action="{{ url_for('friend.accept_friend', friendship_id=req.id) }}" method="post" style="display:inline;">
|
||||||
|
|||||||
+15
-3
@@ -7,13 +7,21 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center mb-2">
|
<div class="d-flex align-items-center mb-2">
|
||||||
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
|
{% if post.user.profile_pic and post.user.profile_pic != 'default.png' %}
|
||||||
|
{% if post.user.profile_pic.startswith('http') %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ post.user.profile_pic }}" class="profile-pic me-2" style="border-color: gold;" alt="Profile Picture">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ post.user.profile_pic }}" class="profile-pic me-2" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
{% if SHOPITEM_ID_GOLDRAHMEN in post.user.shop_items | map(attribute='item_id') | list %}
|
{% 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;">
|
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2">
|
<img src="{{ url_for('static', filename='profile_pics/' ~ post.user.profile_pic) }}" class="profile-pic me-2">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<strong>{{ post.user.username }}</strong>
|
<strong>{{ post.user.username }}</strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,9 +76,13 @@
|
|||||||
{% for comment in post.comments %}
|
{% for comment in post.comments %}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{% if comment.user.profile_pic and comment.user.profile_pic != 'default.png' %}
|
{% 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>:
|
{% if comment.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ comment.user.profile_pic }}" class="rounded me-2" width="32" alt="Profile Picture"><b>{{ comment.user.username }}</b>:
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person me-1"></i><b>{{ comment.user.username }}</b>:
|
<img src="{{ url_for('static', filename='profile_pics/' ~ comment.user.profile_pic) }}" class="rounded me-2" width="32" alt="Profile Picture"><b>{{ comment.user.username }}</b>:
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-person-circle me-2"></i><b>{{ comment.user.username }}</b>:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if comment.user_id == current_user.id %}
|
{% if comment.user_id == current_user.id %}
|
||||||
<form action="{{ url_for('post.delete_comment', comment_id=comment.id) }}" method="post" style="display:inline;">
|
<form action="{{ url_for('post.delete_comment', comment_id=comment.id) }}" method="post" style="display:inline;">
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<p><i class="bi bi-envelope-at me-1"></i>{{ _('Email') }}: {{ user.email }}</p>
|
<p><i class="bi bi-envelope-at me-1"></i>{{ _('Email') }}: {{ user.email }}</p>
|
||||||
|
|
||||||
|
{% if not user.profile_pic.startswith('http') %}
|
||||||
<form action="{{ url_for('user.upload_pic') }}" method="post" enctype="multipart/form-data" class="mb-3">
|
<form action="{{ url_for('user.upload_pic') }}" method="post" enctype="multipart/form-data" class="mb-3">
|
||||||
<input type="file" name="profile_pic" required>
|
<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>
|
<button class="btn btn-secondary btn-sm" type="submit"><i class="bi bi-upload"></i> {{ _('Upload Picture') }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form action="{{ url_for('user.gravatar') }}" method="post" style="display:inline;">
|
||||||
|
<button class="btn btn-secondary btn-sm"><i class="bi bi-globe"></i> {{ _('Use Gravatar') }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
{% if user.profile_pic and user.profile_pic != 'default.png' %}
|
||||||
<form action="{{ url_for('user.delete_pic') }}" method="post" style="display:inline;">
|
<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>
|
<button class="btn btn-danger btn-sm"><i class="bi bi-trash"></i> {{ _('Delete Picture') }}</button>
|
||||||
|
|||||||
@@ -18,9 +18,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if req.user.profile_pic and req.user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if req.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ req.user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ req.user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ req.user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
@@ -48,9 +50,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if req.user.profile_pic and req.user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if req.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ req.user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ req.user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ req.user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
@@ -73,9 +77,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if req.user.profile_pic and req.user.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if req.user.profile_pic.startswith('http') %}
|
||||||
|
<img src="{{ req.user.profile_pic }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ req.user.username }}
|
<img src="{{ url_for('static', filename='profile_pics/' ~ req.user.profile_pic) }}" width="32" class="rounded me-1" alt="Profile Picture">
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td>{{ req.requested_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
+100
-16
@@ -1,33 +1,117 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ _('Shop') }}{% endblock %}
|
{% block title %}{{ _('Shop') }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container-fluid px-0">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2><i class="bi bi-shop me-2"></i>{{ _('Shop') }}</h2>
|
<h2><i class="bi bi-shop me-2"></i>{{ _('Shop') }}</h2>
|
||||||
<p>{{ _('Deine Reward-Punkte:') }} <b>{{ current_user.reward_points() }}</b></p>
|
<div class="d-flex align-items-center gap-3">
|
||||||
{% if message %}
|
<div class="points-display bg-gradient-primary text-white px-4 py-2 rounded-3 shadow-sm">
|
||||||
<div class="alert alert-info">{{ message }}</div>
|
<i class="bi bi-coin me-2"></i>
|
||||||
{% endif %}
|
<span>{{ _('Your Points:') }}</span>
|
||||||
<div class="row">
|
<strong class="ms-1">{{ user_points }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-lg-4 col-md-6">
|
||||||
<div class="card shadow-sm">
|
<div class="card h-100 shadow-sm border-0 {% if item.id in owned_ids %}owned-bg{% endif %}">
|
||||||
<div class="card-body text-center">
|
<div class="card-body d-flex flex-column">
|
||||||
<i class="bi {{ item.icon }} display-4 mb-2"></i>
|
<div class="text-center mb-3">
|
||||||
<h5>{{ item.name }}</h5>
|
<div class="icon-wrapper text-white rounded-circle mx-auto mb-2 d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">
|
||||||
<p>{{ item.description }}</p>
|
<i class="bi {{ item.icon }} display-4"></i>
|
||||||
<p><b>{{ item.price }}</b> {{ _('Points') }}</p>
|
</div>
|
||||||
|
<h5 class="card-title mb-1">{{ item.name }}</h5>
|
||||||
|
<small class="text-muted">{{ item.description }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div class="price-tag">
|
||||||
|
<span class="h4 text-primary mb-0">{{ item.price }}</span>
|
||||||
|
<small class="text-muted">{{ _('Points') }}</small>
|
||||||
|
</div>
|
||||||
{% if item.id in owned_ids %}
|
{% if item.id in owned_ids %}
|
||||||
<button class="btn btn-secondary" disabled>{{ _('Bought') }}</button>
|
<span class="badge bg-success rounded-pill px-3 py-2">
|
||||||
|
<i class="bi bi-check-lg me-1"></i>{{ _('Owned') }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if item.id in owned_ids %}
|
||||||
|
<button class="btn btn-outline-secondary w-100" disabled>
|
||||||
|
<i class="bi bi-bag-check me-2"></i>{{ _('Already Purchased') }}
|
||||||
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post">
|
{% if user_points >= item.price %}
|
||||||
|
<form method="post" class="d-grid">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token_value }}">
|
||||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||||
<button class="btn btn-success" {% if current_user.reward_points() < item.price %}disabled{% endif %}>
|
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||||||
<i class="bi bi-cart"></i> {{ _('Buy') }}
|
<i class="bi bi-cart-plus me-2"></i>{{ _('Buy Now') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-outline-secondary w-100" disabled title="{{ _('Not enough points') }}">
|
||||||
|
<i class="bi bi-coin me-2"></i>{{ _('Need %(needed)d more points', needed=item.price - user_points) }}
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if not items %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox display-1 text-muted mb-3"></i>
|
||||||
|
<h4>{{ _('No items available') }}</h4>
|
||||||
|
<p class="text-muted">{{ _('Check back later for new items in the shop!') }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.points-display {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owned-bg {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border: 2px solid #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-tag .h4 {
|
||||||
|
color: #0d6efd;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+15
-2
@@ -7,10 +7,23 @@
|
|||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span>
|
<span>
|
||||||
{% if user_item.profile_pic and user_item.profile_pic != 'default.png' %}
|
{% 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 }}
|
{% if user_item.profile_pic.startswith('http') %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in user_item.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ user_item.profile_pic }}" class="profile-pic me-2" style="border-color: gold;" alt="Profile Picture">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-person-circle me-1"></i>{{ user_item.username }}
|
<img src="{{ user_item.profile_pic }}" class="profile-pic me-2" alt="Profile Picture">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if SHOPITEM_ID_GOLDRAHMEN in user_item.shop_items | map(attribute='item_id') | list %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user_item.profile_pic) }}" class="profile-pic me-2" style="border-color: gold;">
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ url_for('static', filename='profile_pics/' ~ user_item.profile_pic) }}" class="profile-pic me-2">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-person-circle me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
{{ user_item.username }}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
{% if user_item.id != user.id %}
|
{% if user_item.id != user.id %}
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-09-27 20:09+0200\n"
|
"POT-Creation-Date: 2025-11-22 23:43+0100\n"
|
||||||
"PO-Revision-Date: 2025-09-27 20:09+0200\n"
|
"PO-Revision-Date: 2025-11-22 23:44+0100\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
"Language-Team: en <LL@li.org>\n"
|
"Language-Team: en <LL@li.org>\n"
|
||||||
@@ -18,43 +18,43 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
#: main.py:34
|
#: main.py:37
|
||||||
msgid "Please log in to access this page."
|
msgid "Please log in to access this page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:117 routes/discord.py:44 routes/login.py:46 routes/profile.py:56
|
#: main.py:168 routes/discord.py:47 routes/login.py:46 routes/profile.py:56
|
||||||
msgid "Passwords do not match."
|
msgid "Passwords do not match."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:119 routes/login.py:48
|
#: main.py:170 routes/login.py:48
|
||||||
msgid "Username already exists."
|
msgid "Username already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:121 routes/login.py:50
|
#: main.py:172 routes/login.py:50
|
||||||
msgid "E-Mail already exists."
|
msgid "E-Mail already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:123 routes/discord.py:41 routes/login.py:52 routes/profile.py:53
|
#: main.py:174 routes/discord.py:44 routes/login.py:52 routes/profile.py:53
|
||||||
msgid "Password must be at least 8 characters long."
|
msgid "Password must be at least 8 characters long."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:125 routes/login.py:54 routes/profile.py:44
|
#: main.py:176 routes/login.py:54 routes/profile.py:44
|
||||||
msgid "Invalid email address."
|
msgid "Invalid email address."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:127 routes/login.py:56 routes/profile.py:38
|
#: main.py:178 routes/login.py:56 routes/profile.py:38
|
||||||
msgid "Invalid username. Only alphanumeric characters are allowed."
|
msgid "Invalid username. Only alphanumeric characters are allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:133
|
#: main.py:184
|
||||||
msgid "Admin account created. You can now log in."
|
msgid "Admin account created. You can now log in."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:159
|
#: main.py:210
|
||||||
msgid "Already purchased!"
|
msgid "Already purchased!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: main.py:167
|
#: main.py:218
|
||||||
msgid "Not enough points!"
|
msgid "Not enough points!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -126,27 +126,27 @@ msgstr ""
|
|||||||
msgid "All Data has been deleted."
|
msgid "All Data has been deleted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: routes/discord.py:24
|
#: routes/discord.py:27
|
||||||
msgid "Logged in with Discord."
|
msgid "Logged in with Discord."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: routes/discord.py:27
|
#: routes/discord.py:30
|
||||||
msgid "No account linked with this Discord. Please register."
|
msgid "No account linked with this Discord. Please register."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: routes/discord.py:47
|
#: routes/discord.py:50
|
||||||
msgid "Username already exists. Please Report It."
|
msgid "Username already exists. Please Report It."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: routes/discord.py:60
|
#: routes/discord.py:63
|
||||||
msgid "Account created and logged in with Discord."
|
msgid "Account created and logged in with Discord."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: routes/discord.py:77
|
#: routes/discord.py:80
|
||||||
msgid "Discord account linked!"
|
msgid "Discord account linked!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: routes/discord.py:86
|
#: routes/discord.py:89
|
||||||
msgid "Discord account unlinked!"
|
msgid "Discord account unlinked!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -338,7 +338,11 @@ msgstr ""
|
|||||||
msgid "Account and all your data deleted."
|
msgid "Account and all your data deleted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/403.html:2 templates/base.html:106 templates/index.html:2
|
#: routes/user.py:112
|
||||||
|
msgid "Added Gravatar profile picture."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/403.html:2 templates/base.html:115 templates/index.html:2
|
||||||
msgid "Home"
|
msgid "Home"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -360,11 +364,11 @@ msgstr ""
|
|||||||
msgid "Admin"
|
msgid "Admin"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:4 templates/base.html:39
|
#: templates/admin.html:4 templates/base.html:40
|
||||||
msgid "Admin Panel"
|
msgid "Admin Panel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:8 templates/admin.html:39 templates/base.html:43
|
#: templates/admin.html:8 templates/admin.html:39 templates/base.html:44
|
||||||
#: templates/users.html:2
|
#: templates/users.html:2
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -373,19 +377,19 @@ msgstr ""
|
|||||||
msgid "Posts"
|
msgid "Posts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:14 templates/admin.html:133
|
#: templates/admin.html:14 templates/admin.html:148
|
||||||
msgid "Friendships"
|
msgid "Friendships"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:17 templates/admin.html:176
|
#: templates/admin.html:17 templates/admin.html:195
|
||||||
msgid "Comments"
|
msgid "Comments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:20 templates/admin.html:213
|
#: templates/admin.html:20 templates/admin.html:234
|
||||||
msgid "Uploads"
|
msgid "Uploads"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:23 templates/admin.html:250 templates/base.html:46
|
#: templates/admin.html:23 templates/admin.html:271 templates/base.html:47
|
||||||
#: templates/notifications.html:2 templates/notifications.html:6
|
#: templates/notifications.html:2 templates/notifications.html:6
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -394,18 +398,18 @@ msgstr ""
|
|||||||
msgid "Events"
|
msgid "Events"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:29 templates/admin.html:297
|
#: templates/admin.html:29 templates/admin.html:318
|
||||||
msgid "Shop Orders"
|
msgid "Shop Orders"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:32 templates/admin.html:324
|
#: templates/admin.html:32 templates/admin.html:347
|
||||||
msgid "Reward Points"
|
msgid "Reward Points"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:43 templates/admin.html:94 templates/admin.html:180
|
#: templates/admin.html:43 templates/admin.html:107 templates/admin.html:199
|
||||||
#: templates/admin.html:221 templates/admin.html:258 templates/admin.html:301
|
#: templates/admin.html:242 templates/admin.html:279 templates/admin.html:322
|
||||||
#: templates/admin.html:328 templates/reset_requests.html:11
|
#: templates/admin.html:351 templates/reset_requests.html:11
|
||||||
#: templates/reset_requests.html:42 templates/reset_requests.html:67
|
#: templates/reset_requests.html:44 templates/reset_requests.html:71
|
||||||
msgid "User"
|
msgid "User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -422,152 +426,152 @@ msgstr ""
|
|||||||
msgid "Profile Pic"
|
msgid "Profile Pic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:48 templates/admin.html:98 templates/admin.html:184
|
#: templates/admin.html:48 templates/admin.html:111 templates/admin.html:203
|
||||||
#: templates/admin.html:225 templates/admin.html:330
|
#: templates/admin.html:246 templates/admin.html:353
|
||||||
#: templates/reset_requests.html:13
|
#: templates/reset_requests.html:13
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:62 templates/profile.html:12
|
#: templates/admin.html:75 templates/profile.html:17
|
||||||
msgid "Delete Picture"
|
msgid "Delete Picture"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:69 templates/users.html:34
|
#: templates/admin.html:82 templates/users.html:47
|
||||||
msgid "Make Admin"
|
msgid "Make Admin"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:73 templates/users.html:38
|
#: templates/admin.html:86 templates/users.html:51
|
||||||
msgid "Remove Admin"
|
msgid "Remove Admin"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:78
|
#: templates/admin.html:91
|
||||||
msgid "Delete User"
|
msgid "Delete User"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:90
|
#: templates/admin.html:103
|
||||||
msgid "All Posts"
|
msgid "All Posts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:95 templates/admin.html:182 templates/edit_post.html:7
|
#: templates/admin.html:108 templates/admin.html:201 templates/edit_post.html:7
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:96 templates/edit_post.html:16
|
#: templates/admin.html:109 templates/edit_post.html:16
|
||||||
msgid "Visibility"
|
msgid "Visibility"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:97 templates/admin.html:183 templates/admin.html:260
|
#: templates/admin.html:110 templates/admin.html:202 templates/admin.html:281
|
||||||
#: templates/support.html:26
|
#: templates/support.html:26
|
||||||
msgid "Created"
|
msgid "Created"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:114 templates/edit_post.html:18 templates/feed.html:18
|
#: templates/admin.html:129 templates/edit_post.html:18 templates/feed.html:18
|
||||||
#: templates/feed.html:86 templates/my_posts.html:61
|
#: templates/feed.html:94 templates/my_posts.html:69
|
||||||
msgid "Public"
|
msgid "Public"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:116 templates/base.html:44 templates/friends.html:2
|
#: templates/admin.html:131 templates/base.html:45 templates/friends.html:2
|
||||||
msgid "Friends"
|
msgid "Friends"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:122
|
#: templates/admin.html:137
|
||||||
msgid "Delete Post"
|
msgid "Delete Post"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:137
|
#: templates/admin.html:152
|
||||||
msgid "User 1"
|
msgid "User 1"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:138
|
#: templates/admin.html:153
|
||||||
msgid "User 2"
|
msgid "User 2"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:139 templates/support.html:25
|
#: templates/admin.html:154 templates/support.html:25
|
||||||
#: templates/support_thread.html:5
|
#: templates/support_thread.html:5
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:161
|
#: templates/admin.html:180
|
||||||
msgid "Accepted"
|
msgid "Accepted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:163
|
#: templates/admin.html:182
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:165
|
#: templates/admin.html:184
|
||||||
msgid "Rejected"
|
msgid "Rejected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:181 templates/feed.html:21
|
#: templates/admin.html:200 templates/feed.html:21
|
||||||
msgid "Post"
|
msgid "Post"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:202
|
#: templates/admin.html:223
|
||||||
msgid "Delete Comment"
|
msgid "Delete Comment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:215
|
#: templates/admin.html:236
|
||||||
msgid "Delete All Uploads"
|
msgid "Delete All Uploads"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:222
|
#: templates/admin.html:243
|
||||||
msgid "Filename"
|
msgid "Filename"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:223
|
#: templates/admin.html:244
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:224
|
#: templates/admin.html:245
|
||||||
msgid "Uploaded"
|
msgid "Uploaded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:236 templates/feed.html:55 templates/my_posts.html:32
|
#: templates/admin.html:257 templates/feed.html:63 templates/my_posts.html:40
|
||||||
msgid "Download"
|
msgid "Download"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:238
|
#: templates/admin.html:259
|
||||||
msgid "Delete Upload"
|
msgid "Delete Upload"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:251
|
#: templates/admin.html:272
|
||||||
msgid "Are you sure you want to delete all notifications?"
|
msgid "Are you sure you want to delete all notifications?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:252
|
#: templates/admin.html:273
|
||||||
msgid "Delete All Notifications"
|
msgid "Delete All Notifications"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:259
|
#: templates/admin.html:280
|
||||||
msgid "Message"
|
msgid "Message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:278
|
#: templates/admin.html:299
|
||||||
msgid "Recent Events"
|
msgid "Recent Events"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:279
|
#: templates/admin.html:300
|
||||||
msgid "Are you sure you want to delete all events?"
|
msgid "Are you sure you want to delete all events?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:280
|
#: templates/admin.html:301
|
||||||
msgid "Delete All Events"
|
msgid "Delete All Events"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:302
|
#: templates/admin.html:323
|
||||||
msgid "Item"
|
msgid "Item"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:329 templates/shop.html:17
|
#: templates/admin.html:352 templates/shop.html:17
|
||||||
msgid "Points"
|
msgid "Points"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:347
|
#: templates/admin.html:372
|
||||||
msgid "Add Points"
|
msgid "Add Points"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin.html:350
|
#: templates/admin.html:375
|
||||||
msgid "Remove Points"
|
msgid "Remove Points"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -584,65 +588,65 @@ msgstr ""
|
|||||||
msgid "New Password"
|
msgid "New Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin_set_password.html:10 templates/reset_requests.html:28
|
#: templates/admin_set_password.html:10 templates/reset_requests.html:30
|
||||||
msgid "Set Password"
|
msgid "Set Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:40
|
#: templates/base.html:41
|
||||||
msgid "Reset Requests"
|
msgid "Reset Requests"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:42 templates/base.html:51 templates/feed.html:2
|
#: templates/base.html:43 templates/base.html:52 templates/feed.html:2
|
||||||
msgid "Feed"
|
msgid "Feed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:45 templates/my_posts.html:2 templates/my_posts.html:4
|
#: templates/base.html:46 templates/my_posts.html:2 templates/my_posts.html:4
|
||||||
msgid "My Posts"
|
msgid "My Posts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:47 templates/profile.html:2
|
#: templates/base.html:48 templates/profile.html:2
|
||||||
msgid "Profile"
|
msgid "Profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:48
|
#: templates/base.html:49
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:49 templates/support.html:2 templates/support.html:15
|
#: templates/base.html:50 templates/support.html:2 templates/support.html:15
|
||||||
msgid "Support"
|
msgid "Support"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:52 templates/index.html:16 templates/login.html:2
|
#: templates/base.html:53 templates/index.html:16 templates/login.html:2
|
||||||
#: templates/login.html:7 templates/login.html:18 templates/register.html:30
|
#: templates/login.html:7 templates/login.html:18 templates/register.html:30
|
||||||
#: templates/reset_password.html:17
|
#: templates/reset_password.html:17
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:53 templates/index.html:17 templates/login.html:21
|
#: templates/base.html:54 templates/index.html:17 templates/login.html:21
|
||||||
#: templates/register.html:2 templates/register.html:7
|
#: templates/register.html:2 templates/register.html:7
|
||||||
#: templates/register.html:26
|
#: templates/register.html:26
|
||||||
msgid "Register"
|
msgid "Register"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:57
|
#: templates/base.html:58
|
||||||
msgid "Theme"
|
msgid "Theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:90
|
#: templates/base.html:99
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Welcome, %(username)s!"
|
msgid "Welcome, %(username)s!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:94
|
#: templates/base.html:103
|
||||||
msgid "You are logged in as an admin."
|
msgid "You are logged in as an admin."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:107 templates/privacy_policy.html:2
|
#: templates/base.html:116 templates/privacy_policy.html:2
|
||||||
#: templates/privacy_policy.html:4
|
#: templates/privacy_policy.html:4
|
||||||
msgid "Privacy Policy"
|
msgid "Privacy Policy"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/base.html:108 templates/credits.html:2 templates/credits.html:4
|
#: templates/base.html:117 templates/credits.html:2 templates/credits.html:4
|
||||||
msgid "Credits"
|
msgid "Credits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -750,8 +754,8 @@ msgstr ""
|
|||||||
msgid "Limit: 500"
|
msgid "Limit: 500"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/edit_post.html:19 templates/feed.html:19 templates/feed.html:84
|
#: templates/edit_post.html:19 templates/feed.html:19 templates/feed.html:92
|
||||||
#: templates/my_posts.html:59
|
#: templates/my_posts.html:67
|
||||||
msgid "Friends only"
|
msgid "Friends only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -776,7 +780,7 @@ msgid "No uploads found for this post."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/edit_profile.html:2 templates/edit_profile.html:4
|
#: templates/edit_profile.html:2 templates/edit_profile.html:4
|
||||||
#: templates/profile.html:16
|
#: templates/profile.html:21
|
||||||
msgid "Edit Profile"
|
msgid "Edit Profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -814,41 +818,41 @@ msgstr ""
|
|||||||
msgid "to create a post."
|
msgid "to create a post."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:61 templates/my_posts.html:38
|
#: templates/feed.html:69 templates/my_posts.html:46
|
||||||
msgid "Download Video"
|
msgid "Download Video"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:64 templates/my_posts.html:41
|
#: templates/feed.html:72 templates/my_posts.html:49
|
||||||
msgid "Download Audio"
|
msgid "Download Audio"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:70 templates/my_posts.html:47
|
#: templates/feed.html:78 templates/my_posts.html:55
|
||||||
msgid "Like"
|
msgid "Like"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:73 templates/my_posts.html:50
|
#: templates/feed.html:81 templates/my_posts.html:58
|
||||||
msgid "Unlike"
|
msgid "Unlike"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:77 templates/feed.html:107 templates/my_posts.html:53
|
#: templates/feed.html:85 templates/feed.html:115 templates/my_posts.html:61
|
||||||
#: templates/my_posts.html:77 templates/notifications.html:25
|
#: templates/my_posts.html:85 templates/notifications.html:25
|
||||||
#: templates/support_thread.html:35
|
#: templates/support_thread.html:35
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:80 templates/my_posts.html:56
|
#: templates/feed.html:88 templates/my_posts.html:64
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:91 templates/my_posts.html:66
|
#: templates/feed.html:99 templates/my_posts.html:74
|
||||||
msgid "Comment..."
|
msgid "Comment..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:95
|
#: templates/feed.html:103
|
||||||
msgid "Please login to comment."
|
msgid "Please login to comment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/feed.html:118 templates/my_posts.html:88
|
#: templates/feed.html:126 templates/my_posts.html:96
|
||||||
msgid "No posts available."
|
msgid "No posts available."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -856,27 +860,27 @@ msgstr ""
|
|||||||
msgid "Your Friends"
|
msgid "Your Friends"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/friends.html:16
|
#: templates/friends.html:29
|
||||||
msgid "Remove Friend"
|
msgid "Remove Friend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/friends.html:20
|
#: templates/friends.html:33
|
||||||
msgid "No friends yet."
|
msgid "No friends yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/friends.html:23
|
#: templates/friends.html:36
|
||||||
msgid "Friend Requests"
|
msgid "Friend Requests"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/friends.html:36
|
#: templates/friends.html:62
|
||||||
msgid "Accept"
|
msgid "Accept"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/friends.html:39 templates/reset_requests.html:30
|
#: templates/friends.html:65 templates/reset_requests.html:32
|
||||||
msgid "Reject"
|
msgid "Reject"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/friends.html:44
|
#: templates/friends.html:70
|
||||||
msgid "No new requests"
|
msgid "No new requests"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -932,7 +936,7 @@ msgstr ""
|
|||||||
msgid "Forgot password?"
|
msgid "Forgot password?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/login.html:27 templates/register.html:34
|
#: templates/login.html:28 templates/register.html:35
|
||||||
msgid "Login with Discord"
|
msgid "Login with Discord"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -999,27 +1003,31 @@ msgid ""
|
|||||||
"contact us."
|
"contact us."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/profile.html:8
|
#: templates/profile.html:9
|
||||||
msgid "Upload Picture"
|
msgid "Upload Picture"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/profile.html:15 templates/shop.html:2 templates/shop.html:4
|
#: templates/profile.html:12
|
||||||
|
msgid "Use Gravatar"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/profile.html:20 templates/shop.html:2 templates/shop.html:4
|
||||||
msgid "Shop"
|
msgid "Shop"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/profile.html:18
|
#: templates/profile.html:23
|
||||||
msgid "Delete Account"
|
msgid "Delete Account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/profile.html:22
|
#: templates/profile.html:28
|
||||||
msgid "Link Discord Account"
|
msgid "Link Discord Account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/profile.html:25
|
#: templates/profile.html:31
|
||||||
msgid "Discord Linked"
|
msgid "Discord Linked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/profile.html:28
|
#: templates/profile.html:34
|
||||||
msgid "Unlink Discord"
|
msgid "Unlink Discord"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1043,27 +1051,23 @@ msgstr ""
|
|||||||
msgid "Delete All"
|
msgid "Delete All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/reset_requests.html:12 templates/reset_requests.html:43
|
#: templates/reset_requests.html:12 templates/reset_requests.html:45
|
||||||
#: templates/reset_requests.html:68
|
#: templates/reset_requests.html:72
|
||||||
msgid "Requested At"
|
msgid "Requested At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/reset_requests.html:38
|
#: templates/reset_requests.html:40
|
||||||
msgid "Completed Requests"
|
msgid "Completed Requests"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/reset_requests.html:63
|
#: templates/reset_requests.html:67
|
||||||
msgid "Rejected Requests"
|
msgid "Rejected Requests"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/reset_requests.html:88
|
#: templates/reset_requests.html:94
|
||||||
msgid "No open reset requests."
|
msgid "No open reset requests."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/secret.html:2
|
|
||||||
msgid "Secret"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/setup.html:2
|
#: templates/setup.html:2
|
||||||
msgid "Admin Setup"
|
msgid "Admin Setup"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1148,19 +1152,19 @@ msgstr ""
|
|||||||
msgid "All Users"
|
msgid "All Users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/users.html:20
|
#: templates/users.html:33
|
||||||
msgid "Request sent"
|
msgid "Request sent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/users.html:22
|
#: templates/users.html:35
|
||||||
msgid "Request received"
|
msgid "Request received"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/users.html:24
|
#: templates/users.html:37
|
||||||
msgid "Friend"
|
msgid "Friend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/users.html:27
|
#: templates/users.html:40
|
||||||
msgid "Add Friend"
|
msgid "Add Friend"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user