31 Commits

Author SHA1 Message Date
Michachatz 27eadc840d Merge pull request #1 from Michatec/renovate/configure
Configure Renovate
2026-05-26 00:08:30 +02:00
Michachatz 2d4d48c45c Fixed the command 2026-05-11 11:58:33 +02:00
renovate[bot] 5c2d882b45 Add renovate.json 2026-05-09 02:54:10 +00:00
Michatec 5febf7e64d feat(shop): implement CSRF protection and improve UI/UX
- Add CSRF token validation to the shop purchase process to prevent cross-site request forgery.
- Implement a unique constraint on `UserShopItem` to prevent duplicate purchases of the same item.
- Refactor the shop template with a modern, responsive grid layout and improved visual feedback for owned items.
- Enhance CSS with better dark/light mode support, including improved navbar styling and scrollbar customization.
- Add `.env_example` and update documentation for environment variable setup.
- Integrate `python-dotenv` for environment variable management.
- Improve logging configuration for the application.
- Update `.gitignore` to include `venv/` and `.env`.

Co-authored-by: Copilot <copilot@github.com>
2026-04-26 20:49:42 +02:00
Michachatz 7f8948bba9 Add print statement for serving connections 2026-04-26 15:37:30 +02:00
Michachatz 2b93ae73ff Fix typo in requirements.txt filename 2026-04-26 15:23:25 +02:00
Michachatz b376311cff Maintain Updates 2025-11-24 16:35:43 +01:00
Michachatz 72c9ff6601 Maintain Updates 2025-11-24 16:32:37 +01:00
Michachatz d8ba367b0d Update Fix Tranlations 2025-11-24 16:29:04 +01:00
Michachatz a5766c510d Added the Official German Translation 2025-11-24 16:21:54 +01:00
Michachatz b3e9fd9819 Update messages.po 2025-11-24 16:19:49 +01:00
Michachatz 7811396791 Delete .github/workflows directory 2025-11-23 15:42:19 +01:00
Michachatz 1ac918496d Create django.yml 2025-11-23 15:41:11 +01:00
Michachatz 803d3ca360 Update base.html 2025-11-23 15:30:07 +01:00
Michatec c7b18d76ef API Events Moved 2025-11-23 13:02:56 +01:00
Michatec 1c05248829 Notifications Widget Added 2025-11-23 12:58:22 +01:00
Michachatz 3332a9ca7c Update README.md 2025-11-23 11:23:16 +01:00
Michatec df8ee7703d Auto Adjust Tab Admin 2025-11-23 11:14:15 +01:00
Michatec 0e9024949b Fix comments Gravatar not shown 2025-11-23 10:53:37 +01:00
Michachatz 2cff51e779 Update README.md 2025-11-23 00:19:20 +01:00
Michatec af8b69989c Merge branch 'main' of https://github.com/Michatec/MiniFaceBook 2025-11-23 00:14:35 +01:00
Michatec 1fd5cddd3c Fix Username not shown 2025-11-23 00:14:31 +01:00
Michachatz 1429e50b2b Update requirments.txt 2025-11-22 23:58:27 +01:00
Michatec 2ef98ce897 - Added Gravatar Integration
- Realtime Notify & Notify API
- Some bugs fixed
2025-11-22 23:49:00 +01:00
Michatec 858c98412f gitignore update 2025-11-08 21:24:13 +01:00
Michatec 77e46d03c7 Merge branch 'main' of https://github.com/Michatec/MiniFaceBook 2025-11-08 21:21:44 +01:00
Michatec 986a1a2a25 Secret Page removed. 2025-11-08 21:17:47 +01:00
Michachatz 0e03aa9c33 Update dependabot.yml 2025-09-28 15:28:21 +02:00
Michachatz 1b5976a190 Create dependabot.yml 2025-09-28 15:25:13 +02:00
Michachatz 0fd32dc2b8 Update main.py 2025-09-27 21:46:08 +02:00
Michachatz 1a94d52d61 Update requirments.txt 2025-09-27 21:44:42 +02:00
29 changed files with 1207 additions and 1080 deletions
+3
View File
@@ -0,0 +1,3 @@
# You can change this file, to your preferred Settings.
PORT=YOUR PORT
+11
View File
@@ -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
View File
@@ -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
+16 -5
View File
@@ -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
@@ -28,21 +30,30 @@ MiniFacebook is a minimalist social network built with [Flask](https://flask.pal
2. **Install dependencies** 2. **Install dependencies**
```sh ```sh
pip install -r requirments.txt pip install -r requirements.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
+125 -61
View File
@@ -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)
+7
View File
@@ -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
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}
+2
View File
@@ -6,3 +6,5 @@ flask_babel
waitress waitress
authlib authlib
sqlalchemy sqlalchemy
requests
dotenv
+12 -12
View File
@@ -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
View File
@@ -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
])
+9
View File
@@ -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
View File
@@ -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
View File
@@ -12,5 +12,8 @@ function reloadEvents() {
}); });
} }
setInterval(reloadEvents, 10000); $(function() {
window.onload = reloadEvents; setInterval(function() {
reloadEvents();
}, 1000);
});
-5
View File
@@ -1,5 +0,0 @@
function reload_feed() {
setTimeout(function() {
window.location.reload();
}, 120000);
}
+46 -15
View File
@@ -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
View File
@@ -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 dont 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
View File
@@ -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
View File
@@ -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
View File
@@ -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;">
+5
View File
@@ -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>
+12 -6
View File
@@ -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>
-294
View File
@@ -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 %}
+101 -17
View File
@@ -1,33 +1,117 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ _('Shop') }}{% endblock %} {% block title %}{{ _('Shop') }}{% endblock %}
{% block content %} {% block content %}
<h2><i class="bi bi-shop me-2"></i>{{ _('Shop') }}</h2> <div class="container-fluid px-0">
<p>{{ _('Deine Reward-Punkte:') }} <b>{{ current_user.reward_points() }}</b></p> <div class="d-flex justify-content-between align-items-center mb-4">
{% if message %} <h2><i class="bi bi-shop me-2"></i>{{ _('Shop') }}</h2>
<div class="alert alert-info">{{ message }}</div> <div class="d-flex align-items-center gap-3">
{% endif %} <div class="points-display bg-gradient-primary text-white px-4 py-2 rounded-3 shadow-sm">
<div class="row"> <i class="bi bi-coin me-2"></i>
<span>{{ _('Your Points:') }}</span>
<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>
{% 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> </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
View File
@@ -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
+126 -122
View File
@@ -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 ""