Compare commits
10 commits
70fd59a4be
...
2089ee4161
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2089ee4161 | ||
|
|
cfccd424cb | ||
|
|
34e8d0eae3 | ||
|
|
40486f81fd | ||
|
|
05d5093e23 | ||
|
|
556c1dd7eb | ||
|
|
6a7e996406 | ||
|
|
fa366534f8 | ||
|
|
4c5c8bec04 | ||
| 01589eedeb |
30 changed files with 1058 additions and 442 deletions
|
|
@ -6,3 +6,9 @@ TIMELINE_ORDER_PUNISHED = "ORDER_PUNISHED"
|
||||||
TIMELINE_ORDERS_POOL_CREATED = "ORDERS_POOL_CREATED"
|
TIMELINE_ORDERS_POOL_CREATED = "ORDERS_POOL_CREATED"
|
||||||
TIMELINE_ORDERS_POOL_UPDATED = "ORDERS_POOL_UPDATED"
|
TIMELINE_ORDERS_POOL_UPDATED = "ORDERS_POOL_UPDATED"
|
||||||
TIMELINE_ORDERS_POOL_DELETED = "ORDERS_POOL_DELETED"
|
TIMELINE_ORDERS_POOL_DELETED = "ORDERS_POOL_DELETED"
|
||||||
|
|
||||||
|
TIMELINE_ORDERS_POOL_EVENTS = [
|
||||||
|
TIMELINE_ORDERS_POOL_CREATED,
|
||||||
|
TIMELINE_ORDERS_POOL_UPDATED,
|
||||||
|
TIMELINE_ORDERS_POOL_DELETED
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ class User(BaseModel):
|
||||||
verify_mastodon_favorite = BooleanField(null=False, default=False)
|
verify_mastodon_favorite = BooleanField(null=False, default=False)
|
||||||
verify_delay = IntegerField(null=True)
|
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):
|
def mastodon_account(self):
|
||||||
if self.mastodon_server is None or self.mastodon_username is None:
|
if self.mastodon_server is None or self.mastodon_username is None:
|
||||||
return
|
return
|
||||||
|
|
@ -183,6 +187,9 @@ class OrderStatus(BaseModel):
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pool} {self.id}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = 'order_status'
|
table_name = 'order_status'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import datetime
|
||||||
import json
|
import json
|
||||||
from peewee import JOIN, fn
|
from peewee import JOIN, fn
|
||||||
|
|
||||||
|
from db.constants import TIMELINE_ORDERS_POOL_EVENTS
|
||||||
from util import sqlite_time
|
from util import sqlite_time
|
||||||
|
|
||||||
from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus, MastodonServer, TimelineEvent
|
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):
|
def user_has_doms(id):
|
||||||
return DomSubUsers.select().where(DomSubUsers.sub_id == id).count() > 0
|
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):
|
def mastodon_server_get(name):
|
||||||
return MastodonServer.get(name=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
|
extra=json.dumps(extra) if extra is not None else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def timeline_event_recent(user_ids, actor_ids=None):
|
def timeline_event_recent(user_id, limit=5):
|
||||||
return (TimelineEvent
|
user = User.get_by_id(user_id)
|
||||||
.select()
|
|
||||||
.where((
|
can_view_orders_pools = user_can_orders_pools_view(user, user)
|
||||||
TimelineEvent.user_id.in_(user_ids) |
|
|
||||||
(TimelineEvent.actor_user_id.in_(actor_ids) if actor_ids is not None else True)
|
result = TimelineEvent.select()
|
||||||
))
|
|
||||||
.order_by(TimelineEvent.updated_at.desc())
|
if(can_view_orders_pools):
|
||||||
.limit(10)
|
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
40
main.py
|
|
@ -7,38 +7,24 @@ import json
|
||||||
from db.constants import TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED
|
from db.constants import TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED
|
||||||
from scheduling import OrderScheduler
|
from scheduling import OrderScheduler
|
||||||
from generate import generate_order
|
from generate import generate_order
|
||||||
from orders import order_issue, order_check
|
from orders import order_issue, order_check, punishment_issue
|
||||||
from db.queries import orders_pool_by_id, timeline_event_put
|
from db.queries import orders_pool_by_id, order_status_by_id
|
||||||
from telegram.telegram import handle_commands
|
from telegram.telegram import handle_commands
|
||||||
from telegram.commands import commands
|
from telegram.commands import commands
|
||||||
|
from util import make_session
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def do_order_issue(orders_pool_id):
|
async def do_order_issue(orders_pool_id):
|
||||||
orders_pool = orders_pool_by_id(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:
|
async def do_punishment_issue(order_status_id):
|
||||||
order_status = issue_result['order_status']
|
order_status = order_status_by_id(order_status_id)
|
||||||
logger.info(f'Issued order id {order_status.id}')
|
|
||||||
timeline_event_put(
|
async with make_session() as session:
|
||||||
TIMELINE_ORDER_ISSUED,
|
await punishment_issue(session, order_status)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__=='__main__':
|
||||||
logging.basicConfig(
|
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 = subparsers.add_parser('check', help='Check on the status of an order')
|
||||||
parser_issue.add_argument('order_status_id')
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.command == 'generate':
|
if args.command == 'generate':
|
||||||
|
|
@ -77,6 +66,11 @@ if __name__=='__main__':
|
||||||
loop.run_until_complete(
|
loop.run_until_complete(
|
||||||
order_check(args.order_status_id)
|
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:
|
else:
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
s = OrderScheduler(loop)
|
s = OrderScheduler(loop)
|
||||||
|
|
|
||||||
51
migrations/029_add_sub_permissions.py
Normal file
51
migrations/029_add_sub_permissions.py
Normal 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
148
orders.py
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
import datetime
|
import datetime
|
||||||
import asyncio
|
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 util import make_session
|
||||||
from generate import generate_order, generate_punishment
|
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
|
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 telegram.telegram import Telegram
|
||||||
from settings import ENV
|
from settings import ENV
|
||||||
from util import timezone
|
from util import timezone
|
||||||
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at, verify_at=None):
|
||||||
user = orders_pool.user
|
user = orders_pool.user
|
||||||
|
|
||||||
post = "Here are today's orders for @%s -\n\n" % user.mastodon_account()
|
template = template_env.get_template('mastodon_order')
|
||||||
post += orders_str + "\n\n"
|
post = template.render(
|
||||||
if repeats > 1:
|
user=user,
|
||||||
post += f"These are the same orders from the last {repeats} days\n\n"
|
orders_str=orders_str,
|
||||||
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
|
repeats=repeats,
|
||||||
if verify_at is not None:
|
due_at=due_at,
|
||||||
post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
|
verify_at=verify_at
|
||||||
post += "\n"
|
)
|
||||||
|
|
||||||
if ENV == 'dev':
|
|
||||||
post += "⚠️ DEV"
|
|
||||||
elif user.mastodon_attn_list:
|
|
||||||
post += f"ATTN - {user.mastodon_attn_list}\n"
|
|
||||||
|
|
||||||
m = Mastodon(session)
|
m = Mastodon(session)
|
||||||
return await m.statusPost(post, user)
|
return await m.statusPost(post, user)
|
||||||
|
|
||||||
async def order_telegram_post(session, orders_pool, orders_str, repeats, due_at, m_url, verify_at=None):
|
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"
|
template = template_env.get_template("telegram_order")
|
||||||
post += orders_str + "\n\n"
|
|
||||||
if repeats > 1:
|
post = template.render(
|
||||||
post += f"These are the same orders from the last {repeats} days\n\n"
|
orders_str = orders_str,
|
||||||
post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
|
repeats=repeats,
|
||||||
if verify_at is not None:
|
due_at=due_at,
|
||||||
post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
|
verify_at=verify_at,
|
||||||
post += "\n"
|
m_url=m_url
|
||||||
post += m_url
|
)
|
||||||
if ENV == 'dev':
|
|
||||||
post += "\n⚠️ DEV"
|
|
||||||
|
|
||||||
t = Telegram(session)
|
t = Telegram(session)
|
||||||
await t.message_send(orders_pool.user.telegram_chat_id, post)
|
await t.message_send(orders_pool.user.telegram_chat_id, post)
|
||||||
|
|
||||||
async def order_telegram_post_need_mastodon(session, orders_pool):
|
async def order_telegram_message(session, orders_pool, message):
|
||||||
post = "Cannot issue an order without a mastodon username"
|
template = template_env.get_template("telegram_message")
|
||||||
if ENV == 'dev':
|
|
||||||
post += "\n⚠️ DEV"
|
|
||||||
|
|
||||||
t = Telegram(session)
|
post = template.render(
|
||||||
await t.message_send(orders_pool.user.telegram_chat_id, post)
|
message=message
|
||||||
|
)
|
||||||
async def order_telegram_post_none(session, orders_pool):
|
|
||||||
post = "No orders for today"
|
|
||||||
if ENV == 'dev':
|
|
||||||
post += "\n⚠️ DEV"
|
|
||||||
|
|
||||||
t = Telegram(session)
|
t = Telegram(session)
|
||||||
await t.message_send(orders_pool.user.telegram_chat_id, post)
|
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
|
user = orders_pool.user
|
||||||
|
|
||||||
if user.mastodon_username is None:
|
if user.mastodon_username is None:
|
||||||
logger.info('Cannot issue order without mastodon username')
|
logger.info(f"{orders_pool} - Cannot issue order without mastodon username")
|
||||||
await order_telegram_post_need_mastodon(session, orders_pool)
|
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" }
|
return { "reason": "Cannot issue order without mastodon username" }
|
||||||
|
|
||||||
orders_info = generate_order(orders_pool)
|
orders_info = generate_order(orders_pool)
|
||||||
|
|
||||||
if 'orders' not in orders_info:
|
if 'orders' not in orders_info:
|
||||||
logger.info(f"{orders_pool} - {orders_info['reason']}")
|
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'] }
|
return { "reason": orders_info['reason'] }
|
||||||
|
|
||||||
orders_str = "\n".join(orders_info['orders'])
|
orders_str = "\n".join(orders_info['orders'])
|
||||||
|
|
@ -113,8 +129,7 @@ async def order_issue(orders_pool):
|
||||||
verify_at=verify_at
|
verify_at=verify_at
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
order_status = order_status_put(
|
||||||
"order_status" : order_status_put(
|
|
||||||
orders_pool,
|
orders_pool,
|
||||||
orders_pool.user,
|
orders_pool.user,
|
||||||
m_status['id'],
|
m_status['id'],
|
||||||
|
|
@ -122,20 +137,34 @@ async def order_issue(orders_pool):
|
||||||
due_at,
|
due_at,
|
||||||
orders_str,
|
orders_str,
|
||||||
verify_at=verify_at
|
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
|
"mastodon_status": m_status
|
||||||
}
|
}
|
||||||
|
|
||||||
async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None):
|
async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None):
|
||||||
user = orders_pool.user
|
user = orders_pool.user
|
||||||
|
|
||||||
post = "@%s has failed to post proof of compliance. Here is the punishment -\n\n" % user.mastodon_account()
|
template = template_env.get_template('mastodon_punishment')
|
||||||
post += punishment_str + "\n\n"
|
post = template.render(
|
||||||
|
user=user,
|
||||||
if ENV == 'dev':
|
punishment_str=punishment_str
|
||||||
post += "⚠️ DEV"
|
)
|
||||||
elif user.mastodon_attn_list:
|
|
||||||
post += f"ATTN - {user.mastodon_attn_list}\n"
|
|
||||||
|
|
||||||
m = Mastodon(session)
|
m = Mastodon(session)
|
||||||
return await m.statusPost(
|
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):
|
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"
|
template = template_env.get_template('telegram_punishment')
|
||||||
post += punishment_str + "\n\n"
|
|
||||||
post += m_url
|
post = template.render(
|
||||||
if ENV == 'dev':
|
punishment_str=punishment_str,
|
||||||
post += "\n\n⚠️ DEV"
|
m_url=m_url
|
||||||
|
)
|
||||||
|
|
||||||
t = Telegram(session)
|
t = Telegram(session)
|
||||||
await t.message_send(orders_pool.user.telegram_chat_id, post)
|
await t.message_send(orders_pool.user.telegram_chat_id, post)
|
||||||
|
|
||||||
async def punishment_issue(session, order_status):
|
async def punishment_issue(session, order_status):
|
||||||
if order_status.pool is None or order_status.pool.punishment_pool is None:
|
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"}
|
return { "reason": "No punishment pool"}
|
||||||
|
|
||||||
punishment_pool = order_status.pool.punishment_pool
|
punishment_pool = order_status.pool.punishment_pool
|
||||||
|
|
@ -217,7 +247,7 @@ async def order_check(order_status_id):
|
||||||
user = order_status.user
|
user = order_status.user
|
||||||
|
|
||||||
if order_status.punishment.count() > 0:
|
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
|
return
|
||||||
|
|
||||||
m = Mastodon(session)
|
m = Mastodon(session)
|
||||||
|
|
@ -267,7 +297,7 @@ async def order_check(order_status_id):
|
||||||
|
|
||||||
confirmed_at = d['created_at']
|
confirmed_at = d['created_at']
|
||||||
order_status_confirm(order_status.id, confirmed_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_event_put(
|
||||||
TIMELINE_ORDER_CONFIRMED,
|
TIMELINE_ORDER_CONFIRMED,
|
||||||
order_status.text.split("\n")[0],
|
order_status.text.split("\n")[0],
|
||||||
|
|
@ -281,7 +311,7 @@ async def order_check(order_status_id):
|
||||||
break
|
break
|
||||||
|
|
||||||
if confirmed_at is None:
|
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)
|
due_at = datetime.datetime.fromisoformat(order_status.due_at)
|
||||||
if(due_at < datetime.datetime.now(datetime.UTC)):
|
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:
|
elif user.verify_mastodon_favorite and had_favorites is False:
|
||||||
reason = "No replies had a favorite from a dom or tagged account"
|
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)
|
issue_result = await punishment_issue(session, order_status)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,11 @@ class OrderScheduler():
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f'Issuing order for {orders_pool.name}[{orders_pool.user.telegram_username}]')
|
|
||||||
|
|
||||||
issue_result = await order_issue(orders_pool)
|
issue_result = await order_issue(orders_pool)
|
||||||
|
|
||||||
if 'order_status' in issue_result:
|
if 'order_status' in issue_result:
|
||||||
order_status = issue_result['order_status']
|
order_status = issue_result['order_status']
|
||||||
|
|
||||||
if order_status.due_at is not None or order_status.verify_at is not None:
|
if order_status.due_at is not None or order_status.verify_at is not None:
|
||||||
# Schedule check
|
# Schedule check
|
||||||
check_time = order_status.verify_at if order_status.verify_at is not None else order_status.due_at
|
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,
|
self.scheduled_check,
|
||||||
args=(order_status.id,)
|
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):
|
async def scheduled_check(self, outstanding_order_id):
|
||||||
await order_check(outstanding_order_id)
|
await order_check(outstanding_order_id)
|
||||||
|
|
|
||||||
9
templates/mastodon_base
Normal file
9
templates/mastodon_base
Normal 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
16
templates/mastodon_order
Normal 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 %}
|
||||||
7
templates/mastodon_punishment
Normal file
7
templates/mastodon_punishment
Normal 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
2
templates/telegram_base
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% block main %}{% endblock %}
|
||||||
|
{% if ENV == 'dev' %}⚠️ DEV{% endif %}
|
||||||
4
templates/telegram_message
Normal file
4
templates/telegram_message
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% extends "telegram_base" %}
|
||||||
|
{% block main %}
|
||||||
|
{{message}}
|
||||||
|
{% endblock %}
|
||||||
15
templates/telegram_order
Normal file
15
templates/telegram_order
Normal 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 %}
|
||||||
9
templates/telegram_punishment
Normal file
9
templates/telegram_punishment
Normal 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 %}
|
||||||
110
web/api.py
110
web/api.py
|
|
@ -6,7 +6,7 @@ from flask import Blueprint, jsonify, abort, request
|
||||||
from flask_login import login_required, current_user
|
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.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.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 settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
||||||
from util import time_sqlite
|
from util import time_sqlite
|
||||||
|
|
||||||
|
|
@ -79,13 +79,14 @@ def timeline():
|
||||||
"username": t.user.telegram_username,
|
"username": t.user.telegram_username,
|
||||||
"actor_username": t.actor.telegram_username if t.actor is not None else None,
|
"actor_username": t.actor.telegram_username if t.actor is not None else None,
|
||||||
"orders_pool": {
|
"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,
|
"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,
|
"order_status": t.order_status.text if t.order_status is not None else None,
|
||||||
} for t in timeline_event_recent(
|
} for t in timeline_event_recent(current_user.db_user.id)
|
||||||
subs,
|
|
||||||
[current_user.db_user.id,])
|
|
||||||
])
|
])
|
||||||
|
|
||||||
@api.route('/mastodon_oauth')
|
@api.route('/mastodon_oauth')
|
||||||
|
|
@ -163,16 +164,28 @@ def authorized_sub(func):
|
||||||
@login_required
|
@login_required
|
||||||
@authorized_sub
|
@authorized_sub
|
||||||
def sub(username, 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)
|
abort(403)
|
||||||
return
|
return
|
||||||
|
|
||||||
if request.method == "POST":
|
if 'verify_mastodon_favorite' in request.json:
|
||||||
sub.verify_mastodon_favorite = bool(request.json['verify_mastodon_favorite'])
|
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'])
|
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"])
|
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()
|
sub.save()
|
||||||
|
|
||||||
return ('', 204)
|
return ('', 204)
|
||||||
|
|
@ -183,13 +196,27 @@ def sub(username, sub):
|
||||||
"mastodon_username": sub.mastodon_username,
|
"mastodon_username": sub.mastodon_username,
|
||||||
"verify_mastodon_alt_text": sub.verify_mastodon_alt_text,
|
"verify_mastodon_alt_text": sub.verify_mastodon_alt_text,
|
||||||
"verify_mastodon_favorite": sub.verify_mastodon_favorite,
|
"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/')
|
@api.route('/orders/')
|
||||||
@login_required
|
@login_required
|
||||||
def my_order_sets():
|
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,
|
'id': op.id,
|
||||||
'name': op.name,
|
'name': op.name,
|
||||||
|
|
@ -207,13 +234,39 @@ def my_order_sets():
|
||||||
}
|
}
|
||||||
for op
|
for op
|
||||||
in orders_pool_list(current_user.db_user)
|
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')
|
@api.route('/orders/<username>/sets')
|
||||||
@login_required
|
@login_required
|
||||||
@authorized_sub
|
@authorized_sub
|
||||||
def sub_order_sets(username, 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,
|
'id': op.id,
|
||||||
'name': op.name,
|
'name': op.name,
|
||||||
|
|
@ -231,12 +284,33 @@ def sub_order_sets(username, sub):
|
||||||
}
|
}
|
||||||
for op
|
for op
|
||||||
in orders_pool_list(sub.id)
|
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'])
|
@api.route('/orders/<username>/sets/', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@authorized_sub
|
@authorized_sub
|
||||||
def sub_order_set_create(username, 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
|
# Create new
|
||||||
with database.atomic() as transaction:
|
with database.atomic() as transaction:
|
||||||
try:
|
try:
|
||||||
|
|
@ -287,6 +361,10 @@ def sub_order_set(username, set_id, sub):
|
||||||
op = orders_pool(sub.id, set_id)
|
op = orders_pool(sub.id, set_id)
|
||||||
|
|
||||||
if request.method == 'POST':
|
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):
|
def update_add_ons(order, add_ons):
|
||||||
for updated_add_on in add_ons:
|
for updated_add_on in add_ons:
|
||||||
if isinstance(updated_add_on['id'], int):
|
if isinstance(updated_add_on['id'], int):
|
||||||
|
|
@ -371,6 +449,10 @@ def sub_order_set(username, set_id, sub):
|
||||||
abort(500)
|
abort(500)
|
||||||
elif request.method == 'DELETE':
|
elif request.method == 'DELETE':
|
||||||
try:
|
try:
|
||||||
|
if not user_can_orders_pools_edit(current_user.db_user, sub):
|
||||||
|
abort(403)
|
||||||
|
return
|
||||||
|
|
||||||
op.delete_instance(recursive=True)
|
op.delete_instance(recursive=True)
|
||||||
|
|
||||||
timeline_event_put(
|
timeline_event_put(
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,11 @@ def index():
|
||||||
print('Redirect to dashboard')
|
print('Redirect to dashboard')
|
||||||
return redirect('/dashboard/')
|
return redirect('/dashboard/')
|
||||||
|
|
||||||
@app.route('/profile/', defaults={'path': ''})
|
@app.route('/dashboard/')
|
||||||
@app.route('/dashboard/', defaults={'path': ''})
|
@app.route('/profile/')
|
||||||
@app.route('/orders/<path:path>')
|
@app.route('/orders/<path:path>')
|
||||||
@app.route('/dashboard/<path:path>')
|
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard(path):
|
def dashboard(path=''):
|
||||||
return render_template('dashboard.html')
|
return render_template('dashboard.html')
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
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<{
|
export const ConfirmDialogButton: React.FC<{
|
||||||
text: string;
|
text: string;
|
||||||
|
|
@ -9,7 +9,15 @@ export const ConfirmDialogButton: React.FC<{
|
||||||
buttonColor: string;
|
buttonColor: string;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = ({ text, buttonText, buttonColor, onConfirm, children }) => {
|
variant?: ButtonVariant;
|
||||||
|
}> = ({
|
||||||
|
text,
|
||||||
|
buttonText,
|
||||||
|
buttonColor,
|
||||||
|
onConfirm,
|
||||||
|
children,
|
||||||
|
variant = "filled",
|
||||||
|
}) => {
|
||||||
const [opened, { open, close }] = useDisclosure();
|
const [opened, { open, close }] = useDisclosure();
|
||||||
|
|
||||||
const handleConfirm = React.useCallback(() => {
|
const handleConfirm = React.useCallback(() => {
|
||||||
|
|
@ -22,13 +30,15 @@ export const ConfirmDialogButton: React.FC<{
|
||||||
<Modal opened={opened} onClose={close} p="sm" withCloseButton={false}>
|
<Modal opened={opened} onClose={close} p="sm" withCloseButton={false}>
|
||||||
<Text mb="xl">{text}</Text>
|
<Text mb="xl">{text}</Text>
|
||||||
<Flex gap="md" justify="flex-end">
|
<Flex gap="md" justify="flex-end">
|
||||||
<Button onClick={close}>Cancel</Button>
|
<Button variant="outline" onClick={close}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
<Button color={buttonColor} onClick={handleConfirm}>
|
<Button color={buttonColor} onClick={handleConfirm}>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Button color={buttonColor} onClick={open}>
|
<Button variant={variant} color={buttonColor} onClick={open}>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Text, Title, Flex, Card, Image } from "@mantine/core";
|
||||||
import { useLoaderData } from "react-router";
|
import { useLoaderData } from "react-router";
|
||||||
import { IconPencil } from "@tabler/icons-react";
|
import { IconPencil } from "@tabler/icons-react";
|
||||||
import { NavigateButton } from "./NavigateButton";
|
import { NavigateButton } from "./NavigateButton";
|
||||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
import { OrderSets, OrderSetsResponse } from "./OrderSets";
|
||||||
import { useUserContext } from "./UserContext";
|
import { useUserContext } from "./UserContext";
|
||||||
import { TimelineList } from "./TimelineList";
|
import { TimelineList } from "./TimelineList";
|
||||||
|
|
||||||
|
|
@ -25,15 +25,7 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
|
||||||
</Title>
|
</Title>
|
||||||
<Flex gap="md" wrap="wrap">
|
<Flex gap="md" wrap="wrap">
|
||||||
{subs.map(({ sub_username, telegram_photo_url }) => (
|
{subs.map(({ sub_username, telegram_photo_url }) => (
|
||||||
<Card
|
<Card key={sub_username} padding="lg" withBorder w="320px">
|
||||||
key={sub_username}
|
|
||||||
shadow="sm"
|
|
||||||
padding="lg"
|
|
||||||
radius="md"
|
|
||||||
withBorder
|
|
||||||
bg="gray.2"
|
|
||||||
w="320px"
|
|
||||||
>
|
|
||||||
{telegram_photo_url ? (
|
{telegram_photo_url ? (
|
||||||
<Card.Section>
|
<Card.Section>
|
||||||
<Image
|
<Image
|
||||||
|
|
@ -62,14 +54,20 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
|
||||||
export const Dashboard: React.FC = () => {
|
export const Dashboard: React.FC = () => {
|
||||||
const [orderSets, subs, timeline] =
|
const [orderSets, subs, timeline] =
|
||||||
useLoaderData<
|
useLoaderData<
|
||||||
[OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]]
|
[OrderSetsResponse, SubsListProps["subs"], TimelineEvent[]]
|
||||||
>();
|
>();
|
||||||
const { username } = useUserContext();
|
const { username } = useUserContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{timeline.length > 0 ? <TimelineList timeline={timeline} /> : null}
|
{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}
|
{subs.length > 0 ? <SubsList subs={subs} /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 React from "react";
|
||||||
import { useUserContext } from "./UserContext";
|
import { useUserContext } from "./UserContext";
|
||||||
import { Outlet, useNavigate } from "react-router";
|
import { Link, Outlet, useNavigate } from "react-router";
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
export const Header: React.FC = () => {
|
||||||
const { username, telegram_photo_url } = useUserContext();
|
const { username, telegram_photo_url } = useUserContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleClick = React.useCallback(() => {
|
const handleClick = React.useCallback(() => {
|
||||||
navigate("/profile");
|
navigate("/profile/");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container p="sm">
|
<Container p="sm">
|
||||||
<Flex justify="flex-end">
|
<Flex justify="flex-end">
|
||||||
<UnstyledButton onClick={handleClick}>
|
<Anchor component={Link} to={"/profile/"}>
|
||||||
<Flex align="center" gap="sm">
|
<Flex align="center" gap="sm">
|
||||||
<Text c="blue">{username}</Text>
|
<Text>{username}</Text>
|
||||||
<Avatar src={telegram_photo_url} />
|
<Avatar src={telegram_photo_url} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</UnstyledButton>
|
</Anchor>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
NumberInput,
|
NumberInput,
|
||||||
Text,
|
Text,
|
||||||
Select,
|
Select,
|
||||||
|
Anchor,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { TimeInput } from "@mantine/dates";
|
import { TimeInput } from "@mantine/dates";
|
||||||
|
|
@ -82,9 +83,22 @@ export const OrderSet: React.FC = () => {
|
||||||
|
|
||||||
const [orderSets, setOrderSets] = React.useState([]);
|
const [orderSets, setOrderSets] = React.useState([]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetch(`/api/orders/${username}/sets`)
|
fetch(`/api/orders/${username}/sets`).then(async (response) => {
|
||||||
.then((response) => response.json())
|
if (response.ok) {
|
||||||
.then(setOrderSets);
|
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]);
|
}, [username]);
|
||||||
|
|
||||||
const [showScheduling, setShowScheduling] = React.useState(
|
const [showScheduling, setShowScheduling] = React.useState(
|
||||||
|
|
@ -248,7 +262,9 @@ export const OrderSet: React.FC = () => {
|
||||||
<>
|
<>
|
||||||
<Box mb="lg">
|
<Box mb="lg">
|
||||||
<Title order={1}>{orderSet?.name || "New Order Set"}</Title>
|
<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>
|
</Box>
|
||||||
<form id="order-set" onSubmit={handleSubmit}>
|
<form id="order-set" onSubmit={handleSubmit}>
|
||||||
<TextInput {...form.getInputProps("name")} label="Name" />
|
<TextInput {...form.getInputProps("name")} label="Name" />
|
||||||
|
|
@ -259,7 +275,7 @@ export const OrderSet: React.FC = () => {
|
||||||
mt="lg"
|
mt="lg"
|
||||||
/>
|
/>
|
||||||
<Collapse in={showScheduling}>
|
<Collapse in={showScheduling}>
|
||||||
<Paper bg="gray.2">
|
<Paper>
|
||||||
<Input.Wrapper
|
<Input.Wrapper
|
||||||
error={form.getInputProps("probability").error}
|
error={form.getInputProps("probability").error}
|
||||||
label="Probability"
|
label="Probability"
|
||||||
|
|
@ -316,7 +332,7 @@ export const OrderSet: React.FC = () => {
|
||||||
</Title>
|
</Title>
|
||||||
{form.getValues().orders.map(({ id: order_id, _delete }, idx) =>
|
{form.getValues().orders.map(({ id: order_id, _delete }, idx) =>
|
||||||
_delete ? null : (
|
_delete ? null : (
|
||||||
<Paper key={order_id} bg="gray.2">
|
<Paper key={order_id}>
|
||||||
<Flex gap="xl" justify="space-between">
|
<Flex gap="xl" justify="space-between">
|
||||||
<TextInput
|
<TextInput
|
||||||
{...form.getInputProps(`orders.${idx}.name`)}
|
{...form.getInputProps(`orders.${idx}.name`)}
|
||||||
|
|
@ -406,7 +422,7 @@ export const OrderSet: React.FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Box my="40px" />
|
<Box my="40px" />
|
||||||
<Affix position={{ bottom: 0 }} w="100%">
|
<Affix position={{ bottom: 0 }} w="100%">
|
||||||
<Paper mb={0} bg="gray.1">
|
<Paper mb={0}>
|
||||||
<Container>
|
<Container>
|
||||||
<Flex justify="flex-end">
|
<Flex justify="flex-end">
|
||||||
<Button type="submit" form="order-set">
|
<Button type="submit" form="order-set">
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
RingProgress,
|
RingProgress,
|
||||||
Alert,
|
Alert,
|
||||||
Grid,
|
Grid,
|
||||||
|
Anchor,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||||
import { TimeValue } from "@mantine/dates";
|
import { TimeValue } from "@mantine/dates";
|
||||||
|
|
@ -21,20 +22,38 @@ import { useUserContext } from "./UserContext";
|
||||||
import { DonutChart } from "@mantine/charts";
|
import { DonutChart } from "@mantine/charts";
|
||||||
|
|
||||||
const COLORS_ROTATION = [
|
const COLORS_ROTATION = [
|
||||||
"teal",
|
"orange.8",
|
||||||
"pink",
|
"gray.7",
|
||||||
"lime",
|
"orange.7",
|
||||||
"violet",
|
"gray.6",
|
||||||
"orange",
|
"orange.6",
|
||||||
"blue",
|
"gray.5",
|
||||||
"yellow",
|
"orange.5",
|
||||||
"grape",
|
"gray.4",
|
||||||
"green",
|
|
||||||
"red",
|
|
||||||
"cyan",
|
|
||||||
"gray",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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 {
|
export interface OrderSetProps {
|
||||||
orderSets: (Pick<
|
orderSets: (Pick<
|
||||||
OrderSet,
|
OrderSet,
|
||||||
|
|
@ -52,12 +71,14 @@ export interface OrderSetProps {
|
||||||
})[];
|
})[];
|
||||||
username: string;
|
username: string;
|
||||||
linkBack?: React.ReactNode;
|
linkBack?: React.ReactNode;
|
||||||
|
permissions?: OrderSetsResponse["permissions"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderSets: React.FC<OrderSetProps> = ({
|
export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
orderSets,
|
orderSets,
|
||||||
username,
|
username,
|
||||||
linkBack,
|
linkBack,
|
||||||
|
permissions,
|
||||||
}) => {
|
}) => {
|
||||||
const { username: current_user } = useUserContext();
|
const { username: current_user } = useUserContext();
|
||||||
const fetcher = useFetcher();
|
const fetcher = useFetcher();
|
||||||
|
|
@ -74,12 +95,13 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
const [isMastodonSet, setIsMastodonSet] = React.useState(true);
|
const [isMastodonSet, setIsMastodonSet] = React.useState(true);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (username) {
|
if (username) {
|
||||||
fetch(`/api/subs/${username}`)
|
fetch(`/api/subs/${username}`).then(async (response) => {
|
||||||
.then((response) => response.json())
|
if (response.ok) {
|
||||||
.then((data) => {
|
const data = await response.json();
|
||||||
if (!data.mastodon_server || !data.mastodon_username) {
|
if (!data.mastodon_server || !data.mastodon_username) {
|
||||||
setIsMastodonSet(false);
|
setIsMastodonSet(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
@ -87,12 +109,12 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
|
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box my="lg">
|
||||||
<Box mb="lg">
|
<Box mb="lg">
|
||||||
<Title order={1}>Order Sets for {username}</Title>
|
<Title order={1}>Order Sets for {username}</Title>
|
||||||
{linkBack ? linkBack : null}
|
{linkBack ? linkBack : null}
|
||||||
</Box>
|
</Box>
|
||||||
{orderSets.length > 0 && isMastodonSet ? null : (
|
{isMastodonSet ? null : (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Alert
|
<Alert
|
||||||
variant="light"
|
variant="light"
|
||||||
|
|
@ -107,7 +129,9 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
{username === current_user ? (
|
{username === current_user ? (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<Link to="/profile/">Edit Profile</Link>
|
<Anchor component={Link} to="/profile/">
|
||||||
|
Edit Profile
|
||||||
|
</Anchor>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
@ -120,6 +144,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{permissions?.can_view ? (
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
{orderSets
|
{orderSets
|
||||||
? orderSets.map(
|
? orderSets.map(
|
||||||
|
|
@ -140,7 +165,6 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
padding="lg"
|
padding="lg"
|
||||||
radius="md"
|
radius="md"
|
||||||
withBorder
|
withBorder
|
||||||
bg="gray.2"
|
|
||||||
mb="0"
|
mb="0"
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap="md" h="100%">
|
<Flex direction="column" gap="md" h="100%">
|
||||||
|
|
@ -163,10 +187,10 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap="md" align="center">
|
<Flex gap="md" align="center">
|
||||||
{weekdays ? (
|
{weekdays ? (
|
||||||
<Badge color="blue">Weekdays</Badge>
|
<Badge color="orange.7">Weekdays</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{weekends ? (
|
{weekends ? (
|
||||||
<Badge color="blue">Weekends</Badge>
|
<Badge color="orange.7">Weekends</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
<RingProgress
|
<RingProgress
|
||||||
size={30}
|
size={30}
|
||||||
|
|
@ -177,7 +201,10 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
sections={[
|
sections={[
|
||||||
{ color: "cyan", value: probability * 100 },
|
{
|
||||||
|
color: "orange",
|
||||||
|
value: probability * 100,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
@ -188,8 +215,8 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
<b>Punishments:</b> {punishment_pool_name}
|
<b>Punishments:</b> {punishment_pool_name}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<Flex justify="end" align="flex-end" gap="md">
|
<Flex justify="end" align="flex-end" gap="sm">
|
||||||
{orders.length > 0 ? (
|
{permissions?.can_details && orders.length > 0 ? (
|
||||||
<DonutChart
|
<DonutChart
|
||||||
flex={1}
|
flex={1}
|
||||||
size={130}
|
size={130}
|
||||||
|
|
@ -198,23 +225,34 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
name,
|
name,
|
||||||
value: weight,
|
value: weight,
|
||||||
color:
|
color:
|
||||||
COLORS_ROTATION[idx % COLORS_ROTATION.length],
|
COLORS_ROTATION[
|
||||||
|
idx % COLORS_ROTATION.length
|
||||||
|
],
|
||||||
}))}
|
}))}
|
||||||
tooltipDataSource="segment"
|
tooltipDataSource="segment"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{permissions?.can_edit ? (
|
||||||
|
<>
|
||||||
<ConfirmDialogButton
|
<ConfirmDialogButton
|
||||||
buttonColor="red.8"
|
buttonColor="red.8"
|
||||||
buttonText="Delete"
|
buttonText="Delete"
|
||||||
|
variant="transparent"
|
||||||
text={`Are you sure you want to delete ${name}?`}
|
text={`Are you sure you want to delete ${name}?`}
|
||||||
onConfirm={() => handleDelete(id)}
|
onConfirm={() => handleDelete(id)}
|
||||||
>
|
>
|
||||||
<IconTrash />
|
<IconTrash />
|
||||||
</ConfirmDialogButton>
|
</ConfirmDialogButton>
|
||||||
<NavigateButton to={`/orders/${username}/${id}`}>
|
<NavigateButton
|
||||||
<IconPencil style={{ marginRight: "0.5rem" }} />
|
to={`/orders/${username}/${id}`}
|
||||||
|
>
|
||||||
|
<IconPencil
|
||||||
|
style={{ marginRight: "0.5rem" }}
|
||||||
|
/>
|
||||||
Edit
|
Edit
|
||||||
</NavigateButton>
|
</NavigateButton>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -223,13 +261,29 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
</Grid>
|
</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>
|
</div>
|
||||||
|
{permissions?.can_edit ? (
|
||||||
<Box my="lg">
|
<Box my="lg">
|
||||||
<NavigateButton to={`/orders/${username}/new`}>
|
<NavigateButton to={`/orders/${username}/new`}>
|
||||||
<IconPlus style={{ marginRight: "0.5rem" }} />
|
<IconPlus style={{ marginRight: "0.5rem" }} />
|
||||||
New
|
New
|
||||||
</NavigateButton>
|
</NavigateButton>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
) : null}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useUserContext } from "./UserContext";
|
import { useUserContext } from "./UserContext";
|
||||||
import {
|
import {
|
||||||
|
Anchor,
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -134,9 +135,11 @@ export const Profile: React.FC = () => {
|
||||||
<Title>{username}</Title>
|
<Title>{username}</Title>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box mb="md">
|
<Box mb="md">
|
||||||
<Link to={`/dashboard`}>Return to dashboard</Link>
|
<Anchor component={Link} to={`/dashboard/`}>
|
||||||
|
Return to dashboard
|
||||||
|
</Anchor>
|
||||||
</Box>
|
</Box>
|
||||||
<Paper bg="gray.1">
|
<Paper withBorder>
|
||||||
<Title order={4}>Mastodon</Title>
|
<Title order={4}>Mastodon</Title>
|
||||||
<TextInput label="Account" w="50%" value={mastodon_account} />
|
<TextInput label="Account" w="50%" value={mastodon_account} />
|
||||||
<Button onClick={open}>Authorize with Mastodon</Button>
|
<Button onClick={open}>Authorize with Mastodon</Button>
|
||||||
|
|
|
||||||
129
web/vite/src/ProfilePermissions.tsx
Normal file
129
web/vite/src/ProfilePermissions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -75,7 +75,7 @@ export const ProfileVerification: React.FC<ProfileVerificationProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper bg="gray.1">
|
<Paper>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Title order={4} mb="md">
|
<Title order={4} mb="md">
|
||||||
Order Verification
|
Order Verification
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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 React from "react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
IconLock,
|
IconLock,
|
||||||
IconX,
|
IconX,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
import { useUserContext } from "./UserContext";
|
||||||
|
|
||||||
const TIMELINE_TYPE = {
|
const TIMELINE_TYPE = {
|
||||||
ORDER_NOT_ISSUED: {
|
ORDER_NOT_ISSUED: {
|
||||||
|
|
@ -28,27 +29,27 @@ const TIMELINE_TYPE = {
|
||||||
},
|
},
|
||||||
ORDER_CONFIRMED: {
|
ORDER_CONFIRMED: {
|
||||||
title: "Order confirmed",
|
title: "Order confirmed",
|
||||||
color: "green.6",
|
color: "green.9",
|
||||||
bullet: <IconCheck />,
|
bullet: <IconCheck />,
|
||||||
},
|
},
|
||||||
ORDER_NOT_PUNISHED: {
|
ORDER_NOT_PUNISHED: {
|
||||||
title: "Order not punished",
|
title: "Order not punished",
|
||||||
color: "gray.6",
|
color: "gray.8",
|
||||||
bullet: null,
|
bullet: null,
|
||||||
},
|
},
|
||||||
ORDER_PUNISHED: {
|
ORDER_PUNISHED: {
|
||||||
title: "Order punished",
|
title: "Order punished",
|
||||||
color: "red.6",
|
color: "red.8",
|
||||||
bullet: <IconAlertCircle />,
|
bullet: <IconAlertCircle />,
|
||||||
},
|
},
|
||||||
ORDERS_POOL_CREATED: {
|
ORDERS_POOL_CREATED: {
|
||||||
title: "Orders pool created",
|
title: "Orders pool created",
|
||||||
color: "green.3",
|
color: "green.6",
|
||||||
bullet: <IconFilePlus />,
|
bullet: <IconFilePlus />,
|
||||||
},
|
},
|
||||||
ORDERS_POOL_UPDATED: {
|
ORDERS_POOL_UPDATED: {
|
||||||
title: "Orders pool updated",
|
title: "Orders pool updated",
|
||||||
color: "green.3",
|
color: "green.6",
|
||||||
bullet: <IconFileDiff />,
|
bullet: <IconFileDiff />,
|
||||||
},
|
},
|
||||||
ORDERS_POOL_DELETED: {
|
ORDERS_POOL_DELETED: {
|
||||||
|
|
@ -60,11 +61,15 @@ const TIMELINE_TYPE = {
|
||||||
|
|
||||||
export const TimelineList: React.FC<{
|
export const TimelineList: React.FC<{
|
||||||
timeline: TimelineEvent[];
|
timeline: TimelineEvent[];
|
||||||
}> = ({ timeline }) => (
|
}> = ({ timeline }) => {
|
||||||
|
const { username: my_username } = useUserContext();
|
||||||
|
|
||||||
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={1} mb="lg">
|
<Title order={1} mb="lg">
|
||||||
Timeline
|
Timeline
|
||||||
</Title>
|
</Title>
|
||||||
|
<Card py="xs">
|
||||||
<Timeline bulletSize={24} lineWidth={2} my="lg">
|
<Timeline bulletSize={24} lineWidth={2} my="lg">
|
||||||
{timeline.map(
|
{timeline.map(
|
||||||
({
|
({
|
||||||
|
|
@ -89,16 +94,23 @@ export const TimelineList: React.FC<{
|
||||||
<Flex mt={4} gap="xs">
|
<Flex mt={4} gap="xs">
|
||||||
{extra?.mastodon_status_url ? (
|
{extra?.mastodon_status_url ? (
|
||||||
<Text size="xs">
|
<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" />
|
Mastodon Post <IconExternalLink size="0.75rem" />
|
||||||
</a>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{orders_pool ? (
|
{orders_pool ? (
|
||||||
<Text size="xs">
|
<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}
|
{orders_pool.name}
|
||||||
</Link>
|
</Anchor>
|
||||||
|
) : (
|
||||||
|
orders_pool.name
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{actor_username ? (
|
{actor_username ? (
|
||||||
|
|
@ -107,7 +119,15 @@ export const TimelineList: React.FC<{
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<Text size="xs">
|
<Text size="xs">
|
||||||
|
{username != my_username ? (
|
||||||
|
<Anchor component={Link} to={`/orders/${username}`}>
|
||||||
<IconLock size="0.75rem" /> {username}
|
<IconLock size="0.75rem" /> {username}
|
||||||
|
</Anchor>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconLock size="0.75rem" /> {username}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs">{moment(updated_at).fromNow()}</Text>
|
<Text size="xs">{moment(updated_at).fromNow()}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
@ -115,5 +135,7 @@ export const TimelineList: React.FC<{
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
68
web/vite/src/UserOrderSets.tsx
Normal file
68
web/vite/src/UserOrderSets.tsx
Normal 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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
4
web/vite/src/index.d.ts
vendored
4
web/vite/src/index.d.ts
vendored
|
|
@ -9,6 +9,10 @@ type UserProfile = {
|
||||||
verify_mastodon_alt_text?: boolean;
|
verify_mastodon_alt_text?: boolean;
|
||||||
verify_mastodon_favorite?: boolean;
|
verify_mastodon_favorite?: boolean;
|
||||||
verify_delay?: number;
|
verify_delay?: number;
|
||||||
|
can_edit_permissions?: boolean;
|
||||||
|
permission_orders_pools_view?: boolean;
|
||||||
|
permission_orders_pools_details?: boolean;
|
||||||
|
permission_orders_pools_edit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderSetOrderAddOn = {
|
type OrderSetOrderAddOn = {
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,20 @@ import {
|
||||||
Input,
|
Input,
|
||||||
Paper,
|
Paper,
|
||||||
Slider,
|
Slider,
|
||||||
|
Image,
|
||||||
|
mergeThemeOverrides,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
|
import { mantineTheme } from "./theme";
|
||||||
|
|
||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/dates/styles.css";
|
import "@mantine/dates/styles.css";
|
||||||
import "@mantine/notifications/styles.css";
|
import "@mantine/notifications/styles.css";
|
||||||
import "@mantine/charts/styles.css";
|
import "@mantine/charts/styles.css";
|
||||||
|
|
||||||
import { Dashboard, subsListLoader } from "./Dashboard";
|
import { Dashboard, subsListLoader } from "./Dashboard";
|
||||||
import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";
|
import { UserOrderSets, userOrderSetsLoader } from "./UserOrderSets";
|
||||||
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
|
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
|
||||||
import { UserContextProvider } from "./UserContext";
|
import { UserContextProvider } from "./UserContext";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
|
|
@ -46,6 +50,11 @@ const theme = createTheme({
|
||||||
},
|
},
|
||||||
}) as any,
|
}) as any,
|
||||||
}),
|
}),
|
||||||
|
Image: Image.extend({
|
||||||
|
defaultProps: {
|
||||||
|
radius: "xs",
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -66,8 +75,8 @@ const router = createBrowserRouter([
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "orders/:username",
|
path: "orders/:username",
|
||||||
Component: SubOrderSets,
|
Component: UserOrderSets,
|
||||||
loader: subOrderSetsLoader,
|
loader: userOrderSetsLoader,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "orders/:username/new",
|
path: "orders/:username/new",
|
||||||
|
|
@ -83,9 +92,11 @@ const router = createBrowserRouter([
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const mergedTheme = mergeThemeOverrides(mantineTheme, theme);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider defaultColorScheme="dark" theme={mergedTheme}>
|
||||||
<UserContextProvider>
|
<UserContextProvider>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
|
|
|
||||||
78
web/vite/src/theme.ts
Normal file
78
web/vite/src/theme.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue