feat: Add admin page and made a ton of UI improvements
All checks were successful
Build Docker / Build Image (push) Successful in 25s

This commit is contained in:
Nathan Woodburn 2024-02-26 19:37:03 +11:00
parent 28e11e2596
commit 5430465744
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
10 changed files with 578 additions and 28 deletions

2
dist/bundle.js vendored

File diff suppressed because one or more lines are too long

186
main.py
View File

@ -10,13 +10,13 @@ import nacl.encoding
import nacl.exceptions
import base58
import render
import hashlib
import random
app = Flask(__name__)
dotenv.load_dotenv()
CURRENT_VOTE = os.getenv('CURRENT_VOTE')
OPTIONS = os.getenv('OPTIONS')
OPTIONS = json.loads(OPTIONS)
DISCORD_WEBHOOK = os.getenv('DISCORD_WEBHOOK')
# If votes file doesn't exist, create it
@ -53,10 +53,28 @@ def faviconPNG():
@app.route('/')
def index():
year = datetime.datetime.now().year
votes = render.votes()
options = render.options(OPTIONS)
info = get_vote_info()
options = render.options(info["options"])
return render_template('index.html',year=year,votes=votes, current_vote=CURRENT_VOTE, options=options)
enabled = info["enabled"]
end = datetime.datetime.strptime(info["end"], "%Y-%m-%d")
if end < datetime.datetime.now():
enabled = False
end = "Voting has closed"
info["public"] = True
else:
end = f'Voting ends on {end.strftime("%B %d, %Y")}'
revote = "not" if not info["revote"] else ""
if info["public"]:
votes = render.votes()
else:
votes = ""
return render_template('index.html',year=year,votes=votes, options=options,
current_vote=info["vote"], description=info["description"], end=end,enabled=enabled, public=info["public"], revote=revote)
@app.route('/<path:path>')
def catch_all(path):
@ -87,6 +105,24 @@ def vote():
public_key = data["walletAddress"]
percent = data["percent"]
# Check if revote is enabled
info = get_vote_info()
if not info['revote']:
with open('data/votes.json') as file:
votes = json.load(file)
for vote in votes:
if vote["walletAddress"] == public_key:
return render_template('revotes.html', year=datetime.datetime.now().year)
# Make sure the voting is enabled and hasn't ended
if not info['enabled']:
return render_template('404.html', year=datetime.datetime.now().year)
end = datetime.datetime.strptime(info['end'], "%Y-%m-%d")
if end < datetime.datetime.now():
return render_template('404.html', year=datetime.datetime.now().year)
# Verify signature
try:
# Decode base58 encoded strings
@ -108,7 +144,17 @@ def vote():
# Send message to discord
send_discord_message(data)
save_vote(data)
return render_template('success.html', year=datetime.datetime.now().year, vote=data["message"],percent=percent,signature=signature,votes=render.votes())
vote_chart = ""
if info['public']:
vote_chart = render.votes()
else:
date = datetime.datetime.strptime(info['end'], "%Y-%m-%d")
if date < datetime.datetime.now():
vote_chart = render.votes()
return render_template('success.html', year=datetime.datetime.now().year, vote=data["message"],percent=percent,signature=signature,votes=vote_chart)
def save_vote(data):
# Load votes
@ -176,11 +222,137 @@ def send_discord_message(data):
@app.route('/votes')
def download():
if 'walletAddress' in request.args:
address = request.args['walletAddress']
with open('data/votes.json') as file:
votes = json.load(file)
for vote in votes:
if vote["walletAddress"] == address:
return jsonify([vote])
return jsonify([])
info = get_vote_info()
if not info['public']:
end = datetime.datetime.strptime(info['end'], "%Y-%m-%d")
if end > datetime.datetime.now():
return render_template('blocked.html', year=datetime.datetime.now().year), 404
resp = make_response(send_from_directory('data', 'votes.json'))
# Set as json
resp.headers['Content-Type'] = 'application/json'
return resp
#region admin
@app.route('/login', methods=['POST'])
def login():
# If account.json doesn't exist, create it
if not os.path.isfile('data/account.json'):
user = request.form['email']
# Hash password
password = request.form['password']
hashed = hashlib.sha256(password.encode()).hexdigest()
token = random.randint(100000, 999999)
with open('data/account.json', 'w') as file:
json.dump({'email': user, 'password': hashed, 'token': token}, file)
resp = make_response(redirect('/admin'))
resp.set_cookie('token', str(token))
return resp
# Read account.json
with open('data/account.json') as file:
account = json.load(file)
user = request.form['email']
# Hash password
password = request.form['password']
hashed = hashlib.sha256(password.encode()).hexdigest()
if user == account['email'] and hashed == account['password']:
token = random.randint(100000, 999999)
account['token'] = token
with open('data/account.json', 'w') as file:
json.dump(account, file)
resp = make_response(redirect('/admin'))
resp.set_cookie('token', str(token))
return resp
return redirect('/')
@app.route('/admin')
def admin():
if not 'token' in request.cookies:
return redirect('/login')
with open('data/account.json') as file:
account = json.load(file)
if request.cookies['token'] != str(account['token']):
return redirect('/login')
info = get_vote_info()
options = ','.join(info['options'])
return render_template('admin.html', year=datetime.datetime.now().year, name=info['vote'], description=info['description'], end=info['end'], enabled=info['enabled'], public=info['public'], revote=info['revote'], options=options)
@app.route('/admin', methods=['POST'])
def admin_post():
if not 'token' in request.cookies:
return redirect('/login')
with open('data/account.json') as file:
account = json.load(file)
if request.cookies['token'] != str(account['token']):
return redirect('/login')
info = get_vote_info()
info['vote'] = request.form['name']
info['description'] = request.form['description']
info['end'] = request.form['end']
info['enabled'] = 'enabled' in request.form
info['public'] = 'public' in request.form
info['revote'] = 'revote' in request.form
options = request.form['options']
options = options.split(',')
info['options'] = options
with open('data/info.json', 'w') as file:
json.dump(info, file)
return redirect('/admin')
@app.route('/admin/clear')
def clear():
if not 'token' in request.cookies:
return redirect('/login')
with open('data/account.json') as file:
account = json.load(file)
if request.cookies['token'] != str(account['token']):
return redirect('/login')
with open('data/votes.json', 'w') as file:
json.dump([], file)
return redirect('/admin')
#endregion
def get_vote_info():
if not os.path.isfile('data/info.json'):
with open('data/info.json', 'w') as file:
end = datetime.datetime.now() + datetime.timedelta(days=7)
end = end.strftime("%Y-%m-%d")
json.dump({'vote': '','description':'', 'end': end,'enabled': False, 'public': True, 'revote': True, 'options': []}, file)
with open('data/info.json') as file:
info = json.load(file)
return info
# 404 catch all
@app.errorhandler(404)
def not_found(e):

View File

@ -4,7 +4,7 @@ import base58 from 'bs58'
document.addEventListener('DOMContentLoaded', async function() {
// Set testing to true if running in a local environment
const testing = false;
const testing = true;
let TOKENID = "G9GQFWQmTiBzm1Hh4gM4ydQB4en3wPUxBZ1PS8DruXy8";
let supply = 100000;
let balance = 0;
@ -18,7 +18,6 @@ document.addEventListener('DOMContentLoaded', async function() {
// Initialize Solana connection
const solana = window.solana;
if (!solana || !solana.isPhantom) {
alert('Phantom wallet not detected. Please install Phantom and try again.');
return;
}
@ -89,9 +88,48 @@ document.addEventListener('DOMContentLoaded', async function() {
document.getElementById('percent').textContent = roundedPercent.toString();
balance = output;
});
// Show the user their existing vote if they have one
// Read list of votes from the server
const response = await fetch('/votes?walletAddress=' + solana.publicKey.toString());
const votes = await response.json();
// Find the user's vote
const userVote = votes.find(vote => vote.walletAddress === solana.publicKey.toString());
if (userVote) {
// Display the user's vote
const vote = JSON.parse(userVote.message);
const existingVote = document.getElementById('existingVote');
existingVote.innerHTML = '<h3 class="display-4">Your existing vote:</h3>';
let total = 0;
for (const key in vote) {
if (vote.hasOwnProperty(key)) {
const value = vote[key];
total += parseInt(value);
const li = document.createElement('li');
li.textContent = `${key}: ${value}%`;
// Remove the bullet points
li.style.listStyleType = 'none';
if (parseInt(value) > 0) {
existingVote.appendChild(li);
}
}
}
const sub = document.createElement('p');
sub.textContent = `You have used ${total}% of your voting power`;
existingVote.appendChild(sub);
existingVote.style.display = 'block';
existingVote.style.marginBottom = '20px';
existingVote.style.backgroundColor = 'black';
existingVote.style.color = 'white';
existingVote.style.padding = '10px';
}
} catch (error) {
console.error('Error connecting wallet:', error);
alert('Error connecting wallet. Check the console for details.');
return;
}
@ -152,7 +190,6 @@ document.addEventListener('DOMContentLoaded', async function() {
} catch (error) {
console.error('Error submitting vote:', error);
alert('Error submitting vote. Check the console for details.');
}
});
});

112
templates/admin.html Normal file
View File

@ -0,0 +1,112 @@
<!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>Admin - Vote | Nathan.Woodburn/</title>
<meta name="twitter:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:title" content="Vote | Nathan.Woodburn/">
<meta name="description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:type" content="website">
<meta property="og:description" content="Vote on Nathan.Woodburn/ projects">
<meta name="twitter:title" content="Vote | Nathan.Woodburn/">
<meta property="og:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/img/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="assets/img/android-chrome-512x512.png">
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
<link rel="stylesheet" href="assets/css/default.css">
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body>
<nav class="navbar navbar-expand-md fixed-top navbar-shrink py-3 navbar-light" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><span>Nathan.Woodburn/</span></a><button class="navbar-toggler" data-bs-toggle="collapse"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button></div>
</nav>
<header class="pt-5">
<div class="container pt-4 pt-xl-5">
<div class="row pt-5">
<div class="col-md-8 text-center text-md-start mx-auto">
<div class="text-center">
<h1 class="display-4 fw-bold mb-5">Admin</h1>
<form method="post"><input class="form-control" type="text" name="name" value="{{name}}" placeholder="Vote Name" style="margin-bottom: 5px;"><textarea class="form-control" style="margin-bottom: 5px;" name="description" placeholder="Vote Description">{{description}}</textarea><input class="form-control" type="text" name="options" value="{{options}}" placeholder="Vote Options comma separated" style="margin-bottom: 5px;">
<div style="margin-bottom: 5px;"><label class="form-label" for="end" style="display: inline-block;margin-right: 10px;">End Date</label><input class="form-control" type="date" name="end" value="{{end}}" style="width: auto;display: inline-block;"></div>{% if revote %}
<div class="form-check form-switch form-check-inline">
<input id="formCheck-1" class="form-check-input" type="checkbox" name="revote" checked />
<label class="form-check-label" for="formCheck-1">Allow revotes</label>
</div>
{% else %}
<div class="form-check form-switch form-check-inline">
<input id="formCheck-1" class="form-check-input" type="checkbox" name="revote" />
<label class="form-check-label" for="formCheck-1">Allow revotes</label>
</div>
{% endif %}
{% if public %}
<div class="form-check form-switch form-check-inline">
<input id="formCheck-2" class="form-check-input" type="checkbox" name="public" checked />
<label class="form-check-label" for="formCheck-2">Show results during vote</label>
</div>
{% else %}
<div class="form-check form-switch form-check-inline">
<input id="formCheck-2" class="form-check-input" type="checkbox" name="public" />
<label class="form-check-label" for="formCheck-2">Show results during vote</label>
</div>
{% endif %}{% if enabled %}
<div class="form-check form-switch form-check-inline">
<input id="formCheck-3" class="form-check-input" type="checkbox" name="enabled" checked />
<label class="form-check-label" for="formCheck-3">Enabled</label>
</div>
{% else %}
<div class="form-check form-switch form-check-inline">
<input id="formCheck-3" class="form-check-input" type="checkbox" name="enabled" />
<label class="form-check-label" for="formCheck-3">Enabled</label>
</div>
{% endif %}<input class="btn btn-primary" type="submit" style="display: block;margin: auto;margin-top: 25px;">
</form>
<div style="margin-top: 40px;">
<h1>Reset Votes</h1>
<p>Clear all votes to allow for the next vote</p><a class="btn btn-primary" role="button" href="/admin/clear">Reset Votes</a>
</div>
</div>
</div>
<div class="col-12 col-lg-10 mx-auto">
<div class="text-center position-relative"></div>
</div>
</div>
</div>
</header>
<footer>
<div class="container py-4 py-lg-5">
<div class="row row-cols-2 row-cols-md-4">
<div class="col-12 col-md-3">
<div class="fw-bold d-flex align-items-center mb-2"><span>Nathan.Woodburn/</span></div>
<p class="text-muted">Australian developer and student.</p>
</div>
<div class="col-sm-4 col-md-3 text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Sites</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a></li>
<li><a href="https://hns.au" target="_blank">HNSAU</a></li>
<li><a href="https://hnshosting.au" target="_blank">HNSHosting</a></li>
</ul>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © {{year}} Nathan.Woodburn/</p>
</div>
</div>
</footer>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="assets/js/startup-modern.js"></script>
</body>
</html>

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" width="406" height="306" class="illustration styles_illustrationTablet__1DWOa"><path d="M217.37,104.48s23.73,6,23.9,25.71-10.53,54.05,4.74,56.86,24.4-39.66,24.4-39.66l10.88,4.38s-11.58,72.57-51.6,60.59S217.37,104.48,217.37,104.48Z" fill="#68e1fd"></path><path d="M217.37,104.48s23.73,6,23.9,25.71-10.53,54.05,4.74,56.86,24.4-39.66,24.4-39.66l10.88,4.38s-11.58,72.57-51.6,60.59S217.37,104.48,217.37,104.48Z" fill="#fff" opacity="0.44"></path><ellipse cx="202.28" cy="230.74" rx="157.96" ry="14.45" fill="#e6e6e6" opacity="0.3"></ellipse><path d="M76.51,213.73S66.25,210.93,64,201.39c0,0,15.89-3.21,16.34,13.19Z" fill="#68e1fd" opacity="0.58"></path><path d="M77.76,212.72s-7.16-11.33-.85-21.92c0,0,12.07,7.67,6.71,21.94Z" fill="#68e1fd" opacity="0.73"></path><path d="M79.61,212.72s3.79-12,15.23-14.22c0,0,2.14,7.76-7.41,14.26Z" fill="#68e1fd"></path><polygon points="72.17 212.46 74.25 226.68 87.34 226.73 89.27 212.53 72.17 212.46" fill="#24285b"></polygon><polygon points="196.01 72.21 188.78 97.7 203.12 100.69 202.62 80.94 196.01 72.21" fill="#f4a28c"></polygon><path d="M202.92,85.25a9.57,9.57,0,0,1-4.37-3.36s-.37,4.84,4.35,10.22Z" fill="#ce8172" opacity="0.31"></path><path d="M188.78,97.71l8.5,1.76s38.9,4.19,40.48,26.15-20.39,78.06-20.39,78.06H158.43S111.2,91.9,188.78,97.71Z" fill="#68e1fd"></path><path d="M183.69,118.05s4.75,13.72-7.71,22.67-6.38,42.65,7.26,48.09-8.53,17.08-18,12.31-10.77-8-10.77-8-8.77-25.35-9.7-41.52S183.69,118.05,183.69,118.05Z" opacity="0.09"></path><path d="M158.43,203.68s-33.18-11.8-45.29-6.76-5.26,28.52,34.23,29.68,97.93,7.61,109.52-6.67S224.77,198,158.43,203.68Z" fill="#24285b"></path><path d="M183.69,118.05c0-12.07-12.23-20.32-23.71-16.55-10,3.27-22.15,9.34-29.54,20.44-14.22,21.32-46.7,83.65,35.75,86.05l2.42-9.18S119.12,185.12,141,156.16C158,133.64,183.74,135.15,183.69,118.05Z" fill="#68e1fd"></path><path d="M183.69,118.05c0-12.07-12.23-20.32-23.71-16.55-10,3.27-22.15,9.34-29.54,20.44-14.22,21.32-46.7,83.65,35.75,86.05l2.42-9.18S119.12,185.12,141,156.16C158,133.64,183.74,135.15,183.69,118.05Z" fill="#fff" opacity="0.44"></path><polygon points="166.51 209.87 207.52 209.87 217.37 165.64 160.19 165.64 166.51 209.87" fill="#ffd200"></polygon><path d="M273,148.41s.62-10.5,4.83-13.49,3.51,4.57,3.51,4.57,4.21-4.92,7.72-2.63-7.72,14.91-7.72,14.91Z" fill="#f4a28c"></path><rect x="268.45" y="97.1" width="37.48" height="48.09" transform="translate(42.89 -72.43) rotate(15.52)" fill="#e6e6e6"></rect><rect x="272.34" y="120.86" width="24.78" height="18.25" transform="translate(45.16 -71.45) rotate(15.52)" fill="#c1c1c1"></rect><rect x="295.14" y="106.58" width="7.04" height="12.28" transform="translate(41.05 -75.8) rotate(15.52)" fill="#c1c1c1" opacity="0.24"></rect><path d="M210.86,73s-.72,8.06-3,13.1a4.07,4.07,0,0,1-5.4,2c-2.52-1.18-5.59-3.51-5.72-7.87l-1.15-7.38a7.27,7.27,0,0,1,4.51-7.08C205.17,63.3,211.54,68.07,210.86,73Z" fill="#f4a28c"></path><path d="M208.83,72.93a31.48,31.48,0,0,1-7.24-1.87,6.75,6.75,0,0,1-1.3,7.29,5.48,5.48,0,0,1-6.91,1l2-10.21a8.23,8.23,0,0,1,5.39-6.5,29.4,29.4,0,0,1,3.71-1.06c3.16-.66,7.12,2.07,10.11.55a2,2,0,0,1,2.83,1.8c-.1,3.19-1.52,8-6,8.95A7.56,7.56,0,0,1,208.83,72.93Z" fill="#24285b"></path><path d="M201.19,78.27s.48-3.08-1.86-3.25-3.08,4.26,0,5.23Z" fill="#f4a28c"></path><path d="M210.14,78l1.68,3.47a1.29,1.29,0,0,1-1.16,1.85l-3.14,0Z" fill="#f4a28c"></path><circle cx="187.77" cy="184.73" r="4.08" fill="#fff"></circle><path d="M160.19,219.08s-16.73,7.25-12.32,11.66,15.71-3.47,15.71-3.47Z" fill="#68e1fd"></path><path d="M212.54,220.92s16.73,7.24,12.31,11.65-15.71-3.47-15.71-3.47Z" fill="#68e1fd"></path><circle cx="285.11" cy="108.25" r="6.32" fill="#c1c1c1"></circle><circle cx="286.69" cy="127.03" r="2.02" fill="#e6e6e6"></circle><polygon points="272.95 133.26 281.29 125.01 283.58 133.98 289.69 130.63 292.7 138.87 272.95 133.26" fill="#e6e6e6"></polygon><rect x="275.47" y="187.75" width="39.65" height="39.66" fill="#c1c1c1"></rect><rect x="298.13" y="187.75" width="39.65" height="39.66" fill="#e6e6e6"></rect></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

70
templates/blocked.html Normal file
View File

@ -0,0 +1,70 @@
<!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>Page Not Found - Vote | Nathan.Woodburn/</title>
<meta name="twitter:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:title" content="Vote | Nathan.Woodburn/">
<meta name="description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:type" content="website">
<meta property="og:description" content="Vote on Nathan.Woodburn/ projects">
<meta name="twitter:title" content="Vote | Nathan.Woodburn/">
<meta property="og:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/img/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="assets/img/android-chrome-512x512.png">
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
<link rel="stylesheet" href="assets/css/default.css">
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body>
<nav class="navbar navbar-expand-md fixed-top navbar-shrink py-3 navbar-light" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><span>Nathan.Woodburn/</span></a><button class="navbar-toggler" data-bs-toggle="collapse"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button></div>
</nav>
<section class="py-5 mt-5">
<div class="container">
<div class="row row-cols-1 d-flex justify-content-center align-items-center">
<div class="col text-center">
<h2 class="display-3 fw-bold mb-4">Votes are not public until after this vote.</h2>
<p class="fs-4 text-muted">You will be able to verify the votes after the voting has finished</p><a class="btn btn-primary" role="button" href="/">Home</a>
</div>
</div>
</div>
</section>
<footer>
<div class="container py-4 py-lg-5">
<div class="row row-cols-2 row-cols-md-4">
<div class="col-12 col-md-3">
<div class="fw-bold d-flex align-items-center mb-2"><span>Nathan.Woodburn/</span></div>
<p class="text-muted">Australian developer and student.</p>
</div>
<div class="col-sm-4 col-md-3 text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Sites</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a></li>
<li><a href="https://hns.au" target="_blank">HNSAU</a></li>
<li><a href="https://hnshosting.au" target="_blank">HNSHosting</a></li>
</ul>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © {{year}} Nathan.Woodburn/</p>
</div>
</div>
</footer>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="assets/js/startup-modern.js"></script>
</body>
</html>

View File

@ -44,24 +44,37 @@
<div class="col-md-8 text-center text-md-start mx-auto">
<div class="text-center">
<h1 class="display-4 fw-bold mb-5">Vote on projects from<br><a class="no-a-display" href="https://nathan.woodburn.au" target="_blank"><span class="underline">Nathan.Woodburn/</span></a></h1>
<h2 style="margin-bottom: 20px;">{{current_vote}}</h2>
<h3>You have&nbsp;<span id="balance">0</span>&nbsp;votes.</h3>
<h4><span id="percent">0</span>% of the voting power.</h4>
<p class="fs-5 text-muted mb-5">Vote here</p>
<form class="d-flex justify-content-center flex-wrap" id="signMessageForm" data-bs-theme="light">
<div class="shadow-lg mb-3">
<p>Select your vote or split your votes between options.</p>{{options|safe}}
</div>
<div class="shadow-lg mb-3"><button class="btn btn-primary" type="submit" style="margin-top: 25px;">Vote</button></div>
</form>
</div>{{votes|safe}}
</div>
<div class="col-12 col-lg-10 mx-auto">
<div class="text-center position-relative" style="padding-top: 25px;"><a class="btn btn-primary" role="button" target="_blank" href="/votes">View vote proofs</a><img class="img-fluid" src="assets/img/illustrations/meeting.svg" style="width: 800px;"></div>
</div>
</div>
</div>
</div>
</header>
<section>
<div class="container">
<div class="row">
<div class="col-md-6 text-center">
<h2 style="margin-bottom: 20px;">{{current_vote}}</h2>
<p style="margin: 0px;">{{description}}</p>
<p>{{end}}<br>You are {{revote}} allowed to redo your vote.</p>{{votes|safe}}
</div>
<div class="col-md-6 text-center">
<h3>You have&nbsp;<span id="balance">0</span>&nbsp;votes.</h3>
<h4><span id="percent">0</span>% of the voting power.</h4>
<div id="existingVote"></div>
<h5 class="display-5">Vote Here</h5>
<form class="d-flex justify-content-center flex-wrap" id="signMessageForm" data-bs-theme="light">
<div class="mb-3">
<p>Select your vote or split your votes between options.</p>{{options|safe}}
</div>
<div class="mb-3"><button class="btn btn-primary" type="submit" style="margin-top: 25px;" {% if not enabled %}disabled{% endif %}>Vote</button></div>
</form>
</div>
</div>
<div class="col-12 col-lg-10 mx-auto">
<div class="text-center position-relative" style="padding-top: 25px;"><a class="btn btn-primary" role="button" target="_blank" href="/votes">View vote proofs</a><img class="img-fluid" src="assets/img/illustrations/meeting.svg" style="width: 800px;"></div>
</div>
</div>
</section>
<footer>
<div class="container py-4 py-lg-5">
<div class="row row-cols-2 row-cols-md-4">

75
templates/login.html Normal file
View File

@ -0,0 +1,75 @@
<!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>Log in - Vote | Nathan.Woodburn/</title>
<meta name="twitter:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:title" content="Vote | Nathan.Woodburn/">
<meta name="description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:type" content="website">
<meta property="og:description" content="Vote on Nathan.Woodburn/ projects">
<meta name="twitter:title" content="Vote | Nathan.Woodburn/">
<meta property="og:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/img/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="assets/img/android-chrome-512x512.png">
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
<link rel="stylesheet" href="assets/css/default.css">
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body>
<nav class="navbar navbar-expand-md fixed-top navbar-shrink py-3 navbar-light" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><span>Nathan.Woodburn/</span></a><button class="navbar-toggler" data-bs-toggle="collapse"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button></div>
</nav>
<section class="py-4 py-md-5 my-5">
<div class="container py-md-5">
<div class="row">
<div class="col-md-6 text-center"><img class="img-fluid w-100" src="assets/img/illustrations/login.svg"></div>
<div class="col-md-5 col-xl-4 text-center text-md-start">
<h2 class="display-6 fw-bold mb-5"><span class="underline pb-1"><strong>Login</strong><br></span></h2>
<form method="post" data-bs-theme="light">
<div class="mb-3"><input class="shadow form-control" type="email" name="email" placeholder="Email"></div>
<div class="mb-3"><input class="shadow form-control" type="password" name="password" placeholder="Password"></div>
<div class="mb-5"><button class="btn btn-primary shadow" type="submit">Log in</button></div>
</form>
</div>
</div>
</div>
</section>
<footer>
<div class="container py-4 py-lg-5">
<div class="row row-cols-2 row-cols-md-4">
<div class="col-12 col-md-3">
<div class="fw-bold d-flex align-items-center mb-2"><span>Nathan.Woodburn/</span></div>
<p class="text-muted">Australian developer and student.</p>
</div>
<div class="col-sm-4 col-md-3 text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Sites</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a></li>
<li><a href="https://hns.au" target="_blank">HNSAU</a></li>
<li><a href="https://hnshosting.au" target="_blank">HNSHosting</a></li>
</ul>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © {{year}} Nathan.Woodburn/</p>
</div>
</div>
</footer>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="assets/js/startup-modern.js"></script>
</body>
</html>

70
templates/revotes.html Normal file
View File

@ -0,0 +1,70 @@
<!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>Page Not Found - Vote | Nathan.Woodburn/</title>
<meta name="twitter:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:title" content="Vote | Nathan.Woodburn/">
<meta name="description" content="Vote on Nathan.Woodburn/ projects">
<meta property="og:type" content="website">
<meta property="og:description" content="Vote on Nathan.Woodburn/ projects">
<meta name="twitter:title" content="Vote | Nathan.Woodburn/">
<meta property="og:image" content="https://vote.woodburn.au/assets/img/favicon.png">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="assets/img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="assets/img/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="assets/img/android-chrome-512x512.png">
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
<link rel="stylesheet" href="assets/css/default.css">
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body>
<nav class="navbar navbar-expand-md fixed-top navbar-shrink py-3 navbar-light" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><span>Nathan.Woodburn/</span></a><button class="navbar-toggler" data-bs-toggle="collapse"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button></div>
</nav>
<section class="py-5 mt-5">
<div class="container">
<div class="row row-cols-1 d-flex justify-content-center align-items-center">
<div class="col text-center">
<h2 class="display-3 fw-bold mb-4">Revotes are disabled on this vote.</h2>
<p class="fs-4 text-muted">You are not able to redo your vote.</p><a class="btn btn-primary" role="button" href="/">Home</a>
</div>
</div>
</div>
</section>
<footer>
<div class="container py-4 py-lg-5">
<div class="row row-cols-2 row-cols-md-4">
<div class="col-12 col-md-3">
<div class="fw-bold d-flex align-items-center mb-2"><span>Nathan.Woodburn/</span></div>
<p class="text-muted">Australian developer and student.</p>
</div>
<div class="col-sm-4 col-md-3 text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Sites</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a></li>
<li><a href="https://hns.au" target="_blank">HNSAU</a></li>
<li><a href="https://hnshosting.au" target="_blank">HNSHosting</a></li>
</ul>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © {{year}} Nathan.Woodburn/</p>
</div>
</div>
</footer>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="assets/js/startup-modern.js"></script>
</body>
</html>

View File

@ -34,9 +34,9 @@
<div class="container pt-4 pt-xl-5">
<div class="row pt-5">
<div class="col-md-8 text-center text-md-start mx-auto">
<div class="text-center">
<div class="text-center" style="margin-bottom: 25px;">
<h1 class="display-4 fw-bold mb-5">Thank you for voting!</h1>
<p class="fs-5 text-muted mb-5">Your voice matters!</p>
<p class="fs-5 text-muted">Your voice matters!</p><a class="btn btn-primary" role="button" href="/">Home</a>
</div>
<div class="table-responsive">
<table class="table table-dark">