summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue20
-rw-r--r--src/components/Footer.vue5
-rw-r--r--src/components/Logo.vue9
-rw-r--r--src/components/Navigation.vue6
-rw-r--r--src/components/News.vue47
-rw-r--r--src/components/ServerStatus.vue11
-rw-r--r--src/main.ts17
-rw-r--r--src/reCAPTCHA.ts58
-rw-r--r--src/router.ts79
-rw-r--r--src/router/index.ts71
-rw-r--r--src/router/redirects.ts (renamed from src/redirects.ts)0
-rw-r--r--src/shims-tsx.d.ts13
-rw-r--r--src/shims-vue.d.ts10
-rw-r--r--src/views/AccountRecovery.vue99
-rw-r--r--src/views/Home.vue6
-rw-r--r--src/views/NewsArchive.vue (renamed from src/views/News.vue)13
-rw-r--r--src/views/Registration.vue74
-rw-r--r--src/views/Support.vue5
18 files changed, 283 insertions, 260 deletions
diff --git a/src/App.vue b/src/App.vue
index 463e028..b5f897d 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,52 +8,39 @@
</template>
<style>
-/*
- we might want to consider Normalize
-*/
-
:root {
background: gray(95);
}
-
#app {
z-index: 100;
-
& > .nav {
grid-area: side;
}
-
& > .header {
grid-area: logo;
}
-
& > .content {
grid-area: page;
background: #E1D6CF;
padding: 15px 15px 30px 15px;
border-radius: 15px 15px 0 0;
text-align: justify;
-
& h1 {
margin: 20px 0 0 0 0;
font-weight: bold;
font-size: 1.3em;
border-bottom: 1px solid #9f9894;
color: gray(24);
-
&:nth-of-type(1n + 2) {
margin-top: 2em;
}
}
}
-
& > .footer {
grid-area: footer;
}
-
font-family: sans-serif;
color: #2c3e50;
-
width: 100%;
max-width: 1100px;
margin: 0 auto;
@@ -64,7 +51,6 @@
"side"
"footer";
}
-
@media (min-width: 1100px) {
#app {
grid-column-gap: 0em;
@@ -74,13 +60,11 @@
"logo logo"
"page side"
"footer footer";
-
& > .content {
background: url(assets/page_footer.webp) no-repeat left bottom #E1D6CF;
min-width: 890px;
padding-bottom: 200px;
border-radius: 15px 0 0 15px;
-
& p {
margin: 0px 40px 5px 30px;
}
@@ -90,12 +74,12 @@
</style>
<script lang="ts">
-import { Component, Vue } from "vue-property-decorator";
+import { Options, Vue } from "vue-class-component";
import Navigation from "@/components/Navigation.vue";
import Logo from "@/components/Logo.vue";
import Copyright from "@/components/Footer.vue";
-@Component({
+@Options({
components: {
Navigation,
Logo,
diff --git a/src/components/Footer.vue b/src/components/Footer.vue
index 4d1df2e..076700e 100644
--- a/src/components/Footer.vue
+++ b/src/components/Footer.vue
@@ -13,10 +13,9 @@
</style>
<script lang="ts">
-import Vue from "vue"
-import Component from "vue-class-component"
+import { Options, Vue } from "vue-class-component";
-@Component
+@Options({})
export default class Copyright extends Vue {
year = Reflect.construct(Date, []).getFullYear();
}
diff --git a/src/components/Logo.vue b/src/components/Logo.vue
index 20f071e..8b653e1 100644
--- a/src/components/Logo.vue
+++ b/src/components/Logo.vue
@@ -14,7 +14,6 @@
url("../assets/fonts/AlbertusTMW.woff") format("woff"),
url("../assets/fonts/AlbertusTMW.ttf") format("truetype");
}
-
.logo {
/* this is all relative because our mobile site has to be responsive */
background: url(../assets/logo.svg) no-repeat left top; /* FIXME: the -small logo is fugly */
@@ -23,21 +22,19 @@
font-family: "Albertus TMW", "Arial Black", "Times New Roman", fantasy;
font-size: 7vw;
text-shadow: 0.03ch 0.07ch #070905;
+ text-decoration: none;
color: #34B039;
height: 11vw;
cursor: pointer;
z-index: 200;
max-width: calc(90% - 15vw);
-
&::selection {
text-shadow: none;
}
-
& span:last-of-type {
display: none;
}
}
-
@media (min-width: 800px) {
.logo {
background-image: url(../assets/logo.svg);
@@ -47,14 +44,12 @@
height: 100px;
margin-top: 20px;
position: relative;
-
& span:first-of-type {
position: absolute;
left: 125px;
top: 0;
font-size: 0.6em;
}
-
& span:last-of-type {
display: inline;
position: absolute;
@@ -69,7 +64,6 @@
}
}
}
-
@media (max-width: 300px) {
.logo {
background-image: url(../assets/logo-extrasmall.svg);
@@ -78,7 +72,6 @@
text-shadow: none;
}
}
-
@media (min-width: 1100px) {
.logo {
max-width: 100%;
diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue
index bb40e22..783e12a 100644
--- a/src/components/Navigation.vue
+++ b/src/components/Navigation.vue
@@ -58,6 +58,7 @@
border-radius: 5px;
border: solid 1px #2f2e32;
margin-bottom: 13px;
+ min-width: 17ch;
& ul {
list-style: none;
@@ -149,11 +150,10 @@
</style>
<script lang="ts">
-import { Component, Vue } from "vue-property-decorator";
-import RouteRecord from "vue-router";
+import { Options, Vue } from "vue-class-component";
import ServerStatus from "@/components/ServerStatus.vue";
-@Component({
+@Options({
components: {
ServerStatus,
},
diff --git a/src/components/News.vue b/src/components/News.vue
index 7ad844f..3243b3b 100644
--- a/src/components/News.vue
+++ b/src/components/News.vue
@@ -2,12 +2,17 @@
<div class="news" v-if="count">
<span v-if="!entries.length">(no news entries)</span>
- <article class="entry" v-for="entry in entries" :id="entry.id">
+ <article class="entry" v-for="entry in entries" :id="entry.id" v-bind:key="entry.id">
<a :href="'#' + entry.id">{{ entry.title }}</a>
<time :datetime="entry.date" class="date">{{ entry.date }}</time>
<section class="body" v-html="entry.html"></section>
<q>{{entry.author}}</q>
</article>
+ <article v-if="count > 1 && !fullyLoaded" class="entry loading">
+ <section class="body">
+ Loading... Please wait.
+ </section>
+ </article>
</div>
</template>
@@ -55,7 +60,7 @@
& .body {
margin-top: 2ex;
- &::v-deep > b {
+ &:deep(b) {
display: block;
margin-bottom: 1ex;
}
@@ -95,7 +100,7 @@
</style>
<script lang="ts">
-import { Component, Prop, Vue } from "vue-property-decorator";
+import { Options, Vue } from "vue-class-component";
import newsEntries from "@/assets/news.json";
interface NewsEntry {
@@ -106,36 +111,57 @@ interface NewsEntry {
html: string;
}
-@Component
+@Options({
+ props: {
+ count: Number,
+ from: Number,
+ }
+})
export default class News extends Vue {
- @Prop({ default: Infinity }) private count!: number;
- @Prop({ default: 0 }) private from!: number;
+ private count = Infinity;
+ private from = 0;
private entries: NewsEntry[] = (newsEntries as NewsEntry[]).slice(this.from, this.count);
+ private fullyLoaded = false;
beautify () {
this.entries.forEach(entry => {
// FIXME: weird Vue bug
entry.html = entry.html.replace(/<br\/>/g,"<br></br>");
+
+ // compare the entry title with its first line:
+ const compare = `<b>${entry.title}</b><br></br>`;
+
+ if (entry.html.startsWith(compare)) {
+ // duplicate title: remove the extra one
+ entry.html = entry.html.slice(compare.length);
+ }
});
}
mounted () {
if (!process.env.VUE_APP_NEWS_JSON || !process.env.VUE_APP_NEWS_JSON.startsWith("https")) {
- // TODO: allow arbitrary paths (no hardcoded news.json)
this.beautify();
+ // no extra news to load so end here
return;
}
// restore from cache while we're loading a fresh copy
if (Reflect.has(self, "localStorage")) {
- let newsCache = localStorage.getItem("newsCache");
+ const newsCache = localStorage.getItem("newsCache");
if (newsCache !== null) {
this.entries = (JSON.parse(newsCache) as NewsEntry[]).slice(this.from, this.count);
- this.beautify();
}
}
+ // initial rendering
+ this.beautify();
+
+ if (this.count === 1 && this.entries.length >= 1) {
+ // don't loaad extra news unprompted
+ return;
+ }
+
const req = new Request(process.env.VUE_APP_NEWS_JSON, {
method: "GET",
cache: "default", // serve if fresh, else fetch
@@ -145,13 +171,14 @@ export default class News extends Vue {
.then(data => data.json())
.then(data => {
this.entries = (data as NewsEntry[]).slice(this.from, this.count);
+ this.fullyLoaded = true;
this.beautify();
if (Reflect.has(self, "localStorage")) {
localStorage.setItem("newsCache", JSON.stringify(data));
}
})
- .catch(e => {
+ .catch(() => {
// no news (will use cache, if any)
})
}
diff --git a/src/components/ServerStatus.vue b/src/components/ServerStatus.vue
index 68f7bd4..4b1e0d9 100644
--- a/src/components/ServerStatus.vue
+++ b/src/components/ServerStatus.vue
@@ -2,7 +2,7 @@
<aside>
<a v-if="Online && Players" target="_blank" rel="noopener" href="https://server.themanaworld.org">Online: {{Players}} players</a>
<a v-if="Online && !Players" target="_blank" rel="noopener" href="https://server.themanaworld.org">Online</a>
- <a v-if="!Online" class="offline" target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=ILVfzx5Pe-A">Offline</a>
+ <a v-if="!Online" class="offline" target="_blank" rel="noopener" href="https://www.youtube-nocookie.com/embed/ILVfzx5Pe-A?autoplay=1&amp;modestbranding=1">Offline</a>
</aside>
</template>
@@ -21,15 +21,14 @@ aside :any-link {
</style>
<script lang="ts">
-import Vue from "vue"
-import Component from "vue-class-component"
+import { Options, Vue } from "vue-class-component";
interface StatusResponse {
serverStatus: string;
playersOnline?: number;
}
-@Component
+@Options({})
export default class ServerStatus extends Vue {
Players = 0;
Online = true;
@@ -41,8 +40,8 @@ export default class ServerStatus extends Vue {
});
try {
- const raw_response = await fetch(req);
- const data: StatusResponse = await raw_response.json();
+ const rawResponse = await fetch(req);
+ const data: StatusResponse = await rawResponse.json();
this.Online = data.serverStatus === "Online";
this.Players = data.playersOnline || 0;
diff --git a/src/main.ts b/src/main.ts
index 68d6812..9ee484f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,13 +1,8 @@
-import Vue from "vue"
-import App from "./App.vue"
-import router from "./router"
-import VS2 from "vue-script2"
-import "normalize.css"
+import { createApp } from "vue";
+import App from "./App.vue";
+import router from "./router";
+import "normalize.css";
-Vue.config.productionTip = false
-Vue.use(VS2)
+// TODO: vue-script2
-new Vue({
- router,
- render: h => h(App)
-}).$mount("#app")
+createApp(App).use(router).mount("#app");
diff --git a/src/reCAPTCHA.ts b/src/reCAPTCHA.ts
new file mode 100644
index 0000000..51a6350
--- /dev/null
+++ b/src/reCAPTCHA.ts
@@ -0,0 +1,58 @@
+// handles dynamic reCAPTCHA loading
+
+const loadHandler = "onRecaptchaLoad";
+const script = `https://www.google.com/recaptcha/api.js?onload=${loadHandler}`;
+
+export default class ReCaptchaLoader {
+ /**
+ * asynchronously injects reCAPTCHA and resolves once fully loaded
+ *
+ * @return {Promise<Object>} the grecaptcha inferface
+ */
+ static load () {
+ return new Promise((resolve, reject) => {
+ if (Reflect.has(self, "grecaptcha")) {
+ // we already have it loaded: reset it
+ this.instance.reset();
+ return resolve(this.instance);
+ }
+
+ let el: HTMLScriptElement|null = document.querySelector(`script[src="${script}"]`);
+
+ if (el) {
+ // already loading: don't attempt another load
+ return;
+ } else {
+ el = document.createElement("script");
+ el.type = "text/javascript";
+ el.async = true;
+ el.src = script;
+ }
+
+ // create a load handler:
+ Reflect.set(self, loadHandler, () => {
+ resolve(this.instance);
+ Reflect.deleteProperty(self, loadHandler);
+ });
+
+ // attach the handlers:
+ el.addEventListener("error", reject);
+ el.addEventListener("abort", reject);
+ // no listener for "load": we rely on reCAPTCHA to self-report loading
+
+ // inject the tag and begin loading
+ document.head.appendChild(el);
+ });
+ }
+
+ /**
+ * checks whether reCAPTCHA is ready to use
+ */
+ static get isReady () {
+ return Reflect.has(self, "grecaptcha");
+ }
+
+ static get instance () {
+ return this.isReady ? Reflect.get(self, "grecaptcha"): null;
+ }
+}
diff --git a/src/router.ts b/src/router.ts
deleted file mode 100644
index c58c258..0000000
--- a/src/router.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import Vue from "vue"
-import Router from "vue-router"
-import Home from "./views/Home.vue"
-import NotFound from "./views/NotFound.vue"
-import redirects from "./redirects"
-
-Vue.use(Router)
-
-const router = new Router({
- mode: "history",
- base: process.env.BASE_URL,
- routes: [
- {
- path: "/",
- name: "home",
- component: Home,
- },
- {
- path: "/news",
- name: "news",
- component: () => import(/* webpackChunkName: "news" */ "./views/News.vue"),
- },
- {
- path: "/about",
- name: "about",
- component: () => import(/* webpackChunkName: "about" */ "./views/About.vue"),
- },
- {
- path: "/support",
- name: "support",
- component: () => import(/* webpackChunkName: "support" */ "./views/Support.vue"),
- },
- {
- path: "/recover/password",
- alias: ["/recover/username"],
- name: "account recovery",
- component: () => import(/* webpackChunkName: "recovery" */ "./views/AccountRecovery.vue"),
- },
- // BUG: normally we should be able to put this route under alias but aliases cannot have props:
- {
- path: "/recover/password/:emailToken",
- name: "password reset",
- component: () => import(/* webpackChunkName: "recovery" */ "./views/AccountRecovery.vue"),
- },
- {
- path: "/register",
- name: "registration",
- component: () => import(/* webpackChunkName: "registration" */ "./views/Registration.vue"),
- },
- {
- path: "/404",
- alias: "*",
- name: "not found",
- component: NotFound,
- },
- ...redirects,
- ]
-});
-
-router.afterEach((to, from) => {
- const mainTitle = document.querySelector("#app > .content > h1");
-
- // scroll to the title if we're below it
- if (mainTitle) {
- mainTitle.scrollIntoView({
- block: "nearest", // FIXME: weird behaviour in firefox!
- inline: "nearest",
- behavior: "smooth",
- });
- }
-
- if (to.name && to.path !== "/") {
- document.title = `${process.env.VUE_APP_TITLE} - ${to.name[0].toUpperCase() + to.name.slice(1)}`;
- } else {
- document.title = process.env.VUE_APP_TITLE;
- }
-})
-
-export default router;
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..8165667
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,71 @@
+import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
+import Home from "../views/Home.vue";
+import NotFound from "../views/NotFound.vue";
+import redirects from "./redirects";
+
+const routes: Array<RouteRecordRaw> = [
+ {
+ path: "/",
+ name: "home",
+ component: Home,
+ },
+ {
+ path: "/news",
+ name: "news",
+ component: () => import(/* webpackChunkName: "news" */ "../views/NewsArchive.vue"),
+ },
+ {
+ path: "/about",
+ name: "about",
+ component: () => import(/* webpackChunkName: "about" */ "../views/About.vue"),
+ },
+ {
+ path: "/support",
+ name: "support",
+ component: () => import(/* webpackChunkName: "support" */ "../views/Support.vue"),
+ },
+ {
+ path: "/recover/password:emailToken(.*)",
+ alias: ["/recover/username:emailToken(.*)"],
+ name: "account recovery",
+ component: () => import(/* webpackChunkName: "recovery" */ "../views/AccountRecovery.vue"),
+ },
+ {
+ path: "/register",
+ name: "registration",
+ component: () => import(/* webpackChunkName: "registration" */ "../views/Registration.vue"),
+ },
+ {
+ path: "/404:pathMatch(.*)",
+ alias: "/:pathMatch(.*)",
+ name: "not found",
+ component: NotFound,
+ },
+ ...redirects,
+];
+
+const router = createRouter({
+ history: createWebHistory(process.env.BASE_URL),
+ routes,
+});
+
+router.afterEach((to,) => {
+ const mainTitle = document.querySelector("#app > .content > h1");
+
+ // scroll to the title if we're below it
+ if (mainTitle) {
+ mainTitle.scrollIntoView({
+ block: "nearest", // FIXME: weird behaviour in firefox!
+ inline: "nearest",
+ behavior: "smooth",
+ });
+ }
+
+ if (to.name && typeof to.name === "string" && to.path !== "/") {
+ document.title = `${process.env.VUE_APP_TITLE} - ${to.name[0].toUpperCase() + to.name.slice(1)}`;
+ } else {
+ document.title = process.env.VUE_APP_TITLE;
+ }
+});
+
+export default router;
diff --git a/src/redirects.ts b/src/router/redirects.ts
index 63860cf..63860cf 100644
--- a/src/redirects.ts
+++ b/src/router/redirects.ts
diff --git a/src/shims-tsx.d.ts b/src/shims-tsx.d.ts
deleted file mode 100644
index 0291e88..0000000
--- a/src/shims-tsx.d.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import Vue, { VNode } from "vue"
-
-declare global {
- namespace JSX {
- // tslint:disable no-empty-interface
- interface Element extends VNode {}
- // tslint:disable no-empty-interface
- interface ElementClass extends Vue {}
- interface IntrinsicElements {
- [elem: string]: any
- }
- }
-}
diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts
index ea641cd..2b97bd9 100644
--- a/src/shims-vue.d.ts
+++ b/src/shims-vue.d.ts
@@ -1,7 +1,5 @@
-declare module "*.vue" {
- import Vue from "vue"
- export default Vue
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent<{}, {}, any>
+ export default component
}
-
-// vue-script2 has no types!
-declare module "vue-script2"
diff --git a/src/views/AccountRecovery.vue b/src/views/AccountRecovery.vue
index d78dda2..492c85e 100644
--- a/src/views/AccountRecovery.vue
+++ b/src/views/AccountRecovery.vue
@@ -159,10 +159,10 @@
</template>
<script lang="ts">
-import { Vue, Component, Prop } from "vue-property-decorator"
-import VS2 from "vue-script2"
+import { Options, Vue } from "vue-class-component";
+import reCAPTCHA from "@/reCAPTCHA";
-@Component
+@Options({})
export default class Recovery extends Vue {
step = -3; // ask to use reCAPTCHA
nextStep = 1; // first step after reCAPTCHA confirmation
@@ -180,10 +180,14 @@ export default class Recovery extends Vue {
recaptcha_key = process.env.VUE_APP_RECAPTCHA;
async mounted () {
- let token = document.location.hash.slice(1);
+ let token: string = document.location.hash.slice(1) as string;
if (Reflect.has(this.$route.params, "emailToken")) {
- token = this.$route.params.emailToken;
+ token = this.$route.params.emailToken as string;
+ }
+
+ if (token.startsWith("/")) {
+ token = token.slice(1);
}
if (token.length > 1) {
@@ -197,49 +201,46 @@ export default class Recovery extends Vue {
}
// already loaded (user returned to this page)
- if (Reflect.has(self, "grecaptcha")) {
+ if (reCAPTCHA.isReady) {
if (this.step == -3) {
this.step = this.nextStep;
}
await this.$nextTick();
- (self as any).grecaptcha.render("recaptcha-container", {
+ reCAPTCHA.instance.render("recaptcha-container", {
sitekey: process.env.VUE_APP_RECAPTCHA,
size: "invisible",
});
- (self as any).grecaptcha.reset();
+ reCAPTCHA.instance.reset();
if (this.step == 1) {
- (this.$refs.email as any).focus();
+ (this.$refs.email as HTMLInputElement).focus();
} else if (this.step == 4) {
- (this.$refs.user as any).focus();
+ (this.$refs.user as HTMLInputElement).focus();
}
}
}
async start () {
- (self as any).onRecaptchaLoad = async () => {
+ this.step = -4;
+
+ try {
+ await reCAPTCHA.load();
this.step = this.nextStep;
await this.$nextTick();
if (this.step == 1) {
- (this.$refs.email as any).focus();
+ (this.$refs.email as HTMLInputElement).focus();
} else if (this.step == 4) {
- (this.$refs.user as any).focus();
+ (this.$refs.user as HTMLInputElement).focus();
}
- };
-
- if (Reflect.has(self, "grecaptcha")) {
- (self as any).onRecaptchaLoad();
- } else {
- // load reCAPTCHA
- VS2.load("https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad")
- .catch(() => this.step = -1);
+ } catch (err) {
+ this.step = -1
}
}
async checkEmail () {
- this.step = Reflect.has(self, "grecaptcha") ? 2 : -1;
+ this.step = reCAPTCHA.isReady ? 2 : -1;
// XXX: any actual checks needed here?
}
@@ -248,11 +249,11 @@ export default class Recovery extends Vue {
}
async confirm () {
- (self as any).grecaptcha.execute();
- let token: string = "";
+ reCAPTCHA.instance.execute();
+ let token = "";
// the recaptcha API doesn't play nice with Vue
- while (!(token = (self as any).grecaptcha.getResponse())) {
+ while (!(token = reCAPTCHA.instance.getResponse())) {
await this.sleep(1000);
}
@@ -272,10 +273,10 @@ export default class Recovery extends Vue {
}),
});
- const raw_response = await fetch(req);
- const response: string = await raw_response.text();
+ const rawResponse = await fetch(req);
+ //const response: string = await rawResponse.text();
- switch (raw_response.status) {
+ switch (rawResponse.status) {
// TODO: don't use alerts: embed the error message on the page
case 200:
case 201:
@@ -292,9 +293,9 @@ export default class Recovery extends Vue {
case 404:
this.notFound = true;
this.step = 1;
- (self as any).grecaptcha.reset();
+ reCAPTCHA.instance.reset();
await this.$nextTick();
- (this.$refs.email as any).focus();
+ (this.$refs.email as HTMLInputElement).focus();
break;
case 408:
this.step = -2;
@@ -316,7 +317,7 @@ export default class Recovery extends Vue {
document.location.reload();
break;
default:
- self.alert(`Unknown error: ${raw_response.status}`);
+ self.alert(`Unknown error: ${rawResponse.status}`);
document.location.reload();
break;
}
@@ -324,9 +325,9 @@ export default class Recovery extends Vue {
async checkUser () {
// TODO: check if the token is valid for this username
- this.step = Reflect.has(self, "grecaptcha") ? 5 : -1;
+ this.step = reCAPTCHA.isReady ? 5 : -1;
await this.$nextTick();
- (this.$refs.password as any).focus();
+ (this.$refs.password as HTMLInputElement).focus();
}
// TODO: this is not compatible with Edge! we must polyfill
@@ -347,22 +348,22 @@ export default class Recovery extends Vue {
}
async checkPassword () {
- const full_hash = await this.sha1(this.user.pwd);
- const hash_prefix = full_hash.substring(0, 5);
- const hash_suffix = full_hash.substring(5);
+ const fullHash = await this.sha1(this.user.pwd);
+ const hashPrefix = fullHash.substring(0, 5);
+ const hashSuffix = fullHash.substring(5);
- const req = new Request(`https://api.pwnedpasswords.com/range/${hash_prefix}`, {
+ const req = new Request(`https://api.pwnedpasswords.com/range/${hashPrefix}`, {
mode: "cors",
cache: "force-cache",
referrer: "no-referrer",
});
- const raw_response = await fetch(req);
- const response: string = await raw_response.text();
+ const rawResponse = await fetch(req);
+ const response: string = await rawResponse.text();
const found = response.split("\n").some(h => {
- const [hs, times] = h.split(":");
- return hash_suffix.toUpperCase() === hs.toUpperCase();
+ const [hs,] = h.split(":");
+ return hashSuffix.toUpperCase() === hs.toUpperCase();
});
if (found) {
@@ -374,7 +375,7 @@ export default class Recovery extends Vue {
this.exposed = true;
await this.$nextTick();
- (this.$refs.password as any).focus();
+ (this.$refs.password as HTMLInputElement).focus();
} else {
this.exposed = false;
this.step = 6;
@@ -382,11 +383,11 @@ export default class Recovery extends Vue {
}
async confirm2 () {
- (self as any).grecaptcha.execute();
- let token: string = "";
+ reCAPTCHA.instance.execute();
+ let token = "";
// the recaptcha API doesn't play nice with Vue
- while (!(token = (self as any).grecaptcha.getResponse())) {
+ while (!(token = reCAPTCHA.instance.getResponse())) {
await this.sleep(1000);
}
@@ -408,10 +409,10 @@ export default class Recovery extends Vue {
}),
});
- const raw_response = await fetch(req);
- const response: string = await raw_response.text();
+ const rawResponse = await fetch(req);
+ //const response: string = await rawResponse.text();
- switch (raw_response.status) {
+ switch (rawResponse.status) {
// TODO: don't use alerts: embed the error message on the page
case 200:
case 201:
@@ -445,7 +446,7 @@ export default class Recovery extends Vue {
document.location.reload();
break;
default:
- self.alert(`Unknown error: ${raw_response.status}`);
+ self.alert(`Unknown error: ${rawResponse.status}`);
document.location.reload();
break;
}
diff --git a/src/views/Home.vue b/src/views/Home.vue
index 4fba7b6..b0a7b11 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -7,7 +7,7 @@
</div>
<h1>Recent News</h1>
- <News count="1"/>
+ <News :count="1"/>
<div class="read-more">
<router-link :to="{ name: 'news' }">More News >></router-link>
</div>
@@ -33,10 +33,10 @@
</style>
<script lang="ts">
-import { Component, Vue } from "vue-property-decorator";
+import { Options, Vue } from "vue-class-component";
import News from "@/components/News.vue";
-@Component({
+@Options({
components: {
News,
},
diff --git a/src/views/News.vue b/src/views/NewsArchive.vue
index 58f3e4c..c34e334 100644
--- a/src/views/News.vue
+++ b/src/views/NewsArchive.vue
@@ -1,23 +1,22 @@
+
<template>
<main class="main-content">
<h1>News archive</h1>
- <News count="Infinity"/>
+ <News :count="Infinity"/>
</main>
</template>
<style scoped>
-.main-content {
- & h1 {
- margin-bottom: 0;
- }
+.main-content h1 {
+ margin-bottom: 0;
}
</style>
<script lang="ts">
-import { Component, Vue } from "vue-property-decorator";
+import { Options, Vue } from "vue-class-component";
import News from "@/components/News.vue";
-@Component({
+@Options({
components: {
News,
},
diff --git a/src/views/Registration.vue b/src/views/Registration.vue
index 38bd5d9..214288b 100644
--- a/src/views/Registration.vue
+++ b/src/views/Registration.vue
@@ -137,19 +137,14 @@
</template>
<script lang="ts">
-import Vue from "vue"
-import Component from "vue-class-component"
-import VS2 from "vue-script2"
-
-@Component({
- beforeRouteLeave: (to, from, next) => {
- next();
- },
+import { Options, Vue } from "vue-class-component";
+import reCAPTCHA from "@/reCAPTCHA";
+@Options({
computed: {
isRecaptchaAccepted () {
// the user already agreed to use reCAPTCHA (loaded)
- return Reflect.has(self, 'grecaptcha');
+ return reCAPTCHA.isReady;
}
},
})
@@ -169,43 +164,40 @@ export default class Registration extends Vue {
async mounted () {
// already loaded (user returned to this page)
- if (Reflect.has(self, "grecaptcha")) {
+ if (reCAPTCHA.isReady) {
await this.$nextTick();
- (self as any).grecaptcha.render("recaptcha-container", {
+ reCAPTCHA.instance.render("recaptcha-container", {
sitekey: process.env.VUE_APP_RECAPTCHA,
size: "invisible",
});
- (self as any).grecaptcha.reset();
+ reCAPTCHA.instance.reset();
}
}
async start () {
- (self as any).onRecaptchaLoad = async () => {
+ this.step = -3;
+
+ try {
+ await reCAPTCHA.load();
this.step = 1;
await this.$nextTick();
- (this.$refs.email as any).focus();
- };
-
- if (Reflect.has(self, "grecaptcha")) {
- (self as any).onRecaptchaLoad();
- } else {
- // load reCAPTCHA
- VS2.load("https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad")
- .catch(() => this.step = -1);
+ (this.$refs.email as HTMLInputElement).focus();
+ } catch (err) {
+ this.step = -1
}
}
async checkEmail () {
this.step = 2;
await this.$nextTick();
- (this.$refs.user as any).focus();
+ (this.$refs.user as HTMLInputElement).focus();
}
async checkUser () {
// TODO: check here whether the username is taken
this.step = 3;
await this.$nextTick();
- (this.$refs.password as any).focus();
+ (this.$refs.password as HTMLInputElement).focus();
}
// TODO: this is not compatible with Edge! we must polyfill
@@ -226,22 +218,22 @@ export default class Registration extends Vue {
}
async checkPassword () {
- const full_hash = await this.sha1(this.user.pwd);
- const hash_prefix = full_hash.substring(0, 5);
- const hash_suffix = full_hash.substring(5);
+ const fullHash = await this.sha1(this.user.pwd);
+ const hashPrefix = fullHash.substring(0, 5);
+ const hashSuffix = fullHash.substring(5);
- const req = new Request(`https://api.pwnedpasswords.com/range/${hash_prefix}`, {
+ const req = new Request(`https://api.pwnedpasswords.com/range/${hashPrefix}`, {
mode: "cors",
cache: "force-cache",
referrer: "no-referrer",
});
- const raw_response = await fetch(req);
- const response: string = await raw_response.text();
+ const rawResponse = await fetch(req);
+ const response: string = await rawResponse.text();
const found = response.split("\n").some(h => {
- const [hs, times] = h.split(":");
- return hash_suffix.toUpperCase() === hs.toUpperCase();
+ const [hs,] = h.split(":");
+ return hashSuffix.toUpperCase() === hs.toUpperCase();
});
if (found) {
@@ -253,7 +245,7 @@ export default class Registration extends Vue {
this.exposed = true;
await this.$nextTick();
- (this.$refs.password as any).focus();
+ (this.$refs.password as HTMLInputElement).focus();
} else {
this.exposed = false;
this.step = 4;
@@ -265,11 +257,11 @@ export default class Registration extends Vue {
}
async create () {
- (self as any).grecaptcha.execute();
- let token: string = "";
+ reCAPTCHA.instance.execute();
+ let token = "";
// the recaptcha API doesn't play nice with Vue
- while (!(token = (self as any).grecaptcha.getResponse())) {
+ while (!(token = reCAPTCHA.instance.getResponse())) {
await this.sleep(1000);
}
@@ -291,10 +283,10 @@ export default class Registration extends Vue {
}),
});
- const raw_response = await fetch(req);
- const response: string = await raw_response.text();
+ const rawResponse = await fetch(req);
+ //const response: string = await rawResponse.text();
- switch (raw_response.status) {
+ switch (rawResponse.status) {
// TODO: don't use alerts: embed the error message on the page
case 201:
this.step = 5;
@@ -311,7 +303,7 @@ export default class Registration extends Vue {
this.taken = true;
this.step = 2;
await this.$nextTick();
- (this.$refs.user as any).focus();
+ (this.$refs.user as HTMLInputElement).focus();
break;
case 429:
self.alert("Too many requests.\nPlease try again later");
@@ -326,7 +318,7 @@ export default class Registration extends Vue {
document.location.reload();
break;
default:
- self.alert(`Unknown error: ${raw_response.status}`);
+ self.alert(`Unknown error: ${rawResponse.status}`);
document.location.reload();
break;
}
diff --git a/src/views/Support.vue b/src/views/Support.vue
index ad5847c..2ba9c65 100644
--- a/src/views/Support.vue
+++ b/src/views/Support.vue
@@ -39,10 +39,9 @@ address {
</style>
<script lang="ts">
-import Vue from "vue"
-import Component from "vue-class-component"
+import { Options, Vue } from "vue-class-component";
-@Component
+@Options({})
export default class Copyright extends Vue {
PGP = process.env.VUE_APP_PGP;
}