feat: Add initial code
All checks were successful
Build Docker / Build Image (push) Successful in 39s
41
.gitea/workflows/build.yml
Normal 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
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
|
||||
__pycache__/
|
||||
|
||||
data/
|
17
Dockerfile
Normal 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
184
main.py
Normal 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
18
package.json
Normal 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
@ -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
@ -0,0 +1,7 @@
|
||||
flask
|
||||
python-dotenv
|
||||
gunicorn
|
||||
requests
|
||||
apscheduler
|
||||
PyNaCl
|
||||
base58
|
43
server.py
Normal 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
@ -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
@ -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&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>
|
6
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
6
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
4
templates/assets/css/default.css
Normal file
@ -0,0 +1,4 @@
|
||||
.no-a-display {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
BIN
templates/assets/img/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
templates/assets/img/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
templates/assets/img/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
templates/assets/img/favicon-16x16.png
Normal file
After Width: | Height: | Size: 856 B |
BIN
templates/assets/img/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
templates/assets/img/favicon.png
Normal file
After Width: | Height: | Size: 44 KiB |
1
templates/assets/img/illustrations/404.svg
Normal 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 |
1
templates/assets/img/illustrations/meeting.svg
Normal file
After Width: | Height: | Size: 10 KiB |
9
templates/assets/js/bs-init.js
Normal 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);
|
||||
|
0
templates/assets/js/bundle.js
Normal file
7
templates/assets/js/chart.min.js
vendored
Normal file
26
templates/assets/js/startup-modern.js
Normal 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
@ -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&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 <span id="balance">0</span> 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
@ -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&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
@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|