feat: Add initial code
All checks were successful
Build Docker / Build Image (push) Successful in 39s

This commit is contained in:
Nathan Woodburn 2024-02-19 15:18:49 +11:00
parent 3fca26fffc
commit f8c98214bf
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
30 changed files with 8200 additions and 0 deletions

View File

@ -0,0 +1,41 @@
name: Build Docker
run-name: Build Docker Images
on:
push:
jobs:
Build Image:
runs-on: [ubuntu-latest, amd]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Docker
run : |
apt-get install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install docker-ce-cli -y
- name: Build Docker image
run : |
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
tag=${tag//\//-}
tag_num=${GITHUB_RUN_NUMBER}
echo "tag_num=$tag_num"
if [[ "$tag" == "main" ]]; then
tag="latest"
else
tag_num="${tag}-${tag_num}"
fi
docker build -t sol-vote:$tag_num .
docker tag sol-vote:$tag_num git.woodburn.au/nathanwoodburn/sol-vote:$tag_num
docker push git.woodburn.au/nathanwoodburn/sol-vote:$tag_num
docker tag sol-vote:$tag_num git.woodburn.au/nathanwoodburn/sol-vote:$tag
docker push git.woodburn.au/nathanwoodburn/sol-vote:$tag

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
__pycache__/
data/

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
WORKDIR /app
COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install -r requirements.txt
COPY . /app
# Add mount point for data volume
VOLUME /app/data
ENTRYPOINT ["python3"]
CMD ["server.py"]
FROM builder as dev-envs

881
dist/bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

184
main.py Normal file
View File

@ -0,0 +1,184 @@
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory
import os
import dotenv
import requests
import datetime
import json
import threading
import nacl.signing
import nacl.encoding
import nacl.exceptions
import base58
import render
app = Flask(__name__)
dotenv.load_dotenv()
# If votes file doesn't exist, create it
if not os.path.isfile('data/votes.json'):
with open('data/votes.json', 'w') as file:
json.dump([], file)
#Assets routes
@app.route('/assets/<path:path>')
def send_report(path):
return send_from_directory('templates/assets', path)
@app.route('/assets/js/bundle.js')
def send_bundle():
return send_from_directory('dist', 'bundle.js')
@app.route('/sitemap')
@app.route('/sitemap.xml')
def sitemap():
# Remove all .html from sitemap
with open('templates/sitemap.xml') as file:
sitemap = file.read()
sitemap = sitemap.replace('.html', '')
return make_response(sitemap, 200, {'Content-Type': 'application/xml'})
@app.route('/favicon.png')
def faviconPNG():
return send_from_directory('templates/assets/img', 'favicon.png')
# Main routes
@app.route('/')
def index():
year = datetime.datetime.now().year
votes = render.votes()
return render_template('index.html',year=year,votes=votes)
@app.route('/<path:path>')
def catch_all(path):
year = datetime.datetime.now().year
# If file exists, load it
if os.path.isfile('templates/' + path):
return render_template(path, year=year)
# Try with .html
if os.path.isfile('templates/' + path + '.html'):
return render_template(path + '.html', year=year)
return redirect('/')
@app.route('/vote')
def vote():
print('Voting')
# Get args
args = request.args
# Convert to json
data = args.to_dict()
print(data)
# Verify signature
message = data["message"]
signature = data["signature"]
public_key = data["walletAddress"]
percent = data["percent"]
# Verify signature
try:
# Decode base58 encoded strings
public_key_bytes = base58.b58decode(public_key)
signature_bytes = base58.b58decode(signature)
message_bytes = message.encode('utf-8')
# Verify the signature
verify_key = nacl.signing.VerifyKey(public_key_bytes)
verify_key.verify(message_bytes, signature_bytes)
# Signature is valid
data["verified"] = True
except (nacl.exceptions.BadSignatureError, nacl.exceptions.CryptoError) as e:
# Signature is invalid
data["verified"] = False
# 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())
def save_vote(data):
# Load votes
with open('data/votes.json') as file:
votes = json.load(file)
address = data["walletAddress"]
# Remove old vote
for vote in votes:
if vote["walletAddress"] == address:
votes.remove(vote)
# Add new vote
votes.append(data)
# Save votes
with open('data/votes.json', 'w') as file:
json.dump(votes, file, indent=4)
def send_discord_message(data):
# Define the webhook URL
webhook_url = 'https://discord.com/api/webhooks/1208977249232228362/jPtFZKD7MWzaRbiHwc9rgEOQx-d8DWSDyr7oVMzfoC5QUayts8BF1oi1xxoE8O6ouTFi'
text = f"New vote for `{data['message']}`"
tokens = data['votes']
# Convert to have commas
tokens = "{:,}".format(int(tokens))
# Define the message content
message = {
"embeds": [
{
"title": "New Vote",
"description": text,
"color": 65280 if data["verified"] else 16711680,
"footer": {
"text": "Nathan.Woodburn/"
},
"fields": [
{
"name": "Wallet Address",
"value": '`'+data["walletAddress"]+'`'
},
{
"name": "Vote",
"value": '`'+data["message"]+'`'
},
{
"name": "Verified",
"value": "Yes" if data["verified"] else "No"
},
{
"name": "Signature",
"value": '`'+data["signature"]+'`'
},
{
"name": "Number of tokens",
"value": tokens
}
]
}
]
}
# Send the message as a POST request to the webhook URL
response = requests.post(webhook_url, data=json.dumps(message), headers={'Content-Type': 'application/json'})
# Print the response from the webhook (for debugging purposes)
print(response.text)
# 404 catch all
@app.errorhandler(404)
def not_found(e):
return redirect('/')
if __name__ == '__main__':
app.run(debug=True, port=5000, host='0.0.0.0')

6467
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"babel-loader": "^9.1.3",
"webpack": "^5.90.2",
"webpack-cli": "^5.1.4"
},
"scripts": {
"build": "webpack"
},
"dependencies": {
"@solana/spl-token": "^0.4.0",
"@solana/web3.js": "^1.90.0",
"@walletconnect/web3-provider": "^1.8.0",
"bs58": "^5.0.0"
}
}

47
render.py Normal file
View File

@ -0,0 +1,47 @@
import json
def votes():
# Load votes
with open('data/votes.json') as file:
votes = json.load(file)
options = {}
for vote in votes:
if vote["message"] in options:
options[vote["message"]] += vote["votes"]
else:
options[vote["message"]] = vote["votes"]
labels = list(options.keys())
data = list(options.values())
chart_data = {
"type": "pie",
"data": {
"labels": labels,
"datasets": [{
"label": "Votes",
"backgroundColor": ["rgb(17,255,69)", "rgb(255,0,0)", "rgb(0,0,255)", "rgb(255,255,0)", "rgb(255,0,255)", "rgb(0,255,255)", "rgb(128,0,128)"],
"data": data
}]
},
"options": {
"maintainAspectRatio": True,
"legend": {
"display": True,
"labels": {
"fontStyle": "normal"
}
},
"title": {
"fontStyle": "bold"
}
}
}
html = '<script src="assets/js/bs-init.js"></script>'
html += f'<canvas data-bss-chart=\'{json.dumps(chart_data)}\' class="chartjs-render-monitor"></canvas>'
return html

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
flask
python-dotenv
gunicorn
requests
apscheduler
PyNaCl
base58

43
server.py Normal file
View File

@ -0,0 +1,43 @@
import time
from flask import Flask
from main import app
import main
from gunicorn.app.base import BaseApplication
import os
import dotenv
import sys
import json
from apscheduler.schedulers.background import BackgroundScheduler
class GunicornApp(BaseApplication):
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super().__init__()
def load_config(self):
for key, value in self.options.items():
if key in self.cfg.settings and value is not None:
self.cfg.set(key.lower(), value)
def load(self):
return self.application
if __name__ == '__main__':
workers = os.getenv('WORKERS')
threads = os.getenv('THREADS')
if workers is None:
workers = 1
if threads is None:
threads = 2
workers = int(workers)
threads = int(threads)
options = {
'bind': '0.0.0.0:5000',
'workers': workers,
'threads': threads,
}
gunicorn_app = GunicornApp(app, options)
print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True)
gunicorn_app.run()

136
src/index.js Normal file
View File

@ -0,0 +1,136 @@
import * as solanaWeb3 from '@solana/web3.js';
import base58 from 'bs58'
document.addEventListener('DOMContentLoaded', async function() {
// Set testing to true if running in a local environment
const testing = true;
let TOKENID = "G9GQFWQmTiBzm1Hh4gM4ydQB4en3wPUxBZ1PS8DruXy8";
let supply = 100000;
let balance = 0;
if (testing) {
TOKENID = "9YZ2syoQHvMeksp4MYZoYMtLyFWkkyBgAsVuuJzSZwVu";
supply = 17 * 1000000000;
}
// Initialize Solana connection
const solana = window.solana;
if (!solana || !solana.isPhantom) {
alert('Phantom wallet not detected. Please install Phantom and try again.');
return;
}
// Prompt user to connect wallet
try {
await solana.connect();
console.log('Wallet connected:', solana.publicKey.toString());
// Get token balance
// Connect to https://api.metaplex.solana.com/
const connection = new solanaWeb3.Connection('https://api.metaplex.solana.com/');
const publicKey = new solanaWeb3.PublicKey(solana.publicKey.toString());
const sol_balance = await connection.getBalance(publicKey);
console.log('Balance:', sol_balance/solanaWeb3.LAMPORTS_PER_SOL, 'SOL');
// Add your actual program ID here
const TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
async function getTokenAccountBalance(wallet, solanaConnection) {
const filters = [
{
dataSize: 165, // size of account (bytes)
},
{
memcmp: {
offset: 32, // location of our query in the account (bytes)
bytes: wallet, // our search criteria, a base58 encoded string
},
}
];
const accounts = await solanaConnection.getParsedProgramAccounts(
new solanaWeb3.PublicKey(TOKEN_PROGRAM_ID),
{ filters: filters }
);
console.log(`Found ${accounts.length} token account(s) for wallet ${wallet}.`);
let balance = 0;
for (const account of accounts) {
const parsedAccountInfo = account.account.data;
const mintAddress = parsedAccountInfo["parsed"]["info"]["mint"];
const tokenBalance = parsedAccountInfo["parsed"]["info"]["tokenAmount"]["uiAmount"];
if (mintAddress === TOKENID) {
console.log('Found our token account:', account.pubkey.toString());
console.log('Balance:', tokenBalance);
balance = tokenBalance;
break; // Exit the loop after finding the balance
}
}
return balance;
}
function formatNumber(value) {
const suffixes = ["", "K", "M", "B", "T"];
const suffixNum = Math.floor(("" + value).length / 3);
let shortValue = parseFloat((suffixNum !== 0 ? (value / Math.pow(1000, suffixNum)) : value).toPrecision(2));
if (shortValue % 1 !== 0) {
shortValue = shortValue.toFixed(1);
}
return shortValue + suffixes[suffixNum];
}
var balancePromise = getTokenAccountBalance(publicKey, connection);
balancePromise.then((output) => {
const percent_of_votes = (output / supply) * 100;
const roundedOutput = output > 10 ? Math.round(output) : output;
const roundedPercent = percent_of_votes > 5 ? Math.round(percent_of_votes) : percent_of_votes;
const formattedOutput = formatNumber(roundedOutput);
document.getElementById('balance').textContent = formattedOutput;
document.getElementById('percent').textContent = roundedPercent.toString();
balance = output;
});
} catch (error) {
console.error('Error connecting wallet:', error);
alert('Error connecting wallet. Check the console for details.');
return;
}
document.getElementById('signMessageForm').addEventListener('submit', async function(event) {
event.preventDefault();
const vote = document.getElementById('vote').value.trim(); // Get the value of the vote input field
// Ensure the vote is not empty
if (!vote) {
alert('Please enter your vote.');
return;
}
// Encode the message as a buffer-like object
const messageUint8Array = new TextEncoder().encode(vote);
// Request signature from Phantom
try {
const { public_key, signature } = await solana.request({
method: 'signMessage',
params: {
message: messageUint8Array,
display: "utf8"
},
});
const url = 'http://localhost:5000/vote'; // Update the URL as needed
// Convert signature to readable format
const sig = base58.encode(signature);
console.log(sig);
window.location.href = `/vote?message=${encodeURIComponent(vote)}&signature=${encodeURIComponent(sig)}&walletAddress=${encodeURIComponent(solana.publicKey.toBase58())}&votes=${encodeURIComponent(balance)}&percent=${encodeURIComponent((balance / supply) * 100)}`
} catch (error) {
console.error('Error submitting vote:', error);
alert('Error submitting vote. Check the console for details.');
}
});
});

77
templates/404.html Normal file
View File

@ -0,0 +1,77 @@
<!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>404 | 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 data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<li class="nav-item"><a class="nav-link" href="https://nathan.woodburn.au">Home</a></li>
</ul>
</div>
</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-md-10 text-center"><img class="img-fluid w-100" src="assets/img/illustrations/404.svg"></div>
<div class="col text-center">
<h2 class="display-3 fw-bold mb-4">Page Not Found</h2>
<p class="fs-4 text-muted">Fusce adipiscing sit, torquent porta pulvinar.</p>
</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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
.no-a-display {
text-decoration: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

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="M95.85,121.73c-27.66,4.91-47.21,29-44,55.43,1.71,14,8.64,26.43,26.47,30,47.3,9.51,225.85,45.31,260.93-16.72,27.35-48.34,11.05-81.81-14.35-102.76s-78-16.6-121.53-2.26C171,96.11,146.93,112.65,95.85,121.73Z" fill="#e6e6e6" opacity="0.3"></path><ellipse cx="205.6" cy="245.02" rx="161.02" ry="11.9" fill="#e6e6e6" opacity="0.45"></ellipse><circle cx="115.32" cy="60.66" r="19.14" fill="#ffd200"></circle><circle cx="115.32" cy="60.66" r="36.49" fill="#ffd200" opacity="0.15"></circle><circle cx="195.69" cy="193.3" r="51.17" fill="#24285b"></circle><circle cx="195.69" cy="193.3" r="32.21" fill="#fff"></circle><path d="M133.91,205.51c3.39,0,4.61,1.22,4.61,4.6v5.83c0,3.39-1,4.61-4.61,4.61h-9.35V238.3c0,3.38-1.22,4.6-4.61,4.6h-6.64c-3.38,0-4.6-1.22-4.6-4.6V220.55H64c-3.38,0-4.6-1.22-4.6-4.61v-4.88A9,9,0,0,1,61,205.51l45-55.83a8.07,8.07,0,0,1,6.5-3.25H120c3.39,0,4.61.95,4.61,4.61v54.47Zm-25.2-36.32L79.3,205.51h29.41Z" fill="#68e1fd"></path><path d="M327.42,205.51c3.39,0,4.61,1.22,4.61,4.6v5.83c0,3.39-.95,4.61-4.61,4.61h-9.35V238.3c0,3.38-1.22,4.6-4.61,4.6h-6.64c-3.38,0-4.6-1.22-4.6-4.6V220.55H257.5c-3.38,0-4.6-1.22-4.6-4.61v-4.88a9,9,0,0,1,1.62-5.55l45-55.83a8.07,8.07,0,0,1,6.5-3.25h7.45c3.39,0,4.61.95,4.61,4.61v54.47Zm-25.2-36.32-29.41,36.32h29.41Z" fill="#68e1fd"></path><path d="M307.35,135.53s-8.51-2.32-10.36-10.25c0,0,13.19-2.66,13.57,10.95Z" fill="#68e1fd" opacity="0.58"></path><path d="M308.4,134.69s-5.95-9.41-.72-18.2c0,0,10,6.37,5.58,18.22Z" fill="#68e1fd" opacity="0.73"></path><path d="M309.93,134.7s3.14-9.94,12.65-11.82c0,0,1.78,6.45-6.16,11.84Z" fill="#68e1fd"></path><polygon points="303.76 134.47 305.48 146.28 316.35 146.33 317.95 134.53 303.76 134.47" fill="#24285b"></polygon><path d="M201.88,126.11s1.4,6.75.79,11.43a3.46,3.46,0,0,1-3.91,3c-2.35-.34-5.43-1.48-6.62-5l-2.76-5.75a6.21,6.21,0,0,1,1.93-6.9C194.85,119.63,201.23,122,201.88,126.11Z" fill="#f4a28c"></path><polygon points="189.88 130.8 188.98 153.41 201.47 153.01 197.11 136.72 189.88 130.8" fill="#f4a28c"></polygon><path d="M200.21,126.59a27.36,27.36,0,0,1-6.37.27,5.76,5.76,0,0,1,.74,6.27,4.68,4.68,0,0,1-5.4,2.52l-.93-8.82a7,7,0,0,1,2.8-6.64,24.34,24.34,0,0,1,2.77-1.79c2.41-1.32,6.32-.07,8.39-2.05a1.67,1.67,0,0,1,2.75.78c.72,2.63.74,6.9-2.71,8.79A6.36,6.36,0,0,1,200.21,126.59Z" fill="#24285b"></path><path d="M195.29,132.84s-.36-2.64-2.32-2.2-1.46,4.25,1.28,4.28Z" fill="#f4a28c"></path><path d="M202.54,130.41l2.23,2.41a1.11,1.11,0,0,1-.49,1.81l-2.56.8Z" fill="#f4a28c"></path><path d="M198.22,140.25a8.24,8.24,0,0,1-4.3-1.92s.66,4.09,5.66,7.61Z" fill="#ce8172" opacity="0.31"></path><path d="M189,153.41l12.49-.4s19.62-3.33,26.43,12.67S226,204.29,226,204.29,218.9,228.16,189,225.52c0,0-24.87-1.44-27.68-35.53a33.25,33.25,0,0,0-.68-4.42C159.5,180.24,158.84,164.07,189,153.41Z" fill="#68e1fd"></path><path d="M172.27,174.65s6.66.73,15.91,16.22,27.41,9.81,37.65-1.65l-19,25.14-21.28-1.71-11.57-30.8Z" opacity="0.08"></path><rect x="242.24" y="139" width="3.69" height="10.74" transform="translate(-16.97 33.59) rotate(-7.61)" fill="#ffd200"></rect><rect x="242.24" y="139" width="3.69" height="10.74" transform="translate(-16.97 33.59) rotate(-7.61)" opacity="0.08"></rect><rect x="242.52" y="146.67" width="5.67" height="13.79" transform="translate(-18.17 33.84) rotate(-7.61)" fill="#24285b"></rect><path d="M240,116.77a12.1,12.1,0,1,0,13.6,10.39A12.1,12.1,0,0,0,240,116.77Zm2.74,20.46a8.55,8.55,0,1,1,7.33-9.6A8.56,8.56,0,0,1,242.78,137.23Z" fill="#ffd200"></path><circle cx="241.67" cy="128.8" r="8.59" fill="#fff"></circle><path d="M161,176.33a6.18,6.18,0,0,1,11.25-1.68,141.62,141.62,0,0,1,12,24.39c7.15,19.06,42.21,6.49,55.15-37.95l7.38,4.59s-10.18,57.72-51.16,59.84c0,0-25.58,5.18-33.35-26.21,0,0-2-5.91-2.12-9.25l-.54-3.76a31.69,31.69,0,0,1,1.32-9.87Z" fill="#68e1fd"></path><path d="M161,176.33a6.18,6.18,0,0,1,11.25-1.68,141.62,141.62,0,0,1,12,24.39c7.15,19.06,42.21,6.49,55.15-37.95l7.38,4.59s-10.18,57.72-51.16,59.84c0,0-25.58,5.18-33.35-26.21,0,0-2-5.91-2.12-9.25l-.54-3.76a31.69,31.69,0,0,1,1.32-9.87Z" fill="#fff" opacity="0.39"></path><path d="M241.27,162.21s.75-9.51,4.67-9.49,13.13,7.06-1.09,11.71Z" fill="#f4a28c"></path><path d="M343.49,89.6a6.76,6.76,0,0,0-6.76-6.76,6.59,6.59,0,0,0-1.09.09,9.1,9.1,0,0,0-8-4.8l-.33,0a10.82,10.82,0,1,0-21,0l-.33,0a9.12,9.12,0,1,0,0,18.23h31.63V96.3A6.77,6.77,0,0,0,343.49,89.6Z" fill="#e6e6e6"></path><path d="M80.7,156.51a5.8,5.8,0,0,0-5.8-5.8,7,7,0,0,0-.93.08,7.81,7.81,0,0,0-6.89-4.11H66.8a9.28,9.28,0,1,0-18,0h-.28a7.82,7.82,0,1,0,0,15.63H75.65v-.05A5.81,5.81,0,0,0,80.7,156.51Z" fill="#e6e6e6"></path></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,9 @@
document.addEventListener('DOMContentLoaded', function() {
var charts = document.querySelectorAll('[data-bss-chart]');
for (var chart of charts) {
// Create the chart
chart.chart = new Chart(chart, JSON.parse(chart.dataset.bssChart));
}
}, false);

View File

7
templates/assets/js/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
(function() {
"use strict"; // Start of use strict
var mainNav = document.querySelector('#mainNav');
if (mainNav) {
// Collapse Navbar
var collapseNavbar = function() {
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
if (scrollTop > 100) {
mainNav.classList.add("navbar-shrink");
} else {
mainNav.classList.remove("navbar-shrink");
}
};
// Collapse now if page is not at top
collapseNavbar();
// Collapse the navbar when page is scrolled
document.addEventListener("scroll", collapseNavbar);
}
})(); // End of use strict

96
templates/index.html Normal file
View File

@ -0,0 +1,96 @@
<!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>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">
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"name": "Vote | Nathan.Woodburn/",
"url": "https://vote.woodburn.au"
}
</script>
<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 data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<li class="nav-item"><a class="nav-link" href="https://nathan.woodburn.au">Home</a></li>
</ul>
</div>
</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">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>
<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"><input class="form-control" type="text" id="vote" name="vote" value="{{vote}}" placeholder="Your Vote"></div>
<div class="shadow-lg mb-3"><button class="btn btn-primary" type="submit">Vote</button></div>
</form>
</div>{{votes|safe}}
</div>
<div class="col-12 col-lg-10 mx-auto">
<div class="text-center position-relative"><img class="img-fluid" src="assets/img/illustrations/meeting.svg" style="width: 800px;"></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="assets/js/bundle.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="assets/js/startup-modern.js"></script>
</body>
</html>

99
templates/success.html Normal file
View File

@ -0,0 +1,99 @@
<!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>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 data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<li class="nav-item"><a class="nav-link" href="https://nathan.woodburn.au">Home</a></li>
</ul>
</div>
</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">Thank you for voting!</h1>
<p class="fs-5 text-muted mb-5">Your voice matters!</p>
</div>
<div class="table-responsive">
<table class="table table-dark">
<tbody>
<tr>
<td>Vote</td>
<td>{{vote}}</td>
</tr>
<tr>
<td>Vote Weight</td>
<td>{{percent}}% of total votes</td>
</tr>
<tr>
<td>Signature</td>
<td>{{signature}}</td>
</tr>
</tbody>
</table>
</div>{{votes|safe}}
</div>
<div class="col-12 col-lg-10 mx-auto">
<div class="text-center position-relative"><img class="img-fluid" src="assets/img/illustrations/meeting.svg" style="width: 800px;"></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>

22
webpack.config.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
path: __dirname + '/dist',
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
};