initial checkin with timed order ui
This commit is contained in:
42
build.sh
Executable file
42
build.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# build.sh sets these env vars as output:
|
||||||
|
# DEXORDER_WEB_VERSION
|
||||||
|
# DEXORDER_WEB_IMAGE
|
||||||
|
|
||||||
|
DOCKER=docker
|
||||||
|
CONTAINER=dexorder-web
|
||||||
|
|
||||||
|
SCRIPT_HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||||
|
WEB_HOME=${WEB_HOME:-$SCRIPT_HOME}
|
||||||
|
|
||||||
|
cd $WEB_HOME || exit 1
|
||||||
|
|
||||||
|
DIRTY="$( git status | grep "Changes " )"
|
||||||
|
if [ "$DIRTY" != "" ] && [ "$1" != "dev" ]; then
|
||||||
|
echo "$WEB_HOME has uncommited changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEXORDER_WEB_VERSION="$( git log --oneline | head -1 | cut -d ' ' -f 1 )"
|
||||||
|
if [ "$1" == "dev" ]; then
|
||||||
|
shift
|
||||||
|
DEXORDER_WEB_VERSION="dev`date +%Y%m%d%H%M%S`"
|
||||||
|
fi
|
||||||
|
DEXORDER_WEB_IMAGE="gcr.io/cointrader-211623/$CONTAINER:$DEXORDER_WEB_VERSION"
|
||||||
|
echo $DEXORDER_WEB_IMAGE
|
||||||
|
|
||||||
|
echo "const dexorderWebVersion='$DEXORDER_WEB_VERSION'" > "$WEB_HOME/public/version.js"
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
DOCKERFILE=deploy/Dockerfile
|
||||||
|
$DOCKER build -f "$DOCKERFILE" -t "$DEXORDER_WEB_IMAGE" .
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
echo 'container build failed'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DEXORDER_WEB_VERSION
|
||||||
|
export DEXORDER_WEB_IMAGE
|
||||||
|
|
||||||
|
echo $(date) build complete
|
||||||
14
deploy.sh
Executable file
14
deploy.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
source ./build.sh $@
|
||||||
|
# build.sh sets these env vars:
|
||||||
|
# DEXORDER_WEB_VERSION
|
||||||
|
# DEXORDER_WEB_IMAGE
|
||||||
|
|
||||||
|
$DOCKER push $DEXORDER_WEB_IMAGE > /dev/null
|
||||||
|
if [ $? != 0 ]; then
|
||||||
|
echo 'container push failed'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -e "s|\$DEXORDER_WEB_IMAGE|$DEXORDER_WEB_IMAGE|g" k8s/deployment.yaml | kubectl --context=la apply -f -
|
||||||
5
deploy/Dockerfile
Normal file
5
deploy/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:stable-alpine
|
||||||
|
RUN sed -i '1idaemon off;' /etc/nginx/nginx.conf
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY dist /app
|
||||||
|
CMD ["nginx"]
|
||||||
25
deploy/nginx.conf
Normal file
25
deploy/nginx.conf
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_types text/plain text/xml application/javascript text/css;
|
||||||
|
|
||||||
|
root /app;
|
||||||
|
|
||||||
|
# normal routes
|
||||||
|
# serve given url and default to index.html if not found
|
||||||
|
# e.g. /, /user and /foo/bar will return index.html
|
||||||
|
location / {
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
try_files $uri $uri/index.html /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# files
|
||||||
|
# for all routes matching a dot, check for files and return 404 if not found
|
||||||
|
# e.g. /file.js returns a 404 if not found
|
||||||
|
location ~ \.(?!html) {
|
||||||
|
add_header Cache-Control "public, max-age=2678400";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
index.html
Normal file
18
index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link rel="icon" href="/favicon.ico"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Dexorder</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/version.js"></script>
|
||||||
|
<script src="/generated.js"></script>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
19
jsconfig.json
Normal file
19
jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
13
k8s/certificate.yaml
Normal file
13
k8s/certificate.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: dexorder-trade
|
||||||
|
spec:
|
||||||
|
secretName: dexorder-trade-tls
|
||||||
|
commonName: dexorder.trade
|
||||||
|
dnsNames:
|
||||||
|
- dexorder.trade
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
kind: ClusterIssuer
|
||||||
30
k8s/deployment.yaml
Normal file
30
k8s/deployment.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: dexorder-web
|
||||||
|
labels:
|
||||||
|
app: dexorder-web
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: dexorder-web
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: dexorder-web
|
||||||
|
annotations:
|
||||||
|
prometheus.io/scrape: "false"
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: dexorder-web
|
||||||
|
image: $DEXORDER_WEB_IMAGE
|
||||||
|
ports:
|
||||||
|
- name: www
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 100M
|
||||||
23
k8s/ingress.yaml
Normal file
23
k8s/ingress.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: dexorder-web
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- secretName: dexorder-trade-tls
|
||||||
|
hosts:
|
||||||
|
- dexorder.trade
|
||||||
|
rules:
|
||||||
|
- host: dexorder.trade
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: dexorder-web
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
15
k8s/le.yaml
Normal file
15
k8s/le.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
email: tim.olson.crypto@gmail.com
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-privkey-prod
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: nginx
|
||||||
16
k8s/service.yaml
Normal file
16
k8s/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: dexorder-web
|
||||||
|
labels:
|
||||||
|
app: dexorder-web
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: dexorder-web
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "dexorder",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "7.0.96",
|
||||||
|
"core-js": "^3.29.0",
|
||||||
|
"ethers": "^6.7.1",
|
||||||
|
"pinia": "^2.0.0",
|
||||||
|
"roboto-fontface": "*",
|
||||||
|
"vue": "^3.2.0",
|
||||||
|
"vue-router": "^4.0.0",
|
||||||
|
"vuetify": "^3.0.0",
|
||||||
|
"webfontloader": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"eslint": "^8.37.0",
|
||||||
|
"eslint-plugin-vue": "^9.3.0",
|
||||||
|
"sass": "^1.60.0",
|
||||||
|
"vite": "^4.2.0",
|
||||||
|
"vite-plugin-vuetify": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
91
public/generated.js
Normal file
91
public/generated.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
known_chains = [
|
||||||
|
{
|
||||||
|
name: 'Arbitrum One',
|
||||||
|
id: 42161,
|
||||||
|
icon: null,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
_known_tokens = {
|
||||||
|
42161: [ // Arbitrum
|
||||||
|
{
|
||||||
|
name: 'Wrapped Ether',
|
||||||
|
symbol: 'WETH',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tether USD',
|
||||||
|
symbol: 'USDT',
|
||||||
|
decimals: 6,
|
||||||
|
icon: null,
|
||||||
|
address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'USD Coin',
|
||||||
|
symbol: 'USDC',
|
||||||
|
decimals: 6,
|
||||||
|
icon: null,
|
||||||
|
address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Wrapped Bitcoin',
|
||||||
|
symbol: 'WBTC',
|
||||||
|
decimals: 8,
|
||||||
|
icon: null,
|
||||||
|
address: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DAI Stablecoin',
|
||||||
|
symbol: 'DAI',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Uniswap',
|
||||||
|
symbol: 'UNI',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Chainlink Token',
|
||||||
|
symbol: 'LINK',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0xf97f4df75117a78c1A5a0DBb814Af92458539FB4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TrueUSD',
|
||||||
|
symbol: 'TUSD',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Lido DAO Token',
|
||||||
|
symbol: 'LDO',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Arbitrum',
|
||||||
|
symbol: 'ARB',
|
||||||
|
decimals: 18,
|
||||||
|
icon: null,
|
||||||
|
address: '0x912CE59144191C1204E64559FE8253a0e49E6548',
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
known_tokens = {}
|
||||||
|
|
||||||
|
for( const chainId in _known_tokens ) {
|
||||||
|
known_tokens[chainId] = {}
|
||||||
|
for( const info of _known_tokens[chainId] )
|
||||||
|
known_tokens[chainId][info.address] = info
|
||||||
|
}
|
||||||
6
src/App.vue
Normal file
6
src/App.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<router-view/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
42
src/blockchain/abi.js
Normal file
42
src/blockchain/abi.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const erc20Abi = [
|
||||||
|
'function name() view returns (string)',
|
||||||
|
'function symbol() view returns (string)',
|
||||||
|
'function decimals() view returns (uint8)',
|
||||||
|
'function totalSupply() view returns (uint256)',
|
||||||
|
'function balanceOf(address) view returns (uint256)',
|
||||||
|
'function transfer(address,uint256) returns (bool)',
|
||||||
|
'function transferFrom(address,address,uint256) returns (bool)',
|
||||||
|
'function approve(address,uint256) returns (bool success)',
|
||||||
|
'function allowance(address,address) view returns (uint256)',
|
||||||
|
'event Transfer(address indexed,address indexed,uint256)',
|
||||||
|
'event Approval(address indexed,address indexed,uint256)',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const TimedOrderSpec = '(' +
|
||||||
|
'address tokenIn,' +
|
||||||
|
'address tokenOut,' +
|
||||||
|
'uint24 fee,' +
|
||||||
|
'uint32 deadline,' +
|
||||||
|
'uint32 leeway,' +
|
||||||
|
'uint160 minSqrtPriceX96,' +
|
||||||
|
'uint160 maxSqrtPriceX96,' +
|
||||||
|
'uint8 numTranches,' +
|
||||||
|
'uint256 amount,' +
|
||||||
|
'bool amountIsInput' +
|
||||||
|
')'
|
||||||
|
|
||||||
|
export const timedOrderAbi = [
|
||||||
|
'event TimedOrderCreated (address owner, uint64 index, Spec spec)',
|
||||||
|
'event TimedOrderFilled (address owner, uint64 index, uint256 amountIn, uint256 amountOut)',
|
||||||
|
'event TimedOrderCompleted (address owner, uint64 index)',
|
||||||
|
'event TimedOrderError (address owner, uint64 index, string reason)',
|
||||||
|
`timedOrder(${TimedOrderSpec}) returns (uint64 index)`,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
export const abi = {
|
||||||
|
'ERC20': erc20Abi,
|
||||||
|
'TimedOrder': timedOrderAbi,
|
||||||
|
}
|
||||||
|
|
||||||
112
src/blockchain/wallet.js
Normal file
112
src/blockchain/wallet.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {ethers} from "ethers";
|
||||||
|
import {useStore} from "@/store/store";
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
|
||||||
|
class Wallet {
|
||||||
|
|
||||||
|
connected = false
|
||||||
|
_provider = null
|
||||||
|
_signer = null
|
||||||
|
_onSignerInits = {}
|
||||||
|
_enabled = true // prevents tight loops on errors
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this._enabled = true
|
||||||
|
if( !this.connected )
|
||||||
|
await this.signer()
|
||||||
|
}
|
||||||
|
|
||||||
|
async provider() {
|
||||||
|
if (this._provider === null && this._enabled) {
|
||||||
|
console.log('creating provider')
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum, store.chain.id)
|
||||||
|
await provider.getNetwork() // this invokes a check on having the correct network connected
|
||||||
|
this._provider = provider
|
||||||
|
console.log('wallet connected')
|
||||||
|
}
|
||||||
|
return this._provider
|
||||||
|
}
|
||||||
|
|
||||||
|
async signer() {
|
||||||
|
/*
|
||||||
|
if( !store.geo.approved ) {
|
||||||
|
console.log('not approved')
|
||||||
|
this._connected(false)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const provider = await this.provider()
|
||||||
|
if( provider === null ) {
|
||||||
|
console.log('provider null')
|
||||||
|
this._connected(false)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const signer = await provider.getSigner()
|
||||||
|
if( this._signer?.address !== signer.address ) {
|
||||||
|
console.log('new signer', signer.address)
|
||||||
|
this._signer = signer
|
||||||
|
for (const key in this._onSignerInits) {
|
||||||
|
try {
|
||||||
|
await this._onSignerInits[key](signer)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log('during onSignerInit', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('wallet connected')
|
||||||
|
}
|
||||||
|
this._connected(true)
|
||||||
|
return signer
|
||||||
|
}
|
||||||
|
|
||||||
|
async address() {
|
||||||
|
const signer = await this.signer()
|
||||||
|
if( signer === null )
|
||||||
|
return null
|
||||||
|
return await signer.getAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
_connected(value) {
|
||||||
|
this.connected = value
|
||||||
|
store.$patch({wallet:{connected:value}})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSignerInit(id, cb) {
|
||||||
|
this._onSignerInits[id] = cb
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handler = {
|
||||||
|
get(target, prop, proxy) {
|
||||||
|
const got = Reflect.get(target, prop, proxy);
|
||||||
|
if( typeof got !== 'function' ) {
|
||||||
|
return got
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return async function (...args) {
|
||||||
|
try {
|
||||||
|
return await got.apply(target, args)
|
||||||
|
}
|
||||||
|
catch (x) {
|
||||||
|
target._connected(false)
|
||||||
|
target._enabled = false
|
||||||
|
if( x.code === 'NETWORK_ERROR' ) {
|
||||||
|
store.error('Wrong Blockchain', 'Your wallet is connected to a different blockchain. Please select '+import.meta.env.VITE_CHAIN_NAME+' in your wallet.')
|
||||||
|
console.log('wallet network error', x)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('wallet error')
|
||||||
|
throw x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default new Proxy(new Wallet(), handler)
|
||||||
22
src/components/BuySellToggle.vue
Normal file
22
src/components/BuySellToggle.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<v-btn :text="modelValue ? 'Buy' : 'Sell'" :color="modelValue ? 'green' : 'red'"
|
||||||
|
variant="outlined" size="x-large" @click="toggle"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useStore} from "@/store/store";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
const props = defineProps(['modelValue'])
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
emit('update:modelValue', !props.modelValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "src/styles/vars" as *;
|
||||||
|
|
||||||
|
</style>
|
||||||
75
src/components/HelloWorld.vue
Normal file
75
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="fill-height">
|
||||||
|
<v-responsive class="align-center text-center fill-height">
|
||||||
|
<v-img height="300" src="@/assets/logo.svg" />
|
||||||
|
|
||||||
|
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||||
|
|
||||||
|
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||||
|
|
||||||
|
<div class="py-14" />
|
||||||
|
|
||||||
|
<v-row class="d-flex align-center justify-center">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
href="https://vuetifyjs.com/components/all/"
|
||||||
|
min-width="164"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-view-dashboard"
|
||||||
|
size="large"
|
||||||
|
start
|
||||||
|
/>
|
||||||
|
|
||||||
|
Components
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||||
|
min-width="228"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
size="x-large"
|
||||||
|
target="_blank"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-speedometer"
|
||||||
|
size="large"
|
||||||
|
start
|
||||||
|
/>
|
||||||
|
|
||||||
|
Get Started
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn
|
||||||
|
href="https://community.vuetifyjs.com/"
|
||||||
|
min-width="164"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
variant="text"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-account-group"
|
||||||
|
size="large"
|
||||||
|
start
|
||||||
|
/>
|
||||||
|
|
||||||
|
Community
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-responsive>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
40
src/components/PairEntry.vue
Normal file
40
src/components/PairEntry.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<token-choice v-model="modelValue.tokenA" class="token-choice mb-1">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-btn :text="modelValue.buy ? 'Buy' : 'Sell'" :color="modelValue.buy ? 'green' : 'red'"
|
||||||
|
variant="outlined" @click="modelValue.buy=!modelValue.buy" class="bs-button"/>
|
||||||
|
</template>
|
||||||
|
</token-choice>
|
||||||
|
<token-choice v-model="modelValue.tokenB" class="token-choice">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-btn :text="!modelValue.buy ? 'Buy' : 'Sell'" :color="!modelValue.buy ? 'green' : 'red'"
|
||||||
|
variant="outlined" @click="modelValue.buy=!modelValue.buy" class="bs-button"/>
|
||||||
|
</template>
|
||||||
|
</token-choice>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useStore} from "@/store/store";
|
||||||
|
import TokenChoice from "@/components/TokenChoice.vue";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
|
||||||
|
// {
|
||||||
|
// tokenA, tokenB, buy
|
||||||
|
// }
|
||||||
|
|
||||||
|
const props = defineProps(['modelValue'])
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "src/styles/vars" as *;
|
||||||
|
.token-choice {
|
||||||
|
width: 16em;
|
||||||
|
}
|
||||||
|
.bs-button {
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
177
src/components/TimedOrderEntry.vue
Normal file
177
src/components/TimedOrderEntry.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<v-card title="DCA / TWAP" subtitle="Split order across time" class="order-card" elevation="4">
|
||||||
|
<v-card-text>
|
||||||
|
<pair-entry v-model="pair"/>
|
||||||
|
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
|
||||||
|
:model-value="amount" :rules="[validateRequired,validateAmount]" class="amount">
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn @click="amountIsBase=!amountIsBase" variant="outlined">
|
||||||
|
{{ amountIsBase ? pair.tokenA.symbol : pair.tokenB.symbol }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-btn :text="amountIsTotal ? 'Total' : 'Per Tranche'" variant="outlined"
|
||||||
|
@click="amountIsTotal=!amountIsTotal" class="total"/>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1"
|
||||||
|
:model-value="tranches" :rules="[validateRequired]">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-btn class="split-into" variant="outlined" @click="amountIsTotal=!amountIsTotal">
|
||||||
|
{{ amountIsTotal ? 'Split into' : 'Times' }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field type="number" variant="outlined" :min="1" v-model="interval" class="interval"
|
||||||
|
:label="intervalIsTotal ? 'Complete within' : 'Time between tranches'">
|
||||||
|
<!-- <template v-slot:append>APART</template>-->
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within"
|
||||||
|
@click="intervalIsTotal=!intervalIsTotal"/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" :text="timeUnits[timeUnitIndex]" @click="toggleTimeUnits" class="time-units"/>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field v-model="limitPrice" :label="(limitIsMinimum?'Minimum':'Maximum')+' Price'" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||||
|
clearable :rules="[validateAmount, validateMin]">
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||||
|
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
<!--
|
||||||
|
<v-text-field v-model="minPrice" label="Minimum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||||
|
clearable :rules="[validateAmount, validateMin]">
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||||
|
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field v-model="maxPrice" label="Maximum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||||
|
clearable :rules="[validateAmount, validateMax]">
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||||
|
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
||||||
|
<v-btn variant="outlined" color="red">Cancel</v-btn>
|
||||||
|
<v-btn variant="flat" color="green">Place Order</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useStore} from "@/store/store";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import PairEntry from "@/components/PairEntry.vue";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
const pair = ref({tokenA: s.tokens[0], tokenB: s.tokens[1], buy: true})
|
||||||
|
const amount = ref(1)
|
||||||
|
const amountIsBase = ref(false)
|
||||||
|
const amountIsTotal = ref(true)
|
||||||
|
const tranches = ref(3)
|
||||||
|
const inverted = ref(false)
|
||||||
|
const minPrice = ref(null)
|
||||||
|
const maxPrice = ref(null)
|
||||||
|
const limitPrice = ref(null)
|
||||||
|
const interval = ref(10)
|
||||||
|
const intervalIsTotal = ref(true)
|
||||||
|
const timeUnits = ['minutes', 'hours', 'days']
|
||||||
|
const timeUnitIndex = ref(1)
|
||||||
|
|
||||||
|
const limitIsMinimum = computed(()=>!(pair.value.buy ^ inverted.value))
|
||||||
|
|
||||||
|
|
||||||
|
function toggleTimeUnits() {
|
||||||
|
timeUnitIndex.value++
|
||||||
|
if( timeUnitIndex.value >= timeUnits.length )
|
||||||
|
timeUnitIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function isEmpty(v) {
|
||||||
|
return v === null || typeof v === 'string' && v.trim() === ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function validateRequired(v) {
|
||||||
|
if( isEmpty(v) )
|
||||||
|
return 'Required'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function validateAmount(v) {
|
||||||
|
if( isEmpty(v) )
|
||||||
|
return true
|
||||||
|
const floatRegex = /^-?\d*(?:[.,]\d*?)?$/
|
||||||
|
if (!floatRegex.test(v))
|
||||||
|
return 'Amount must be a number'
|
||||||
|
if (parseFloat(v) <= 0)
|
||||||
|
return 'Amount must be positive'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMax(v) {
|
||||||
|
console.log('validate max',v,isEmpty(v),minPrice,isEmpty(minPrice))
|
||||||
|
if( !isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value) )
|
||||||
|
return 'Must be greater than the minimum price'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateMin(v) {
|
||||||
|
console.log('validate min',v,isEmpty(v),maxPrice,isEmpty(maxPrice))
|
||||||
|
if( !isEmpty(maxPrice.value) && !isEmpty(v) && parseFloat(v) > parseFloat(maxPrice.value) )
|
||||||
|
return 'Must be less than the maximum price'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/styles/vars" as *;
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
width: 23em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
width: 9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-into {
|
||||||
|
width: 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-input {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval {
|
||||||
|
//width: 18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.within {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-units {
|
||||||
|
width: 8em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
src/components/TokenChip.vue
Normal file
18
src/components/TokenChip.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<v-chip>
|
||||||
|
<!-- <v-avatar start :image="token.icon === null ? '' : token.icon"/>-->
|
||||||
|
{{token.symbol}} {{token.name}}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useStore} from "@/store/store";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
const props = defineProps(['token'])
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "src/styles/vars" as *;
|
||||||
|
</style>
|
||||||
105
src/components/TokenChoice.vue
Normal file
105
src/components/TokenChoice.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<v-combobox :items="s.tokens" :auto-select-first="true"
|
||||||
|
item-title="symbol"
|
||||||
|
:filter-keys="['raw.name','raw.symbol','raw.address']"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:error-messages="errors"
|
||||||
|
:loading="loading"
|
||||||
|
:hide-selected="true"
|
||||||
|
:label="label"
|
||||||
|
v-bind="modelValue" @update:modelValue="updateValue"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend><slot name="prepend"/></template>
|
||||||
|
<template v-slot:item="{props,item}">
|
||||||
|
<v-list-item v-bind="props" :prepend-avatar="item.raw.image===null?'':item.raw.image" :title="item.raw.symbol" :subtitle="item.raw.name"/>
|
||||||
|
</template>
|
||||||
|
</v-combobox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useStore as useStore2} from "@/store/store";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {ethers} from "ethers";
|
||||||
|
|
||||||
|
const s = useStore2()
|
||||||
|
const props = defineProps(['modelValue', 'label'])
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const loading = ref(false)
|
||||||
|
const errors = ref([])
|
||||||
|
|
||||||
|
function good() {
|
||||||
|
errors.value = []
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(msg) {
|
||||||
|
errors.value = [msg]
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValue(v) {
|
||||||
|
if( v === null ) {
|
||||||
|
error('Type a token name, symbol, or address')
|
||||||
|
}
|
||||||
|
else if( typeof v !== 'string' ) {
|
||||||
|
emit('update:modelValue', v);
|
||||||
|
good()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const addr = ethers.getAddress(v);
|
||||||
|
const found = s.tokens[addr]
|
||||||
|
if(found !== undefined ) {
|
||||||
|
good()
|
||||||
|
emit('update:modelValue', found)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errors.value = []
|
||||||
|
loading.value = true;
|
||||||
|
addExtraToken(addr).then((info)=>{
|
||||||
|
if(loading.value && errors.value.length===0) {
|
||||||
|
good()
|
||||||
|
emit('update:modelValue', info)
|
||||||
|
}
|
||||||
|
}).catch((e)=>{
|
||||||
|
console.log(e)
|
||||||
|
error(`${addr} is not a valid ERC20 token`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
error('Invalid token or address')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {ethers} from "ethers";
|
||||||
|
import wallet from "@/blockchain/wallet.js";
|
||||||
|
import {erc20Abi} from "@/blockchain/abi.js";
|
||||||
|
import {useStore} from "@/store/store.js";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
|
||||||
|
async function addExtraToken(addr) {
|
||||||
|
const token = new ethers.Contract(addr, erc20Abi, await wallet.provider())
|
||||||
|
const symbol = await token.symbol()
|
||||||
|
const decimals = Number(await token.decimals())
|
||||||
|
const info = {name:`${symbol} (${addr})`, symbol, decimals, address:addr}
|
||||||
|
s.$patch((state)=>{
|
||||||
|
let extras = state.extraTokens[state.chain.id]
|
||||||
|
if( extras === undefined ) {
|
||||||
|
extras = {}
|
||||||
|
state.extraTokens[state.chain.id] = extras
|
||||||
|
}
|
||||||
|
extras[info.address] = info
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "src/styles/vars" as *;
|
||||||
|
|
||||||
|
</style>
|
||||||
11
src/layouts/default/AppBar.vue
Normal file
11
src/layouts/default/AppBar.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<v-app-bar flat>
|
||||||
|
<v-app-bar-title class="sc">
|
||||||
|
<v-icon icon="mdi-chart-line" />
|
||||||
|
DexOrder
|
||||||
|
</v-app-bar-title>
|
||||||
|
</v-app-bar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
11
src/layouts/default/Default.vue
Normal file
11
src/layouts/default/Default.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<default-bar />
|
||||||
|
<default-view />
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import DefaultBar from './AppBar.vue'
|
||||||
|
import DefaultView from './View.vue'
|
||||||
|
</script>
|
||||||
10
src/layouts/default/View.vue
Normal file
10
src/layouts/default/View.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<v-main>
|
||||||
|
<v-container>
|
||||||
|
<router-view />
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
26
src/main.js
Normal file
26
src/main.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* main.js
|
||||||
|
*
|
||||||
|
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import { registerPlugins } from '@/plugins'
|
||||||
|
import '@/styles/style.scss'
|
||||||
|
import {useStore} from "@/store/store.js";
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
registerPlugins(app)
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
s.chains = known_chains
|
||||||
|
s.chain = known_chains[0]
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
19
src/plugins/index.js
Normal file
19
src/plugins/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* plugins/index.js
|
||||||
|
*
|
||||||
|
* Automatically included in `./src/main.js`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import { loadFonts } from './webfontloader'
|
||||||
|
import vuetify from './vuetify'
|
||||||
|
import pinia from '../store'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
|
export function registerPlugins (app) {
|
||||||
|
loadFonts()
|
||||||
|
app
|
||||||
|
.use(vuetify)
|
||||||
|
.use(router)
|
||||||
|
.use(pinia)
|
||||||
|
}
|
||||||
35
src/plugins/vuetify.js
Normal file
35
src/plugins/vuetify.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* plugins/vuetify.js
|
||||||
|
*
|
||||||
|
* Framework documentation: https://vuetifyjs.com`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
import 'vuetify/styles'
|
||||||
|
|
||||||
|
// Composables
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
|
||||||
|
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||||
|
export default createVuetify({
|
||||||
|
theme: {
|
||||||
|
themes: {
|
||||||
|
light: {
|
||||||
|
colors: {
|
||||||
|
background: '#fffefd',
|
||||||
|
surface: '#fffefd',
|
||||||
|
primary: '#1A6CAB',
|
||||||
|
// secondary: '#59B8FF',
|
||||||
|
success: '#00CC33',
|
||||||
|
// info: '#fba92c',
|
||||||
|
warning: '#ffcc00',
|
||||||
|
error: '#CC0033',
|
||||||
|
},
|
||||||
|
dark: false,
|
||||||
|
variables: {},
|
||||||
|
},
|
||||||
|
// todo dark mode
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
17
src/plugins/webfontloader.js
Normal file
17
src/plugins/webfontloader.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* plugins/webfontloader.js
|
||||||
|
*
|
||||||
|
* webfontloader documentation: https://github.com/typekit/webfontloader
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function loadFonts () {
|
||||||
|
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
|
||||||
|
|
||||||
|
webFontLoader.load({
|
||||||
|
google: {
|
||||||
|
// families: ['Roboto:100,300,400,500,700,900&display=swap'],
|
||||||
|
// families: ['Marcellus','Marcellus SC'],
|
||||||
|
families: ['Victor Mono', 'Tektur'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
26
src/router/index.js
Normal file
26
src/router/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Composables
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: () => import('@/layouts/default/Default.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'Home',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (about.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(process.env.BASE_URL),
|
||||||
|
routes,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
4
src/store/index.js
Normal file
4
src/store/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Utilities
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
export default createPinia()
|
||||||
32
src/store/store.js
Normal file
32
src/store/store.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Utilities
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useStore = defineStore('app', {
|
||||||
|
state: () => ({
|
||||||
|
chain: {name:'Not Connected', id:0, icon:''},
|
||||||
|
wallet: {
|
||||||
|
connected: false,
|
||||||
|
},
|
||||||
|
errors: [],
|
||||||
|
extraTokens: {},
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
tokens: (s)=>{
|
||||||
|
const extras = s.extraTokens[s.chain.id]
|
||||||
|
return extras === undefined ? Object.values(known_tokens[s.chain.id])
|
||||||
|
: [...Object.values(known_tokens[s.chain.id]), ...Object.values(extras)]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
error(title, message) {
|
||||||
|
this.errors.push({title:title, message:message})
|
||||||
|
},
|
||||||
|
closeError(title, message) {
|
||||||
|
console.log('closing error', title, message)
|
||||||
|
const result = []
|
||||||
|
this.errors.forEach((i)=>{if(i.title!==title && i.message!==message) result.push(i)})
|
||||||
|
this.errors = result
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
14
src/styles/settings.scss
Normal file
14
src/styles/settings.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* src/styles/settings.scss
|
||||||
|
*
|
||||||
|
* Configures SASS variables and Vuetify overwrites
|
||||||
|
*/
|
||||||
|
|
||||||
|
@use "src/styles/vars" as v;
|
||||||
|
|
||||||
|
// https://vuetifyjs.com/features/sass-variables/`
|
||||||
|
@forward 'vuetify/settings' with (
|
||||||
|
//$variable: false,
|
||||||
|
$body-font-family: v.$body-font-family,
|
||||||
|
$heading-font-family: v.$heading-font-family,
|
||||||
|
);
|
||||||
26
src/styles/style.scss
Normal file
26
src/styles/style.scss
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@use "/src/styles/vars" as v;
|
||||||
|
|
||||||
|
//.app {
|
||||||
|
// .v-btn {
|
||||||
|
// font-family: v.$heading-font-family;
|
||||||
|
// text-transform: none;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .v-card-title {
|
||||||
|
// font-family: v.$heading-font-family;
|
||||||
|
// color: v.$blue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .title {
|
||||||
|
// font-family: v.$heading-font-family;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// .v-list-subheader {
|
||||||
|
// font-family: v.$heading-font-family;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
.sc {
|
||||||
|
font-family: v.$heading-font-family;
|
||||||
|
}
|
||||||
38
src/styles/vars.scss
Normal file
38
src/styles/vars.scss
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// these must also be set in vuetify.ts for the "theme"
|
||||||
|
|
||||||
|
$green: #00CC33;
|
||||||
|
$red: #CC0033;
|
||||||
|
$yellow: #ffcc00;
|
||||||
|
$blue: #0033CC;
|
||||||
|
$white: #fffefd; // just a touch green
|
||||||
|
$black: #000102;
|
||||||
|
|
||||||
|
$primary: $blue;
|
||||||
|
$primary-50: transparentize($primary,0.5);
|
||||||
|
$primary-25: transparentize($primary,0.75);
|
||||||
|
|
||||||
|
$theme: (
|
||||||
|
background: $white,
|
||||||
|
surface: $white,
|
||||||
|
primary: $blue,
|
||||||
|
//secondary: $sky,
|
||||||
|
success: $green,
|
||||||
|
info: $primary-25,
|
||||||
|
warning: $yellow,
|
||||||
|
error: $red,
|
||||||
|
);
|
||||||
|
|
||||||
|
$all-colors: (
|
||||||
|
white: $white,
|
||||||
|
black: $black,
|
||||||
|
blue: $blue,
|
||||||
|
green: $green,
|
||||||
|
yellow: $yellow,
|
||||||
|
red: $red,
|
||||||
|
);
|
||||||
|
|
||||||
|
$body-font-family: 'Victor Mono', monospace, sans-serif;
|
||||||
|
$heading-font-family: 'Tektur', sans-serif;
|
||||||
|
|
||||||
|
$sm-breakpoint: 600px;
|
||||||
|
$card-maxw: 30rem;
|
||||||
7
src/views/Home.vue
Normal file
7
src/views/Home.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<TimedOrder/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import TimedOrder from "@/components/TimedOrderEntry.vue";
|
||||||
|
</script>
|
||||||
41
vite.config.js
Normal file
41
vite.config.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Plugins
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue({
|
||||||
|
template: { transformAssetUrls }
|
||||||
|
}),
|
||||||
|
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
||||||
|
vuetify({
|
||||||
|
autoImport: true,
|
||||||
|
styles: {
|
||||||
|
configFile: 'src/styles/settings.scss',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
define: { 'process.env': {} },
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
'.js',
|
||||||
|
'.json',
|
||||||
|
'.jsx',
|
||||||
|
'.mjs',
|
||||||
|
'.ts',
|
||||||
|
'.tsx',
|
||||||
|
'.vue',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user