rm -r vue/
@ -1,4 +0,0 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
@ -1,5 +0,0 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
@ -1,18 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
}
|
23
vue/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -1,24 +0,0 @@
|
||||
# delphi.market
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<title>delphi.market</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#8787a4">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "delphi.market",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --port 4224",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"chart.js": "^4.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"pinia": "^2.1.7",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"s-ago": "^2.2.0",
|
||||
"vite": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/eslint-config-standard": "^6.1.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 449 KiB |
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 799 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.2 KiB |
@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 215 B |
@ -1,29 +0,0 @@
|
||||
{
|
||||
"short_name": "dm",
|
||||
"name": "Delphi Market",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"type": "image/jpeg",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"background_color": "#091833",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#8787a4",
|
||||
"description": "Prediction Market on Lightning",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/app_screenshot_001.png",
|
||||
"type": "image/png",
|
||||
"sizes": "750x1334",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<NavBar />
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSession } from './stores/session'
|
||||
const session = useSession()
|
||||
session.checkSession()
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable -->
|
||||
<!-- eslint wants to combine this <script> and <script setup> which breaks the code ... -->
|
||||
<script>
|
||||
import NavBar from './components/NavBar'
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { NavBar }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background-color: #091833;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#container {
|
||||
margin: 1em auto;
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 6.7 KiB |
@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="font-mono mb-3">
|
||||
Payment Required
|
||||
</div>
|
||||
<router-link v-if="invoice?.ConfirmedAt" :to="callbackUrl" class="label success font-mono">
|
||||
<div>Paid</div>
|
||||
<small v-if="redirectTimeout < 4">Redirecting in {{ redirectTimeout }} ...</small>
|
||||
</router-link>
|
||||
<div v-if="invoice && !invoice.ConfirmedAt && new Date(invoice.ExpiresAt) < new Date()" class="label error font-mono">
|
||||
<div>Expired</div>
|
||||
</div>
|
||||
<div v-if="notFound" class="label error font-mono">
|
||||
<div>Not Found</div>
|
||||
</div>
|
||||
<div v-if="invoice">
|
||||
<figure class="flex flex-col m-auto">
|
||||
<a class="m-auto" :href="'lightning:' + invoice.PaymentRequest">
|
||||
<img :src="'data:image/png;base64,' + invoice.Qr" />
|
||||
</a>
|
||||
<figcaption class="flex flex-row my-3 font-mono text-xs">
|
||||
<span class="w-[80%] text-ellipsis overflow-hidden">{{ invoice.PaymentRequest }}</span>
|
||||
<button @click.prevent="copy">{{ label }}</button>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<div class="grid text-muted text-xs">
|
||||
<span v-if="faucet" class="mx-3 my-1">faucet</span>
|
||||
<span v-if="faucet" class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||
<a href="https://faucet.mutinynet.com/" target="_blank">faucet.mutinynet.com</a>
|
||||
</span>
|
||||
<span class="mx-3 my-1">payment hash</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||
{{ invoice.Hash }}
|
||||
</span>
|
||||
<span class="mx-3 my-1">created at</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||
{{ invoice.CreatedAt }} ({{ ago(new Date(invoice.CreatedAt)) }})
|
||||
</span>
|
||||
<span class="mx-3 my-1">expires at</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||
{{ invoice.ExpiresAt }} ({{ ago(new Date(invoice.ExpiresAt)) }})
|
||||
</span>
|
||||
<span class="mx-3 my-1">sats</span><span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||
{{ invoice.Msats / 1000 }}
|
||||
</span>
|
||||
<span class="mx-3 my-1">description</span>
|
||||
<span class="text-ellipsis overflow-hidden font-mono me-3 my-1">
|
||||
<span v-if="invoice.DescriptionMarketId">
|
||||
<span v-if="invoice.Description">
|
||||
<span>{{ invoice.Description }}</span>
|
||||
<router-link :to="'/market/' + invoice.DescriptionMarketId + '/orders'">[market]</router-link>
|
||||
</span>
|
||||
<span v-else><empty></span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span v-if="invoice.Description">{{ invoice.Description }}</span>
|
||||
<span v-else><empty></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ago from 's-ago'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const invoice = ref(undefined)
|
||||
const redirectTimeout = ref(4)
|
||||
const notFound = ref(undefined)
|
||||
const label = ref('copy')
|
||||
let copyTimeout = null
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard?.writeText(invoice.value.PaymentRequest)
|
||||
label.value = 'copied'
|
||||
if (copyTimeout) clearTimeout(copyTimeout)
|
||||
copyTimeout = setTimeout(() => { label.value = 'copy' }, 1500)
|
||||
}
|
||||
|
||||
const callbackUrl = ref('/')
|
||||
let pollCount = 0
|
||||
let pollTimeout
|
||||
let redirectInterval
|
||||
const INVOICE_POLL = 2000
|
||||
const fetchInvoice = async () => {
|
||||
const url = window.origin + '/api/invoice/' + route.params.id
|
||||
const res = await fetch(url)
|
||||
notFound.value = res.status === 404
|
||||
if (res.status === 404) {
|
||||
return
|
||||
}
|
||||
const body = await res.json()
|
||||
if (body.Description) {
|
||||
// parse invoice description to show links
|
||||
const regexp = /\[market:(?<id>[0-9]+)\]/
|
||||
const m = body.Description.match(regexp)
|
||||
const marketId = m?.groups?.id
|
||||
if (marketId) {
|
||||
body.DescriptionMarketId = marketId
|
||||
body.Description = body.Description.replace(regexp, '')
|
||||
callbackUrl.value = '/market/' + marketId + '/orders'
|
||||
}
|
||||
}
|
||||
invoice.value = body
|
||||
if (new Date(invoice.value.ExpiresAt) < new Date()) {
|
||||
// invoice expired
|
||||
return
|
||||
}
|
||||
if (!invoice.value.ConfirmedAt) {
|
||||
// invoice not pad (yet?)
|
||||
pollTimeout = setTimeout(() => {
|
||||
pollCount++
|
||||
fetchInvoice()
|
||||
}, INVOICE_POLL)
|
||||
} else {
|
||||
// invoice paid
|
||||
// we check for pollCount > 0 to only redirect if invoice wasn't already paid when we visited the page
|
||||
if (pollCount > 0) {
|
||||
redirectInterval = setInterval(() => {
|
||||
redirectTimeout.value--
|
||||
if (redirectTimeout.value === 0) {
|
||||
clearInterval(redirectInterval)
|
||||
return router.push(callbackUrl.value)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
await fetchInvoice()
|
||||
|
||||
onUnmounted(() => { clearTimeout(pollTimeout); clearInterval(redirectInterval) })
|
||||
|
||||
const faucet = window.location.hostname === 'delphi.market' ? 'https://faucet.mutinynet.com' : ''
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
width: 256px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin: 0.75em auto;
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: auto;
|
||||
margin-bottom: 0.75em
|
||||
}
|
||||
|
||||
a.label {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.grid {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
</style>
|
@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<router-link v-if="success" to="/" class="label success font-mono">
|
||||
<div>Authenticated</div>
|
||||
<small>Redirecting in {{ redirectTimeout }} ...</small>
|
||||
</router-link>
|
||||
<div class="font-mono my-3">
|
||||
LNURL-auth
|
||||
</div>
|
||||
<div v-if="error" class="label error font-mono">
|
||||
<div>Authentication error</div>
|
||||
<small>{{ error }}</small>
|
||||
</div>
|
||||
<figure v-if="lnurl && qr" class="flex flex-col m-auto">
|
||||
<a class="m-auto" :href="'lightning:' + lnurl">
|
||||
<img :src="'data:image/png;base64,' + qr" />
|
||||
</a>
|
||||
<figcaption class="flex flex-row my-3 font-mono text-xs">
|
||||
<span class="w-[80%] text-ellipsis overflow-hidden">{{ lnurl }}</span>
|
||||
<button @click.prevent="copy">{{ label }}</button>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSession } from '@/stores/session'
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSession()
|
||||
|
||||
const qr = ref(null)
|
||||
const lnurl = ref(null)
|
||||
let interval = null
|
||||
const LOGIN_POLL = 2000
|
||||
const redirectTimeout = ref(3)
|
||||
const success = ref(null)
|
||||
const error = ref(null)
|
||||
const label = ref('copy')
|
||||
let copyTimeout = null
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard?.writeText(lnurl.value)
|
||||
label.value = 'copied'
|
||||
if (copyTimeout) clearTimeout(copyTimeout)
|
||||
copyTimeout = setTimeout(() => { label.value = 'copy' }, 1500)
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
await session.checkSession()
|
||||
if (session.isAuthenticated) {
|
||||
success.value = true
|
||||
clearInterval(interval)
|
||||
interval = setInterval(() => {
|
||||
if (--redirectTimeout.value === 0) {
|
||||
router.push('/')
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore 404 errors
|
||||
if (err.reason !== 'session not found') {
|
||||
console.error(err)
|
||||
error.value = err.reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
const s = await session.login()
|
||||
qr.value = s.qr
|
||||
lnurl.value = s.lnurl
|
||||
interval = setInterval(poll, LOGIN_POLL)
|
||||
}
|
||||
|
||||
await (async () => {
|
||||
// redirect to / if session already exists
|
||||
if (session.initialized) {
|
||||
if (session.isAuthenticated) return router.push('/')
|
||||
return login()
|
||||
}
|
||||
// else subscribe to changes
|
||||
return session.$subscribe(() => {
|
||||
if (session.initialized) {
|
||||
// for some reason, computed property is only updated when accessing the store directly
|
||||
// it is not updated inside the second argument
|
||||
if (session.isAuthenticated) return router.push('/')
|
||||
return login()
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
onUnmounted(() => { clearInterval(interval) })
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
width: 256px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin: 0.75em auto;
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
a.label {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<div class="my-3">
|
||||
<pre>
|
||||
_ _
|
||||
_ __ ___ __ _ _ __| | _____| |_
|
||||
| '_ ` _ \ / _` | '__| |/ / _ \ __|
|
||||
| | | | | | (_| | | | ( __/ |_
|
||||
|_| |_| |_|\__,_|_| |_|\_\___|\__|</pre>
|
||||
</div>
|
||||
<div class="font-mono mx-1">{{ market.Description }}</div>
|
||||
<div v-if="!!market.SettledAt" class="label info font-mono m-auto my-3">
|
||||
<div>Settled: {{ winShareDescription }}</div>
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
<header class="flex flex-row text-center justify-center pt-1">
|
||||
<nav>
|
||||
<StyledLink :to="'/market/' + marketId + '/form'">form</StyledLink>
|
||||
<StyledLink :to="'/market/' + marketId + '/orders'">orders</StyledLink>
|
||||
<StyledLink :to="'/market/' + marketId + '/stats'">stats</StyledLink>
|
||||
<StyledLink v-if="mine" :to="'/market/' + marketId + '/settings'"><i>settings</i></StyledLink>
|
||||
</nav>
|
||||
</header>
|
||||
<Suspense>
|
||||
<router-view :market="market" />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useSession } from '@/stores/session'
|
||||
import StyledLink from '@/components/StyledLink'
|
||||
|
||||
const session = useSession()
|
||||
const route = useRoute()
|
||||
const marketId = route.params.id
|
||||
|
||||
const market = ref(null)
|
||||
const mine = ref(false)
|
||||
const winShareDescription = ref(null)
|
||||
const url = '/api/market/' + marketId
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
market.value = body
|
||||
mine.value = market.value.Pubkey === session.pubkey
|
||||
})
|
||||
.then(() => {
|
||||
if (market.value.SettledAt) {
|
||||
winShareDescription.value = market.value.Shares.find(({ Win }) => Win).Description
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
nav>a {
|
||||
margin: 0 3px;
|
||||
}
|
||||
</style>
|
@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<form ref="form" class="flex flex-col mx-auto text-left" method="post" action="/api/market"
|
||||
@submit.prevent="submitForm">
|
||||
<label for="desc">event description</label>
|
||||
<textarea v-model="description" class="mb-1" id="desc" name="desc" type="text"></textarea>
|
||||
<label for="endDate">end date</label>
|
||||
<input v-model="endDate" class="mb-3" id="endDate" name="endDate" type="date" />
|
||||
<div class="flex flex-row justify-center">
|
||||
<button type="button" class="me-1" @click.prevent="$props.onCancel">cancel</button>
|
||||
<button type="submit">submit</button>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineProps(['onCancel'])
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref(null)
|
||||
const description = ref(null)
|
||||
const endDate = ref(null)
|
||||
const err = ref(null)
|
||||
|
||||
const parseEndDate = endDate => {
|
||||
const [yyyy, mm, dd] = endDate.split('-')
|
||||
return `${yyyy}-${mm}-${dd}T00:00:00.000Z`
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
const url = window.origin + '/api/market'
|
||||
const body = JSON.stringify({ description: description.value, endDate: parseEndDate(endDate.value) })
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
const resBody = await res.json()
|
||||
if (res.status !== 402) {
|
||||
err.value = `error: server responded with HTTP ${resBody.status}`
|
||||
return
|
||||
}
|
||||
const invoiceId = resBody.id
|
||||
router.push('/invoice/' + invoiceId)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
textarea {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<ul>
|
||||
<li class="my-3" v-for="market in markets" :key="market.id">
|
||||
<router-link :to="'/market/' + market.id + '/form'">{{ market.description }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<button v-if="!showForm" @click.prevent="toggleForm">+ create market</button>
|
||||
<div v-else class="flex flex-col justify-center">
|
||||
<MarketForm :onCancel="toggleForm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketForm from './MarketForm'
|
||||
import { ref } from 'vue'
|
||||
import { useSession } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
const markets = ref([])
|
||||
const showForm = ref(false)
|
||||
|
||||
// TODO only load markets once per session
|
||||
const url = window.origin + '/api/markets'
|
||||
await fetch(url).then(async r => {
|
||||
const body = await r.json()
|
||||
markets.value = body
|
||||
})
|
||||
|
||||
const toggleForm = () => {
|
||||
if (!session.isAuthenticated) {
|
||||
return router.push('/login')
|
||||
}
|
||||
showForm.value = !showForm.value
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
padding: 0 1em;
|
||||
}
|
||||
</style>
|
@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<div class="w-auto mt-3">
|
||||
<table>
|
||||
<thead>
|
||||
<th>description</th>
|
||||
<th class="hidden-sm">created at</th>
|
||||
<th>status</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<OrderRow :order="o" v-for="o in orders" :key="o.Id" @mouseover="() => mouseover(o.Id)" :selected="selected"
|
||||
:onMatchClick="onMatchClick" />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import OrderRow from './OrderRow.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const marketId = route.params.id
|
||||
|
||||
const selected = ref([])
|
||||
|
||||
const mouseover = (oid) => {
|
||||
const o2id = orders.value.find(i => i.OrderId === oid)?.Id
|
||||
if (o2id) {
|
||||
selected.value = [oid, o2id]
|
||||
} else {
|
||||
// reset selection
|
||||
selected.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const onMatchClick = (order) => {
|
||||
// redirect to form with prefilled inputs to match order
|
||||
if (order.side === 'BUY') {
|
||||
// match BUY YES with BUY NO and vice versa
|
||||
const stake = order.quantity * (100 - order.price)
|
||||
const certainty = (100 - order.price) / 100
|
||||
const share = order.ShareDescription === 'YES' ? 'NO' : 'YES'
|
||||
router.push(`/market/${marketId}/form?stake=${stake}&certainty=${certainty}&share=${share}&side=BUY`)
|
||||
}
|
||||
if (order.side === 'SELL') {
|
||||
// SELL YES -> BUY YES, SELL NO -> BUY NO
|
||||
const stake = order.quantity * order.price
|
||||
const certainty = order.price / 100
|
||||
const share = order.ShareDescription === 'YES' ? 'YES' : 'NO'
|
||||
router.push(`/market/${marketId}/form?stake=${stake}&certainty=${certainty}&share=${share}&side=BUY`)
|
||||
}
|
||||
}
|
||||
|
||||
const orders = ref([])
|
||||
const url = `/api/market/${marketId}/orders`
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
orders.value = body?.map(o => {
|
||||
// remove market column
|
||||
delete o.MarketId
|
||||
return o
|
||||
})
|
||||
})
|
||||
.catch(console.error)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
table {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
th {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hidden-sm {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div id="container2" class="mt-3 mx-3">
|
||||
<div class="grid grid-cols-2 my-3 items-center">
|
||||
<label>market settlement</label>
|
||||
<div class="grid grid-cols-2 my-3">
|
||||
<button :class="yesClass" class="label success font-mono mx-1" @click.prevent="() => click('YES')">YES</button>
|
||||
<button :class="noClass" class="label error font-mono mx-1" @click.prevent="() => click('NO')">NO</button>
|
||||
</div>
|
||||
<div class="col-span-2 mb-3" v-if="selected">
|
||||
<p><b>Are you sure you want to settle this market?</b></p>
|
||||
<p>
|
||||
This will cancel all pending orders and halt trading indefinitely.
|
||||
Users with winning shares will receive 100 sats per winning share. Users with losing shares receive nothing.
|
||||
</p>
|
||||
<p class="red"><b>You cannot undo this action.</b></p>
|
||||
</div>
|
||||
<button class="col-span-2" v-if="selected" @click.prevent="confirm" :disabled="!!market.SettledAt">confirm</button>
|
||||
</div>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
<div v-if="success" class="green text-center">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { computed, defineProps, ref } from 'vue'
|
||||
|
||||
const props = defineProps(['market'])
|
||||
const market = ref(props.market)
|
||||
|
||||
const err = ref(null)
|
||||
const success = ref(null)
|
||||
|
||||
const selected = ref(null)
|
||||
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
|
||||
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
|
||||
const click = (sel) => {
|
||||
selected.value = selected.value === sel ? null : sel
|
||||
}
|
||||
|
||||
const confirm = async () => {
|
||||
success.value = null
|
||||
err.value = null
|
||||
const sid = market.value.Shares.find(s => s.Description === selected.value).sid
|
||||
const url = '/api/market/' + market.value.Id + '/settle'
|
||||
const body = JSON.stringify({ sid })
|
||||
try {
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
if (res.status === 200) {
|
||||
success.value = 'Market settled'
|
||||
return
|
||||
}
|
||||
const resBody = await res.json()
|
||||
err.value = resBody.reason || `error: server responded with HTTP ${res.status}`
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
textarea {
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: auto;
|
||||
padding: 0.2em 1em
|
||||
}
|
||||
|
||||
.success.active {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error.active {
|
||||
background-color: #ff7386;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#container2 {
|
||||
max-width: 33vw;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
#container2 {
|
||||
max-width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#container2 {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
#container2 {
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div class="mt-3">
|
||||
<div class="mb-1">
|
||||
<span>YES: {{ typeof currentYes === "string" ? currentYes : (currentYes * 100).toFixed(2) + "%" }}</span>
|
||||
<span>NO: {{ typeof currentNo === "string" ? currentNo : (currentNo * 100).toFixed(2) + "%" }}</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span>Volume: {{ volume }} sats</span>
|
||||
</div>
|
||||
<Line :data="chartData" :options="chartOptions" :plugins="chartPlugins" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, LineElement, PointElement } from 'chart.js'
|
||||
import ago from 's-ago'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, LineElement, PointElement)
|
||||
|
||||
const route = useRoute()
|
||||
const marketId = route.params.id
|
||||
|
||||
const stats = ref([])
|
||||
const url = '/api/market/' + marketId + '/stats'
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
stats.value = body
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
const getFilterData = key => {
|
||||
const y = []
|
||||
for (let i = 0; i < stats.value?.length - 1; i += 2) {
|
||||
const s1 = stats.value[i]
|
||||
const s2 = stats.value[i + 1]
|
||||
const yes = 'YES' in s1.y ? s1.y.YES : s2.y.YES
|
||||
const no = 'NO' in s1.y ? s1.y.NO : s2.y.NO
|
||||
const sum = yes + no
|
||||
volume = sum
|
||||
key === 'YES' ? y.push(yes / sum) : y.push(no / sum)
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
let volume = 0
|
||||
const yesData = getFilterData('YES')
|
||||
const noData = getFilterData('NO')
|
||||
|
||||
let currentYes = yesData.at(-1)
|
||||
if (!currentYes) currentYes = 'n/a'
|
||||
let currentNo = noData.at(-1)
|
||||
if (!currentNo) currentNo = 'n/a'
|
||||
|
||||
const chartData = {
|
||||
labels: stats.value ? stats.value.map(({ x }) => ago(new Date(x))) : [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'YES',
|
||||
borderColor: '#35df8d',
|
||||
backgroundColor: '#35df8d',
|
||||
data: yesData,
|
||||
fill: 'origin'
|
||||
},
|
||||
{
|
||||
label: 'NO',
|
||||
borderColor: '#ff7386',
|
||||
backgroundColor: '#ff7386',
|
||||
data: noData,
|
||||
fill: 'origin'
|
||||
}
|
||||
]
|
||||
}
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
background: {
|
||||
color: 'white'
|
||||
}
|
||||
}
|
||||
}
|
||||
const chartPlugins = [{
|
||||
id: 'background',
|
||||
beforeDraw: (chart, args, options) => {
|
||||
const { ctx } = chart
|
||||
ctx.save()
|
||||
ctx.globalCompositeOperation = 'destination-over'
|
||||
ctx.fillStyle = options.color
|
||||
ctx.fillRect(0, 0, chart.width, chart.height)
|
||||
ctx.restore()
|
||||
}
|
||||
}]
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<header class="flex flex-row text-center justify-center pt-1">
|
||||
<nav>
|
||||
<router-link to="/">market</router-link>
|
||||
<router-link to="/user" v-if="session.isAuthenticated">user</router-link>
|
||||
<router-link to="/login" v-else-if="session.isAuthenticated === false" href="/login">login</router-link>
|
||||
<router-link disabled to="/" v-else>...</router-link>
|
||||
<router-link to="/about">about</router-link>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSession } from '@/stores/session'
|
||||
const session = useSession()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
nav>a {
|
||||
margin: 0 3px;
|
||||
}
|
||||
</style>
|
@ -1,198 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
|
||||
@click.prevent="toggleYes">YES</button>
|
||||
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
|
||||
</div>
|
||||
<form v-if="side === 'BUY'" v-show="showForm" @submit.prevent="submitBuyForm">
|
||||
<label v-if="session.isAuthenticated">you have:</label>
|
||||
<label v-if="session.isAuthenticated">{{ session.msats / 1000 }} sats and {{ userShares }} shares</label>
|
||||
<label v-if="session.isAuthenticated">sell?</label>
|
||||
<input v-if="session.isAuthenticated" v-model="side" true-value="SELL" false-value="BUY" type="checkbox" class="m-1"
|
||||
:disabled="userShares === 0" />
|
||||
<label for="stake">how much?</label>
|
||||
<input id="stake" v-model="stake" type="number" min="0" placeholder="sats" required />
|
||||
<label for="certainty">how sure?</label>
|
||||
<input id="certainty" v-model="certainty" type="number" min="0.01" max="0.99" step="0.01" required />
|
||||
<label>you receive:</label>
|
||||
<label>{{ format(buyShares) }} {{ selected }} shares @ {{ format(buyPrice) }} sats</label>
|
||||
<label>you pay:</label>
|
||||
<label>{{ format(buyCost) }} sats</label>
|
||||
<label>if you win:</label>
|
||||
<label>+{{ format(buyProfit) }} sats</label>
|
||||
<button class="col-span-2" type="submit" :disabled="!!market.SettledAt">submit buy order</button>
|
||||
</form>
|
||||
<form v-else v-show="showForm" @submit.prevent="submitSellForm">
|
||||
<label v-if="session.isAuthenticated">you have:</label>
|
||||
<label v-if="session.isAuthenticated">{{ session.msats / 1000 }} sats and {{ userShares }} shares</label>
|
||||
<label v-if="session.isAuthenticated">sell?</label>
|
||||
<input v-if="session.isAuthenticated" v-model="side" true-value="SELL" false-value="BUY" type="checkbox" class="m-1"
|
||||
:disabled="userShares === 0" />
|
||||
<label for="shares">how many?</label>
|
||||
<input id="shares" v-model="sellShares" type="number" min="1" :max="userShares" placeholder="shares" required />
|
||||
<label for="price">price?</label>
|
||||
<input id="price" v-model="sellPrice" type="number" min="1" max="99" step="1" required />
|
||||
<label>you sell:</label>
|
||||
<label>{{ sellShares }} {{ selected }} shares @ {{ format(sellPrice) }} sats</label>
|
||||
<label>you make:</label>
|
||||
<label>+{{ format(sellProfit) }} sats</label>
|
||||
<button class="col-span-2" type="submit" :disabled="userShares === 0 || !!market.SettledAt">
|
||||
submit sell order
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
<div v-if="success" class="green text-center">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSession } from '@/stores/session'
|
||||
import { ref, computed, defineProps } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps(['market'])
|
||||
const market = ref(props.market)
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// YES NO button logic
|
||||
// -- which button was pressed?
|
||||
const selected = ref(route.query.share || null)
|
||||
// -- button css
|
||||
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
|
||||
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
|
||||
// -- show form if any button was pressed
|
||||
const showForm = computed(() => selected.value !== null)
|
||||
const toggleYes = () => {
|
||||
selected.value = selected.value === 'YES' ? null : 'YES'
|
||||
}
|
||||
const toggleNo = () => {
|
||||
selected.value = selected.value === 'NO' ? null : 'NO'
|
||||
}
|
||||
// show error and success below form
|
||||
const err = ref(null)
|
||||
const success = ref(null)
|
||||
// BUY or SELL?
|
||||
const side = ref(route.query.side || 'BUY')
|
||||
|
||||
// -- BUY params
|
||||
// how much wants the user bet?
|
||||
const stake = ref(route.query.stake || 100)
|
||||
// how sure is the user he will win?
|
||||
const certainty = ref(route.query.certainty || 0.5)
|
||||
const buyPrice = computed(() => Math.round(certainty.value * 100))
|
||||
const buyShares = computed(() => {
|
||||
const val = buyPrice.value > 0 ? stake.value / buyPrice.value : null
|
||||
// only full shares can be bought
|
||||
return Math.round(val)
|
||||
})
|
||||
// how much does this order cost?
|
||||
const buyCost = computed(() => {
|
||||
return buyShares.value * buyPrice.value
|
||||
})
|
||||
// how high is the potential reward?
|
||||
const buyProfit = computed(() => {
|
||||
// shares expire at 10 or 0 sats
|
||||
const val = (100 * buyShares.value) - buyCost.value
|
||||
return isNaN(val) ? 0 : val
|
||||
})
|
||||
// -- SELL params
|
||||
// how many shares does the user own?
|
||||
const userShares = computed(() => (((selected.value === 'YES' ? market.value.user?.YES : market.value.user?.NO) || 0) - sold.value))
|
||||
// how many shares does the user want to sell?
|
||||
const sellShares = ref(2)
|
||||
// at which price wants the user to sell each share?
|
||||
const sellPrice = ref(50)
|
||||
const sellProfit = computed(() => sellShares.value * sellPrice.value)
|
||||
// how many share did the user sell since we refreshed our data?
|
||||
const sold = ref(0)
|
||||
|
||||
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
|
||||
|
||||
// Currently, we only support binary markets.
|
||||
// (only events which can be answered with YES and NO)
|
||||
const yesShareId = computed(() => {
|
||||
return market?.value.Shares.find(s => s.Description === 'YES').sid
|
||||
})
|
||||
const noShareId = computed(() => {
|
||||
return market?.value.Shares.find(s => s.Description === 'NO').sid
|
||||
})
|
||||
const shareId = computed(() => {
|
||||
return selected.value === 'YES' ? yesShareId.value : noShareId.value
|
||||
})
|
||||
|
||||
const submitBuyForm = async () => {
|
||||
if (!session.isAuthenticated) return router.push('/login')
|
||||
// TODO validate form
|
||||
const url = window.origin + '/api/order'
|
||||
const body = JSON.stringify({
|
||||
sid: shareId.value,
|
||||
quantity: buyShares.value,
|
||||
price: buyPrice.value,
|
||||
side: 'BUY'
|
||||
})
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
const resBody = await res.json()
|
||||
if (res.status !== 402) {
|
||||
err.value = `error: server responded with HTTP ${res.status}`
|
||||
return
|
||||
}
|
||||
const invoiceId = resBody.id
|
||||
router.push('/invoice/' + invoiceId)
|
||||
}
|
||||
|
||||
const submitSellForm = async () => {
|
||||
if (!session.isAuthenticated) return router.push('/login')
|
||||
// TODO validate form
|
||||
const url = window.origin + '/api/order'
|
||||
const body = JSON.stringify({
|
||||
sid: shareId.value,
|
||||
quantity: sellShares.value,
|
||||
price: sellPrice.value,
|
||||
side: 'SELL'
|
||||
})
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
const resBody = await res.json()
|
||||
if (res.status === 201) {
|
||||
success.value = 'Order created'
|
||||
return
|
||||
}
|
||||
if (res.status !== 402) {
|
||||
err.value = `error: server responded with HTTP ${resBody.status}`
|
||||
return
|
||||
}
|
||||
const invoiceId = resBody.id
|
||||
router.push('/invoice/' + invoiceId)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.success.active {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error.active {
|
||||
background-color: #ff7386;
|
||||
color: white;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form>* {
|
||||
margin: 0.5em 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<tr @mouseleave="mouseleave">
|
||||
<td v-if="order.MarketId"><router-link :to="/market/ + order.MarketId">{{ order.MarketId }}</router-link></td>
|
||||
<td>{{ order.side }} {{ order.quantity }} {{ order.ShareDescription }} @ {{ order.price }} sats
|
||||
</td>
|
||||
<td :title="order.CreatedAt" class="hidden-sm">{{ ago(new Date(order.CreatedAt)) }}</td>
|
||||
<td :class="'font-mono ' + statusClassName + ' ' + selectedClassName" @mouseover="mouseover">{{ order.Status }}</td>
|
||||
<td v-if="showContextMenu">
|
||||
<button @click="() => onMatchClick?.(order)" v-if="showMatch">match</button>
|
||||
<button @click="() => cancelOrder(order)" v-if="showCancel">cancel</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, computed } from 'vue'
|
||||
import ago from 's-ago'
|
||||
import { useSession } from '@/stores/session'
|
||||
|
||||
const session = useSession()
|
||||
const props = defineProps(['order', 'selected', 'onMatchClick'])
|
||||
|
||||
const order = ref(props.order)
|
||||
const showContextMenu = ref(false)
|
||||
const onMatchClick = ref(props.onMatchClick)
|
||||
const mine = computed(() => order.value.Pubkey === session?.pubkey)
|
||||
const showMatch = computed(() => !mine.value && order.value.Status === 'PENDING')
|
||||
const showCancel = computed(() => mine.value && order.value.Status === 'PENDING')
|
||||
|
||||
const statusClassName = computed(() => {
|
||||
const status = order.value.Status
|
||||
if (status === 'EXECUTED') return 'success'
|
||||
if (status === 'PENDING') return 'info'
|
||||
return 'error'
|
||||
})
|
||||
|
||||
const selectedClassName = computed(() => {
|
||||
if (typeof props.selected === 'boolean') {
|
||||
return props.selected ? 'selected' : ''
|
||||
}
|
||||
if (Array.isArray(props.selected)) {
|
||||
return props.selected.some(id => id === order.value.Id) ? 'selected' : ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const mouseover = () => {
|
||||
showContextMenu.value = true && !!session.pubkey
|
||||
}
|
||||
|
||||
const mouseleave = () => {
|
||||
showContextMenu.value = false
|
||||
}
|
||||
|
||||
const cancelOrder = async () => {
|
||||
const url = '/api/order/' + order.value.Id
|
||||
await fetch(url, { method: 'DELETE' }).then(() => {
|
||||
order.value.Status = 'CANCELED'
|
||||
// update session since we might have more msats now
|
||||
return session.checkSession()
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
td {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<router-link :to="to" :class="{ selected }">
|
||||
<slot></slot>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { computed, defineProps } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const props = defineProps(['to'])
|
||||
const selected = computed(() => props.to !== '/' ? route.path.startsWith(props.to) : route.path === props.to, [route.path])
|
||||
|
||||
</script>
|
@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div class="text w-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<th>sats</th>
|
||||
<th>created at</th>
|
||||
<th class="hidden-sm">expires at</th>
|
||||
<th>status</th>
|
||||
<th>details</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="i in invoices" :key="i.id">
|
||||
<td>{{ i.Msats / 1000 }}</td>
|
||||
<td :title="i.CreatedAt">{{ ago(new Date(i.CreatedAt)) }}</td>
|
||||
<td :title="i.ExpiresAt" class="hidden-sm">{{ ago(new Date(i.ExpiresAt)) }}</td>
|
||||
<td :class="'font-mono ' + classFromStatus(i.Status)">{{ i.Status }}</td>
|
||||
<td>
|
||||
<router-link :to="/invoice/ + i.Id">open</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ago from 's-ago'
|
||||
|
||||
const classFromStatus = (status) => status === 'PAID' ? 'success' : status === 'PENDING' ? 'info' : 'error'
|
||||
|
||||
const invoices = ref(null)
|
||||
|
||||
const url = '/api/invoices'
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
invoices.value = body
|
||||
})
|
||||
.catch(console.error)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
th {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hidden-sm {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div class="text w-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<th>market</th>
|
||||
<th>description</th>
|
||||
<th class="hidden-sm">created at</th>
|
||||
<th>status</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<OrderRow :order="o" v-for="o in orders" :key="o.id" />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import OrderRow from './OrderRow.vue'
|
||||
|
||||
const orders = ref(null)
|
||||
|
||||
const url = '/api/orders'
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
orders.value = body
|
||||
})
|
||||
.catch(console.error)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
th {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hidden-sm {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="session.pubkey" class="grid flex-row items-center">
|
||||
<div>authenticated as {{ session.pubkey.slice(0, 8) }}</div>
|
||||
<button class="ms-2 my-3" @click="logout">logout</button>
|
||||
<div>you have {{ session.msats / 1000 }} sats</div>
|
||||
<button class="ms-2 my-3" @click="toggleWithdrawalForm" :disabled="session.msats === 0">
|
||||
<span v-if="showWithdrawalForm">cancel</span>
|
||||
<span v-else>withdraw</span>
|
||||
</button>
|
||||
</div>
|
||||
<form v-show="showWithdrawalForm" @submit.prevent="submitWithdrawal">
|
||||
<label for="bolt11">bolt11</label>
|
||||
<input name="bolt11" id="bolt11" type="text" required v-model="bolt11" />
|
||||
<button type="submit" class="col-span-2">submit withdrawal</button>
|
||||
</form>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
<div v-if="success" class="green text-center">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useSession } from '@/stores/session'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const session = useSession()
|
||||
const router = useRouter()
|
||||
|
||||
const logout = async () => {
|
||||
await session.logout()
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const showWithdrawalForm = ref(false)
|
||||
const bolt11 = ref(null)
|
||||
const toggleWithdrawalForm = () => {
|
||||
showWithdrawalForm.value = !showWithdrawalForm.value
|
||||
}
|
||||
const err = ref(null)
|
||||
const success = ref(null)
|
||||
const submitWithdrawal = async () => {
|
||||
success.value = null
|
||||
err.value = null
|
||||
const url = '/api/withdrawal'
|
||||
const body = JSON.stringify({ bolt11: bolt11.value })
|
||||
try {
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
if (res.status === 200) {
|
||||
success.value = 'invoice paid'
|
||||
return session.checkSession()
|
||||
}
|
||||
const resBody = await res.json()
|
||||
err.value = resBody.reason || `error: server responded with HTTP ${res.status}`
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form>* {
|
||||
margin: 0.5em 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
@ -1,92 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
button {
|
||||
color: #ffffff;
|
||||
border: solid 1px #8787A4;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: #ffffff;
|
||||
background: #8787A4;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.67;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
input {
|
||||
color: #000;
|
||||
}
|
||||
textarea {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8787a4;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ffffff;
|
||||
background: #8787A4;
|
||||
}
|
||||
|
||||
a.selected {
|
||||
color: #ffffff;
|
||||
background: #8787A4;
|
||||
}
|
||||
|
||||
.label {
|
||||
border: none;
|
||||
width: fit-content;
|
||||
padding: 0.5em 3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: rgba(20, 158, 97, .24);
|
||||
color: #35df8d;
|
||||
}
|
||||
|
||||
.success:hover {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ff7386;
|
||||
}
|
||||
.green {
|
||||
color: #35df8d;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: rgba(245, 57, 94, .24);
|
||||
color: #ff7386;
|
||||
}
|
||||
|
||||
.error:hover {
|
||||
background-color: #ff7386;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: #f9db6d3d;
|
||||
color: rgba(245, 198, 57, 0.78);
|
||||
}
|
||||
.info:hover {
|
||||
background-color: rgba(245, 198, 57, 0.78);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
opacity: 0.67
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import * as VueRouter from 'vue-router'
|
||||
import App from './App.vue'
|
||||
import './registerServiceWorker'
|
||||
import './index.css'
|
||||
|
||||
import HomeView from '@/views/HomeView'
|
||||
import AboutView from '@/views/AboutView'
|
||||
import LoginView from '@/views/LoginView'
|
||||
import UserView from '@/views/UserView'
|
||||
import MarketView from '@/views/MarketView'
|
||||
import InvoiceView from '@/views/InvoiceView'
|
||||
import UserWallet from '@/components/UserWallet'
|
||||
import UserInvoices from '@/components/UserInvoices'
|
||||
import UserOrders from '@/components/UserOrders'
|
||||
import OrderForm from '@/components/OrderForm'
|
||||
import MarketOrders from '@/components/MarketOrders'
|
||||
import MarketStats from '@/components/MarketStats'
|
||||
import MarketSettings from '@/components/MarketSettings'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/', component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/about', component: AboutView
|
||||
},
|
||||
{
|
||||
path: '/login', component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
component: UserView,
|
||||
children: [
|
||||
{ path: 'wallet', name: 'user', component: UserWallet },
|
||||
{ path: 'invoices', name: 'invoices', component: UserInvoices },
|
||||
{ path: 'orders', name: 'orders', component: UserOrders }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/market/:id',
|
||||
component: MarketView,
|
||||
children: [
|
||||
{ path: 'form', name: 'form', component: OrderForm },
|
||||
{ path: 'orders', name: 'market-orders', component: MarketOrders },
|
||||
{ path: 'stats', name: 'market-stats', component: MarketStats },
|
||||
{ path: 'settings', name: 'market-settings', component: MarketSettings }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/invoice/:id', component: InvoiceView
|
||||
}
|
||||
]
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
@ -1,32 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from 'register-service-worker'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready () {
|
||||
console.log(
|
||||
'App is being served from cache by a service worker.\n' +
|
||||
'For more details, visit https://goo.gl/AFskqB'
|
||||
)
|
||||
},
|
||||
registered () {
|
||||
console.log('Service worker has been registered.')
|
||||
},
|
||||
cached () {
|
||||
console.log('Content has been cached for offline use.')
|
||||
},
|
||||
updatefound () {
|
||||
console.log('New content is downloading.')
|
||||
},
|
||||
updated () {
|
||||
console.log('New content is available; please refresh.')
|
||||
},
|
||||
offline () {
|
||||
console.log('No internet connection found. App is running in offline mode.')
|
||||
},
|
||||
error (error) {
|
||||
console.error('Error during service worker registration:', error)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export const useSession = defineStore('session', () => {
|
||||
const pubkey = ref(null)
|
||||
const msats = ref(0)
|
||||
const initialized = ref(false)
|
||||
const isAuthenticated = computed(() => initialized.value ? !!pubkey.value : undefined)
|
||||
|
||||
function checkSession () {
|
||||
const url = window.origin + '/api/session'
|
||||
return fetch(url, {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(async r => {
|
||||
const body = await r.json()
|
||||
if (body.pubkey) {
|
||||
pubkey.value = body.pubkey
|
||||
console.log('authenticated as', body.pubkey)
|
||||
} else console.log('unauthenticated')
|
||||
if (body.msats) {
|
||||
msats.value = body.msats
|
||||
}
|
||||
initialized.value = true
|
||||
return body
|
||||
}).catch(err => {
|
||||
console.error('error:', err.reason || err)
|
||||
})
|
||||
}
|
||||
|
||||
function login () {
|
||||
const url = window.origin + '/api/login'
|
||||
return fetch(url, { credentials: 'include' }).then(r => r.json())
|
||||
}
|
||||
|
||||
function logout () {
|
||||
const url = window.origin + '/api/logout'
|
||||
return fetch(url, { method: 'POST', credentials: 'include' })
|
||||
.then(async r => {
|
||||
const body = await r.json()
|
||||
if (body.status === 'OK') {
|
||||
pubkey.value = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { pubkey, isAuthenticated, initialized, msats, checkSession, login, logout }
|
||||
})
|
@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<div class="my-3">
|
||||
<pre>
|
||||
_ _
|
||||
__ _| |__ ___ _ _| |_
|
||||
/ _` | '_ \ / _ \| | | | __|
|
||||
| (_| | |_) | (_) | |_| | |_
|
||||
\__,_|_.__/ \___/ \__,_|\__|</pre>
|
||||
</div>
|
||||
<div id="container2" class="text-left mx-3">
|
||||
<h1>What is this?</h1>
|
||||
<div class="mx-1">
|
||||
delphi.market is a prediction market based on the bitcoin lightning network.
|
||||
</div>
|
||||
<h1>Prediction what?</h1>
|
||||
<div class="mx-1">
|
||||
Prediction markets are awesome! In prediction markets, traders bet on the outcome of events.
|
||||
Political elections are the classic examples. Watch <a target="_blank"
|
||||
href="https://www.youtube.com/watch?v=xA27x7GRMZQ">this video</a> for more information.
|
||||
</div>
|
||||
<h1>When mainnet?</h1>
|
||||
<div class="mx-1">
|
||||
Currently, the market is running on
|
||||
<a target="_blank" href="https://blog.mutinywallet.com/mutinynet/">mutinynet</a> since it's still WIP.
|
||||
This means that sats traded on here don't have any real value yet and you need to use the
|
||||
<a target="_blank" href="https://faucet.mutinywallet.com/">mutiny faucet</a> to pay invoices and
|
||||
<a target="_blank" href="https://signet-app.mutinywallet.com/">mutiny wallet</a> for withdrawals.
|
||||
When I am confident that there are no (serious) bugs, I will use a lightning node connected to mainnet.
|
||||
However, no ETA yet.
|
||||
</div>
|
||||
<h1>FOSS?</h1>
|
||||
<div class="mx-1">
|
||||
Yes! The code is mirrored from
|
||||
<a target="_blank" href="https://git.ekzyis.com/ekzyis/delphi.market">git.ekzyis.com</a> to
|
||||
<a target="_blank" href="https://github.com/ekzyis/delphi.market">Github</a>
|
||||
and is licensed under the terms of the MIT license.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: large;
|
||||
font-family: monospace;
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
#container2 {
|
||||
max-width: 33vw;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
#container2 {
|
||||
max-width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#container2 {
|
||||
max-width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 640px) {
|
||||
#container2 {
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<div class="my-3">
|
||||
<pre>
|
||||
_ _ _ _
|
||||
__| | ___| |_ __ | |__ (_)
|
||||
/ _` |/ _ \ | '_ \| '_ \| |
|
||||
| (_| | __/ | |_) | | | | |
|
||||
\__,_|\___|_| .__/|_| |_|_|
|
||||
|_|.market </pre>
|
||||
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
<Suspense>
|
||||
<MarketList />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketList from '@/components/MarketList'
|
||||
</script>
|
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<div class="my-3">
|
||||
<pre>
|
||||
_ _ ___ ____
|
||||
| || | / _ \___ \
|
||||
| || |_| | | |__) |
|
||||
|__ _| |_| / __/
|
||||
|_| \___/_____|</pre>
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
<Suspense>
|
||||
<Invoice />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Invoice from '@/components/Invoice'
|
||||
</script>
|
@ -1,20 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<div class="my-3">
|
||||
<pre>
|
||||
_ _
|
||||
| | ___ __ _(_)_ __
|
||||
| |/ _ \ / _` | | '_ \
|
||||
| | (_) | (_| | | | | |
|
||||
|_|\___/ \__, |_|_| |_|
|
||||
|___/ </pre>
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
<Suspense>
|
||||
<LoginQRCode class="flex justify-center m-3" />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LoginQRCode from '@/components/LoginQRCode'
|
||||
</script>
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<Suspense>
|
||||
<Market />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Market from '@/components/Market'
|
||||
</script>
|
@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable -->
|
||||
<div class="my-3">
|
||||
<pre>
|
||||
|
||||
_ _ ___ ___ _ __
|
||||
| | | / __|/ _ \ '__|
|
||||
| |_| \__ \ __/ |
|
||||
\__,_|___/\___|_| </pre>
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
<header class="flex flex-row text-center justify-center pt-1">
|
||||
<nav>
|
||||
<StyledLink to="/user/wallet">wallet</StyledLink>
|
||||
<StyledLink to="/user/invoices">invoices</StyledLink>
|
||||
<StyledLink to="/user/orders">orders</StyledLink>
|
||||
</nav>
|
||||
</header>
|
||||
<Suspense>
|
||||
<router-view class="m-3" />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import StyledLink from '@/components/StyledLink'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
nav>a {
|
||||
margin: 0 3px;
|
||||
}
|
||||
</style>
|
@ -1,10 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{html,js,vue}'
|
||||
],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: []
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
@ -1,4 +0,0 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|