Add the first version of the history site
This commit is contained in:
parent
e616466f98
commit
cd2349a449
16 changed files with 1696 additions and 0 deletions
116
site/src/App.vue
Normal file
116
site/src/App.vue
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<script setup>
|
||||
import LoginView from "./views/Login.vue";
|
||||
import GuildSelect from "./views/GuildSelect.vue";
|
||||
import InputID from "./views/InputID.vue";
|
||||
import History from "./views/History.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoginView
|
||||
class="inner-view"
|
||||
v-if="state === `login`"
|
||||
@change-state="state = $event"
|
||||
/>
|
||||
<GuildSelect
|
||||
class="inner-view"
|
||||
v-else-if="state === `guild-select`"
|
||||
@set-guild="setGuild($event)"
|
||||
@change-state="state = $event"
|
||||
/>
|
||||
<InputID
|
||||
class="inner-view"
|
||||
v-else-if="state === `id-entry`"
|
||||
@set-guild="setGuild($event)"
|
||||
@change-state="state = $event"
|
||||
/>
|
||||
<History
|
||||
class="inner-view"
|
||||
v-else-if="state === `view-history`"
|
||||
:gid="gid"
|
||||
@set-guild="setGuild($event)"
|
||||
@change-state="state = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {return {
|
||||
state: `login`,
|
||||
gid: null,
|
||||
}},
|
||||
methods: {
|
||||
setGuild(guild) {
|
||||
this.gid = guild;
|
||||
|
||||
// Don't set null as a parameter
|
||||
if (guild) {
|
||||
let url = new URL(window.location.href);
|
||||
let qs = url.searchParams;
|
||||
|
||||
qs.set(`gid`, guild);
|
||||
|
||||
window.history.replaceState(null, null, url);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
let qs = new URLSearchParams(window.location.search);
|
||||
|
||||
if (qs.has(`gid`)) {
|
||||
this.gid = qs.get(`gid`);
|
||||
this.state = `view-history`;
|
||||
return;
|
||||
};
|
||||
|
||||
let hash = new URLSearchParams(window.location.hash);
|
||||
|
||||
if (hash.has(`access_token`)) {
|
||||
|
||||
// Check if the state is enabled
|
||||
if (this.discord.auth.useState) {
|
||||
|
||||
// Assert state validity
|
||||
if (sessionStorage.getItem(`qb-auth-state`) === hash.get(`state`)) {
|
||||
console.info(`State compare success`);
|
||||
sessionStorage.setItem(`qb-auth-token`, hash.get(`access_token`));
|
||||
sessionStorage.removeItem(`qb-auth-state`);
|
||||
window.location.hash = ``;
|
||||
} else {
|
||||
console.error(`State compare failed`);
|
||||
window.location.hash = ``;
|
||||
};
|
||||
} else {
|
||||
sessionStorage.setItem(`qb-auth-token`, hash.get(`access_token`));
|
||||
};
|
||||
};
|
||||
|
||||
if (sessionStorage.getItem(`qb-auth-token`)) {
|
||||
this.state = `guild-select`;
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import "css/themes/dark.css";
|
||||
@import "css/inputs.css";
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--primary-background);
|
||||
color: var(--light-text);
|
||||
font-family: var(--fonts);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
164
site/src/components/GuildDropdown.vue
Normal file
164
site/src/components/GuildDropdown.vue
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<div
|
||||
class="custom-select"
|
||||
:tabindex="tabindex"
|
||||
@blur="open = false"
|
||||
>
|
||||
<div
|
||||
class="selected"
|
||||
:class="{open: open}"
|
||||
@click.stop="open = !open"
|
||||
>
|
||||
<span v-if="selected === null">
|
||||
Select a Server
|
||||
</span>
|
||||
<div v-else class="guild-card">
|
||||
<img
|
||||
class="guild-icon"
|
||||
:src="`https://cdn.discordapp.com/icons/${selected.id}/${selected.icon}.png`"
|
||||
:alt="`${selected.name}'s Server Icon`"
|
||||
>
|
||||
{{ selected.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="items"
|
||||
:class="{selectHide: !open}"
|
||||
>
|
||||
<div
|
||||
class="item"
|
||||
v-for="(guild, i) of options"
|
||||
:key="i"
|
||||
@click.stop="handleSelect(guild)"
|
||||
>
|
||||
<div class="guild-card">
|
||||
<img
|
||||
v-if="guild.icon"
|
||||
class="guild-icon"
|
||||
:src="`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`"
|
||||
:alt="`${guild.name}'s Server Icon`"
|
||||
>
|
||||
{{ guild.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: [`setGuild`],
|
||||
props: {
|
||||
options: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
defaultOption: {
|
||||
required: false,
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
tabindex: {
|
||||
required: false,
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {return {
|
||||
selected: null,
|
||||
open: false,
|
||||
}},
|
||||
methods: {
|
||||
handleSelect(guild) {
|
||||
this.$emit(`setGuild`, guild);
|
||||
this.selected = guild;
|
||||
this.open = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.options?.length && this.defaultOption !== null) {
|
||||
this.selected = this.options[this.defaultOption];
|
||||
this.$emit('setGuild', this.selected);
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.guild-card {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.guild-icon {
|
||||
--size: 50px;
|
||||
border-radius: calc(var(--size) / 2);
|
||||
height: var(--size);
|
||||
margin-right: 10px;
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
height: 47px;
|
||||
line-height: 47px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--tertiary-background);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--accent-neutral);
|
||||
color: var(--light-text);
|
||||
padding-left: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.selected.open{
|
||||
border: 1px solid var(--accent-positive);
|
||||
border-radius: 6px 6px 0px 0px;
|
||||
}
|
||||
|
||||
.selected:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 22px;
|
||||
right: 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 4px solid transparent;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
|
||||
.items {
|
||||
color: #ffffff;
|
||||
border-radius: 0px 0px 6px 6px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--accent-positive);
|
||||
border-left: 1px solid var(--accent-positive);
|
||||
border-bottom: 1px solid var(--accent-positive);
|
||||
position: absolute;
|
||||
background-color: var(--tertiary-background);
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.item{
|
||||
color: var(--light-text);
|
||||
padding-left: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item:hover{
|
||||
background: #2b3035;
|
||||
}
|
||||
|
||||
.selectHide {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
51
site/src/css/inputs.css
Normal file
51
site/src/css/inputs.css
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
button {
|
||||
background: var(--blurple);
|
||||
border-color: transparent;
|
||||
border-radius: var(--medium-border-radius);
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
color: var(--light-text);
|
||||
cursor: pointer;
|
||||
font-family: var(--fonts);
|
||||
font-size: larger;
|
||||
margin-bottom: 5px;
|
||||
outline: none;
|
||||
padding: 5px 10px;
|
||||
user-select: none;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
border-color: var(--accent-neutral);
|
||||
}
|
||||
button:active:not(:disabled) {
|
||||
border-color: var(--accent-positive);
|
||||
}
|
||||
button:disabled {
|
||||
background-color: var(--blurple-faded);
|
||||
color: #4D4D4D;
|
||||
}
|
||||
|
||||
|
||||
input[type="text"] {
|
||||
background: var(--tertiary-background);
|
||||
border-color: var(--accent-neutral);
|
||||
border-radius: var(--medium-border-radius);
|
||||
border-style: solid;
|
||||
color: var(--light-text);
|
||||
font-family: var(--fonts);
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
}
|
||||
input[type="text"]:focus,
|
||||
input[type="text"]:active {
|
||||
border-color: var(--accent-positive);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-neutral);
|
||||
}
|
||||
a:visited {
|
||||
color: var(--accent-positive);
|
||||
}
|
||||
54
site/src/css/themes/dark.css
Normal file
54
site/src/css/themes/dark.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500&family=Electrolize&display=swap');
|
||||
|
||||
:root {
|
||||
/* ===================================================================== */
|
||||
/* Default Themes for backgrounds and accent colours */
|
||||
--primary-background: #23272A;
|
||||
--primary-border: unset;
|
||||
--secondary-background: #2C2F33;
|
||||
--secondary-border: unset;
|
||||
--tertiary-background: #343A40;
|
||||
--tertiary-border: unset;
|
||||
|
||||
/* Border indicators for better support in high contrast themes */
|
||||
--large-border-radius: 10px;
|
||||
--medium-border-radius: 7px;
|
||||
--small-border-radius: 5px;
|
||||
|
||||
/* Text colours and the default font family */
|
||||
--fonts: 'Electrolize', 'Chakra Petch', sans-serif;
|
||||
--light-text: #E9ECEF;
|
||||
--dark-text: #000000;
|
||||
|
||||
/* Accent colours used for smaller sections on the site */
|
||||
--accent-positive: #1DB954;
|
||||
--accent-neutral: #7289DA;
|
||||
--accent-negative: #ED4245;
|
||||
|
||||
/* Scrollbar styling */
|
||||
--scrollbar-background: #0f0f0f;
|
||||
--scrollbar-handle: #4D4D4D;
|
||||
--scrollbar-handle-hover: #5E5E5E;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Additional Themes for the specific site */
|
||||
--blurple-faded: #383e77;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Discord branding colours */
|
||||
|
||||
/* Pre-Rebranding */
|
||||
--og-blurple: #7289da;
|
||||
--greyple: #99aab5;
|
||||
--dark-but-not-black: #2c2f33;
|
||||
--not-quite-black: #23272a;
|
||||
/* + black and white */
|
||||
|
||||
/* Post-Rebranding */
|
||||
--blurple: #5865f2;
|
||||
--green: #57f287;
|
||||
--yellow: #fee75c;
|
||||
--fuchsia: #eb459e;
|
||||
--red: #ed4245;
|
||||
/* + black and white */
|
||||
}
|
||||
33
site/src/main.js
Normal file
33
site/src/main.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
let app = createApp(App);
|
||||
|
||||
app.mixin({
|
||||
data() {return {
|
||||
discord: {
|
||||
client: {
|
||||
id: `863968565353906226`,
|
||||
},
|
||||
auth: {
|
||||
base: `https://discord.com/api/oauth2/authorize`,
|
||||
scopes: [
|
||||
`identify`,
|
||||
`guilds`,
|
||||
],
|
||||
useState: true,
|
||||
},
|
||||
api: {
|
||||
base: `https://discord.com/api/v9`,
|
||||
getGuilds: `/users/@me/guilds`,
|
||||
},
|
||||
},
|
||||
private: {
|
||||
api: `http://localhost:3001`,
|
||||
},
|
||||
}},
|
||||
methods: {},
|
||||
computed: {},
|
||||
})
|
||||
|
||||
app.mount('#app');
|
||||
133
site/src/views/GuildSelect.vue
Normal file
133
site/src/views/GuildSelect.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<script setup>
|
||||
import Dropdown from "../components/GuildDropdown.vue";
|
||||
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="guild-select">
|
||||
<div class="card">
|
||||
<h1>Quote Bracket</h1>
|
||||
<div v-if="loading">
|
||||
<h2>{{ message }}</h2>
|
||||
<button
|
||||
v-if="errored"
|
||||
@click.stop="$emit(`change-state`, `login`)"
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Dropdown
|
||||
:options="userGuilds"
|
||||
:default-option="null"
|
||||
@set-guild="selectGuild"
|
||||
/>
|
||||
<br>
|
||||
<button
|
||||
v-if="selectedGuild !== null"
|
||||
@click.stop="loadHistory"
|
||||
>
|
||||
Load History
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
data() {return {
|
||||
userGuilds: [],
|
||||
loading: true,
|
||||
selectedGuild: null,
|
||||
message: `Loading Your Servers...`,
|
||||
errored: false,
|
||||
}},
|
||||
methods: {
|
||||
loadHistory() {
|
||||
this.$emit(`set-guild`, this.selectedGuild.id);
|
||||
this.$emit(`change-state`, `view-history`);
|
||||
},
|
||||
selectGuild(guild) {
|
||||
this.selectedGuild = guild;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
// Get the user's guilds from Discord
|
||||
let token = sessionStorage.getItem(`qb-auth-token`);
|
||||
try {
|
||||
var response = await axios.get(
|
||||
`${this.discord.api.base}${this.discord.api.getGuilds}`,
|
||||
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||
);
|
||||
} catch (err) {
|
||||
this.message = `Error Getting Your Server List From Discord`;
|
||||
this.errored = true;
|
||||
return;
|
||||
};
|
||||
|
||||
let allGuilds = response.data;
|
||||
|
||||
if (200 <= response.status && response.status < 300) {
|
||||
|
||||
// Request the guild intersection from the server
|
||||
try {
|
||||
response = await axios.post(
|
||||
`${this.private.api}/guilds/compare`,
|
||||
response.data.map(g => g.id)
|
||||
);
|
||||
} catch (err) {
|
||||
this.message = `Error Comparing Server Lists`;
|
||||
this.errored = true;
|
||||
return;
|
||||
};
|
||||
|
||||
let intersectedGuilds = response.data;
|
||||
|
||||
if (intersectedGuilds.length === 1) {
|
||||
this.$emit(`set-guild`, intersectedGuilds[0]);
|
||||
this.$emit(`change-state`, `view-history`);
|
||||
return;
|
||||
};
|
||||
|
||||
// Find all the guild objects that were returned from the private API
|
||||
for (var guild of allGuilds) {
|
||||
if (intersectedGuilds.includes(guild.id)) {
|
||||
this.userGuilds.push(guild);
|
||||
};
|
||||
};
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#guild-select {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--secondary-background);
|
||||
border-radius: 7px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.card {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
235
site/src/views/History.vue
Normal file
235
site/src/views/History.vue
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<script setup>
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="history">
|
||||
<div class="card">
|
||||
<h2>Quote Bracket History</h2>
|
||||
<div
|
||||
v-if="bracket"
|
||||
>
|
||||
<div class="flex-row controls">
|
||||
<button
|
||||
class="no-mobile"
|
||||
:disabled="isFirst"
|
||||
@click.stop="newestBracket"
|
||||
>
|
||||
Newest
|
||||
</button>
|
||||
<button
|
||||
:disabled="isFirst"
|
||||
@click.stop="newerBracket"
|
||||
>
|
||||
Newer
|
||||
</button>
|
||||
<span>{{ date }}</span>
|
||||
<button
|
||||
:disabled="isLast"
|
||||
@click.stop="olderBracket"
|
||||
>
|
||||
Older
|
||||
</button>
|
||||
<button
|
||||
class="no-mobile"
|
||||
:disabled="isLast"
|
||||
@click.stop="oldestBracket"
|
||||
>
|
||||
Oldest
|
||||
</button>
|
||||
</div>
|
||||
<div class="quotes">
|
||||
<div
|
||||
class="quote"
|
||||
v-for="(quote, i) in bracket.quotes"
|
||||
:key="i"
|
||||
:class="quoteClasses(i)"
|
||||
>
|
||||
<span class="text">
|
||||
{{ quote.text }}
|
||||
</span>
|
||||
<div class="metadata flex-row">
|
||||
<span class="votes">Votes: {{ quote.votes }}</span>
|
||||
<span
|
||||
class="streak"
|
||||
v-if="quote.win_streak > 0"
|
||||
>
|
||||
Win Streak: {{ quote.win_streak }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>
|
||||
There was an error loading the quote bracket information. Please
|
||||
wait a minute and then try again. If the issue continues, let
|
||||
Oliver know.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click.stop="resetGuild"
|
||||
>
|
||||
Pick a Different Server
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
gid: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {return {
|
||||
history: [],
|
||||
page: 0,
|
||||
}},
|
||||
computed: {
|
||||
bracket() {
|
||||
return this.history[this.page];
|
||||
},
|
||||
isFirst() {
|
||||
return this.page === this.history.length - 1;
|
||||
},
|
||||
isLast() {
|
||||
return this.page === 0;
|
||||
},
|
||||
date() {
|
||||
let date = new Date(this.bracket.date);
|
||||
return date.toLocaleString();
|
||||
},
|
||||
winners() {
|
||||
let max = -1;
|
||||
let quotes = [];
|
||||
|
||||
for (var qi in this.bracket.quotes) {
|
||||
let q = this.bracket.quotes[qi];
|
||||
if (q.votes === max) {
|
||||
quotes.push(qi);
|
||||
} else if (q.votes > max) {
|
||||
max = q.votes;
|
||||
quotes = [qi];
|
||||
};
|
||||
};
|
||||
|
||||
return quotes;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
quoteClasses(qi) {
|
||||
if (this.winners.includes(`${qi}`)) {
|
||||
return [`winner`];
|
||||
};
|
||||
return [];
|
||||
},
|
||||
updateQuery() {
|
||||
let url = new URL(window.location.href);
|
||||
let qs = url.searchParams;
|
||||
|
||||
qs.set(`page`, this.page);
|
||||
|
||||
history.replaceState(null, null, url);
|
||||
},
|
||||
oldestBracket() {
|
||||
this.page = 0;
|
||||
this.updateQuery();
|
||||
},
|
||||
olderBracket() {
|
||||
this.page--;
|
||||
this.updateQuery();
|
||||
},
|
||||
newerBracket() {
|
||||
this.page++;
|
||||
this.updateQuery();
|
||||
},
|
||||
newestBracket() {
|
||||
this.page = this.history.length - 1;
|
||||
this.updateQuery();
|
||||
},
|
||||
resetGuild() {
|
||||
history.replaceState(null, null, `/`);
|
||||
|
||||
this.$emit(`set-guild`, null);
|
||||
this.$emit(`change-state`, `login`);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
let qs = new URLSearchParams(window.location.search);
|
||||
|
||||
try {
|
||||
let r = await axios.get(`${this.private.api}/${this.gid}/history`);
|
||||
if (r.status === 200) {
|
||||
this.history = r.data;
|
||||
};
|
||||
|
||||
if (qs.has(`page`)) {
|
||||
this.page = parseInt(qs.get(`page`));
|
||||
this.updateQuery();
|
||||
} else {
|
||||
this.newestBracket();
|
||||
};
|
||||
} catch (err) {};
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#history {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--secondary-background);
|
||||
border-radius: 7px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.quote {
|
||||
background: var(--tertiary-background);
|
||||
border-radius: var(--large-border-radius);
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--tertiary-background);
|
||||
}
|
||||
.quote.winner {
|
||||
border-color: gold;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.quote .metadata {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.no-mobile {
|
||||
display: none;
|
||||
}
|
||||
.card {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
113
site/src/views/InputID.vue
Normal file
113
site/src/views/InputID.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<script setup>
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="guild-id-input">
|
||||
<div class="card">
|
||||
<h1>Quote Bracket</h1>
|
||||
<div>
|
||||
<p>
|
||||
Enter a server ID in the box below in order to load the
|
||||
quote bracket history. If you need help finding out how to
|
||||
get the server's ID, you can read Discord's help article
|
||||
about getting IDs here:
|
||||
<a href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-">
|
||||
Where can I find my User/Server/Message ID?
|
||||
</a>
|
||||
</p>
|
||||
<div class="flex-row">
|
||||
<input
|
||||
type="text"
|
||||
name="Server ID"
|
||||
id="server-id"
|
||||
v-model="guildID"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasError"
|
||||
>
|
||||
The server ID you entered is invalid, please make sure that
|
||||
you entered it correctly.
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<button
|
||||
@click.stop="goBack"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="guildID && !hasError"
|
||||
@click.stop="loadHistory"
|
||||
>
|
||||
Load History
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {return {
|
||||
guildID: ``,
|
||||
}},
|
||||
computed: {
|
||||
hasError() {
|
||||
return this.guildID.match(/[^0-9]/g) != null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$emit(`change-state`, `login`);
|
||||
},
|
||||
loadHistory() {
|
||||
this.$emit(`set-guild`, this.guildID);
|
||||
this.$emit(`change-state`, `view-history`);
|
||||
},
|
||||
},
|
||||
async mounted() {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#guild-id-input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--secondary-background);
|
||||
border-radius: 7px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#server-id {
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.card {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
site/src/views/Login.vue
Normal file
82
site/src/views/Login.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<script setup>
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="login-view">
|
||||
<div class="card">
|
||||
<h1>Quote Bracket</h1>
|
||||
<button
|
||||
@click.stop="handleDiscordLogin"
|
||||
class="discord-login"
|
||||
>
|
||||
Login With Discord
|
||||
</button>
|
||||
<br>
|
||||
<button
|
||||
@click.stop="handleGuildID"
|
||||
class="server-id-login"
|
||||
>
|
||||
Enter a Server ID
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {},
|
||||
methods: {
|
||||
handleDiscordLogin() {
|
||||
let qs = new URLSearchParams();
|
||||
qs.set(`response_type`, `token`);
|
||||
qs.set(`client_id`, this.discord.client.id);
|
||||
qs.set(`scope`, this.discord.auth.scopes.join(` `));
|
||||
|
||||
// Construct the redirect URI for Discord
|
||||
qs.set(
|
||||
`redirect_uri`,
|
||||
window.location.origin + window.location.pathname
|
||||
);
|
||||
|
||||
// Add state for verifying the response redirect from Discord
|
||||
if (this.discord.auth.useState) {
|
||||
let state = Math.random().toString(36).substring(2, 15)
|
||||
+ Math.random().toString(36).substring(2, 15);
|
||||
sessionStorage.setItem(`qb-auth-state`, state);
|
||||
qs.set(`state`, state);
|
||||
};
|
||||
|
||||
window.location.href = `${this.discord.auth.base}?${qs.toString()}`;
|
||||
},
|
||||
handleGuildID() {
|
||||
this.$emit(`change-state`, `id-entry`)
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#login-view {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--secondary-background);
|
||||
border-radius: 7px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.card {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue