First pass at vite ui
This commit is contained in:
parent
4880ef61cf
commit
1f94855504
16 changed files with 276 additions and 42 deletions
1
Pipfile
1
Pipfile
|
|
@ -12,6 +12,7 @@ peewee = "*"
|
||||||
peewee-migrate = "*"
|
peewee-migrate = "*"
|
||||||
flask = "*"
|
flask = "*"
|
||||||
flask-login = "*"
|
flask-login = "*"
|
||||||
|
flask-vite = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
|
|
|
||||||
11
Pipfile.lock
generated
11
Pipfile.lock
generated
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "a598b3b97fc476eba41807a363b2528dfd7ec66f33b33f6b0703c004aaad22f9"
|
"sha256": "cbf2aa09aa805f5a444be073d6495925f6d37d04290e62140fb65bc02a170a75"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -201,6 +201,15 @@
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==0.6.3"
|
"version": "==0.6.3"
|
||||||
},
|
},
|
||||||
|
"flask-vite": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1681765668fa9dc7ca985f5ee395b1f7ed1d4db861407cccbcfdaf0346d4112c",
|
||||||
|
"sha256:5267abdd0bb4c8c9bb5b1a44b1260dc015a3f7e39befcf120babedc386d43558"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9' and python_version < '4.0'",
|
||||||
|
"version": "==0.6.1"
|
||||||
|
},
|
||||||
"frozenlist": {
|
"frozenlist": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686",
|
"sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ class DomSubUsers(BaseModel):
|
||||||
class OrdersPool(BaseModel):
|
class OrdersPool(BaseModel):
|
||||||
name = TextField()
|
name = TextField()
|
||||||
|
|
||||||
|
user = ForeignKeyField(column_name='user_id', field='id', model=User, backref='orders_pools')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = 'orders_pool'
|
table_name = 'orders_pool'
|
||||||
|
|
||||||
|
|
@ -38,6 +40,8 @@ class Order(BaseModel):
|
||||||
weight = IntegerField()
|
weight = IntegerField()
|
||||||
repeat = FloatField()
|
repeat = FloatField()
|
||||||
|
|
||||||
|
pool = ForeignKeyField(column_name='orders_pool_id', field='id', model=OrdersPool, backref='orders')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = 'order'
|
table_name = 'order'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from .models import database, User, DomSubUsers, Repeat, SkipDay, OrderStatus, PunishmentStatus
|
from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus, PunishmentStatus
|
||||||
|
|
||||||
def initdb():
|
def initdb():
|
||||||
database.connect()
|
database.connect()
|
||||||
|
|
@ -22,6 +22,9 @@ def user_get(username):
|
||||||
telegram_username=username
|
telegram_username=username
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def orders_pool_list(user_id):
|
||||||
|
return OrdersPool.select().where(OrdersPool.user_id == user_id)
|
||||||
|
|
||||||
def domsubusers_add(sub, dom):
|
def domsubusers_add(sub, dom):
|
||||||
return DomSubUsers.create(
|
return DomSubUsers.create(
|
||||||
sub=sub,
|
sub=sub,
|
||||||
|
|
@ -34,6 +37,9 @@ def domsubusers_delete(sub, dom):
|
||||||
).delete()
|
).delete()
|
||||||
return q.execute()
|
return q.execute()
|
||||||
|
|
||||||
|
def domsubusers_list(dom):
|
||||||
|
return DomSubUsers.select().where(DomSubUsers.dom == dom)
|
||||||
|
|
||||||
def repeat_get():
|
def repeat_get():
|
||||||
try:
|
try:
|
||||||
return Repeat.get()
|
return Repeat.get()
|
||||||
|
|
|
||||||
42
flask/api.py
Normal file
42
flask/api.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from flask import Blueprint, jsonify, abort
|
||||||
|
from flask_login import current_user
|
||||||
|
from db.queries import user_get, domsubusers_list, orders_pool_list
|
||||||
|
|
||||||
|
api = Blueprint('api', __name__)
|
||||||
|
|
||||||
|
@api.route('/subs')
|
||||||
|
def subs():
|
||||||
|
return jsonify(
|
||||||
|
[
|
||||||
|
{"sub_username": dsu.sub.telegram_username}
|
||||||
|
for dsu
|
||||||
|
in domsubusers_list(current_user.db_user)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.route('/subs/<username>/orders')
|
||||||
|
def sub_orders(username):
|
||||||
|
try:
|
||||||
|
sub = user_get(username)
|
||||||
|
except:
|
||||||
|
abort(500)
|
||||||
|
return
|
||||||
|
|
||||||
|
if sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]:
|
||||||
|
abort(500)
|
||||||
|
return
|
||||||
|
|
||||||
|
return jsonify([
|
||||||
|
{
|
||||||
|
'id': op.id,
|
||||||
|
'name': op.name,
|
||||||
|
'orders': [{
|
||||||
|
'id': order.id,
|
||||||
|
'name': order.name,
|
||||||
|
'weight': order.weight,
|
||||||
|
'repeat': order.repeat
|
||||||
|
} for order in op.orders]
|
||||||
|
}
|
||||||
|
for op
|
||||||
|
in orders_pool_list(sub.id)
|
||||||
|
])
|
||||||
13
flask/app.py
13
flask/app.py
|
|
@ -6,10 +6,10 @@ from flask import Flask, render_template, request, redirect, flash
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import base64
|
|
||||||
|
|
||||||
from settings import FLASK_SECRET_KEY, TELEGRAM_API_TOKEN, TELEGRAM_BOT_NAME, TELEGRAM_BOT_DOMAIN
|
from settings import FLASK_SECRET_KEY, TELEGRAM_API_TOKEN, TELEGRAM_BOT_NAME, TELEGRAM_BOT_DOMAIN
|
||||||
from db.queries import user_get
|
from db.queries import user_get
|
||||||
|
from api import api
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
@ -34,17 +34,20 @@ def load_user(user_id):
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
app.register_blueprint(api, url_prefix='/api')
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
data = {'bot_name': TELEGRAM_BOT_NAME, 'bot_damin': TELEGRAM_BOT_DOMAIN}
|
data = {'bot_name': TELEGRAM_BOT_NAME, 'bot_damin': TELEGRAM_BOT_DOMAIN}
|
||||||
return render_template('index.html', data = data)
|
return render_template('index.html', data = data)
|
||||||
else:
|
else:
|
||||||
return redirect('/dashboard')
|
return redirect('/dashboard/')
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard/', defaults={'path': ''})
|
||||||
|
@app.route('/dashboard/<path:path>')
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard(path):
|
||||||
return render_template('dashboard.html')
|
return render_template('dashboard.html')
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route('/logout')
|
||||||
|
|
@ -90,7 +93,7 @@ def login():
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
return redirect('/dashboard')
|
return redirect('/dashboard/')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', debug=True, port=8080)
|
app.run(host='0.0.0.0', debug=True, port=8080)
|
||||||
|
|
@ -1,35 +1,16 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!-- Required meta tags -->
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
<title>Gear Orders Bot</title>
|
<title>Gear Orders Bot</title>
|
||||||
|
|
||||||
<style>
|
<script type="module" src="/v/@vite/client"></script>
|
||||||
body {
|
<script type="module" src="/v/src/main.tsx"></script>
|
||||||
font-family: Arial;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 18pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
justify-items: center;
|
|
||||||
align-content: center;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="grid">
|
<div id="root"></div>
|
||||||
<h1>Dashboard</h1>
|
</body>
|
||||||
<p>Successfuly logged in and the login verified</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!-- Required meta tags -->
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
|
|
||||||
4
flask/vite/.gitignore
vendored
Normal file
4
flask/vite/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
|
/package-lock.json
|
||||||
|
/yarn.lock
|
||||||
11
flask/vite/index.html
Normal file
11
flask/vite/index.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Used only to please vite</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
flask/vite/package.json
Normal file
24
flask/vite/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "demo-tailwind",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^5.0.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^8.3.12",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"react-router": "^7.12.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
76
flask/vite/src/SubOrders.tsx
Normal file
76
flask/vite/src/SubOrders.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Accordion,
|
||||||
|
TextInput,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Slider,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import React from "react";
|
||||||
|
import { Params, useLoaderData, useParams } from "react-router";
|
||||||
|
|
||||||
|
export const subOrdersLoader = async ({
|
||||||
|
params: { username },
|
||||||
|
}: {
|
||||||
|
params: Params<string>;
|
||||||
|
}) => fetch(`/api/subs/${username}/orders`).then((response) => response.json());
|
||||||
|
|
||||||
|
export const SubOrders: React.FC = () => {
|
||||||
|
const { username: sub_username } = useParams();
|
||||||
|
const orders = useLoaderData<
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
orders: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
weight: number;
|
||||||
|
repeat: number;
|
||||||
|
}[];
|
||||||
|
}[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<h1>Orders for {sub_username}</h1>
|
||||||
|
<Accordion>
|
||||||
|
{orders
|
||||||
|
? orders.map(({ id, name, orders }) => (
|
||||||
|
<Accordion.Item key={id} value={`${id}`}>
|
||||||
|
<Accordion.Control>{name}</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<>
|
||||||
|
<TextInput label="Name" value={name} />
|
||||||
|
{orders.map(
|
||||||
|
({ id: order_id, name: order_name, weight, repeat }) => (
|
||||||
|
<React.Fragment key={order_id}>
|
||||||
|
<Divider my="md" />
|
||||||
|
<TextInput label="Name" value={order_name} />
|
||||||
|
<Input.Wrapper
|
||||||
|
label="Weight"
|
||||||
|
description="Chance this order will be selected"
|
||||||
|
>
|
||||||
|
<Slider domain={[0, 100]} value={weight} />
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper
|
||||||
|
label="Repeat"
|
||||||
|
description="Percent chance that this order will repeat the next day"
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
domain={[0, 1]}
|
||||||
|
step={0.05}
|
||||||
|
value={repeat}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Accordion>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
flask/vite/src/SubsList.tsx
Normal file
21
flask/vite/src/SubsList.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Container } from "@mantine/core";
|
||||||
|
import { useLoaderData, Link } from "react-router";
|
||||||
|
|
||||||
|
export const subsListLoader = () =>
|
||||||
|
fetch("/api/subs").then((response) => response.json());
|
||||||
|
|
||||||
|
export const SubsList: React.FC = () => {
|
||||||
|
const subs = useLoaderData<{ sub_username: string }[]>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<h1>Subs</h1>
|
||||||
|
{subs.map(({ sub_username }) => (
|
||||||
|
<Link key={sub_username} to={`/dashboard/subs/${sub_username}`}>
|
||||||
|
Orders for {sub_username}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
42
flask/vite/src/main.tsx
Normal file
42
flask/vite/src/main.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { createBrowserRouter } from "react-router";
|
||||||
|
import { RouterProvider } from "react-router/dom";
|
||||||
|
import { createTheme, MantineProvider, Input } from "@mantine/core";
|
||||||
|
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
|
||||||
|
import { SubsList, subsListLoader } from "./SubsList";
|
||||||
|
import { SubOrders, subOrdersLoader } from "./SubOrders";
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
components: {
|
||||||
|
InputWrapper: Input.Wrapper.extend({
|
||||||
|
defaultProps: {
|
||||||
|
inputWrapperOrder: ["label", "input", "description", "error"],
|
||||||
|
my: "md",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "dashboard",
|
||||||
|
Component: SubsList,
|
||||||
|
loader: subsListLoader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "dashboard/subs/:username",
|
||||||
|
Component: SubOrders,
|
||||||
|
loader: subOrdersLoader,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<MantineProvider theme={theme}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</MantineProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
9
flask/vite/vite.config.js
Normal file
9
flask/vite/vite.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default {
|
||||||
|
base: '/v/',
|
||||||
|
|
||||||
|
server: {
|
||||||
|
host: 'orders-dev.johnnygear.net',
|
||||||
|
port: 3000,
|
||||||
|
strictPort: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -63,24 +63,26 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
table_name = "dom_sub"
|
table_name = "dom_sub"
|
||||||
indexes = [(('dom_id', 'sub_id'), True)]
|
indexes = [(('dom_id', 'sub_id'), True)]
|
||||||
|
|
||||||
|
@migrator.create_model
|
||||||
|
class OrdersPool(pw.Model):
|
||||||
|
id = pw.AutoField()
|
||||||
|
name = pw.TextField()
|
||||||
|
user = pw.ForeignKeyField(column_name='user_id', field='id', model=migrator.orm['user'])
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "orders_pool"
|
||||||
|
|
||||||
@migrator.create_model
|
@migrator.create_model
|
||||||
class Order(pw.Model):
|
class Order(pw.Model):
|
||||||
id = pw.AutoField()
|
id = pw.AutoField()
|
||||||
name = pw.TextField()
|
name = pw.TextField()
|
||||||
weight = pw.IntegerField()
|
weight = pw.IntegerField()
|
||||||
repeat = pw.FloatField()
|
repeat = pw.FloatField()
|
||||||
|
pool = pw.ForeignKeyField(column_name='orders_pool_id', field='id', model=migrator.orm['orders_pool'])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "order"
|
table_name = "order"
|
||||||
|
|
||||||
@migrator.create_model
|
|
||||||
class OrdersPool(pw.Model):
|
|
||||||
id = pw.AutoField()
|
|
||||||
name = pw.TextField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table_name = "orders_pool"
|
|
||||||
|
|
||||||
@migrator.create_model
|
@migrator.create_model
|
||||||
class PunishmentStatus(pw.Model):
|
class PunishmentStatus(pw.Model):
|
||||||
id = pw.AutoField()
|
id = pw.AutoField()
|
||||||
|
|
@ -135,10 +137,10 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||||
|
|
||||||
migrator.remove_model('punishment_status')
|
migrator.remove_model('punishment_status')
|
||||||
|
|
||||||
migrator.remove_model('orders_pool')
|
|
||||||
|
|
||||||
migrator.remove_model('order')
|
migrator.remove_model('order')
|
||||||
|
|
||||||
|
migrator.remove_model('orders_pool')
|
||||||
|
|
||||||
migrator.remove_model('dom_sub')
|
migrator.remove_model('dom_sub')
|
||||||
|
|
||||||
migrator.remove_model('user')
|
migrator.remove_model('user')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue