Replaced events extension with almost working vuejs one
This commit is contained in:
parent
41277e6931
commit
ea3b858695
|
@ -1,8 +1,11 @@
|
|||
<h1> LNEVENTS</h1>
|
||||
<h2>Create/sell tickets for an event</h2>
|
||||
<p>Events is an easy way to create/sell tickets for an event.
|
||||
<h1>Example Extension</h1>
|
||||
<h2>*tagline*</h2>
|
||||
This is an example extension to help you organise and build you own.
|
||||
|
||||
It is advised to setup a specific wallet in lnbits for the event.</p>
|
||||
Try to include an image
|
||||
<img src="https://i.imgur.com/9i4xcQB.png">
|
||||
|
||||
<img src="https://i.imgur.com/qHi7ExL.png">
|
||||
|
||||
<h2>If your extension has API endpoints, include useful ones here</h2>
|
||||
|
||||
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
"name": "Events",
|
||||
"short_description": "Sell/register event tickets",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["benarc"]
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "Events",
|
||||
"short_description": "LN tickets for events.",
|
||||
"icon": "local_activity",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
116
lnbits/extensions/events/crud.py
Normal file
116
lnbits/extensions/events/crud.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import open_ext_db
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import Tickets, Events
|
||||
|
||||
|
||||
#######TICKETS########
|
||||
|
||||
|
||||
def create_ticket(wallet: str, event: str, name: str, email: str) -> Tickets:
|
||||
with open_ext_db("events") as db:
|
||||
eventdata = get_event(event)
|
||||
sold = eventdata.sold + 1
|
||||
amount_tickets = eventdata.amount_tickets - 1
|
||||
ticket_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO tickets (id, wallet, event, name, email, registered)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(ticket_id, wallet, event, name, email, False),
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE events
|
||||
SET sold = ?, amount_tickets = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(sold, amount_tickets, event),
|
||||
)
|
||||
return get_ticket(ticket_id)
|
||||
|
||||
|
||||
def get_ticket(ticket_id: str) -> Optional[Tickets]:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM tickets WHERE id = ?", (ticket_id,))
|
||||
|
||||
return Tickets(**row) if row else None
|
||||
|
||||
|
||||
def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM tickets WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
print("scrum")
|
||||
return [Tickets(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_ticket(ticket_id: str) -> None:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("DELETE FROM tickets WHERE id = ?", (ticket_id,))
|
||||
|
||||
|
||||
|
||||
########EVENTS#########
|
||||
|
||||
|
||||
def create_event(*, wallet: str, name: str, info: str, closing_date: str, event_start_date: str, event_end_date: str, amount_tickets: int, price_per_ticket: int) -> Events:
|
||||
with open_ext_db("events") as db:
|
||||
event_id = urlsafe_short_hash()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(event_id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, 0),
|
||||
)
|
||||
print(event_id)
|
||||
|
||||
return get_event(event_id)
|
||||
|
||||
def update_event(event_id: str, **kwargs) -> Events:
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
with open_ext_db("events") as db:
|
||||
db.execute(f"UPDATE events SET {q} WHERE id = ?", (*kwargs.values(), event_id))
|
||||
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
return Events(**row) if row else None
|
||||
|
||||
|
||||
def get_event(event_id: str) -> Optional[Events]:
|
||||
with open_ext_db("events") as db:
|
||||
row = db.fetchone("SELECT * FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
return Events(**row) if row else None
|
||||
|
||||
|
||||
def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = db.fetchall(f"SELECT * FROM events WHERE wallet IN ({q})", (*wallet_ids,))
|
||||
|
||||
return [Events(**row) for row in rows]
|
||||
|
||||
|
||||
def delete_event(event_id: str) -> None:
|
||||
with open_ext_db("events") as db:
|
||||
db.execute("DELETE FROM events WHERE id = ?", (event_id,))
|
||||
|
||||
########EVENTTICKETS#########
|
||||
|
||||
def get_event_tickets(event_id: str, wallet_id: str) -> Tickets:
|
||||
|
||||
with open_ext_db("events") as db:
|
||||
rows = db.fetchall("SELECT * FROM tickets WHERE wallet = ? AND event = ?", (wallet_id, event_id))
|
||||
print(rows)
|
||||
|
||||
return [Tickets(**row) for row in rows]
|
|
@ -1,5 +1,40 @@
|
|||
from lnbits.db import open_ext_db
|
||||
|
||||
def m001_initial(db):
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
info TEXT NOT NULL,
|
||||
closing_date TEXT NOT NULL,
|
||||
event_start_date TEXT NOT NULL,
|
||||
event_end_date TEXT NOT NULL,
|
||||
amount_tickets INTEGER NOT NULL,
|
||||
price_per_ticket INTEGER NOT NULL,
|
||||
sold INTEGER NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
event TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
registered BOOLEAN NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
def migrate():
|
||||
print("pending")
|
||||
with open_ext_db("events") as db:
|
||||
m001_initial(db)
|
||||
|
||||
|
|
24
lnbits/extensions/events/models.py
Normal file
24
lnbits/extensions/events/models.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Events(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
name: str
|
||||
info: str
|
||||
closing_date: str
|
||||
event_start_date: str
|
||||
event_end_date: str
|
||||
amount_tickets: int
|
||||
price_per_ticket: int
|
||||
sold: int
|
||||
time: int
|
||||
|
||||
class Tickets(NamedTuple):
|
||||
id: str
|
||||
wallet: str
|
||||
event: str
|
||||
name: str
|
||||
email: str
|
||||
registered: bool
|
||||
time: int
|
|
@ -1,26 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS events (
|
||||
key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
usr TEXT,
|
||||
wal TEXT,
|
||||
walnme TEXT,
|
||||
walinvkey INTEGER,
|
||||
uni TEXT,
|
||||
tit TEXT,
|
||||
cldate TEXT,
|
||||
notickets INTEGER,
|
||||
sold INTEGER DEFAULT 0,
|
||||
prtick INTEGER,
|
||||
descr TEXT,
|
||||
unireg TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eventssold (
|
||||
key INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uni TEXT,
|
||||
email TEXT,
|
||||
name TEXT,
|
||||
hash TEXT,
|
||||
paid INTEGER DEFAULT 0,
|
||||
reg INTEGER DEFAULT 0
|
||||
);
|
||||
|
27
lnbits/extensions/events/templates/events/_api_docs.html
Normal file
27
lnbits/extensions/events/templates/events/_api_docs.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">Events: Sell and register tickets for an event</h5>
|
||||
<p>Events alows you to make a wave of tickets for an event. Once an attendee has paid for a ticket they get a unqiue code. Events comes with a shareable scanning frontend, so you can register the attendees<br/>
|
||||
<small> Created by, <a href="https://github.com/benarc">Ben Arc</a></small></p>
|
||||
</q-card>
|
||||
</q-card-section>
|
||||
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
|
||||
|
||||
|
||||
</q-expansion-item>
|
|
@ -1,567 +1,202 @@
|
|||
<!-- @format -->
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none">{{ event_name }}</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">{{ event_info }}</h5>
|
||||
<br />
|
||||
<q-form @submit="Invoice()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Your name "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.email"
|
||||
type="email"
|
||||
label="Your email "
|
||||
></q-input>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background: #1f2234;
|
||||
border-left-color: #8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper,
|
||||
.right-side {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color: #1f2234;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color: #4b646f;
|
||||
background: #1f2234;
|
||||
}
|
||||
.skin-blue .wrapper,
|
||||
.skin-blue .main-sidebar,
|
||||
.skin-blue .left-side {
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover,
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background: #3e355a;
|
||||
border-left-color: #8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background: #3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red,
|
||||
.callout.callout-danger,
|
||||
.alert-danger,
|
||||
.alert-error,
|
||||
.label-danger,
|
||||
.modal-danger .modal-body {
|
||||
background-color: #1f2234 !important;
|
||||
}
|
||||
.alert-danger,
|
||||
.alert-error {
|
||||
border-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover,
|
||||
.skin-blue .main-header .navbar .nav > li > a:active,
|
||||
.skin-blue .main-header .navbar .nav > li > a:focus,
|
||||
.skin-blue .main-header .navbar .nav .open > a,
|
||||
.skin-blue .main-header .navbar .nav .open > a:hover,
|
||||
.skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color: #f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua,
|
||||
.callout.callout-info,
|
||||
.alert-info,
|
||||
.label-info,
|
||||
.modal-info .modal-body {
|
||||
background-color: #3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n + 1) {
|
||||
background-color: #333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active,
|
||||
a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
background-color: #333646;
|
||||
color: #fff;
|
||||
}
|
||||
.box-footer {
|
||||
border-top: none;
|
||||
|
||||
background-color: #333646;
|
||||
}
|
||||
.modal-footer {
|
||||
border-top: none;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
background-color: #333646;
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color: #3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover,
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
<p>{% raw %}{{amountWords}}{% endraw %}</p>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.name == '' || formDialog.data.email == '' || paymentReq"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;"></section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNBits Events
|
||||
<small>Lightning powered tickets</small>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<br /><br />
|
||||
<center><h1 style="font-size: 500%;">{{ nme }}</h1></center>
|
||||
<center>
|
||||
<h2 style="width: 55%; word-wrap: break-word;">{{ descr }}</h2>
|
||||
</center>
|
||||
<div id="theform">
|
||||
<br /><br /><br />
|
||||
<center>
|
||||
<form role="form">
|
||||
<div class="form-group" style="width: 300px;">
|
||||
<input
|
||||
id="Nam"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Name"
|
||||
/>
|
||||
<input
|
||||
id="Ema"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Email"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onclick="submitforticket()"
|
||||
type="button"
|
||||
class="btn btn-info"
|
||||
>
|
||||
Go to payment
|
||||
</button>
|
||||
<p style="color: red;" id="error"></p>
|
||||
</form>
|
||||
</center>
|
||||
</div>
|
||||
<center>
|
||||
<br /><br />
|
||||
<div id="qrcode" style="width: 340px;"></div>
|
||||
<br /><br />
|
||||
<div
|
||||
style="width: 55%; word-wrap: break-word;"
|
||||
id="qrcodetxt"
|
||||
></div>
|
||||
<br />
|
||||
</center>
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
<q-card v-show="ticketLink.show" class="q-pa-lg">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="ticketLink.data.link" target="_blank">
|
||||
<q-btn unelevated size="xl" color="deep-purple"
|
||||
>Link to your ticket!</q-btn
|
||||
></a
|
||||
>
|
||||
<br /><br />
|
||||
<p>You'll be redirected in 5 seconds...</p>
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</div>
|
||||
</body>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function postAjax(url, data, thekey, success) {
|
||||
var params =
|
||||
typeof data == 'string'
|
||||
? data
|
||||
: Object.keys(data)
|
||||
.map(function (k) {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
|
||||
})
|
||||
.join('&')
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('POST', url)
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<a :href="'lightning:' + receive.paymentReq">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode
|
||||
:value="paymentReq"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
console.log('{{ form_costpword }}')
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
name: '',
|
||||
email: ''
|
||||
}
|
||||
},
|
||||
ticketLink: {
|
||||
show: false,
|
||||
data: {
|
||||
link: ''
|
||||
}
|
||||
},
|
||||
receive: {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.send(params)
|
||||
return xhr
|
||||
}
|
||||
},
|
||||
|
||||
function getAjax(url, thekey, success) {
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('GET', url, true)
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
methods: {
|
||||
resetForm: function (e) {
|
||||
e.preventDefault()
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
},
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
closeReceiveDialog: function () {
|
||||
var checker = this.receive.paymentChecker
|
||||
dismissMsg()
|
||||
|
||||
function submitforticket() {
|
||||
nam = document.getElementById('Nam').value
|
||||
ema = document.getElementById('Ema').value
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(function () {}, 10000)
|
||||
},
|
||||
Invoice: function () {
|
||||
var self = this
|
||||
axios
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('events.api_getticket') }}?ema=" + ema,
|
||||
JSON.stringify({unireg: '{{wave }}', name: nam}),
|
||||
'filla',
|
||||
.get('/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}')
|
||||
|
||||
function (data) {
|
||||
theinvoice = JSON.parse(data).pay_req
|
||||
thehash = JSON.parse(data).payment_hash
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.checking_id
|
||||
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: theinvoice,
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
document.getElementById('theform').innerHTML = ''
|
||||
|
||||
document.getElementById('qrcode').style.backgroundColor = 'white'
|
||||
document.getElementById('qrcode').style.padding = '20px'
|
||||
|
||||
document.getElementById('qrcodetxt').innerHTML =
|
||||
theinvoice + '<br/><br/>'
|
||||
|
||||
var refreshId = setInterval(function () {
|
||||
getAjax('/api/v1/invoice/' + thehash, '{{wave}}', function (datab) {
|
||||
console.log(JSON.parse(datab).PAID)
|
||||
if (JSON.parse(datab).PAID == 'TRUE') {
|
||||
location.replace(
|
||||
"{{ url_for('events.ticket') }}?hash=" +
|
||||
thehash +
|
||||
'&unireg={{wave}}'
|
||||
)
|
||||
clearInterval(refreshId)
|
||||
}
|
||||
dismissMsg = self.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
}, 3000)
|
||||
}
|
||||
)
|
||||
|
||||
self.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: self.paymentReq
|
||||
}
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.post('/events/api/v1/tickets/' + self.paymentCheck, {
|
||||
event: '{{ event_id }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: self.formDialog.data.email
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
dismissMsg()
|
||||
self.formDialog.data.name = ''
|
||||
self.formDialog.data.email = ''
|
||||
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
self.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
|
||||
self.ticketLink = {
|
||||
show: true,
|
||||
data: {
|
||||
link: '/events/ticket/' + res.data.ticket_id
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
window.location.href =
|
||||
'/events/ticket/' + res.data.ticket_id
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
35
lnbits/extensions/events/templates/events/error.html
Normal file
35
lnbits/extensions/events/templates/events/error.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ event_name }} error</h3>
|
||||
<br />
|
||||
<q-icon
|
||||
name="warning"
|
||||
class="text-grey"
|
||||
style="font-size: 20rem;"
|
||||
></q-icon>
|
||||
|
||||
<h5 class="q-my-none">{{ event_error }}</h5>
|
||||
<br />
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
|
@ -1,501 +1,512 @@
|
|||
<!-- @format -->
|
||||
|
||||
{% extends "legacy.html" %} {% block messages %}
|
||||
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-bell-o"></i>
|
||||
<span class="label label-danger">!</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li class="header"><b>Instant wallet, bookmark to save</b></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
{% endblock %} {% block menuitems %}
|
||||
<li class="treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-bitcoin"></i> <span>Wallets</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for w in user_wallets %}
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?wal={{ w.id }}&usr={{ w.user }}"
|
||||
><i class="fa fa-bolt"></i> {{ w.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li><a onclick="sidebarmake()">Add a wallet +</a></li>
|
||||
<div id="sidebarmake"></div>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="active treeview">
|
||||
<a href="#">
|
||||
<i class="fa fa-th"></i> <span>Extensions</span>
|
||||
<i class="fa fa-angle-left pull-right"></i>
|
||||
</a>
|
||||
<ul class="treeview-menu">
|
||||
{% for extension in EXTENSIONS %} {% if extension.code in user_ext %}
|
||||
<li>
|
||||
<a href="{{ url_for(extension.code + '.index') }}?usr={{ user }}"
|
||||
><i class="fa fa-plus"></i> {{ extension.name }}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %} {% endfor %}
|
||||
<li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}">Manager</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endblock %} {% block body %}
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Events
|
||||
<small>bitcoin tickets</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
<a href="{{ url_for('wallet') }}?usr={{ user }}"
|
||||
><i class="fa fa-dashboard"></i> Home</a
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="deep-purple" @click="formDialog.show = true"
|
||||
>New Event</q-btn
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('core.extensions') }}?usr={{ user }}"
|
||||
><li class="fa fa-dashboard">Extensions</li></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<i class="active" class="fa fa-dashboard">Lightning tickets</i>
|
||||
</li>
|
||||
</ol>
|
||||
<br /><br />
|
||||
</section>
|
||||
<style>
|
||||
.datepicker-days {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
</style>
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<!-- Small boxes (Stat box) -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Make a ticket wave</h3>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Events</h5>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
|
||||
<!-- form start -->
|
||||
<form role="form">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label for="exampleInputEmail1">Ticket title</label>
|
||||
<input
|
||||
id="tit"
|
||||
type="text"
|
||||
pattern="^[A-Za-z]+$"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description of event</label>
|
||||
<textarea id="descr" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- select -->
|
||||
<div class="form-group">
|
||||
<label>Select a wallet</label>
|
||||
<select id="wal" class="form-control">
|
||||
<option></option>
|
||||
{% for w in user_wallets %}
|
||||
<option>{{w.name}}-{{w.id}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nooftickets">No. of tickets</label>
|
||||
<input
|
||||
id="notickets"
|
||||
type="number"
|
||||
class="form-control"
|
||||
placeholder="10"
|
||||
max="86400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Close date:</label>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" id="datepicker" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="prpertick">Price per ticket</label>
|
||||
<input
|
||||
id="prtickets"
|
||||
type="number"
|
||||
class="form-control"
|
||||
placeholder="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-footer">
|
||||
<button onclick="postev()" type="button" class="btn btn-info">
|
||||
Create Wave
|
||||
</button>
|
||||
<p style="color: red;" id="error"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<!-- general form elements -->
|
||||
<div class="box box-primary">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Select a link</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<form role="form">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<select
|
||||
class="form-control"
|
||||
id="waveselect"
|
||||
onchange="drawwithdraw()"
|
||||
>
|
||||
<option value="none" selected>
|
||||
Select an Option
|
||||
</option>
|
||||
{% for w in user_ev %}
|
||||
<option id="{{w.uni}}" value="{{w.tit}}-{{w.unireg}}-{{w.uni}}"
|
||||
>{{w.tit}}-{{w.unireg}}-{{w.uni}}</option
|
||||
>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<center>
|
||||
<br />
|
||||
<div id="qrcode" style="width: 340px;"></div>
|
||||
<br />
|
||||
<div
|
||||
style="width: 75%; word-wrap: break-word;"
|
||||
id="qrcodetxt"
|
||||
></div>
|
||||
</center>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Ticket Waves<b id="withdraws"></b></h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body no-padding">
|
||||
<table
|
||||
id="pagnation"
|
||||
class="table table-bswearing anchorordered table-striped"
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exporteventsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th style="width: 15%;">Amt</th>
|
||||
<th style="width: 15%;">Sold</th>
|
||||
<th style="width: 15%;">Closing</th>
|
||||
<th style="width: 15%;">Price</th>
|
||||
<th style="width: 15%;">Wallet</th>
|
||||
<th style="width: 10%;">Edit</th>
|
||||
<th style="width: 10%;">Del</th>
|
||||
</tr>
|
||||
<tbody id="ticketwaves"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="events"
|
||||
row-key="id"
|
||||
:columns="eventsTable.columns"
|
||||
:pagination.sync="eventsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
|
||||
<div id="editlink"></div>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
|
||||
<!-- /.content -->
|
||||
</section>
|
||||
|
||||
<script>
|
||||
|
||||
//Date picker
|
||||
$('#datepicker').datepicker({
|
||||
autoclose: true
|
||||
})
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="link"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.displayUrl"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="how_to_reg"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/register/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteEvent(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
window.user = {{ user | tojson | safe }}
|
||||
window.user_wallets = {{ user_wallets | tojson | safe }}
|
||||
window.user_ext = {{ user_ext | tojson | safe }}
|
||||
window.user_ev = {{ user_ev | tojson | safe }}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="local_activity"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
const user_ev = window.user_ev
|
||||
console.log(user_ev)
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTicket(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Events extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "events/_api_docs.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
function drawChart(user_ev) {
|
||||
var transactionsHTML = ''
|
||||
|
||||
for (var i = 0; i < user_ev.length; i++) {
|
||||
var ev = user_ev[i]
|
||||
|
||||
transactionsHTML =
|
||||
"<tr><td style='width: 50%'>" +
|
||||
ev.tit +
|
||||
'</td><td>' +
|
||||
ev.notickets +
|
||||
'</td><td>' +
|
||||
ev.sold +
|
||||
'</td><td>' +
|
||||
ev.cldate +
|
||||
'</td><td>' +
|
||||
ev.prtick +
|
||||
'</td><td>' +
|
||||
"<a href='{{ url_for('wallet') }}?usr="+ ev.usr +"'>" + ev.wal.substring(0, 4) + "...</a>" +
|
||||
'</td><td>' +
|
||||
"<i onclick='editlink("+ i +")'' class='fa fa-edit'></i>" +
|
||||
'</td><td>' +
|
||||
"<b><a style='color:red;' href='" + "{{ url_for('events.index') }}?del=" + ev.uni + "&usr=" + ev.usr +"'>" + "<i class='fa fa-trash'></i>" + "</a></b>" +
|
||||
'</td></tr>' +
|
||||
transactionsHTML
|
||||
document.getElementById('ticketwaves').innerHTML = transactionsHTML
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendEventData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Name of event "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.info"
|
||||
type="textarea"
|
||||
label="Info about the event "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.closing_date"
|
||||
type="date"
|
||||
label="Closing for tickets "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_start_date"
|
||||
type="date"
|
||||
label="Event begins "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_end_date"
|
||||
type="date"
|
||||
label="Event ends "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.amount_tickets"
|
||||
type="number"
|
||||
label="Amount of tickets "
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.price_per_ticket"
|
||||
type="number"
|
||||
label="Price per ticket "
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
>Update Event</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null || formDialog.data.info == null || formDialog.data.closing_date == null || formDialog.data.event_start_date == null || formDialog.data.event_end_date == null || formDialog.data.amount_tickets == null || formDialog.data.price_per_ticket == null"
|
||||
type="submit"
|
||||
>Create Event</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapEvents = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
events: [],
|
||||
tickets: [],
|
||||
eventsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'info', align: 'left', label: 'Info', field: 'info'},
|
||||
{
|
||||
name: 'event_start_date',
|
||||
align: 'left',
|
||||
label: 'Start date',
|
||||
field: 'event_start_date'
|
||||
},
|
||||
{
|
||||
name: 'event_end_date',
|
||||
align: 'left',
|
||||
label: 'End date',
|
||||
field: 'event_end_date'
|
||||
},
|
||||
{
|
||||
name: 'closing_date',
|
||||
align: 'left',
|
||||
label: 'Ticket close',
|
||||
field: 'closing_date'
|
||||
},
|
||||
{
|
||||
name: 'price_per_ticket',
|
||||
align: 'left',
|
||||
label: 'Price',
|
||||
field: 'price_per_ticket'
|
||||
},
|
||||
{
|
||||
name: 'amount_tickets',
|
||||
align: 'left',
|
||||
label: 'No tickets',
|
||||
field: 'amount_tickets'
|
||||
},
|
||||
{
|
||||
name: 'sold',
|
||||
align: 'left',
|
||||
label: 'Sold',
|
||||
field: 'sold'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'event', align: 'left', label: 'Event', field: 'event'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{name: 'email', align: 'left', label: 'Email', field: 'email'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
|
||||
if (user_ev.length) {
|
||||
drawChart(user_ev)
|
||||
}
|
||||
|
||||
function postev(){
|
||||
|
||||
wal = document.getElementById('wal').value
|
||||
tit = document.getElementById('tit').value
|
||||
cldate = document.getElementById('datepicker').value
|
||||
notickets = document.getElementById('notickets').value
|
||||
prtickets = document.getElementById('prtickets').value
|
||||
descr = document.getElementById('descr').value
|
||||
|
||||
if (tit == "") {
|
||||
document.getElementById("error").innerHTML = "Only use letters in title"
|
||||
return amt
|
||||
}
|
||||
if (wal == "") {
|
||||
document.getElementById("error").innerHTML = "No wallet selected"
|
||||
return amt
|
||||
}
|
||||
if (cldate == "") {
|
||||
document.getElementById("error").innerHTML = "No date selected"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(notickets) || notickets < 1) {
|
||||
document.getElementById("error").innerHTML = "Must be more than 1"
|
||||
return amt
|
||||
}
|
||||
if (isNaN(prtickets) || prtickets < 10) {
|
||||
document.getElementById("error").innerHTML = "Must be higher 10"
|
||||
return amt
|
||||
}
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('events.create') }}",
|
||||
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": notickets,"cldate": cldate, "prtickets": prtickets, "descr": descr}),
|
||||
"filla",
|
||||
|
||||
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
function editlink(evnum){
|
||||
|
||||
evdetails = user_ev[evnum]
|
||||
|
||||
console.log(evdetails.descr)
|
||||
wallpick = ""
|
||||
|
||||
checkbox = ""
|
||||
if (evdetails.uniq == 1){
|
||||
checkbox = "checked"}
|
||||
|
||||
document.getElementById('editlink').innerHTML = "<div class='row'>"+
|
||||
"<div class='col-md-6'>"+
|
||||
" <!-- general form elements -->"+
|
||||
"<div class='box box-primary' style='min-height: 300px;'>"+
|
||||
"<div class='box-header'>"+
|
||||
"<h3 class='box-title'> Edit: <i id='unid'>" + evdetails.tit + "-" + evdetails.uni + "-" + evdetails.unireg + "</i> </h3>"+
|
||||
"<div class='box-tools pull-right'>" +
|
||||
"<button class='btn btn-box-tool' data-widget='remove'><i class='fa fa-times'></i></button>" +
|
||||
"</div>" +
|
||||
" </div><!-- /.box-header -->"+
|
||||
" <!-- form start -->"+
|
||||
"<form role='form'>"+
|
||||
"<div class='box-body'>"+
|
||||
|
||||
"<div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Link title</label>"+
|
||||
"<input id='edittit' type='text' class='form-control' value='"+
|
||||
evdetails.tit +
|
||||
"'> </div>"+
|
||||
" </div>"+
|
||||
|
||||
"<div class='col-sm-1 col-md-8'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Description of event</label>"+
|
||||
"<textarea id='editdescr' type='textarea' rows='1' class='form-control'>"+evdetails.descr+"</textarea> </div>"+
|
||||
" </div>"+
|
||||
|
||||
|
||||
" <div class='col-sm-4 col-md-4'>"+
|
||||
" <!-- select -->"+
|
||||
" <div class='form-group'>"+
|
||||
" <label>Select a wallet</label>"+
|
||||
"<select id='editwal' class='form-control'>"+
|
||||
" <option>" + evdetails.walnme + "-" + evdetails.wal + "</option>"+
|
||||
" {% for w in user_wallets %}"+
|
||||
|
||||
" <option>{{w.name}}-{{w.id}}</option>"+
|
||||
|
||||
" {% endfor %}"+
|
||||
" </select>"+
|
||||
" </div>"+
|
||||
" </div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
" <label for='exampleInputPassword1'>No of tickets:</label>"+
|
||||
" <input id='editnooftickets' type='number' class='form-control' placeholder='0' max='86400' value='"+
|
||||
evdetails.notickets +
|
||||
"'>"+
|
||||
"</div> </div>"+
|
||||
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<div class='form-group'>"+
|
||||
"<label for='exampleInputEmail1'>Price per ticket:</label>"+
|
||||
" <input id='editprtick' type='number' class='form-control' placeholder='1' value='"+
|
||||
evdetails.prtick +
|
||||
"'>"+
|
||||
" </div></div>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
" <div class='input-group date'>"+
|
||||
" <label for='exampleInputEmail1'>Close date:</label>"+
|
||||
" <input id='datepicker2' type='text' class='form-control' placeholder='1' value='"+
|
||||
evdetails.cldate +
|
||||
"'>"+
|
||||
" </div></div>"+
|
||||
|
||||
|
||||
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"</div><!-- /.box-body -->"+
|
||||
" </div><br/>"+
|
||||
" <div class='box-footer'>"+
|
||||
" <div class='col-sm-3 col-md-4'>"+
|
||||
"<button onclick='editlinkcont()' type='button' style='margin: 24px;' class='btn btn-info'>Edit link(s)</button>"+
|
||||
"</div>"+
|
||||
"<p style='color:red;' id='error2'>.</p>"+
|
||||
" </div></form></div><!-- /.box --></div></div>"
|
||||
|
||||
|
||||
}
|
||||
|
||||
//Date picker
|
||||
$('#datepicker2').datepicker({
|
||||
autoclose: true
|
||||
})
|
||||
|
||||
function editlinkcont(){
|
||||
|
||||
unid = document.getElementById('unid').innerHTML
|
||||
wal = document.getElementById('editwal').value
|
||||
tit = document.getElementById('edittit').value
|
||||
nooftickets = document.getElementById('editnooftickets').value
|
||||
prtick = document.getElementById('editprtick').value
|
||||
cldate = document.getElementById('datepicker2').value
|
||||
descr = document.getElementById('editdescr').value
|
||||
uni = unid.split("-")[1]
|
||||
|
||||
if (tit == "") {
|
||||
document.getElementById("error2").innerHTML = "Only use letters in title"
|
||||
return amt
|
||||
}
|
||||
if (wal == "") {
|
||||
document.getElementById("error2").innerHTML = "No wallet selected"
|
||||
return amt
|
||||
}
|
||||
|
||||
if (isNaN(nooftickets) || nooftickets < 1 || nooftickets > 1000000) {
|
||||
document.getElementById("error2").innerHTML = "No. of tickets must be between 1 - 1000000"
|
||||
return amt
|
||||
}
|
||||
if (isNaN(prtick) || prtick < 10 ) {
|
||||
document.getElementById("error2").innerHTML = "Ticket pricket must be higher than 10"
|
||||
return amt
|
||||
}
|
||||
|
||||
|
||||
postAjax(
|
||||
"{{ url_for('events.create') }}",
|
||||
JSON.stringify({"tit": tit, "usr": user, "wal": wal, "notickets": nooftickets,"cldate": cldate, "prtickets": prtick, "id": unid, "descr": descr}),
|
||||
|
||||
"filla",
|
||||
|
||||
function(data) { location.replace("{{ url_for('events.index') }}?usr=" + user)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
//draws withdraw QR code
|
||||
function drawwithdraw() {
|
||||
|
||||
document.getElementById("qrcode").innerHTML = "";
|
||||
walname = document.getElementById("waveselect").value
|
||||
|
||||
thewave = walname.split("-");
|
||||
console.log(window.location.hostname + "-" + thewave[1])
|
||||
toencode = "/events/wave/" + thewave[1]
|
||||
toreg = "/events/registration/" + thewave[2]
|
||||
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: toencode,
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
},
|
||||
methods: {
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/tickets?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
console.log(obj)
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteTicket: function (ticketId) {
|
||||
var self = this
|
||||
var tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = _.reject(self.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportticketsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
|
||||
},
|
||||
|
||||
document.getElementById('qrcodetxt').innerHTML = "<a href='" + toencode + "'><h2>Payment link</a> , " +
|
||||
"<a href='" + toreg + "'>Registration</h2></a>"
|
||||
getEvents: function () {
|
||||
var self = this
|
||||
|
||||
document.getElementById("qrcode").style.backgroundColor = "white";
|
||||
document.getElementById("qrcode").style.padding = "20px";
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/events?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = response.data.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
sendEventData: function () {
|
||||
var wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
var data = this.formDialog.data
|
||||
|
||||
if (data.id) {
|
||||
this.updateEvent(wallet, data)
|
||||
} else {
|
||||
this.createEvent(wallet, data)
|
||||
}
|
||||
},
|
||||
|
||||
createEvent: function (wallet, data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request('POST', '/events/api/v1/events', wallet.inkey, data)
|
||||
.then(function (response) {
|
||||
self.events.push(mapEvents(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.events, {id: formId})
|
||||
console.log(link.id)
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
this.formDialog.data.name = link.name
|
||||
this.formDialog.data.info = link.info
|
||||
this.formDialog.data.closing_date = link.closing_date
|
||||
this.formDialog.data.event_start_date = link.event_start_date
|
||||
this.formDialog.data.event_end_date = link.event_end_date
|
||||
this.formDialog.data.amount_tickets = link.amount_tickets
|
||||
this.formDialog.data.price_per_ticket = link.price_per_ticket
|
||||
this.formDialog.show = true
|
||||
},
|
||||
updateEvent: function (wallet, data) {
|
||||
var self = this
|
||||
console.log(data)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/events/api/v1/events/' + data.id,
|
||||
wallet.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = _.reject(self.events, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.events.push(mapEvents(response.data))
|
||||
self.formDialog.show = false
|
||||
self.formDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteEvent: function (eventsId) {
|
||||
var self = this
|
||||
var events = _.findWhere(this.events, {id: eventsId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this form link?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/events/' + eventsId,
|
||||
_.findWhere(self.g.user.wallets, {id: events.wallet}).inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.events = _.reject(self.events, function (obj) {
|
||||
return obj.id == eventsId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exporteventsCSV: function () {
|
||||
LNbits.utils.exportCSV(this.eventsTable.columns, this.events)
|
||||
}
|
||||
},
|
||||
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getEvents()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
146
lnbits/extensions/events/templates/events/register.html
Normal file
146
lnbits/extensions/events/templates/events/register.html
Normal file
|
@ -0,0 +1,146 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ event_name }} Registration</h3>
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
<q-btn unelevated color="deep-purple" @click="showCamera" size="xl"
|
||||
>Scan ticket</q-btn
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
:pagination.sync="ticketsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="local_activity"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block styles %}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ url_for('static', filename='vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.css') }}"
|
||||
/>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var mapEvents = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tickets: [],
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
sendCamera: {
|
||||
show: true,
|
||||
camera: 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeCamera: function () {
|
||||
this.sendCamera.show = false
|
||||
},
|
||||
showCamera: function () {
|
||||
this.sendCamera.show = true
|
||||
},
|
||||
getEventTickets: function () {
|
||||
var self = this
|
||||
console.log('obj')
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/eventtickets/{{ wallet_id }}/{{ event_id }}'
|
||||
)
|
||||
.then(function (response) {
|
||||
self.tickets = response.data.map(function (obj) {
|
||||
console.log(obj)
|
||||
return mapEvents(obj)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.getEventTickets()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% assets filters='rjsmin', output='__bundle__/core/chart.js',
|
||||
'vendor/moment@2.25.1/moment.min.js', 'vendor/chart.js@2.9.3/chart.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %} {% assets filters='rjsmin', output='__bundle__/core/wallet.js',
|
||||
'vendor/bolt11/utils.js', 'vendor/bolt11/decoder.js',
|
||||
'vendor/vue-qrcode-reader@2.2.0/vue-qrcode-reader.min.js' %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %} {% endblock %}
|
|
@ -1,670 +0,0 @@
|
|||
<!-- @format -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background: #1f2234;
|
||||
border-left-color: #8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper,
|
||||
.right-side {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color: #1f2234;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color: #4b646f;
|
||||
background: #1f2234;
|
||||
}
|
||||
.skin-blue .wrapper,
|
||||
.skin-blue .main-sidebar,
|
||||
.skin-blue .left-side {
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover,
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background: #3e355a;
|
||||
border-left-color: #8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background: #3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red,
|
||||
.callout.callout-danger,
|
||||
.alert-danger,
|
||||
.alert-error,
|
||||
.label-danger,
|
||||
.modal-danger .modal-body {
|
||||
background-color: #1f2234 !important;
|
||||
}
|
||||
.alert-danger,
|
||||
.alert-error {
|
||||
border-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover,
|
||||
.skin-blue .main-header .navbar .nav > li > a:active,
|
||||
.skin-blue .main-header .navbar .nav > li > a:focus,
|
||||
.skin-blue .main-header .navbar .nav .open > a,
|
||||
.skin-blue .main-header .navbar .nav .open > a:hover,
|
||||
.skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color: #f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua,
|
||||
.callout.callout-info,
|
||||
.alert-info,
|
||||
.label-info,
|
||||
.modal-info .modal-body {
|
||||
background-color: #3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n + 1) {
|
||||
background-color: #333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active,
|
||||
a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
background-color: #333646;
|
||||
color: #fff;
|
||||
}
|
||||
.box-footer {
|
||||
border-top: none;
|
||||
|
||||
background-color: #333646;
|
||||
}
|
||||
.modal-footer {
|
||||
border-top: none;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
background-color: #333646;
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color: #3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover,
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;">
|
||||
<!-- Sidebar user panel -->
|
||||
</section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNBits Events
|
||||
<small>Lightning powered tickets</small>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<br /><br />
|
||||
<center><h1 style="font-size: 500%;">{{ user_ev[0][6] }}</h1></center>
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
<div
|
||||
class="modal fade sends"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="myLargeModalLabel"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div id="scantickets" style="padding: 0 10px 0 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<center>
|
||||
<button
|
||||
onclick="scanQRsend()"
|
||||
class="btn btn-block btn-primary btn-lg"
|
||||
data-toggle="modal"
|
||||
data-target=".sends"
|
||||
style="width: 300px;"
|
||||
>
|
||||
Scan ticket
|
||||
</button>
|
||||
</center>
|
||||
|
||||
<div id="scantickets"></div>
|
||||
<br /><br /><br />
|
||||
<div id="qrcodetxt"></div>
|
||||
<br />
|
||||
<br /><br /><br />
|
||||
|
||||
<center>
|
||||
<div class="row" style="width: 80%; margin-top: 80px;">
|
||||
<style>
|
||||
.ema,
|
||||
button:focus .txt {
|
||||
display: none;
|
||||
}
|
||||
button:focus .ema {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Attendees<b id="withdraws"></b></h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<div class="box-body no-padding">
|
||||
<table
|
||||
id="pagnation"
|
||||
class="table table-bswearing anchorordered table-striped"
|
||||
>
|
||||
<tr>
|
||||
<th style="width: 20%;">Name</th>
|
||||
<th style="width: 20%;">Email</th>
|
||||
<th style="width: 50%;">Ticket</th>
|
||||
<th style="width: 10%;">Registered</th>
|
||||
</tr>
|
||||
<tbody id="ticketwaves"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
</center>
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
window.user_ev = {{ user_ev | tojson | safe }}
|
||||
window.user_ev_sold = {{ user_ev_sold | tojson | safe }}
|
||||
console.log(user_ev)
|
||||
console.log(user_ev_sold)
|
||||
|
||||
|
||||
function drawChart(user_ev_sold) {
|
||||
var transactionsHTML = ''
|
||||
|
||||
for (var i = 0; i < user_ev_sold.length; i++) {
|
||||
var ev = user_ev_sold[i]
|
||||
|
||||
transactionsHTML =
|
||||
"<tr><td>" +
|
||||
ev.name +
|
||||
'</td><td>' +
|
||||
'<button style="background-color: #333646;padding: 0;border: none;background: none;" class="lost"><span class="txt">xxxxxx</span>' +
|
||||
'<span class="ema">' + ev.email + '</span></button>'+
|
||||
'</td><td>' +
|
||||
ev.hash +
|
||||
'</td><td>' +
|
||||
ev.reg +
|
||||
'</td></tr>' +
|
||||
transactionsHTML
|
||||
document.getElementById('ticketwaves').innerHTML = transactionsHTML
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (user_ev_sold.length) {
|
||||
drawChart(user_ev_sold)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function postAjax(url, data, thekey, success) {
|
||||
var params =
|
||||
typeof data == 'string'
|
||||
? data
|
||||
: Object.keys(data)
|
||||
.map(function(k) {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
|
||||
})
|
||||
.join('&')
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('POST', url)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.send(params)
|
||||
return xhr
|
||||
}
|
||||
|
||||
function getAjax(url, thekey, success) {
|
||||
var xhr = window.XMLHttpRequest
|
||||
? new XMLHttpRequest()
|
||||
: new ActiveXObject('Microsoft.XMLHTTP')
|
||||
xhr.open('GET', url, true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState > 3 && xhr.status == 200) {
|
||||
success(xhr.responseText)
|
||||
}
|
||||
}
|
||||
xhr.setRequestHeader('X-Api-Key', thekey)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
|
||||
function scanQRsend() {
|
||||
document.getElementById('scantickets').innerHTML =
|
||||
"<div class='modal-content'>"+
|
||||
"<br/><div id='registered'><div id='loadingMessage'>🎥 Unable to access video stream (please make sure you have a webcam enabled)</div>" +
|
||||
"<canvas id='canvas' hidden></canvas><div id='output' hidden><div id='outputMessage'></div>" +
|
||||
"<br/><span id='outputData'></span></div></div><div class='modal-footer'>"+
|
||||
"<button type='submit' class='btn btn-primary' onclick='cancelsend()'>Cancel</button><br/><br/></div>"
|
||||
var video = document.createElement('video')
|
||||
var canvasElement = document.getElementById('canvas')
|
||||
var canvas = canvasElement.getContext('2d')
|
||||
var loadingMessage = document.getElementById('loadingMessage')
|
||||
var outputContainer = document.getElementById('output')
|
||||
var outputMessage = document.getElementById('outputMessage')
|
||||
var outputData = document.getElementById('outputData')
|
||||
|
||||
// Use facingMode: environment to attemt to get the front camera on phones
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video: {facingMode: 'environment'}})
|
||||
.then(function(stream) {
|
||||
video.srcObject = stream
|
||||
video.setAttribute('playsinline', true) // required to tell iOS safari we don't want fullscreen
|
||||
video.play()
|
||||
requestAnimationFrame(tick)
|
||||
})
|
||||
|
||||
function tick() {
|
||||
loadingMessage.innerText = '⌛ Loading video...'
|
||||
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
||||
loadingMessage.hidden = true
|
||||
canvasElement.hidden = false
|
||||
outputContainer.hidden = false
|
||||
canvasElement.height = video.videoHeight
|
||||
canvasElement.width = video.videoWidth
|
||||
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height)
|
||||
var imageData = canvas.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvasElement.width,
|
||||
canvasElement.height
|
||||
)
|
||||
var code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: 'dontInvert'
|
||||
})
|
||||
if (code) {
|
||||
thehash = code.data
|
||||
document.getElementById('registered').innerHTML = "<h1 style='color:green;font-size:300%;'>Registered!</h1>"
|
||||
outputMessage.hidden = true
|
||||
outputData.parentElement.hidden = false
|
||||
outputData.innerText = JSON.stringify(code.data)
|
||||
getAjax("{{ url_for('events.api_checkticket') }}?thehash=" + thehash, "filla", function(datab) {
|
||||
if (JSON.parse(datab).status == 'TRUE') {
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
outputMessage.hidden = false
|
||||
outputData.parentElement.hidden = true
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelsend() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
function getUrlVars() {
|
||||
var vars = {};
|
||||
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
|
||||
vars[key] = value;
|
||||
});
|
||||
return vars;
|
||||
}
|
||||
var name = getUrlVars()["name"];
|
||||
var thehash = getUrlVars()["thehash"];
|
||||
console.log(thehash)
|
||||
if(thehash != null){
|
||||
document.getElementById('qrcodetxt').innerHTML = "<center><h1>" + name + " is registered!</h1></center>"
|
||||
|
||||
}
|
||||
</script>
|
||||
</html>
|
|
@ -1,457 +1,33 @@
|
|||
<!-- @format -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>LNBits Wallet</title>
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
||||
name="viewport"
|
||||
/>
|
||||
<!-- Bootstrap 3.3.2 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}"
|
||||
/>
|
||||
<!-- FontAwesome 4.3.0 -->
|
||||
<link
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<!-- Ionicons 2.0.0 -->
|
||||
<link
|
||||
href="https://code.ionicframework.com/ionicons/2.0.0/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
<!-- Theme style -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/AdminLTE.min.css') }}"
|
||||
/>
|
||||
<!-- AdminLTE Skins. Choose a skin from the css/skins
|
||||
folder instead of downloading all of them to reduce the load. -->
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='dist/css/skins/_all-skins.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- Morris chart -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/morris/morris.css') }}"
|
||||
/>
|
||||
|
||||
<!-- jvectormap -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.css') }}"
|
||||
/>
|
||||
|
||||
<!-- bootstrap wysihtml5 - text editor -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
href="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') }}"
|
||||
/>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
.small-box > .small-box-footer {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#output {
|
||||
margin-top: 20px;
|
||||
background: #eee;
|
||||
padding: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#output div {
|
||||
padding-bottom: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#noQRFound {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- jQuery 2.1.3 -->
|
||||
<script src="{{ url_for('static', filename='plugins/jQuery/jQuery-2.1.3.min.js') }}"></script>
|
||||
<!-- jQuery UI 1.11.2 -->
|
||||
<script
|
||||
src="https://code.jquery.com/ui/1.11.2/jquery-ui.min.js"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Resolve conflict in jQuery UI tooltip with Bootstrap tooltip -->
|
||||
<script>
|
||||
$.widget.bridge('uibutton', $.ui.button)
|
||||
</script>
|
||||
<!-- Bootstrap 3.3.2 JS -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Morris.js charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/morris/morris.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Sparkline -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/sparkline/jquery.sparkline.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jvectormap -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-1.2.2.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jvectormap/jquery-jvectormap-world-mill-en.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- jQuery Knob Chart -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/knob/jquery.knob.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Bootstrap WYSIHTML5 -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- Slimscroll -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/slimScroll/jquery.slimscroll.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<!-- FastClick -->
|
||||
<script src="{{ url_for('static', filename='plugins/fastclick/fastclick.min.js') }}"></script>
|
||||
<!-- AdminLTE App -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/app.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE dashboard demo (This is only for demo purposes) -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/pages/dashboard.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<!-- AdminLTE for demo purposes -->
|
||||
<script
|
||||
src="{{ url_for('static', filename='dist/js/demo.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/datatables/jquery.dataTables.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/JS.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/jscam/qrcode.min.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/decoder.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
<script
|
||||
src="{{ url_for('static', filename='plugins/bolt11/utils.js') }}"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
//GOOFY CSS HACK TO GO DARK
|
||||
|
||||
.skin-blue .wrapper {
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background: #1f2234;
|
||||
border-left-color: #8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #2e507d;
|
||||
}
|
||||
|
||||
.content-wrapper,
|
||||
.right-side {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
.skin-blue .main-header .logo {
|
||||
background-color: #1f2234;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li.header {
|
||||
color: #4b646f;
|
||||
background: #1f2234;
|
||||
}
|
||||
.skin-blue .wrapper,
|
||||
.skin-blue .main-sidebar,
|
||||
.skin-blue .left-side {
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > .treeview-menu {
|
||||
margin: 0 1px;
|
||||
background: #1f2234;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a {
|
||||
border-left: 3px solid transparent;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.skin-blue .sidebar-menu > li > a:hover,
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
color: #fff;
|
||||
background: #3e355a;
|
||||
border-left-color: #8964a9;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .logo:hover {
|
||||
background: #3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .sidebar-toggle:hover {
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.main-footer {
|
||||
background-color: #1f2234;
|
||||
padding: 15px;
|
||||
color: #fff;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar {
|
||||
background-color: #1f2234;
|
||||
}
|
||||
|
||||
.bg-red,
|
||||
.callout.callout-danger,
|
||||
.alert-danger,
|
||||
.alert-error,
|
||||
.label-danger,
|
||||
.modal-danger .modal-body {
|
||||
background-color: #1f2234 !important;
|
||||
}
|
||||
.alert-danger,
|
||||
.alert-error {
|
||||
border-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.skin-blue .main-header .navbar .nav > li > a:hover,
|
||||
.skin-blue .main-header .navbar .nav > li > a:active,
|
||||
.skin-blue .main-header .navbar .nav > li > a:focus,
|
||||
.skin-blue .main-header .navbar .nav .open > a,
|
||||
.skin-blue .main-header .navbar .nav .open > a:hover,
|
||||
.skin-blue .main-header .navbar .nav .open > a:focus {
|
||||
color: #f6f6f6;
|
||||
background-color: #3e355a;
|
||||
}
|
||||
.bg-aqua,
|
||||
.callout.callout-info,
|
||||
.alert-info,
|
||||
.label-info,
|
||||
.modal-info .modal-body {
|
||||
background-color: #3e355a !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
background-color: #333646;
|
||||
border-top: 3px solid #8964a9;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.table-striped > tbody > tr:nth-of-type(2n + 1) {
|
||||
background-color: #333646;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.box.box-danger {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
.box.box-primary {
|
||||
border-top-color: #8964a9;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8964a9;
|
||||
}
|
||||
.box-header.with-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:active,
|
||||
a:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
}
|
||||
// .modal.in .modal-dialog{
|
||||
// color:#000;
|
||||
// }
|
||||
|
||||
.form-control {
|
||||
background-color: #333646;
|
||||
color: #fff;
|
||||
}
|
||||
.box-footer {
|
||||
border-top: none;
|
||||
|
||||
background-color: #333646;
|
||||
}
|
||||
.modal-footer {
|
||||
border-top: none;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #333646;
|
||||
}
|
||||
.modal.in .modal-dialog {
|
||||
background-color: #333646;
|
||||
}
|
||||
|
||||
.layout-boxed {
|
||||
background: none;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
background-color: #3e355a;
|
||||
}
|
||||
|
||||
.skin-blue .sidebar-menu > li > a:hover,
|
||||
.skin-blue .sidebar-menu > li.active > a {
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="skin-blue layout-boxed sidebar-collapse sidebar-open">
|
||||
<div class="wrapper">
|
||||
<header class="main-header">
|
||||
<!-- Logo -->
|
||||
<a href="{{ url_for('core.home') }}" class="logo"><b>LN</b>bits</a>
|
||||
<!-- Header Navbar: style can be found in header.less -->
|
||||
<nav class="navbar navbar-static-top" role="navigation">
|
||||
<!-- Sidebar toggle button-->
|
||||
<a
|
||||
href="#"
|
||||
class="sidebar-toggle"
|
||||
data-toggle="offcanvas"
|
||||
role="button"
|
||||
>
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
</a>
|
||||
<div class="navbar-custom-menu">
|
||||
<ul class="nav navbar-nav">
|
||||
<!-- Messages: style can be found in dropdown.less-->
|
||||
<li class="dropdown messages-menu">
|
||||
{% block messages %}{% endblock %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<aside class="main-sidebar">
|
||||
<!-- sidebar: style can be found in sidebar.less -->
|
||||
<section class="sidebar" style="height: auto;"></section>
|
||||
<!-- /.sidebar -->
|
||||
</aside>
|
||||
|
||||
<!-- Right side column. Contains the navbar and content of the page -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
LNBits Events
|
||||
<small>Lightning powered tickets</small>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<br /><br />
|
||||
|
||||
<center>
|
||||
<h2 style="width: 70%; font-size: 400%;">
|
||||
Bookmark/Screenshot this page. <br />It is your ticket!
|
||||
</h2>
|
||||
</center>
|
||||
<center>
|
||||
<div
|
||||
style="width: 340px; background-color: white; padding: 20px;"
|
||||
id="qrcode"
|
||||
></div>
|
||||
</center>
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
</div>
|
||||
<!-- /.content-wrapper -->
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: '{{ticket}}',
|
||||
width: 300,
|
||||
height: 300,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M
|
||||
})
|
||||
</script>
|
||||
</html>
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">{{ ticket_name }} Ticket</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">
|
||||
Bookmark or screenshot this page,<br />
|
||||
and present it for registration!
|
||||
</h5>
|
||||
<br />
|
||||
<qrcode
|
||||
:value="{{ ticket_id }}"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin]
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,174 +1,45 @@
|
|||
import uuid
|
||||
from flask import g, abort, render_template
|
||||
from datetime import date, datetime
|
||||
|
||||
from flask import jsonify, render_template, request, redirect, url_for
|
||||
from datetime import datetime
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.extensions.events import events_ext
|
||||
from .crud import get_ticket, get_event
|
||||
|
||||
|
||||
@events_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
def index():
|
||||
"""Main events link page."""
|
||||
usr = request.args.get("usr")
|
||||
return render_template("events/index.html", user=g.user)
|
||||
|
||||
if usr:
|
||||
if not len(usr) > 20:
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# Get all the data
|
||||
with open_db() as db:
|
||||
user_wallets = db.fetchall("SELECT * FROM wallets WHERE user = ?", (usr,))
|
||||
user_ext = db.fetchall("SELECT extension FROM extensions WHERE user = ? AND active = 1", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
@events_ext.route("/<event_id>")
|
||||
def display(event_id):
|
||||
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
if event.amount_tickets < 1:
|
||||
return render_template("events/error.html", event_name=event.name, event_error="Sorry, tickets are sold out :(")
|
||||
datetime_object = datetime.strptime(event.closing_date, '%Y-%m-%d').date()
|
||||
if date.today() > datetime_object:
|
||||
return render_template("events/error.html", event_name=event.name, event_error="Sorry, ticket closing date has passed :(")
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
|
||||
|
||||
# If del is selected by user from events page, the event link is to be deleted
|
||||
evdel = request.args.get("del")
|
||||
if evdel:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (evdel,))
|
||||
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (evdel,))
|
||||
if user_ev[0][9] > 0:
|
||||
events_ext_db.execute("DELETE FROM eventssold WHERE uni = ?", (user_ev[0][12],))
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
|
||||
print(user_ext)
|
||||
return render_template("events/display.html", event_id=event_id, event_name=event.name, event_info=event.info, event_price=event.price_per_ticket)
|
||||
|
||||
return render_template(
|
||||
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
|
||||
)
|
||||
|
||||
@events_ext.route("/create", methods=["GET", "POST"])
|
||||
def create():
|
||||
"""."""
|
||||
@events_ext.route("/ticket/<ticket_id>")
|
||||
def ticket(ticket_id):
|
||||
ticket = get_ticket(ticket_id) or abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.")
|
||||
event = get_event(ticket.event) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
return render_template("events/ticket.html", ticket_id=ticket_id, ticket_name=event.name, ticket_info=event.info)
|
||||
|
||||
data = request.json
|
||||
tit = data["tit"]
|
||||
wal = data["wal"]
|
||||
cldate = data["cldate"]
|
||||
notickets = data["notickets"]
|
||||
prtick = data["prtickets"]
|
||||
usr = data["usr"]
|
||||
descr = data["descr"]
|
||||
wall = wal.split("-")
|
||||
|
||||
# Form validation
|
||||
if (
|
||||
not tit.replace(" ", "").isalnum()
|
||||
or wal == ""
|
||||
or int(notickets) < 0
|
||||
or int(prtick) < 0
|
||||
):
|
||||
return jsonify({"ERROR": "FORM ERROR"}), 401
|
||||
|
||||
# If id that means its a link being edited, delete the record first
|
||||
if "id" in data:
|
||||
unid = data["id"].split("-")
|
||||
uni = unid[1]
|
||||
unireg = unid[2]
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
events_ext_db.execute("DELETE FROM events WHERE uni = ?", (unid[1],))
|
||||
else:
|
||||
uni = uuid.uuid4().hex
|
||||
unireg = uuid.uuid4().hex
|
||||
|
||||
@events_ext.route("/register/<event_id>")
|
||||
def register(event_id):
|
||||
event = get_event(event_id) or abort(HTTPStatus.NOT_FOUND, "Event does not exist.")
|
||||
|
||||
with open_db() as dbb:
|
||||
user_wallets = dbb.fetchall("SELECT * FROM wallets WHERE user = ? AND id = ?", (usr, wall[1],))
|
||||
if not user_wallets:
|
||||
return jsonify({"ERROR": "NO WALLET USER"}), 401
|
||||
|
||||
with open_db() as db:
|
||||
user_ext = db.fetchall("SELECT * FROM extensions WHERE user = ?", (usr,))
|
||||
user_ext = [v[0] for v in user_ext]
|
||||
|
||||
# Add to DB
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
events_ext_db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO events
|
||||
(usr, wal, walnme, walinvkey, uni, tit, cldate, notickets, prtick, descr, unireg)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
usr,
|
||||
wall[1],
|
||||
user_wallets[0][1],
|
||||
user_wallets[0][4],
|
||||
uni,
|
||||
tit,
|
||||
cldate,
|
||||
notickets,
|
||||
prtick,
|
||||
descr,
|
||||
unireg,
|
||||
),
|
||||
)
|
||||
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE usr = ?", (usr,))
|
||||
|
||||
if not user_ev:
|
||||
return jsonify({"ERROR": "NO WALLET USER"}), 401
|
||||
|
||||
return render_template(
|
||||
"events/index.html", user_wallets=user_wallets, user=usr, user_ext=user_ext, user_ev=user_ev
|
||||
)
|
||||
return render_template("events/register.html", event_id=event_id, event_name=event.name, wallet_id=event.wallet)
|
||||
|
||||
|
||||
|
||||
@events_ext.route("/wave/<wave>/", methods=["GET", "POST"])
|
||||
def wave(wave):
|
||||
"""."""
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (wave,))
|
||||
if not user_ev:
|
||||
return jsonify({"ERROR": "NO RECORD"}), 401
|
||||
|
||||
return render_template(
|
||||
"events/display.html", wave=wave, nme=user_ev[0][6], descr=user_ev[0][11]
|
||||
)
|
||||
|
||||
@events_ext.route("/registration/<wave>", methods=["GET", "POST"])
|
||||
def registration(wave):
|
||||
"""."""
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE uni = ?", (wave,))
|
||||
user_ev_sold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE uni = ? AND paid = 1", (user_ev[0][12],))
|
||||
if not user_ev:
|
||||
return jsonify({"ERROR": "NO RECORD"}), 401
|
||||
|
||||
return render_template(
|
||||
"events/registration.html", user_ev=user_ev, user_ev_sold=user_ev_sold
|
||||
)
|
||||
|
||||
@events_ext.route("/ticket/", methods=["GET"])
|
||||
def ticket():
|
||||
"""."""
|
||||
thehash = request.args.get("hash")
|
||||
unireg = request.args.get("unireg")
|
||||
|
||||
#Double check the payment has cleared
|
||||
with open_db() as db:
|
||||
payment = db.fetchall("SELECT * FROM apipayments WHERE payhash = ?", (thehash,))
|
||||
|
||||
if not payment:
|
||||
return jsonify({"status": "ERROR", "reason":"NO RECORD OF PAYMENT"}), 400
|
||||
|
||||
if payment[0][4] == 1:
|
||||
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 400
|
||||
|
||||
#Update databases
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
|
||||
updatesold = user_ev[0][9] + 1
|
||||
events_ext_db.execute("UPDATE events SET sold = ? WHERE unireg = ?", (updatesold, unireg,))
|
||||
events_ext_db.execute("UPDATE eventssold SET paid = 1 WHERE hash = ?", (thehash,))
|
||||
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
|
||||
if not eventssold:
|
||||
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
|
||||
|
||||
return render_template(
|
||||
"events/ticket.html", name=eventssold[0][3], ticket=thehash
|
||||
)
|
||||
|
|
|
@ -1,65 +1,155 @@
|
|||
import uuid
|
||||
import json
|
||||
import requests
|
||||
from flask import g, jsonify, request
|
||||
from http import HTTPStatus
|
||||
|
||||
from flask import jsonify, request, url_for
|
||||
from datetime import datetime
|
||||
from lnbits.core.crud import get_user, get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
from lnbits.db import open_db, open_ext_db
|
||||
from lnbits.extensions.events import events_ext
|
||||
|
||||
@events_ext.route("/api/v1/getticket/", methods=["GET","POST"])
|
||||
def api_getticket():
|
||||
"""."""
|
||||
|
||||
data = request.json
|
||||
unireg = data["unireg"]
|
||||
name = data["name"]
|
||||
email = request.args.get("ema")
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
user_ev = events_ext_db.fetchall("SELECT * FROM events WHERE unireg = ?", (unireg,))
|
||||
from .crud import create_ticket, get_ticket, get_tickets, delete_ticket, create_event, update_event, get_event, get_events, delete_event, get_event_tickets
|
||||
|
||||
|
||||
header = {"Content-Type": "application/json", "X-Api-Key": user_ev[0][4]}
|
||||
data = {"value": str(user_ev[0][10]), "memo": user_ev[0][6]}
|
||||
print(url_for("api_invoices", _external=True))
|
||||
r = requests.post(url=url_for("api_invoices", _external=True), headers=header, data=json.dumps(data))
|
||||
r_json = r.json()
|
||||
#########Events##########
|
||||
|
||||
if "ERROR" in r_json:
|
||||
return jsonify({"status": "ERROR", "reason": r_json["ERROR"]}), 400
|
||||
|
||||
events_ext_db.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO eventssold
|
||||
(uni, email, name, hash)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
unireg,
|
||||
email,
|
||||
name,
|
||||
r_json["payment_hash"]
|
||||
),
|
||||
@events_ext.route("/api/v1/events", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_events():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([event._asdict() for event in get_events(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events", methods=["POST"])
|
||||
@events_ext.route("/api/v1/events/<event_id>", methods=["PUT"])
|
||||
@api_check_wallet_key("invoice")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"wallet": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"info": {"type": "string", "min": 0, "required": True},
|
||||
"closing_date": {"type": "string", "empty": False, "required": True},
|
||||
"event_start_date": {"type": "string", "empty": False, "required": True},
|
||||
"event_end_date": {"type": "string", "empty": False, "required": True},
|
||||
"amount_tickets": {"type": "integer", "min": 0, "required": True},
|
||||
"price_per_ticket": {"type": "integer", "min": 0, "required": True}
|
||||
}
|
||||
)
|
||||
def api_event_create(event_id=None):
|
||||
if event_id:
|
||||
event = get_event(event_id)
|
||||
print(g.data)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if event.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
event = update_event(event_id, **g.data)
|
||||
else:
|
||||
event = create_event(**g.data)
|
||||
print(event)
|
||||
return jsonify(event._asdict()), HTTPStatus.CREATED
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_form_delete(event_id):
|
||||
event = get_event(event_id)
|
||||
|
||||
if not event:
|
||||
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if event.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_event(event_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
|
||||
@events_ext.route("/api/v1/tickets", methods=["GET"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_tickets():
|
||||
wallet_ids = [g.wallet.id]
|
||||
|
||||
if "all_wallets" in request.args:
|
||||
wallet_ids = get_user(g.wallet.user).wallet_ids
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in get_tickets(wallet_ids)]), HTTPStatus.OK
|
||||
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["GET"])
|
||||
def api_ticket_create(event_id, sats):
|
||||
event = get_event(event_id)
|
||||
|
||||
try:
|
||||
checking_id, payment_request = create_invoice(
|
||||
wallet_id=event.wallet, amount=int(sats), memo=f"#lnticket {event_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
return jsonify({"status": "TRUE", "pay_req": r_json["pay_req"], "payment_hash": r_json["payment_hash"]}), 200
|
||||
return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/checkticket/", methods=["GET"])
|
||||
def api_checkticket():
|
||||
"""."""
|
||||
thehash = request.args.get("thehash")
|
||||
#Check databases
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
eventssold = events_ext_db.fetchall("SELECT * FROM eventssold WHERE hash = ?", (thehash,))
|
||||
if not eventssold:
|
||||
return jsonify({"status": "ERROR", "reason":"NO TICKET RECORD"}), 200
|
||||
if eventssold[0][4] == 0:
|
||||
return jsonify({"status": "ERROR", "reason":"NOT PAID"}), 200
|
||||
|
||||
with open_ext_db("events") as events_ext_db:
|
||||
events_ext_db.execute("UPDATE eventssold SET reg = 1 WHERE hash = ?", (thehash,))
|
||||
@events_ext.route("/api/v1/tickets/<checking_id>", methods=["POST"])
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"event": {"type": "string", "empty": False, "required": True},
|
||||
"name": {"type": "string", "empty": False, "required": True},
|
||||
"email": {"type": "string", "empty": False, "required": True}
|
||||
})
|
||||
def api_ticket_send_ticket(checking_id):
|
||||
|
||||
event = get_event(g.data['event'])
|
||||
if not event:
|
||||
return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
try:
|
||||
is_paid = not WALLET.get_invoice_status(checking_id).pending
|
||||
except Exception:
|
||||
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if is_paid:
|
||||
wallet = get_wallet(event.wallet)
|
||||
payment = wallet.get_payment(checking_id)
|
||||
payment.set_pending(False)
|
||||
ticket = create_ticket(wallet=event.wallet, **g.data)
|
||||
|
||||
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
|
||||
|
||||
return jsonify({"paid": False}), HTTPStatus.OK
|
||||
|
||||
|
||||
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
|
||||
@api_check_wallet_key("invoice")
|
||||
def api_ticket_delete(ticket_id):
|
||||
ticket = get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
|
||||
|
||||
if ticket.wallet != g.wallet.id:
|
||||
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
|
||||
|
||||
delete_ticket(ticket_id)
|
||||
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########EventTickets##########
|
||||
|
||||
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
|
||||
def api_event_tickets(wallet_id, event_id):
|
||||
|
||||
return jsonify([ticket._asdict() for ticket in get_event_tickets(wallet_id=wallet_id, event_id=event_id)]), HTTPStatus.OK
|
||||
|
||||
return jsonify({"status": "TRUE", "name": eventssold[0][3]}), 200
|
||||
|
|
Loading…
Reference in New Issue
Block a user