Merge branch 'master' into master

This commit is contained in:
goose121 2019-04-27 12:29:01 -06:00 committed by GitHub
commit bc197f2fbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 3939 additions and 22 deletions

View file

@ -25,6 +25,7 @@ services:
environment:
- "DATABASE_URI=postgres://postgres:postgres@db:5432/postgres"
- "CLIENT_ID"
- "INVITE_CLIENT_ID_OVERRIDE"
- "CLIENT_SECRET"
- "REDIRECT_URI"
db:

View file

@ -36,13 +36,24 @@ async def db_middleware(request, handler):
@web.middleware
async def auth_middleware(request, handler):
token = request.headers.get("X-Token") or request.query.get("token")
token = request.headers.get("Authorization") or request.query.get("token")
if token:
system = await System.get_by_token(request["conn"], token)
if system:
request["system"] = system
return await handler(request)
@web.middleware
async def cors_middleware(request, handler):
try:
resp = await handler(request)
except web.HTTPException as r:
resp = r
resp.headers["Access-Control-Allow-Origin"] = "*"
resp.headers["Access-Control-Allow-Methods"] = "GET, POST, PATCH"
resp.headers["Access-Control-Allow-Headers"] = "Authorization"
return resp
class Handlers:
@require_system
async def get_system(request):
@ -52,14 +63,14 @@ class Handlers:
system_id = request.match_info.get("system")
system = await System.get_by_hid(request["conn"], system_id)
if not system:
raise web.HTTPNotFound()
raise web.HTTPNotFound(body="null")
return web.json_response(system.to_json())
async def get_system_members(request):
system_id = request.match_info.get("system")
system = await System.get_by_hid(request["conn"], system_id)
if not system:
raise web.HTTPNotFound()
raise web.HTTPNotFound(body="null")
members = await system.get_members(request["conn"])
return web.json_response([m.to_json() for m in members])
@ -68,7 +79,7 @@ class Handlers:
system_id = request.match_info.get("system")
system = await System.get_by_hid(request["conn"], system_id)
if not system:
raise web.HTTPNotFound()
raise web.HTTPNotFound(body="null")
switches = await system.get_switches(request["conn"], 9999)
@ -85,17 +96,17 @@ class Handlers:
system = await System.get_by_hid(request["conn"], system_id)
if not system:
raise web.HTTPNotFound()
raise web.HTTPNotFound(body="null")
members, stamp = await utils.get_fronters(request["conn"], system.id)
if not stamp:
# No switch has been registered at all
raise web.HTTPNotFound()
raise web.HTTPNotFound(body="null")
data = {
"timestamp": stamp.isoformat(),
"members": [member.to_json() for member in members]
}
}
return web.json_response(data)
@require_system
@ -117,8 +128,11 @@ class Handlers:
member_id = request.match_info.get("member")
member = await Member.get_member_by_hid(request["conn"], None, member_id)
if not member:
raise web.HTTPNotFound()
return web.json_response(member.to_json())
raise web.HTTPNotFound(body="{}")
system = await System.get_by_id(request["conn"], member.system)
member_json = member.to_json()
member_json["system"] = system.to_json()
return web.json_response(member_json)
@require_system
async def post_member(request):
@ -228,8 +242,9 @@ class Handlers:
return web.json_response(message.to_json())
async def run():
app = web.Application(middlewares=[db_middleware, auth_middleware, error_middleware])
app = web.Application(middlewares=[cors_middleware, db_middleware, auth_middleware, error_middleware])
def cors_fallback(req):
return web.Response(headers={"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Authorization", "Access-Control-Allow-Methods": "GET, POST, PATCH"}, status=404 if req.method != "OPTIONS" else 200)
app.add_routes([
web.get("/s", Handlers.get_system),
web.post("/s/switches", Handlers.post_switch),
@ -244,6 +259,7 @@ async def run():
web.delete("/m/{member}", Handlers.delete_member),
web.post("/discord_oauth", Handlers.discord_oauth),
web.get("/message/{message}", Handlers.get_message)
web.route("*", "/{tail:.*}", cors_fallback)
])
app["pool"] = await db.connect(
os.environ["DATABASE_URI"]

View file

@ -158,8 +158,11 @@ async def export(ctx: CommandContext):
await working_msg.delete()
f = io.BytesIO(json.dumps(data).encode("utf-8"))
await ctx.reply_ok("DM'd!")
await ctx.message.author.send(content="Here you go!", file=discord.File(fp=f, filename="pluralkit_system.json"))
if not isinstance(ctx.message.channel, discord.DMChannel):
await ctx.reply_ok("DM'd!")
await ctx.message.author.send(content="Here you go! \u2709", file=discord.File(fp=f, filename="pluralkit_system.json"))
async def tell(ctx: CommandContext):

View file

@ -306,7 +306,7 @@
"root": [
{
"name": "PluralKit",
"content": "PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.\n\n**Who's this for? What are systems?**\nPut simply, a system is a person that shares their body with at least 1 other sentient \"self\". This may be a result of having a dissociative disorder like DID/OSDD or a practice known as Tulpamancy, but people that aren't tulpamancers or undiagnosed and have headmates are also systems.\n\n**Why are people's names saying [BOT] next to them? What's going on?**\nThese people are not actually bots, this is simply a caveat to the message proxying feature of PluralKit.\nType `pk;help proxy` for an in-depth explanation."
"content": "PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.\n\n**What's this for? What are systems?**\nThis bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account.\n\n**Why are people's names saying [BOT] next to them?**\nThese people are not actually bots, this is just a Discord a limitation.\nType `pk;help proxy` for an in-depth explanation."
},
{
"name": "Getting started",
@ -314,11 +314,11 @@
},
{
"name": "Useful tips",
"content": "React with ❌ on a proxied message to delete it (if you sent it!).\nReact with ❓ on a proxied message to look up information about it, like who sent it."
"content": "React with ❌ on a proxied message to delete it (if you sent it!).\nReact with ❓ on a proxied message to look up information about it, like who sent it.\nType `pk;invite` for a link to invite this bot to your own server!"
},
{
"name": "More information",
"content": "For a full list of commands, type `pk;commands`.\nFor a more in-depth explanation of message proxying, type `pk;help proxy`.\nIf you're an existing user of the Tupperbox proxy bot, type `pk;import` to import your data from there."
"content": "For a full list of commands, type `pk;commands`.\nFor a more in-depth explanation of message proxying, type `pk;help proxy`.\nIf you're an existing user of Tupperbox, type `pk;import` to import your data from there."
},
{
"name": "Support server",

View file

@ -51,6 +51,8 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
raise errors.ExistingSystemError()
new_hid = generate_hid()
while await System.get_by_hid(conn, new_hid):
new_hid = generate_hid()
async with conn.transaction():
new_system = await db.create_system(conn, system_name, new_hid)
@ -122,11 +124,12 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
return await self.refresh_token(conn)
async def create_member(self, conn, member_name: str) -> Member:
# TODO: figure out what to do if this errors out on collision on generate_hid
new_hid = generate_hid()
if len(member_name) > self.get_member_name_limit():
raise errors.MemberNameTooLongError(tag_present=bool(self.tag))
new_hid = generate_hid()
while await db.get_member_by_hid(conn, new_hid):
new_hid = generate_hid()
member = await db.create_member(conn, self.id, member_name, new_hid)
return member
@ -189,9 +192,8 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
"""Tries to find a member with proxy tags matching the given message. Returns the member and the inner contents."""
members = await db.get_all_members(conn, self.id)
# Sort by specificity (members with both prefix and suffix defined go higher)
# This will make sure more "precise" proxy tags get tried first and match properly
members = sorted(members, key=lambda x: int(bool(x.prefix)) + int(bool(x.suffix)), reverse=True)
# Sort by match specificity (longer prefix/suffix = smaller match = more specific)
members = sorted(members, key=lambda x: len(x.prefix or "") + len(x.suffix or ""), reverse=True)
for member in members:
proxy_prefix = member.prefix or ""

14
web/.babelrc Normal file
View file

@ -0,0 +1,14 @@
{
"presets": [
[
"env",
{
"targets": {
"browsers": [
"last 2 Chrome versions"
]
}
}
]
]
}

3
web/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.cache/
dist/
node_modules/

63
web/app/API.js Normal file
View file

@ -0,0 +1,63 @@
import { EventEmitter } from "eventemitter3"
const SITE_ROOT = process.env.NODE_ENV === "production" ? "https://pluralkit.me" : "http://localhost:1234";
const API_ROOT = process.env.NODE_ENV === "production" ? "https://api.pluralkit.me" : "http://localhost:2939";
const CLIENT_ID = process.env.NODE_ENV === "production" ? "466378653216014359" : "467772037541134367";
export const AUTH_URI = `https://discordapp.com/api/oauth2/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(SITE_ROOT + "/auth/discord")}&response_type=code&scope=identify`
class API extends EventEmitter {
async init() {
this.token = localStorage.getItem("pk-token");
if (this.token) {
this.me = await fetch(API_ROOT + "/s", {headers: {"X-Token": this.token}}).then(r => r.json());
this.emit("update", this.me);
}
}
async fetchSystem(id) {
return await fetch(API_ROOT + "/s/" + id).then(r => r.json()) || null;
}
async fetchSystemMembers(id) {
return await fetch(API_ROOT + "/s/" + id + "/members").then(r => r.json()) || [];
}
async fetchSystemSwitches(id) {
return await fetch(API_ROOT + "/s/" + id + "/switches").then(r => r.json()) || [];
}
async fetchMember(id) {
return await fetch(API_ROOT + "/m/" + id).then(r => r.json()) || null;
}
async saveSystem(system) {
return await fetch(API_ROOT + "/s", {
method: "PATCH",
headers: {"X-Token": this.token},
body: JSON.stringify(system)
});
}
async login(code) {
this.token = await fetch(API_ROOT + "/discord_oauth", {method: "POST", body: code}).then(r => r.text());
this.me = await fetch(API_ROOT + "/s", {headers: {"X-Token": this.token}}).then(r => r.json());
if (this.me) {
localStorage.setItem("pk-token", this.token);
this.emit("update", this.me);
} else {
this.logout();
}
return this.me;
}
logout() {
localStorage.removeItem("pk-token");
this.emit("update", null);
this.token = null;
this.me = null;
}
}
export default new API();

84
web/app/App.vue Normal file
View file

@ -0,0 +1,84 @@
<template>
<div class="app">
<b-navbar>
<b-navbar-brand :to="{name: 'home'}">PluralKit</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item v-if="me" :to="{name: 'system', params: {id: me.id}}">My system</b-nav-item>
<b-nav-item variant="primary" :href="authUri" v-if="!me">Log in</b-nav-item>
<b-nav-item v-on:click="logout" v-if="me">Log out</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<router-view :me="me"></router-view>
</div>
</template>
<script>
import BCollapse from 'bootstrap-vue/es/components/collapse/collapse';
import BNav from 'bootstrap-vue/es/components/nav/nav';
import BNavItem from 'bootstrap-vue/es/components/nav/nav-item';
import BNavbar from 'bootstrap-vue/es/components/navbar/navbar';
import BNavbarBrand from 'bootstrap-vue/es/components/navbar/navbar-brand';
import BNavbarNav from 'bootstrap-vue/es/components/navbar/navbar-nav';
import BNavbarToggle from 'bootstrap-vue/es/components/navbar/navbar-toggle';
import API from "./API";
import { AUTH_URI } from "./API";
export default {
data() {
return {
me: null
}
},
created() {
API.on("update", this.apply);
API.init();
},
methods: {
apply(system) {
this.me = system;
},
logout() {
API.logout();
}
},
computed: {
authUri() {
return AUTH_URI;
}
},
components: {BCollapse, BNav, BNavItem, BNavbar, BNavbarBrand, BNavbarNav, BNavbarToggle}
};
</script>
<style lang="scss">
$font-family-sans-serif: "PT Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 959px,
xl: 960px,
) !default;
@import '~bootstrap/scss/_functions';
@import '~bootstrap/scss/_variables';
@import '~bootstrap/scss/_mixins';
@import '~bootstrap/scss/_buttons';
@import '~bootstrap/scss/_code';
@import '~bootstrap/scss/_forms';
@import '~bootstrap/scss/_grid';
@import '~bootstrap/scss/_nav';
@import '~bootstrap/scss/_navbar';
@import '~bootstrap/scss/_reboot';
@import '~bootstrap/scss/_type';
@import '~bootstrap/scss/_utilities';
@import '~bootstrap-vue/src/index.scss';
</style>

9
web/app/HomePage.vue Normal file
View file

@ -0,0 +1,9 @@
<template>
<h1>Hello</h1>
</template>
<script>
export default {
}
</script>

66
web/app/MemberCard.vue Normal file
View file

@ -0,0 +1,66 @@
<template>
<div class="member-card">
<div
class="member-avatar"
:style="{backgroundImage: `url(${member.avatar_url})`, borderColor: member.color}"
></div>
<div class="member-body">
<span class="member-name">{{ member.name }}</span>
<div class="member-description">{{ member.description }}</div>
<ul class="taglist">
<li>
<hash-icon></hash-icon>
{{ member.id }}
</li>
<li v-if="member.birthday">
<calendar-icon></calendar-icon>
{{ member.birthday }}
</li>
<li v-if="member.pronouns">
<message-circle-icon></message-circle-icon>
{{ member.pronouns }}
</li>
</ul>
</div>
</div>
</template>
<script>
import CalendarIcon from "vue-feather-icons/icons/CalendarIcon";
import HashIcon from "vue-feather-icons/icons/HashIcon";
import MessageCircleIcon from "vue-feather-icons/icons/MessageCircleIcon";
export default {
props: ["member"],
components: { HashIcon, CalendarIcon, MessageCircleIcon }
};
</script>
<style lang="scss">
.member-card {
display: flex;
flex-direction: row;
.member-avatar {
margin: 1.5rem 1rem 0 0;
border-radius: 50%;
background-size: cover;
background-position: top center;
flex-basis: 4rem;
height: 4rem;
border: 4px solid white;
}
.member-body {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem 1rem 1rem 0;
.member-name {
font-size: 13pt;
font-weight: bold;
}
}
}
</style>

View file

@ -0,0 +1,93 @@
<template>
<b-container v-if="loading" class="d-flex justify-content-center">
<b-spinner class="m-5"></b-spinner>
</b-container>
<b-container v-else-if="error">Error</b-container>
<b-container v-else>
<h1>Editing "{{member.name}}"</h1>
<b-form>
<b-form-group label="Name">
<b-form-input v-model="member.name" required></b-form-input>
</b-form-group>
<b-form-group label="Description">
<b-form-textarea v-model="member.description" rows="3" max-rows="6"></b-form-textarea>
</b-form-group>
<b-form-group label="Proxy tags">
<b-row>
<b-col>
<b-input-group prepend="Prefix">
<b-form-input class="text-right" v-model="member.prefix" placeholder="ex: ["></b-form-input>
</b-input-group>
</b-col>
<b-col>
<b-input-group append="Suffix">
<b-form-input v-model="member.suffix" placeholder="ex: ]"></b-form-input>
</b-input-group>
</b-col>
<b-col></b-col>
</b-row>
<template
v-slot:description
v-if="member.prefix || member.suffix"
>Example proxy message: {{member.prefix}}text{{member.suffix}}</template>
<template v-slot:description v-else>(no prefix or suffix defined, proxying will be disabled)</template>
</b-form-group>
<b-form-group label="Pronouns" description="Free text field - put anything you'd like :)">
<b-form-input v-model="member.pronouns" placeholder="eg. he/him"></b-form-input>
</b-form-group>
<b-row>
<b-col md>
<b-form-group label="Birthday">
<b-input-group>
<b-input-group-prepend is-text>
<input type="checkbox" v-model="hideBirthday" label="uwu">&nbsp;Hide year
</b-input-group-prepend>
<b-form-input v-model="member.birthday" type="date"></b-form-input>
</b-input-group>
</b-form-group>
</b-col>
<b-col md>
<b-form-group label="Color" description="Will be displayed on system profile cards.">
<b-form-input type="color" v-model="member.color"></b-form-input>
</b-form-group>
</b-col>
</b-row>
</b-form>
</b-container>
</template>
<script>
import API from "./API";
export default {
props: ["id"],
data() {
return {
loading: false,
error: false,
hideBirthday: false,
member: null
};
},
created() {
this.fetch();
},
methods: {
async fetch() {
this.loading = true;
this.error = false;
this.member = await API.fetchMember(this.id);
if (!this.member) this.error = true;
this.loading = false;
}
}
};
</script>
<style>
</style>

View file

@ -0,0 +1,20 @@
<template>
<b-container class="d-flex justify-content-center"><span class="sr-only">Loading...</span><b-spinner class="m-5"></b-spinner></b-container>
</template>
<script>
import API from "./API";
export default {
async created() {
const code = this.$route.query.code;
if (!code) this.$router.push({ name: "home" });
const me = await API.login(code);
if (me) this.$router.push({ name: "home" });
}
}
</script>
<style>
</style>

View file

@ -0,0 +1,83 @@
<template>
<b-container>
<b-container v-if="loading" class="d-flex justify-content-center"><b-spinner class="m-5"></b-spinner></b-container>
<b-form v-else>
<h1>Editing "{{ system.name || system.id }}"</h1>
<b-form-group label="System name">
<b-form-input v-model="system.name" placeholder="Enter something..."></b-form-input>
</b-form-group>
<b-form-group label="Description">
<b-form-textarea v-model="system.description" placeholder="Enter something..." rows="3" max-rows="3" maxlength="1000"></b-form-textarea>
</b-form-group>
<b-form-group label="System tag">
<b-form-input maxlength="30" v-model="system.tag" placeholder="Enter something..."></b-form-input>
<template v-slot:description>
This is added to the names of proxied accounts. For example: <code>John {{ system.tag }}</code>
</template>
</b-form-group>
<b-form-group class="d-flex justify-content-end">
<b-button type="reset" variant="outline-secondary">Back</b-button>
<b-button v-if="!saving" type="submit" variant="primary" v-on:click="save">Save</b-button>
<b-button v-else variant="primary" disabled>
<b-spinner small></b-spinner>
<span class="sr-only">Saving...</span>
</b-button>
<b-form-group>
</b-form>
</b-container>
</template>
<script>
import BButton from 'bootstrap-vue/es/components/button/button';
import BContainer from 'bootstrap-vue/es/components/layout/container';
import BLink from 'bootstrap-vue/es/components/link/link';
import BSpinner from 'bootstrap-vue/es/components/spinner/spinner';
import BForm from 'bootstrap-vue/es/components/form/form';
import BFormInput from 'bootstrap-vue/es/components/form-input/form-input';
import BFormGroup from 'bootstrap-vue/es/components/form-group/form-group';
import BFormTextarea from 'bootstrap-vue/es/components/form-textarea/form-textarea';
import API from "./API";
export default {
data() {
return {
loading: false,
saving: false,
system: null
}
},
props: ["me", "id"],
created() {
this.fetch()
},
watch: {
"id": "fetch"
},
methods: {
async fetch() {
this.loading = true;
this.system = await API.fetchSystem(this.id);
if (!this.me || !this.system || this.system.id != this.me.id) {
this.$router.push({name: "system", params: {id: this.id}});
}
this.loading = false;
},
async save() {
this.saving = true;
if (await API.saveSystem(this.system)) {
this.$router.push({ name: "system", params: {id: this.system.id} });
}
this.saving = false;
}
},
components: {BButton, BContainer, BLink, BSpinner, BForm, BFormGroup, BFormInput, BFormTextarea}
}
</script>
<style>
</style>

113
web/app/SystemPage.vue Normal file
View file

@ -0,0 +1,113 @@
<template>
<b-container v-if="loading" class="d-flex justify-content-center"><b-spinner class="m-5"></b-spinner></b-container>
<b-container v-else-if="error">An error occurred.</b-container>
<b-container v-else>
<ul v-if="system" class="taglist">
<li>
<hash-icon></hash-icon>
{{ system.id }}
</li>
<li v-if="system.tag">
<tag-icon></tag-icon>
{{ system.tag }}
</li>
<li v-if="system.tz">
<clock-icon></clock-icon>
{{ system.tz }}
</li>
<li v-if="isMine" class="ml-auto">
<b-link :to="{name: 'edit-system', params: {id: system.id}}">
<edit-2-icon></edit-2-icon>
Edit
</b-link>
</li>
</ul>
<h1 v-if="system && system.name">{{ system.name }}</h1>
<div v-if="system && system.description">{{ system.description }}</div>
<h2>Members</h2>
<div v-if="members">
<MemberCard v-for="member in members" :member="member" :key="member.id"/>
</div>
</b-container>
</template>
<script>
import API from "./API";
import BContainer from 'bootstrap-vue/es/components/layout/container';
import BLink from 'bootstrap-vue/es/components/link/link';
import BSpinner from 'bootstrap-vue/es/components/spinner/spinner';
import MemberCard from "./MemberCard.vue";
import Edit2Icon from "vue-feather-icons/icons/Edit2Icon";
import ClockIcon from "vue-feather-icons/icons/ClockIcon";
import HashIcon from "vue-feather-icons/icons/HashIcon";
import TagIcon from "vue-feather-icons/icons/TagIcon";
export default {
data() {
return {
loading: false,
error: false,
system: null,
members: null
};
},
props: ["me", "id"],
created() {
this.fetch();
},
methods: {
async fetch() {
this.loading = true;
this.system = await API.fetchSystem(this.id);
if (!this.system) {
this.error = true;
this.loading = false;
return;
}
this.members = await API.fetchSystemMembers(this.id);
this.loading = false;
}
},
watch: {
id: "fetch"
},
computed: {
isMine() {
return this.system && this.me && this.me.id == this.system.id;
}
},
components: {
Edit2Icon,
ClockIcon,
HashIcon,
TagIcon,
MemberCard,
BContainer, BLink, BSpinner
}
};
</script>
<style lang="scss">
.taglist {
margin: 0;
padding: 0;
color: #aaa;
display: flex;
li {
display: inline-block;
margin-right: 1rem;
list-style-type: none;
.feather {
display: inline-block;
margin-top: -2px;
width: 1em;
}
}
}
</style>

23
web/app/index.js Normal file
View file

@ -0,0 +1,23 @@
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const App = () => import("./App.vue");
const HomePage = () => import("./HomePage.vue");
const SystemPage = () => import("./SystemPage.vue");
const SystemEditPage = () => import("./SystemEditPage.vue");
const MemberEditPage = () => import("./MemberEditPage.vue");
const OAuthRedirectPage = () => import("./OAuthRedirectPage.vue");
const router = new VueRouter({
mode: "history",
routes: [
{ name: "home", path: "/", component: HomePage },
{ name: "system", path: "/s/:id", component: SystemPage, props: true },
{ name: "edit-system", path: "/s/:id/edit", component: SystemEditPage, props: true },
{ name: "edit-member", path: "/m/:id/edit", component: MemberEditPage, props: true },
{ name: "auth-discord", path: "/auth/discord", component: OAuthRedirectPage }
]
})
new Vue({ el: "#app", render: r => r(App), router });

9
web/index.html Normal file
View file

@ -0,0 +1,9 @@
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=PT+Sans:400,700" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script src="app/index.js"></script>
</body>
</html>

19
web/package.json Normal file
View file

@ -0,0 +1,19 @@
{
"dependencies": {
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.0-rc.16",
"eventemitter3": "^3.1.0",
"vue": "^2.6.10",
"vue-feather-icons": "^4.10.0",
"vue-router": "^3.0.2"
},
"devDependencies": {
"@vue/component-compiler-utils": "^2.6.0",
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"cssnano": "^4.1.10",
"parcel-plugin-bundle-visualiser": "^1.2.0",
"sass": "^1.17.3",
"vue-template-compiler": "^2.6.10"
}
}

3296
web/yarn.lock Normal file

File diff suppressed because it is too large Load diff