generated from nathanwoodburn/python-webserver-template
feat: Initial concept code
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
This commit is contained in:
parent
a51d2e89c4
commit
15bf3f0b01
@ -9,7 +9,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
|
|||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
# Optionally mount /data to store the data
|
# Optionally mount /data to store the data
|
||||||
# VOLUME /data
|
VOLUME /app/data
|
||||||
|
|
||||||
ENTRYPOINT ["python3"]
|
ENTRYPOINT ["python3"]
|
||||||
CMD ["main.py"]
|
CMD ["main.py"]
|
||||||
|
BIN
FireSales.bsdesign
Normal file
BIN
FireSales.bsdesign
Normal file
Binary file not shown.
9
data/listings.json
Normal file
9
data/listings.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"domain": "woodburn43",
|
||||||
|
"description": "This is a test listing update",
|
||||||
|
"price": 100.2,
|
||||||
|
"tx": "somelonghexstring",
|
||||||
|
"updated": 1738214534
|
||||||
|
}
|
||||||
|
]
|
2
example.env
Normal file
2
example.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
HSD_IP=127.0.0.1
|
||||||
|
HSD_API=hsd-api-key
|
5
requests/buy.hurl
Normal file
5
requests/buy.hurl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
POST http://127.0.0.1:5000/api/v1/delete
|
||||||
|
{
|
||||||
|
"domain":"woodburn43",
|
||||||
|
"tx":"somelonghexstring"
|
||||||
|
}
|
5
requests/cancel.hurl
Normal file
5
requests/cancel.hurl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
POST http://127.0.0.1:5000/api/v1/delete
|
||||||
|
{
|
||||||
|
"domain":"woodburn43",
|
||||||
|
"signature":"zodqz5r8ibyGeoC9yZZJPmJXMFSGhno3M0lVSdZXblI1iBIIaVYzMsSaYrWiAaXZoh2FBnFv8fAI+m/U3BhiaA=="
|
||||||
|
}
|
7
requests/list.hurl
Normal file
7
requests/list.hurl
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
POST http://127.0.0.1:5000/api/v1/list
|
||||||
|
{
|
||||||
|
"domain":"woodburn43",
|
||||||
|
"price":100.2,
|
||||||
|
"description":"This is a test listing update",
|
||||||
|
"tx":"somelonghexstring"
|
||||||
|
}
|
136
sales.py
Normal file
136
sales.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import dotenv
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
HSD_IP = os.getenv("HSD_IP")
|
||||||
|
HSD_API = os.getenv("HSD_API")
|
||||||
|
|
||||||
|
if not os.path.exists("data/listings.json"):
|
||||||
|
with open("data/listings.json", "w") as f:
|
||||||
|
f.write("[]")
|
||||||
|
|
||||||
|
|
||||||
|
# region Classes
|
||||||
|
class Listing:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.domain = ''
|
||||||
|
self.price = 0
|
||||||
|
self.description = ''
|
||||||
|
self.tx = ''
|
||||||
|
self.updated = -1
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
self.updated = int(time.time())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Listing of {self.domain} for {self.price}, Description: {self.description}, Contact: {self.contact}, Signed: {self.signed}, Signature: {self.signature}, Updated: {self.updated}"
|
||||||
|
|
||||||
|
def toHTML(self):
|
||||||
|
return f"""
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">{self.domain}/</h4>
|
||||||
|
<h6 class="text-muted card-subtitle mb-2">{self.price} HNS</h6>
|
||||||
|
<p class="card-text">{self.description}</p>
|
||||||
|
<p class="card-text">TX: <code>{self.tx}</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def toJSON(self):
|
||||||
|
return {
|
||||||
|
'domain': self.domain,
|
||||||
|
'description': self.description,
|
||||||
|
'price': self.price,
|
||||||
|
'tx': self.tx,
|
||||||
|
'updated': self.updated
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return self.toJSON()
|
||||||
|
|
||||||
|
def txValid(self):
|
||||||
|
# TODO Validate tx is valid
|
||||||
|
return True
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
def get_listings() -> list[Listing]:
|
||||||
|
with open("data/listings.json", "r") as f:
|
||||||
|
data = json.loads(f.read())
|
||||||
|
|
||||||
|
listings = []
|
||||||
|
for listing in data:
|
||||||
|
listings.append(Listing(**listing))
|
||||||
|
return listings
|
||||||
|
|
||||||
|
def search_listings(domain) -> Listing:
|
||||||
|
listings = get_listings()
|
||||||
|
for listing in listings:
|
||||||
|
if listing.domain == domain:
|
||||||
|
return listing
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_listing(domain) -> bool:
|
||||||
|
listings = get_listings()
|
||||||
|
for listing in listings:
|
||||||
|
if listing.domain == domain:
|
||||||
|
listings.remove(listing)
|
||||||
|
saveListings(listings)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_listings_rendered() -> str:
|
||||||
|
listings = get_listings()
|
||||||
|
html = ""
|
||||||
|
for listing in listings:
|
||||||
|
html += listing.toHTML()
|
||||||
|
return html
|
||||||
|
|
||||||
|
def saveListings(listings:list[Listing]):
|
||||||
|
with open("data/listings.json", "w") as f:
|
||||||
|
f.write(json.dumps(listings,default=Listing.toJSON,indent=4))
|
||||||
|
|
||||||
|
def add_listing(listing:Listing):
|
||||||
|
if not listing.txValid():
|
||||||
|
return "Invalid tx"
|
||||||
|
|
||||||
|
# Remove any listings with the same domain
|
||||||
|
remove_listing(listing.domain)
|
||||||
|
|
||||||
|
listings = get_listings()
|
||||||
|
listings.append(listing)
|
||||||
|
saveListings(listings)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate_signature(domain,signature,message) -> bool:
|
||||||
|
response = requests.post(f"http://x:{HSD_API}@{HSD_IP}:12037", json={
|
||||||
|
"method":"verifymessagewithname",
|
||||||
|
"params":[
|
||||||
|
domain,
|
||||||
|
signature,
|
||||||
|
message
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if response.status_code != 200:
|
||||||
|
return False
|
||||||
|
|
||||||
|
response = response.json()
|
||||||
|
if response['result'] != True:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validate_buy_tx(domain,tx) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_cancel_signature(domain,signature) -> bool:
|
||||||
|
message = f"FS: {domain}"
|
||||||
|
if (not validate_signature(domain,signature,message)):
|
||||||
|
return False
|
||||||
|
return True
|
78
server.py
78
server.py
@ -15,6 +15,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dotenv
|
import dotenv
|
||||||
|
import sales
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@ -74,8 +75,83 @@ def wellknown(path):
|
|||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
listings = sales.get_listings_rendered()
|
||||||
|
return render_template("index.html", listings=listings)
|
||||||
|
|
||||||
|
@app.route("/view/<path:domain>")
|
||||||
|
def view(domain: str):
|
||||||
|
listing = sales.search_listings(domain)
|
||||||
|
if not listing:
|
||||||
|
return render_template("404.html"), 404
|
||||||
|
return render_template("view.html", listing=listing)
|
||||||
|
|
||||||
|
@app.route("/view/<path:domain>", methods=["POST"])
|
||||||
|
def post_offer(domain: str):
|
||||||
|
data = request.form
|
||||||
|
# Convert to JSON
|
||||||
|
data = json.loads(json.dumps(data))
|
||||||
|
offer = sales.send_offer(domain, data)
|
||||||
|
listing = sales.search_listings(domain)
|
||||||
|
return render_template("view.html", listing=listing,message=offer['message'])
|
||||||
|
|
||||||
|
@app.route("/report/<path:domain>")
|
||||||
|
def report(domain: str):
|
||||||
|
listing = sales.search_listings(domain)
|
||||||
|
if not listing:
|
||||||
|
return render_template("404.html"), 404
|
||||||
|
return redirect("https://l.woodburn.au/contact")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#region API Routes
|
||||||
|
def validate_data(data,required):
|
||||||
|
for key in required:
|
||||||
|
if key not in data:
|
||||||
|
return jsonify({'error': f'Missing {key}','success': False})
|
||||||
|
return True
|
||||||
|
|
||||||
|
@app.route("/api/v1/list", methods=["POST"])
|
||||||
|
def list():
|
||||||
|
data = request.get_json()
|
||||||
|
# Validate data has domain,description,price,contact
|
||||||
|
valid = validate_data(data,['domain','description','price','tx'])
|
||||||
|
if valid != True:
|
||||||
|
return valid
|
||||||
|
|
||||||
|
listing = sales.Listing(**data)
|
||||||
|
status = sales.add_listing(listing)
|
||||||
|
if status != True:
|
||||||
|
return jsonify({'error': 'Failed to add listing','success': False,'message': status})
|
||||||
|
return jsonify({'success': True,'error': None})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/delete", methods=["POST"])
|
||||||
|
def delete():
|
||||||
|
data = request.get_json()
|
||||||
|
# Validate data has domain
|
||||||
|
if not 'domain' in data:
|
||||||
|
return jsonify({'error': 'Domain is required','success': False})
|
||||||
|
|
||||||
|
if 'tx' in data:
|
||||||
|
if not sales.validate_buy_tx(data['domain'],data['tx']):
|
||||||
|
return jsonify({'error': 'Invalid tx','success': False})
|
||||||
|
|
||||||
|
sales.remove_listing(data['domain'])
|
||||||
|
return jsonify({'success': True,'error': None})
|
||||||
|
|
||||||
|
if 'signature' in data:
|
||||||
|
if not sales.validate_cancel_signature(data['domain'],data['signature']):
|
||||||
|
return jsonify({'error': 'Invalid signature','success': False,'message': f"FS: {data['domain']}"})
|
||||||
|
|
||||||
|
sales.remove_listing(data['domain'])
|
||||||
|
return jsonify({'success': True,'error': None})
|
||||||
|
|
||||||
|
return jsonify({'error': 'Signature or tx is required','success': False})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all(path: str):
|
def catch_all(path: str):
|
||||||
|
12
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
12
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
6
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
57
templates/assets/css/Navbar-Right-Links-Dark-icons.css
Normal file
57
templates/assets/css/Navbar-Right-Links-Dark-icons.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.bs-icon {
|
||||||
|
--bs-icon-size: .75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--bs-icon-size);
|
||||||
|
width: calc(var(--bs-icon-size) * 2);
|
||||||
|
height: calc(var(--bs-icon-size) * 2);
|
||||||
|
color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon-xs {
|
||||||
|
--bs-icon-size: 1rem;
|
||||||
|
width: calc(var(--bs-icon-size) * 1.5);
|
||||||
|
height: calc(var(--bs-icon-size) * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon-sm {
|
||||||
|
--bs-icon-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon-md {
|
||||||
|
--bs-icon-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon-lg {
|
||||||
|
--bs-icon-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon-xl {
|
||||||
|
--bs-icon-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon.bs-icon-primary {
|
||||||
|
color: var(--bs-white);
|
||||||
|
background: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon.bs-icon-primary-light {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
background: rgba(var(--bs-primary-rgb), .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon.bs-icon-semi-white {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
background: rgba(255, 255, 255, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon.bs-icon-rounded {
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-icon.bs-icon-circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 62 KiB |
@ -1,19 +1,49 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html data-bs-theme="dark" lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
<title>Nathan.Woodburn/</title>
|
<title>FireSales</title>
|
||||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
<meta name="twitter:description" content="Self Custodial Domain listings">
|
||||||
<link rel="stylesheet" href="/assets/css/index.css">
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta name="twitter:image" content="/assets/img/favicon.png">
|
||||||
|
<meta name="twitter:title" content="FireSales">
|
||||||
|
<meta name="description" content="Self Custodial Domain listings">
|
||||||
|
<meta property="og:image" content="/assets/img/favicon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
|
||||||
|
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap">
|
||||||
|
<link rel="stylesheet" href="/assets/css/Navbar-Right-Links-Dark-icons.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="spacer"></div>
|
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
|
||||||
<div class="centre">
|
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><img src="/assets/img/favicon.png" width="64px"><span style="margin: 10px;">FireSales</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-5"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|
||||||
<h1>Nathan.Woodburn/</h1>
|
<div class="collapse navbar-collapse" id="navcol-5">
|
||||||
</div>
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item"><a class="nav-link active" href="/">Home</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/docs">Docs</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/plugin">FireWallet Plugin</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<section class="py-4 py-xl-5">
|
||||||
|
<div class="container h-100">
|
||||||
|
<div class="row h-100">
|
||||||
|
<div class="col-md-10 col-xl-8 text-center d-flex d-sm-flex d-md-flex justify-content-center align-items-center mx-auto justify-content-md-start align-items-md-center justify-content-xl-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-uppercase fw-bold mb-3">FireSales</h2>
|
||||||
|
<p class="mb-4">Self custodial domain sales</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section style="margin: 10px;">{{listings|safe}}</section>
|
||||||
|
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
84
templates/view.html
Normal file
84
templates/view.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-bs-theme="dark" lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
|
<title>FireSales</title>
|
||||||
|
<meta name="twitter:description" content="Self Custodial Domain listings">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta name="twitter:image" content="/assets/img/favicon.png">
|
||||||
|
<meta name="twitter:title" content="FireSales">
|
||||||
|
<meta name="description" content="Self Custodial Domain listings">
|
||||||
|
<meta property="og:image" content="/assets/img/favicon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
|
||||||
|
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap">
|
||||||
|
<link rel="stylesheet" href="/assets/css/Navbar-Right-Links-Dark-icons.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
|
||||||
|
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><img src="/assets/img/favicon.png" width="64px"><span style="margin: 10px;">FireSales</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-5"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|
||||||
|
<div class="collapse navbar-collapse" id="navcol-5">
|
||||||
|
<ul class="navbar-nav ms-auto">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/docs">Docs</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/plugin">FireWallet Plugin</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<section class="py-4 py-xl-5">
|
||||||
|
<div class="container h-100">
|
||||||
|
<div class="row h-100">
|
||||||
|
<div class="col-md-10 col-xl-8 text-center d-flex d-sm-flex d-md-flex justify-content-center align-items-center mx-auto justify-content-md-start align-items-md-center justify-content-xl-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-uppercase fw-bold mb-3">FireSales</h2>
|
||||||
|
<p class="mb-4">Self custodial domain sales</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-center">{{message}}</h1>
|
||||||
|
</section>
|
||||||
|
<section class="d-xl-flex justify-content-xl-center" style="margin: 10px;">
|
||||||
|
<div class="card" style="width: fit-content;padding-right: 2em;padding-left: 2em;margin: 10px;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">{{listing['domain']}}/</h4>
|
||||||
|
<h6 class="text-muted card-subtitle mb-2">{{listing['price']}} HNS</h6>
|
||||||
|
<p class="card-text">{{listing['description']}}</p>
|
||||||
|
<p class="card-text">Signature: <code>{{listing['signature']}}</code></p>
|
||||||
|
<div class="card mb-5">
|
||||||
|
<div class="card-body p-sm-5">
|
||||||
|
<h2 class="text-center mb-4">Send Offer</h2>
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-3"><input class="form-control" type="text" id="name-2" name="address" placeholder="Receiving HNS address"><label class="form-label">Where you would like the domain sent to</label></div>
|
||||||
|
<div class="mb-3"><input class="form-control" type="text" id="name-1" name="price" placeholder="HNS Price of offer"></div>
|
||||||
|
<div class="mb-3"><textarea class="form-control" id="message-2" name="message" rows="6" placeholder="Message"></textarea></div>
|
||||||
|
<div><button class="btn btn-primary d-block w-100" type="submit">Send </button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card" style="width: fit-content;padding-right: 2em;padding-left: 2em;margin: 10px;">
|
||||||
|
<div class="card-body">{% for offer in listing['offers'] %}
|
||||||
|
<div class="card" style="margin: 10px;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">{{offer['address']}} - {{offer['status']}}</h4>
|
||||||
|
<h6 class="text-muted card-subtitle mb-2">Price: {{offer['price']}}</h6>
|
||||||
|
<h6 class="text-muted card-subtitle mb-2">Submitted: {{offer['date']}}</h6>
|
||||||
|
<p class="card-text">{{offer['message']}}</p>
|
||||||
|
<p class="card-text">{% if offer['response'] %}Owner Response: {{offer['response']}}{% endif %}{% if offer['tx'] %}<br>TX: {{offer['tx']}}{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="d-xl-flex justify-content-xl-center" style="margin: 10px;"></section>
|
||||||
|
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user