rm -r vue/

This commit is contained in:
ekzyis 2024-07-09 12:20:58 +02:00
parent d40cd1b8d9
commit ae4223512c
62 changed files with 0 additions and 6701 deletions

View File

@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -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
View File

@ -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?

View File

@ -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/).

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -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>

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

View File

@ -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"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -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>&lt;empty&gt;</span>
</span>
<span v-else>
<span v-if="invoice.Description">{{ invoice.Description }}</span>
<span v-else>&lt;empty&gt;</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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -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')

View File

@ -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)
}
})
}

View File

@ -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 }
})

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,9 +0,0 @@
<template>
<Suspense>
<Market />
</Suspense>
</template>
<script setup>
import Market from '@/components/Market'
</script>

View File

@ -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>

View File

@ -1,10 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{html,js,vue}'
],
theme: {
extend: {}
},
plugins: []
}

View File

@ -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))
}
}
})

View File

@ -1,4 +0,0 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})

File diff suppressed because it is too large Load Diff