Compare commits

..

10 commits

Author SHA1 Message Date
John Groszko
2089ee4161 Timeline - Link to subs 2026-04-23 16:53:27 -05:00
John Groszko
cfccd424cb Timeline Permissions 2026-04-23 16:46:08 -05:00
John Groszko
34e8d0eae3 UI for advanced sub permissions 2026-04-23 16:46:08 -05:00
John Groszko
40486f81fd Add advanced sub permissions 2026-04-23 16:46:08 -05:00
John Groszko
05d5093e23 Use jinja templates
Modify 2 files
2026-04-23 11:58:16 -05:00
John Groszko
556c1dd7eb Add punish cli command 2026-04-23 11:55:58 -05:00
John Groszko
6a7e996406 Refactor logging
Update 4 files
2026-04-13 21:55:31 -05:00
John Groszko
fa366534f8 More color tweaks 2026-04-07 21:09:00 -05:00
John Groszko
4c5c8bec04 Dark theme 2026-04-07 20:52:59 -05:00
01589eedeb Fix nav issue 2026-03-30 16:51:09 -05:00
30 changed files with 1058 additions and 442 deletions

View file

@ -6,3 +6,9 @@ TIMELINE_ORDER_PUNISHED = "ORDER_PUNISHED"
TIMELINE_ORDERS_POOL_CREATED = "ORDERS_POOL_CREATED"
TIMELINE_ORDERS_POOL_UPDATED = "ORDERS_POOL_UPDATED"
TIMELINE_ORDERS_POOL_DELETED = "ORDERS_POOL_DELETED"
TIMELINE_ORDERS_POOL_EVENTS = [
TIMELINE_ORDERS_POOL_CREATED,
TIMELINE_ORDERS_POOL_UPDATED,
TIMELINE_ORDERS_POOL_DELETED
]

View file

@ -40,6 +40,10 @@ class User(BaseModel):
verify_mastodon_favorite = BooleanField(null=False, default=False)
verify_delay = IntegerField(null=True)
permission_orders_pools_view = BooleanField(null=False, default=True)
permission_orders_pools_details = BooleanField(null=False, default=True)
permission_orders_pools_edit = BooleanField(null=False, default=True)
def mastodon_account(self):
if self.mastodon_server is None or self.mastodon_username is None:
return
@ -183,6 +187,9 @@ class OrderStatus(BaseModel):
null=True
)
def __str__(self):
return f"{self.pool} {self.id}"
class Meta:
table_name = 'order_status'

View file

@ -2,6 +2,7 @@ import datetime
import json
from peewee import JOIN, fn
from db.constants import TIMELINE_ORDERS_POOL_EVENTS
from util import sqlite_time
from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus, MastodonServer, TimelineEvent
@ -54,6 +55,45 @@ def user_preferences_set(id, mastodon_post_public, mastodon_attn_list):
def user_has_doms(id):
return DomSubUsers.select().where(DomSubUsers.sub_id == id).count() > 0
def user_doms(id):
return [d.dom for d in DomSubUsers.select(DomSubUsers.dom).where(DomSubUsers.sub_id == id)]
def user_subs(id):
return [d.sub for d in DomSubUsers.select(DomSubUsers.sub).where(DomSubUsers.dom_id == id)]
def user_can_orders_pools_view(user, sub):
doms = user_doms(sub.id)
if len(doms) > 0:
if user == sub:
return user.permission_orders_pools_view
else:
return user in doms
else:
return user.id == sub.id
def user_can_orders_pools_details(user, sub):
doms = user_doms(sub.id)
if len(doms) > 0:
if user == sub:
return user.permission_orders_pools_details
else:
return user in doms
else:
return user.id == sub.id
def user_can_orders_pools_edit(user, sub):
doms = user_doms(sub.id)
if len(doms) > 0:
if user == sub:
return user.permission_orders_pools_edit
else:
return user in doms
else:
return user.id == sub.id
def mastodon_server_get(name):
return MastodonServer.get(name=name)
@ -186,13 +226,22 @@ def timeline_event_put(type, text, user, orders_pool=None, order_status=None, ac
extra=json.dumps(extra) if extra is not None else None
)
def timeline_event_recent(user_ids, actor_ids=None):
return (TimelineEvent
.select()
.where((
TimelineEvent.user_id.in_(user_ids) |
(TimelineEvent.actor_user_id.in_(actor_ids) if actor_ids is not None else True)
))
.order_by(TimelineEvent.updated_at.desc())
.limit(10)
def timeline_event_recent(user_id, limit=5):
user = User.get_by_id(user_id)
can_view_orders_pools = user_can_orders_pools_view(user, user)
result = TimelineEvent.select()
if(can_view_orders_pools):
result = result.where(
(TimelineEvent.user_id == user_id) |
(TimelineEvent.actor_user_id == user_id)
)
else:
result = result.where(
((TimelineEvent.user_id == user_id) & TimelineEvent.type.not_in(TIMELINE_ORDERS_POOL_EVENTS)) |
(TimelineEvent.actor_user_id == user_id)
)
return result.order_by(TimelineEvent.updated_at.desc()).limit(limit)

40
main.py
View file

@ -7,38 +7,24 @@ import json
from db.constants import TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED
from scheduling import OrderScheduler
from generate import generate_order
from orders import order_issue, order_check
from db.queries import orders_pool_by_id, timeline_event_put
from orders import order_issue, order_check, punishment_issue
from db.queries import orders_pool_by_id, order_status_by_id
from telegram.telegram import handle_commands
from telegram.commands import commands
from util import make_session
logger = logging.getLogger(__name__)
async def do_order_issue(orders_pool_id):
orders_pool = orders_pool_by_id(orders_pool_id)
issue_result = await order_issue(orders_pool)
await order_issue(orders_pool)
if 'order_status' in issue_result:
order_status = issue_result['order_status']
logger.info(f'Issued order id {order_status.id}')
timeline_event_put(
TIMELINE_ORDER_ISSUED,
order_status.text.split("\n")[0],
user=orders_pool.user,
orders_pool=orders_pool,
order_status=order_status,
extra={
"mastodon_status_url": issue_result['mastodon_status']['url']
}
)
elif 'reason' in issue_result:
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
issue_result['reason'],
user=orders_pool.user,
orders_pool=orders_pool
)
async def do_punishment_issue(order_status_id):
order_status = order_status_by_id(order_status_id)
async with make_session() as session:
await punishment_issue(session, order_status)
if __name__=='__main__':
logging.basicConfig(
@ -61,6 +47,9 @@ if __name__=='__main__':
parser_issue = subparsers.add_parser('check', help='Check on the status of an order')
parser_issue.add_argument('order_status_id')
parser_punish = subparsers.add_parser('punish', help="Punish an order")
parser_punish.add_argument('order_status_id')
args = parser.parse_args()
if args.command == 'generate':
@ -77,6 +66,11 @@ if __name__=='__main__':
loop.run_until_complete(
order_check(args.order_status_id)
)
elif args.command == 'punish':
loop = asyncio.new_event_loop()
loop.run_until_complete(
do_punishment_issue(args.order_status_id)
)
else:
loop = asyncio.new_event_loop()
s = OrderScheduler(loop)

View file

@ -0,0 +1,51 @@
"""Peewee migrations -- 029_add_sub_permissions.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
'user',
permission_orders_pools_view=pw.BooleanField(default=True),
permission_orders_pools_details=pw.BooleanField(default=True),
permission_orders_pools_edit=pw.BooleanField(default=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('user', 'permission_orders_pools_view', 'permission_orders_pools_details', 'permission_orders_pools_edit')

148
orders.py
View file

@ -2,7 +2,7 @@ import logging
import datetime
import asyncio
from db.constants import TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_NOT_PUNISHED, TIMELINE_ORDER_PUNISHED
from db.constants import TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED, TIMELINE_ORDER_NOT_PUNISHED, TIMELINE_ORDER_PUNISHED
from util import make_session
from generate import generate_order, generate_punishment
from db.queries import domsubusers_doms, order_status_by_id, order_status_put, order_status_confirm, timeline_event_put
@ -10,57 +10,55 @@ from mastodon import Mastodon
from telegram.telegram import Telegram
from settings import ENV
from util import timezone
from jinja2 import Environment, PackageLoader, select_autoescape
logger = logging.getLogger(__name__)
template_env = Environment(
loader=PackageLoader("orders"),
autoescape=select_autoescape()
)
template_env.globals['ENV'] = ENV
def filter_short_time(value):
return value.strftime("%I:%M %p")
template_env.filters["short_time"] = filter_short_time
async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at, verify_at=None):
user = orders_pool.user
post = "Here are today's orders for @%s -\n\n" % user.mastodon_account()
post += orders_str + "\n\n"
if repeats > 1:
post += f"These are the same orders from the last {repeats} days\n\n"
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
if verify_at is not None:
post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
post += "\n"
if ENV == 'dev':
post += "⚠️ DEV"
elif user.mastodon_attn_list:
post += f"ATTN - {user.mastodon_attn_list}\n"
template = template_env.get_template('mastodon_order')
post = template.render(
user=user,
orders_str=orders_str,
repeats=repeats,
due_at=due_at,
verify_at=verify_at
)
m = Mastodon(session)
return await m.statusPost(post, user)
async def order_telegram_post(session, orders_pool, orders_str, repeats, due_at, m_url, verify_at=None):
post = "Here are your orders -\n\n"
post += orders_str + "\n\n"
if repeats > 1:
post += f"These are the same orders from the last {repeats} days\n\n"
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
if verify_at is not None:
post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
post += "\n"
post += m_url
if ENV == 'dev':
post += "\n⚠️ DEV"
template = template_env.get_template("telegram_order")
post = template.render(
orders_str = orders_str,
repeats=repeats,
due_at=due_at,
verify_at=verify_at,
m_url=m_url
)
t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post)
async def order_telegram_post_need_mastodon(session, orders_pool):
post = "Cannot issue an order without a mastodon username"
if ENV == 'dev':
post += "\n⚠️ DEV"
async def order_telegram_message(session, orders_pool, message):
template = template_env.get_template("telegram_message")
t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post)
async def order_telegram_post_none(session, orders_pool):
post = "No orders for today"
if ENV == 'dev':
post += "\n⚠️ DEV"
post = template.render(
message=message
)
t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post)
@ -70,15 +68,33 @@ async def order_issue(orders_pool):
user = orders_pool.user
if user.mastodon_username is None:
logger.info('Cannot issue order without mastodon username')
await order_telegram_post_need_mastodon(session, orders_pool)
logger.info(f"{orders_pool} - Cannot issue order without mastodon username")
await order_telegram_message(
session, orders_pool,
"Cannot issue order without mastodon username"
)
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
"Cannot issue order without mastodon username",
user=orders_pool.user,
orders_pool=orders_pool
)
return { "reason": "Cannot issue order without mastodon username" }
orders_info = generate_order(orders_pool)
if 'orders' not in orders_info:
logger.info(f"{orders_pool} - {orders_info['reason']}")
await order_telegram_post_none(session, orders_pool)
await order_telegram_message(
session, orders_pool,
"No orders this time"
)
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
orders_info['reason'],
user=orders_pool.user,
orders_pool=orders_pool
)
return { "reason": orders_info['reason'] }
orders_str = "\n".join(orders_info['orders'])
@ -113,8 +129,7 @@ async def order_issue(orders_pool):
verify_at=verify_at
)
return {
"order_status" : order_status_put(
order_status = order_status_put(
orders_pool,
orders_pool.user,
m_status['id'],
@ -122,20 +137,34 @@ async def order_issue(orders_pool):
due_at,
orders_str,
verify_at=verify_at
),
)
timeline_event_put(
TIMELINE_ORDER_ISSUED,
order_status.text.split("\n")[0],
user=orders_pool.user,
orders_pool=orders_pool,
order_status=order_status,
extra={
"mastodon_status_url": m_status['url']
}
)
logger.info(f"Issued {order_status}")
return {
"order_status" : order_status,
"mastodon_status": m_status
}
async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None):
user = orders_pool.user
post = "@%s has failed to post proof of compliance. Here is the punishment -\n\n" % user.mastodon_account()
post += punishment_str + "\n\n"
if ENV == 'dev':
post += "⚠️ DEV"
elif user.mastodon_attn_list:
post += f"ATTN - {user.mastodon_attn_list}\n"
template = template_env.get_template('mastodon_punishment')
post = template.render(
user=user,
punishment_str=punishment_str
)
m = Mastodon(session)
return await m.statusPost(
@ -145,18 +174,19 @@ async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_i
)
async def punishment_telegram_post(session, orders_pool, punishment_str, m_url):
post = "You failed to show proof of compliance. Here is your punishment -\n\n"
post += punishment_str + "\n\n"
post += m_url
if ENV == 'dev':
post += "\n\n⚠️ DEV"
template = template_env.get_template('telegram_punishment')
post = template.render(
punishment_str=punishment_str,
m_url=m_url
)
t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post)
async def punishment_issue(session, order_status):
if order_status.pool is None or order_status.pool.punishment_pool is None:
logger.info(f'Unable to issue a punishment for {order_status.id}, no punishment pool for order pool {order_status.pool.name}')
logger.info(f'Unable to issue a punishment for {order_status}, no punishment pool for order pool {order_status.pool.name}')
return { "reason": "No punishment pool"}
punishment_pool = order_status.pool.punishment_pool
@ -217,7 +247,7 @@ async def order_check(order_status_id):
user = order_status.user
if order_status.punishment.count() > 0:
logger.info(f'Punishment already issued for {order_status.id}')
logger.info(f'Punishment already issued for {order_status}')
return
m = Mastodon(session)
@ -267,7 +297,7 @@ async def order_check(order_status_id):
confirmed_at = d['created_at']
order_status_confirm(order_status.id, confirmed_at)
logger.info('Confirmed order %s' % (order_status.id))
logger.info(f"Confirmed order {order_status}")
timeline_event_put(
TIMELINE_ORDER_CONFIRMED,
order_status.text.split("\n")[0],
@ -281,7 +311,7 @@ async def order_check(order_status_id):
break
if confirmed_at is None:
logger.info('Order %s remains unconfirmed' % (order_status.id))
logger.info(f"Order {order_status} remains unconfirmed")
due_at = datetime.datetime.fromisoformat(order_status.due_at)
if(due_at < datetime.datetime.now(datetime.UTC)):
@ -297,7 +327,7 @@ async def order_check(order_status_id):
elif user.verify_mastodon_favorite and had_favorites is False:
reason = "No replies had a favorite from a dom or tagged account"
logger.info('Time to issue a punishment for %s' % order_status.id)
logger.info(f"Time to issue a punishment for {order_status}")
issue_result = await punishment_issue(session, order_status)

View file

@ -92,12 +92,11 @@ class OrderScheduler():
)
return
logger.info(f'Issuing order for {orders_pool.name}[{orders_pool.user.telegram_username}]')
issue_result = await order_issue(orders_pool)
if 'order_status' in issue_result:
order_status = issue_result['order_status']
if order_status.due_at is not None or order_status.verify_at is not None:
# Schedule check
check_time = order_status.verify_at if order_status.verify_at is not None else order_status.due_at
@ -106,23 +105,6 @@ class OrderScheduler():
self.scheduled_check,
args=(order_status.id,)
)
timeline_event_put(
TIMELINE_ORDER_ISSUED,
order_status.text.split("\n")[0],
user=orders_pool.user,
orders_pool=orders_pool,
order_status=order_status,
extra={
"mastodon_status_url": issue_result['mastodon_status']['url']
}
)
elif 'reason' in issue_result:
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
issue_result['reason'],
user=orders_pool.user,
orders_pool=orders_pool
)
async def scheduled_check(self, outstanding_order_id):
await order_check(outstanding_order_id)

9
templates/mastodon_base Normal file
View file

@ -0,0 +1,9 @@
{% block main %}{% endblock %}
{% block attn %}
{% if user.mastodon_attn_list %}
ATTN - {{user.mastodon_attn_list}}
{% endif %}
{% endblock %}
{% block env %}
{% if ENV == 'dev' %}⚠️ DEV{% endif %}
{% endblock %}

16
templates/mastodon_order Normal file
View file

@ -0,0 +1,16 @@
{% extends "mastodon_base" %}
{% block main %}
Here are today's orders for @{{user.mastodon_account()}}
{{orders_str}}
{% if repeats > 1 %}
These are the same orders from the last {{repeats}} days.
{% endif %}
Proof of compliance is due by {{due_at|short_time}}
{% if verify_at %}
Verification due by {{verify_at|short_time}}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "mastodon_base" %}
{% block main %}
@{{user.mastodon_account()}} failed to show proof of compliance. Here is the punishment -
{{punishment_str}}
{% endblock %}

2
templates/telegram_base Normal file
View file

@ -0,0 +1,2 @@
{% block main %}{% endblock %}
{% if ENV == 'dev' %}⚠️ DEV{% endif %}

View file

@ -0,0 +1,4 @@
{% extends "telegram_base" %}
{% block main %}
{{message}}
{% endblock %}

15
templates/telegram_order Normal file
View file

@ -0,0 +1,15 @@
{% extends "telegram_base" %}
{% block main %}
Here are your orders -
{{orders_str}}
{% if repeats > 1 %}
These are the same orders from the last {{repeats}} days.
{% endif %}
Proof of compliance is due by {{due_at|short_time}}
{% if verify_at %}
Proof of verification is due by {{verify_at|short_time}}
{% endif %}
{{m_url}}
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "telegram_base" %}
{% block main %}
You failed to show proof of compliance. Here is your punishment -
{{punishment_str}}
{{m_url}}
{% endblock %}

View file

@ -6,7 +6,7 @@ from flask import Blueprint, jsonify, abort, request
from flask_login import login_required, current_user
from db.constants import TIMELINE_ORDERS_POOL_CREATED, TIMELINE_ORDERS_POOL_DELETED, TIMELINE_ORDERS_POOL_UPDATED
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
from db.queries import timeline_event_put, timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set
from db.queries import timeline_event_put, timeline_event_recent, user_can_orders_pools_details, user_can_orders_pools_edit, user_can_orders_pools_view, user_doms, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
from util import time_sqlite
@ -79,13 +79,14 @@ def timeline():
"username": t.user.telegram_username,
"actor_username": t.actor.telegram_username if t.actor is not None else None,
"orders_pool": {
"id": t.orders_pool.id,
"id": t.orders_pool.id if user_can_orders_pools_edit(current_user.db_user, t.user) else None,
"name": t.orders_pool.name,
} if t.orders_pool is not None else None,
} if (
t.orders_pool is not None and
user_can_orders_pools_view(current_user.db_user, t.user)
) else None,
"order_status": t.order_status.text if t.order_status is not None else None,
} for t in timeline_event_recent(
subs,
[current_user.db_user.id,])
} for t in timeline_event_recent(current_user.db_user.id)
])
@api.route('/mastodon_oauth')
@ -163,16 +164,28 @@ def authorized_sub(func):
@login_required
@authorized_sub
def sub(username, sub):
if user_has_doms(sub) and sub.id == current_user.db_user.id:
if request.method == "POST":
if (user_has_doms(sub) and sub.id == current_user.db_user.id and
('permission_orders_pools_view' in request.json or
'permission_orders_pools_details' in request.json or
'permission_orders_pools_edit' in request.json)):
abort(403)
return
if request.method == "POST":
if 'verify_mastodon_favorite' in request.json:
sub.verify_mastodon_favorite = bool(request.json['verify_mastodon_favorite'])
if 'verify_mastodon_alt_text' in request.json:
sub.verify_mastodon_alt_text = bool(request.json['verify_mastodon_alt_text'])
if request.json['verify_delay'] is not None:
if 'verify_delay' in request.json and request.json['verify_delay'] is not None:
sub.verify_delay = int(request.json["verify_delay"])
if 'permission_orders_pools_view' in request.json:
sub.permission_orders_pools_view = bool(request.json['permission_orders_pools_view'])
if 'permission_orders_pools_details' in request.json:
sub.permission_orders_pools_details = bool(request.json['permission_orders_pools_details'])
if 'permission_orders_pools_edit' in request.json:
sub.permission_orders_pools_edit = bool(request.json['permission_orders_pools_edit'])
sub.save()
return ('', 204)
@ -183,13 +196,27 @@ def sub(username, sub):
"mastodon_username": sub.mastodon_username,
"verify_mastodon_alt_text": sub.verify_mastodon_alt_text,
"verify_mastodon_favorite": sub.verify_mastodon_favorite,
"verify_delay": sub.verify_delay
"verify_delay": sub.verify_delay,
"can_edit_permissions": current_user.db_user in user_doms(sub.id),
"permission_orders_pools_view": sub.permission_orders_pools_view,
"permission_orders_pools_details": sub.permission_orders_pools_details,
"permission_orders_pools_edit": sub.permission_orders_pools_edit
})
@api.route('/orders/')
@login_required
def my_order_sets():
return jsonify([
user = current_user.db_user
result = {
'permissions': {
'can_view': user_can_orders_pools_view(user, user),
'can_details': user_can_orders_pools_details(user, user),
'can_edit': user_can_orders_pools_edit(user, user)
}
}
if result['permissions']['can_details']:
result['pools'] = [
{
'id': op.id,
'name': op.name,
@ -207,13 +234,39 @@ def my_order_sets():
}
for op
in orders_pool_list(current_user.db_user)
])
]
elif result['permissions']['can_view']:
result['pools'] = [
{
'id': op.id,
'name': op.name,
'scheduled': op.scheduled,
'time': op.time,
'weekends': op.weekends,
'weekdays': op.weekdays,
'probability': op.probability,
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
}
for op
in orders_pool_list(current_user.db_user)
]
return jsonify(result)
@api.route('/orders/<username>/sets')
@login_required
@authorized_sub
def sub_order_sets(username, sub):
return jsonify([
result = {
'permissions': {
'can_view': user_can_orders_pools_view(current_user.db_user, sub),
'can_details': user_can_orders_pools_details(current_user.db_user, sub),
'can_edit': user_can_orders_pools_edit(current_user.db_user, sub)
}
}
if result['permissions']['can_details']:
result['pools'] = [
{
'id': op.id,
'name': op.name,
@ -231,12 +284,33 @@ def sub_order_sets(username, sub):
}
for op
in orders_pool_list(sub.id)
])
]
elif result['permissions']['can_view']:
result['pools'] = [
{
'id': op.id,
'name': op.name,
'scheduled': op.scheduled,
'time': op.time,
'weekends': op.weekends,
'weekdays': op.weekdays,
'probability': op.probability,
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
}
for op
in orders_pool_list(sub.id)
]
return jsonify(result)
@api.route('/orders/<username>/sets/', methods=['POST'])
@login_required
@authorized_sub
def sub_order_set_create(username, sub):
if not user_can_orders_pools_edit(current_user.db_user, sub):
abort(403)
return
# Create new
with database.atomic() as transaction:
try:
@ -287,6 +361,10 @@ def sub_order_set(username, set_id, sub):
op = orders_pool(sub.id, set_id)
if request.method == 'POST':
if not user_can_orders_pools_edit(current_user.db_user, sub):
abort(403)
return
def update_add_ons(order, add_ons):
for updated_add_on in add_ons:
if isinstance(updated_add_on['id'], int):
@ -371,6 +449,10 @@ def sub_order_set(username, set_id, sub):
abort(500)
elif request.method == 'DELETE':
try:
if not user_can_orders_pools_edit(current_user.db_user, sub):
abort(403)
return
op.delete_instance(recursive=True)
timeline_event_put(

View file

@ -81,12 +81,11 @@ def index():
print('Redirect to dashboard')
return redirect('/dashboard/')
@app.route('/profile/', defaults={'path': ''})
@app.route('/dashboard/', defaults={'path': ''})
@app.route('/dashboard/')
@app.route('/profile/')
@app.route('/orders/<path:path>')
@app.route('/dashboard/<path:path>')
@login_required
def dashboard(path):
def dashboard(path=''):
return render_template('dashboard.html')
@app.route('/logout')

View file

@ -1,7 +1,7 @@
import React from "react";
import { useDisclosure } from "@mantine/hooks";
import { Button, Modal, Text, Flex } from "@mantine/core";
import { Button, Modal, Text, Flex, ButtonVariant } from "@mantine/core";
export const ConfirmDialogButton: React.FC<{
text: string;
@ -9,7 +9,15 @@ export const ConfirmDialogButton: React.FC<{
buttonColor: string;
onConfirm: () => void;
children: React.ReactNode;
}> = ({ text, buttonText, buttonColor, onConfirm, children }) => {
variant?: ButtonVariant;
}> = ({
text,
buttonText,
buttonColor,
onConfirm,
children,
variant = "filled",
}) => {
const [opened, { open, close }] = useDisclosure();
const handleConfirm = React.useCallback(() => {
@ -22,13 +30,15 @@ export const ConfirmDialogButton: React.FC<{
<Modal opened={opened} onClose={close} p="sm" withCloseButton={false}>
<Text mb="xl">{text}</Text>
<Flex gap="md" justify="flex-end">
<Button onClick={close}>Cancel</Button>
<Button variant="outline" onClick={close}>
Cancel
</Button>
<Button color={buttonColor} onClick={handleConfirm}>
{buttonText}
</Button>
</Flex>
</Modal>
<Button color={buttonColor} onClick={open}>
<Button variant={variant} color={buttonColor} onClick={open}>
{children}
</Button>
</>

View file

@ -3,7 +3,7 @@ import { Text, Title, Flex, Card, Image } from "@mantine/core";
import { useLoaderData } from "react-router";
import { IconPencil } from "@tabler/icons-react";
import { NavigateButton } from "./NavigateButton";
import { OrderSetProps, OrderSets } from "./OrderSets";
import { OrderSets, OrderSetsResponse } from "./OrderSets";
import { useUserContext } from "./UserContext";
import { TimelineList } from "./TimelineList";
@ -25,15 +25,7 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
</Title>
<Flex gap="md" wrap="wrap">
{subs.map(({ sub_username, telegram_photo_url }) => (
<Card
key={sub_username}
shadow="sm"
padding="lg"
radius="md"
withBorder
bg="gray.2"
w="320px"
>
<Card key={sub_username} padding="lg" withBorder w="320px">
{telegram_photo_url ? (
<Card.Section>
<Image
@ -62,14 +54,20 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
export const Dashboard: React.FC = () => {
const [orderSets, subs, timeline] =
useLoaderData<
[OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]]
[OrderSetsResponse, SubsListProps["subs"], TimelineEvent[]]
>();
const { username } = useUserContext();
return (
<>
{timeline.length > 0 ? <TimelineList timeline={timeline} /> : null}
<OrderSets orderSets={orderSets} username={username} />
{orderSets.permissions?.can_view && orderSets.pools.length > 0 ? (
<OrderSets
orderSets={orderSets.pools}
permissions={orderSets.permissions}
username={username}
/>
) : null}
{subs.length > 0 ? <SubsList subs={subs} /> : null}
</>
);

View file

@ -1,25 +1,32 @@
import { Avatar, Text, Container, Flex, UnstyledButton } from "@mantine/core";
import {
Avatar,
Text,
Container,
Flex,
UnstyledButton,
Anchor,
} from "@mantine/core";
import React from "react";
import { useUserContext } from "./UserContext";
import { Outlet, useNavigate } from "react-router";
import { Link, Outlet, useNavigate } from "react-router";
export const Header: React.FC = () => {
const { username, telegram_photo_url } = useUserContext();
const navigate = useNavigate();
const handleClick = React.useCallback(() => {
navigate("/profile");
navigate("/profile/");
}, []);
return (
<Container p="sm">
<Flex justify="flex-end">
<UnstyledButton onClick={handleClick}>
<Anchor component={Link} to={"/profile/"}>
<Flex align="center" gap="sm">
<Text c="blue">{username}</Text>
<Text>{username}</Text>
<Avatar src={telegram_photo_url} />
</Flex>
</UnstyledButton>
</Anchor>
</Flex>
<Outlet />
</Container>

View file

@ -16,6 +16,7 @@ import {
NumberInput,
Text,
Select,
Anchor,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { TimeInput } from "@mantine/dates";
@ -82,9 +83,22 @@ export const OrderSet: React.FC = () => {
const [orderSets, setOrderSets] = React.useState([]);
React.useEffect(() => {
fetch(`/api/orders/${username}/sets`)
.then((response) => response.json())
.then(setOrderSets);
fetch(`/api/orders/${username}/sets`).then(async (response) => {
if (response.ok) {
const data = await response.json();
if (!data.permissions?.can_edit) {
notifications.show({
title: "Error",
message: "Not authorized",
color: "red",
});
navigate(`/dashboard/`);
}
setOrderSets(data.pools);
}
});
}, [username]);
const [showScheduling, setShowScheduling] = React.useState(
@ -248,7 +262,9 @@ export const OrderSet: React.FC = () => {
<>
<Box mb="lg">
<Title order={1}>{orderSet?.name || "New Order Set"}</Title>
<Link to={`/orders/${username}`}>Return to {username}</Link>
<Anchor component={Link} to={`/orders/${username}`}>
Return to {username}
</Anchor>
</Box>
<form id="order-set" onSubmit={handleSubmit}>
<TextInput {...form.getInputProps("name")} label="Name" />
@ -259,7 +275,7 @@ export const OrderSet: React.FC = () => {
mt="lg"
/>
<Collapse in={showScheduling}>
<Paper bg="gray.2">
<Paper>
<Input.Wrapper
error={form.getInputProps("probability").error}
label="Probability"
@ -316,7 +332,7 @@ export const OrderSet: React.FC = () => {
</Title>
{form.getValues().orders.map(({ id: order_id, _delete }, idx) =>
_delete ? null : (
<Paper key={order_id} bg="gray.2">
<Paper key={order_id}>
<Flex gap="xl" justify="space-between">
<TextInput
{...form.getInputProps(`orders.${idx}.name`)}
@ -406,7 +422,7 @@ export const OrderSet: React.FC = () => {
</Button>
<Box my="40px" />
<Affix position={{ bottom: 0 }} w="100%">
<Paper mb={0} bg="gray.1">
<Paper mb={0}>
<Container>
<Flex justify="flex-end">
<Button type="submit" form="order-set">

View file

@ -9,6 +9,7 @@ import {
RingProgress,
Alert,
Grid,
Anchor,
} from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react";
import { TimeValue } from "@mantine/dates";
@ -21,20 +22,38 @@ import { useUserContext } from "./UserContext";
import { DonutChart } from "@mantine/charts";
const COLORS_ROTATION = [
"teal",
"pink",
"lime",
"violet",
"orange",
"blue",
"yellow",
"grape",
"green",
"red",
"cyan",
"gray",
"orange.8",
"gray.7",
"orange.7",
"gray.6",
"orange.6",
"gray.5",
"orange.5",
"gray.4",
];
export interface OrderSetsResponse {
pools: (Pick<
OrderSet,
| "id"
| "name"
| "scheduled"
| "time"
| "weekends"
| "weekdays"
| "orders"
| "probability"
> & {
orders: Pick<OrderSetOrder, "id" | "name" | "weight">;
punishment_pool_name: string;
})[];
permissions: {
can_view: boolean;
can_details: boolean;
can_edit: boolean;
};
}
export interface OrderSetProps {
orderSets: (Pick<
OrderSet,
@ -52,12 +71,14 @@ export interface OrderSetProps {
})[];
username: string;
linkBack?: React.ReactNode;
permissions?: OrderSetsResponse["permissions"];
}
export const OrderSets: React.FC<OrderSetProps> = ({
orderSets,
username,
linkBack,
permissions,
}) => {
const { username: current_user } = useUserContext();
const fetcher = useFetcher();
@ -74,12 +95,13 @@ export const OrderSets: React.FC<OrderSetProps> = ({
const [isMastodonSet, setIsMastodonSet] = React.useState(true);
React.useEffect(() => {
if (username) {
fetch(`/api/subs/${username}`)
.then((response) => response.json())
.then((data) => {
fetch(`/api/subs/${username}`).then(async (response) => {
if (response.ok) {
const data = await response.json();
if (!data.mastodon_server || !data.mastodon_username) {
setIsMastodonSet(false);
}
}
});
}
}, [username]);
@ -87,12 +109,12 @@ export const OrderSets: React.FC<OrderSetProps> = ({
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
return (
<>
<Box my="lg">
<Box mb="lg">
<Title order={1}>Order Sets for {username}</Title>
{linkBack ? linkBack : null}
</Box>
{orderSets.length > 0 && isMastodonSet ? null : (
{isMastodonSet ? null : (
<Flex justify="center">
<Alert
variant="light"
@ -107,7 +129,9 @@ export const OrderSets: React.FC<OrderSetProps> = ({
{username === current_user ? (
<>
<br />
<Link to="/profile/">Edit Profile</Link>
<Anchor component={Link} to="/profile/">
Edit Profile
</Anchor>
</>
) : null}
</Alert>
@ -120,6 +144,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
}
}}
>
{permissions?.can_view ? (
<Grid gutter="md">
{orderSets
? orderSets.map(
@ -140,7 +165,6 @@ export const OrderSets: React.FC<OrderSetProps> = ({
padding="lg"
radius="md"
withBorder
bg="gray.2"
mb="0"
>
<Flex direction="column" gap="md" h="100%">
@ -163,10 +187,10 @@ export const OrderSets: React.FC<OrderSetProps> = ({
</Flex>
<Flex gap="md" align="center">
{weekdays ? (
<Badge color="blue">Weekdays</Badge>
<Badge color="orange.7">Weekdays</Badge>
) : null}
{weekends ? (
<Badge color="blue">Weekends</Badge>
<Badge color="orange.7">Weekends</Badge>
) : null}
<RingProgress
size={30}
@ -177,7 +201,10 @@ export const OrderSets: React.FC<OrderSetProps> = ({
</Text>
}
sections={[
{ color: "cyan", value: probability * 100 },
{
color: "orange",
value: probability * 100,
},
]}
/>
</Flex>
@ -188,8 +215,8 @@ export const OrderSets: React.FC<OrderSetProps> = ({
<b>Punishments:</b> {punishment_pool_name}
</Text>
) : null}
<Flex justify="end" align="flex-end" gap="md">
{orders.length > 0 ? (
<Flex justify="end" align="flex-end" gap="sm">
{permissions?.can_details && orders.length > 0 ? (
<DonutChart
flex={1}
size={130}
@ -198,23 +225,34 @@ export const OrderSets: React.FC<OrderSetProps> = ({
name,
value: weight,
color:
COLORS_ROTATION[idx % COLORS_ROTATION.length],
COLORS_ROTATION[
idx % COLORS_ROTATION.length
],
}))}
tooltipDataSource="segment"
/>
) : null}
{permissions?.can_edit ? (
<>
<ConfirmDialogButton
buttonColor="red.8"
buttonText="Delete"
variant="transparent"
text={`Are you sure you want to delete ${name}?`}
onConfirm={() => handleDelete(id)}
>
<IconTrash />
</ConfirmDialogButton>
<NavigateButton to={`/orders/${username}/${id}`}>
<IconPencil style={{ marginRight: "0.5rem" }} />
<NavigateButton
to={`/orders/${username}/${id}`}
>
<IconPencil
style={{ marginRight: "0.5rem" }}
/>
Edit
</NavigateButton>
</>
) : null}
</Flex>
</Flex>
</Card>
@ -223,13 +261,29 @@ export const OrderSets: React.FC<OrderSetProps> = ({
)
: null}
</Grid>
) : (
<Flex justify="center">
<Alert
variant="light"
color="orange"
title="Warning"
icon={<IconAlertTriangle />}
my="md"
w="40rem"
>
You are not authorized to view order pools for <b>{username}</b>.
</Alert>
</Flex>
)}
</div>
{permissions?.can_edit ? (
<Box my="lg">
<NavigateButton to={`/orders/${username}/new`}>
<IconPlus style={{ marginRight: "0.5rem" }} />
New
</NavigateButton>
</Box>
</>
) : null}
</Box>
);
};

View file

@ -1,6 +1,7 @@
import React from "react";
import { useUserContext } from "./UserContext";
import {
Anchor,
Avatar,
Box,
Button,
@ -134,9 +135,11 @@ export const Profile: React.FC = () => {
<Title>{username}</Title>
</Flex>
<Box mb="md">
<Link to={`/dashboard`}>Return to dashboard</Link>
<Anchor component={Link} to={`/dashboard/`}>
Return to dashboard
</Anchor>
</Box>
<Paper bg="gray.1">
<Paper withBorder>
<Title order={4}>Mastodon</Title>
<TextInput label="Account" w="50%" value={mastodon_account} />
<Button onClick={open}>Authorize with Mastodon</Button>

View file

@ -0,0 +1,129 @@
import React from "react";
import { Button, Checkbox, Paper, Title } from "@mantine/core";
import { useForm } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import { fetchHeaders } from "./fetch";
type ProfilePermissionsProps = Pick<
UserProfile,
| "permission_orders_pools_view"
| "permission_orders_pools_details"
| "permission_orders_pools_edit"
> & { username: string };
type ProfilePermissionsForm = Pick<
UserProfile,
| "permission_orders_pools_view"
| "permission_orders_pools_details"
| "permission_orders_pools_edit"
>;
export const ProfilePermissions: React.FC<ProfilePermissionsProps> = ({
username,
permission_orders_pools_view,
permission_orders_pools_details,
permission_orders_pools_edit,
}) => {
const [loading, setLoading] = React.useState(false);
const [detailsDisabled, setDetailsDisabled] = React.useState(
!permission_orders_pools_view,
);
const [editDisabled, setEditDisabled] = React.useState(
!permission_orders_pools_details,
);
const form = useForm<ProfilePermissionsForm>({
mode: "uncontrolled",
initialValues: {
permission_orders_pools_view,
permission_orders_pools_details,
permission_orders_pools_edit,
},
});
form.watch("permission_orders_pools_view", ({ value }) => {
if (!value) {
form.setFieldValue("permission_orders_pools_details", false);
setDetailsDisabled(true);
} else {
setDetailsDisabled(false);
}
});
form.watch("permission_orders_pools_details", ({ value }) => {
if (!value) {
form.setFieldValue("permission_orders_pools_edit", false);
setEditDisabled(true);
} else {
setEditDisabled(false);
}
});
const handleSubmit = React.useCallback(
form.onSubmit((values) => {
setLoading(true);
fetch(`/api/subs/${username}`, {
method: "POST",
headers: fetchHeaders(),
body: JSON.stringify(values),
})
.then((response) => {
if (response.ok) {
notifications.show({
title: "Success",
message: "Permissions have been saved",
color: "green",
});
} else {
notifications.show({
title: "Error",
message: "There was a problem saving permissions",
color: "red",
});
}
})
.finally(() => {
setLoading(false);
});
}),
[],
);
return (
<Paper>
<form onSubmit={handleSubmit}>
<Title order={4} mb="md">
Permissions
</Title>
<Checkbox
{...form.getInputProps("permission_orders_pools_view", {
type: "checkbox",
})}
key={form.key("permission_orders_pools_view")}
label="Sub can view orders sets"
mb="md"
/>
<Checkbox
{...form.getInputProps("permission_orders_pools_details", {
type: "checkbox",
})}
key={form.key("permission_orders_pools_details")}
disabled={detailsDisabled}
label="Sub can view orders sets details"
mb="md"
/>
<Checkbox
{...form.getInputProps("permission_orders_pools_edit", {
type: "checkbox",
})}
key={form.key("permission_orders_pools_edit")}
disabled={editDisabled}
label="Sub can edit orders sets"
mb="md"
/>
<Button type="submit" loading={loading} mt="md">
Save
</Button>
</form>
</Paper>
);
};

View file

@ -75,7 +75,7 @@ export const ProfileVerification: React.FC<ProfileVerificationProps> = ({
);
return (
<Paper bg="gray.1">
<Paper>
<form onSubmit={handleSubmit}>
<Title order={4} mb="md">
Order Verification

View file

@ -1,46 +0,0 @@
import React from "react";
import { Params, useLoaderData, useParams, Link } from "react-router";
import { OrderSetProps, OrderSets } from "./OrderSets";
import { ProfileVerification } from "./ProfileVerification";
import { Title } from "@mantine/core";
export const subOrderSetsLoader = async ({
params: { username },
}: {
params: Params<string>;
}) => fetch(`/api/orders/${username}/sets`).then((response) => response.json());
export const SubOrderSets: React.FC = () => {
const { username: sub_username } = useParams();
const orderSets = useLoaderData<OrderSetProps["orderSets"]>();
const [profile, setProfile] = React.useState<UserProfile | null>(null);
React.useEffect(() => {
fetch(`/api/subs/${sub_username}`)
.then((response) => response.json())
.then(setProfile);
}, [sub_username]);
return (
<>
<OrderSets
username={sub_username}
orderSets={orderSets}
linkBack={<Link to={`/dashboard/`}>Return to dashboard</Link>}
/>
{profile ? (
<>
<Title order={1} mb="md">
Sub Profile
</Title>
<ProfileVerification
username={sub_username}
verify_mastodon_alt_text={profile.verify_mastodon_alt_text}
verify_mastodon_favorite={profile.verify_mastodon_favorite}
verify_delay={profile.verify_delay}
/>
</>
) : null}
</>
);
};

View file

@ -1,4 +1,4 @@
import { Timeline, Text, Title, Box, Flex } from "@mantine/core";
import { Timeline, Text, Title, Box, Flex, Card, Anchor } from "@mantine/core";
import React from "react";
import moment from "moment";
import { Link } from "react-router";
@ -14,6 +14,7 @@ import {
IconLock,
IconX,
} from "@tabler/icons-react";
import { useUserContext } from "./UserContext";
const TIMELINE_TYPE = {
ORDER_NOT_ISSUED: {
@ -28,27 +29,27 @@ const TIMELINE_TYPE = {
},
ORDER_CONFIRMED: {
title: "Order confirmed",
color: "green.6",
color: "green.9",
bullet: <IconCheck />,
},
ORDER_NOT_PUNISHED: {
title: "Order not punished",
color: "gray.6",
color: "gray.8",
bullet: null,
},
ORDER_PUNISHED: {
title: "Order punished",
color: "red.6",
color: "red.8",
bullet: <IconAlertCircle />,
},
ORDERS_POOL_CREATED: {
title: "Orders pool created",
color: "green.3",
color: "green.6",
bullet: <IconFilePlus />,
},
ORDERS_POOL_UPDATED: {
title: "Orders pool updated",
color: "green.3",
color: "green.6",
bullet: <IconFileDiff />,
},
ORDERS_POOL_DELETED: {
@ -60,11 +61,15 @@ const TIMELINE_TYPE = {
export const TimelineList: React.FC<{
timeline: TimelineEvent[];
}> = ({ timeline }) => (
}> = ({ timeline }) => {
const { username: my_username } = useUserContext();
return (
<Box>
<Title order={1} mb="lg">
Timeline
</Title>
<Card py="xs">
<Timeline bulletSize={24} lineWidth={2} my="lg">
{timeline.map(
({
@ -89,16 +94,23 @@ export const TimelineList: React.FC<{
<Flex mt={4} gap="xs">
{extra?.mastodon_status_url ? (
<Text size="xs">
<a href={extra.mastodon_status_url} target="_blank">
<Anchor href={extra.mastodon_status_url} target="_blank">
Mastodon Post <IconExternalLink size="0.75rem" />
</a>
</Anchor>
</Text>
) : null}
{orders_pool ? (
<Text size="xs">
<Link to={`/orders/${username}/${orders_pool.id}`}>
{orders_pool.id ? (
<Anchor
component={Link}
to={`/orders/${username}/${orders_pool.id}`}
>
{orders_pool.name}
</Link>
</Anchor>
) : (
orders_pool.name
)}
</Text>
) : null}
{actor_username ? (
@ -107,7 +119,15 @@ export const TimelineList: React.FC<{
</Text>
) : null}
<Text size="xs">
{username != my_username ? (
<Anchor component={Link} to={`/orders/${username}`}>
<IconLock size="0.75rem" /> {username}
</Anchor>
) : (
<>
<IconLock size="0.75rem" /> {username}
</>
)}
</Text>
<Text size="xs">{moment(updated_at).fromNow()}</Text>
</Flex>
@ -115,5 +135,7 @@ export const TimelineList: React.FC<{
),
)}
</Timeline>
</Card>
</Box>
);
};

View file

@ -0,0 +1,68 @@
import React from "react";
import { Params, useLoaderData, useParams, Link } from "react-router";
import { OrderSetProps, OrderSets, OrderSetsResponse } from "./OrderSets";
import { ProfileVerification } from "./ProfileVerification";
import { Anchor, Title } from "@mantine/core";
import { ProfilePermissions } from "./ProfilePermissions";
export const userOrderSetsLoader = async ({
params: { username },
}: {
params: Params<string>;
}) => fetch(`/api/orders/${username}/sets`).then((response) => response.json());
export const UserOrderSets: React.FC = () => {
const { username: sub_username } = useParams();
const orderSets = useLoaderData<OrderSetsResponse>();
const [profile, setProfile] = React.useState<UserProfile | null>(null);
React.useEffect(() => {
fetch(`/api/subs/${sub_username}`).then(async (response) => {
if (response.ok) {
setProfile(await response.json());
}
});
}, [sub_username]);
return (
<>
<OrderSets
username={sub_username}
orderSets={orderSets.pools}
permissions={orderSets.permissions}
linkBack={
<Anchor component={Link} to={`/dashboard/`}>
Return to dashboard
</Anchor>
}
/>
{profile ? (
<>
<Title order={1} mb="md">
Sub Profile
</Title>
<ProfileVerification
username={sub_username}
verify_mastodon_alt_text={profile.verify_mastodon_alt_text}
verify_mastodon_favorite={profile.verify_mastodon_favorite}
verify_delay={profile.verify_delay}
/>
{profile.can_edit_permissions ? (
<ProfilePermissions
username={sub_username}
permission_orders_pools_view={
profile.permission_orders_pools_view
}
permission_orders_pools_details={
profile.permission_orders_pools_details
}
permission_orders_pools_edit={
profile.permission_orders_pools_edit
}
/>
) : null}
</>
) : null}
</>
);
};

View file

@ -9,6 +9,10 @@ type UserProfile = {
verify_mastodon_alt_text?: boolean;
verify_mastodon_favorite?: boolean;
verify_delay?: number;
can_edit_permissions?: boolean;
permission_orders_pools_view?: boolean;
permission_orders_pools_details?: boolean;
permission_orders_pools_edit?: boolean;
}
type OrderSetOrderAddOn = {

View file

@ -8,16 +8,20 @@ import {
Input,
Paper,
Slider,
Image,
mergeThemeOverrides,
} from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import { mantineTheme } from "./theme";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/notifications/styles.css";
import "@mantine/charts/styles.css";
import { Dashboard, subsListLoader } from "./Dashboard";
import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";
import { UserOrderSets, userOrderSetsLoader } from "./UserOrderSets";
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
import { UserContextProvider } from "./UserContext";
import { Header } from "./Header";
@ -46,6 +50,11 @@ const theme = createTheme({
},
}) as any,
}),
Image: Image.extend({
defaultProps: {
radius: "xs",
},
}),
},
});
@ -66,8 +75,8 @@ const router = createBrowserRouter([
},
{
path: "orders/:username",
Component: SubOrderSets,
loader: subOrderSetsLoader,
Component: UserOrderSets,
loader: userOrderSetsLoader,
},
{
path: "orders/:username/new",
@ -83,9 +92,11 @@ const router = createBrowserRouter([
},
]);
const mergedTheme = mergeThemeOverrides(mantineTheme, theme);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<MantineProvider theme={theme}>
<MantineProvider defaultColorScheme="dark" theme={mergedTheme}>
<UserContextProvider>
<Notifications />
<RouterProvider router={router} />

78
web/vite/src/theme.ts Normal file
View file

@ -0,0 +1,78 @@
import { Card, Container, createTheme, Paper, rem, Select } from "@mantine/core";
import type { MantineThemeOverride } from "@mantine/core";
const CONTAINER_SIZES: Record<string, string> = {
xxs: rem("200px"),
xs: rem("300px"),
sm: rem("400px"),
md: rem("500px"),
lg: rem("600px"),
xl: rem("1400px"),
xxl: rem("1600px"),
};
export const mantineTheme: MantineThemeOverride = createTheme({
/** Put your mantine theme override here */
fontSizes: {
xs: rem("12px"),
sm: rem("14px"),
md: rem("16px"),
lg: rem("18px"),
xl: rem("20px"),
"2xl": rem("24px"),
"3xl": rem("30px"),
"4xl": rem("36px"),
"5xl": rem("48px"),
},
spacing: {
"3xs": rem("4px"),
"2xs": rem("8px"),
xs: rem("10px"),
sm: rem("12px"),
md: rem("16px"),
lg: rem("20px"),
xl: rem("24px"),
"2xl": rem("28px"),
"3xl": rem("32px"),
},
primaryColor: "orange",
components: {
/** Put your mantine component override here */
Container: Container.extend({
vars: (_, { size, fluid }) => ({
root: {
"--container-size": fluid
? "100%"
: size !== undefined && size in CONTAINER_SIZES
? CONTAINER_SIZES[size]
: rem(size),
},
}),
}),
Paper: Paper.extend({
defaultProps: {
p: "md",
shadow: "xl",
radius: "md",
withBorder: true,
},
}),
Card: Card.extend({
defaultProps: {
p: "xl",
shadow: "xl",
radius: "var(--mantine-radius-default)",
withBorder: true,
},
}),
Select: Select.extend({
defaultProps: {
checkIconPosition: "right",
},
}),
},
other: {
style: "mantine",
},
});