diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.vue | 20 | ||||
-rw-r--r-- | src/components/Footer.vue | 5 | ||||
-rw-r--r-- | src/components/Logo.vue | 9 | ||||
-rw-r--r-- | src/components/Navigation.vue | 6 | ||||
-rw-r--r-- | src/components/News.vue | 47 | ||||
-rw-r--r-- | src/components/ServerStatus.vue | 11 | ||||
-rw-r--r-- | src/main.ts | 17 | ||||
-rw-r--r-- | src/reCAPTCHA.ts | 58 | ||||
-rw-r--r-- | src/router.ts | 79 | ||||
-rw-r--r-- | src/router/index.ts | 71 | ||||
-rw-r--r-- | src/router/redirects.ts (renamed from src/redirects.ts) | 0 | ||||
-rw-r--r-- | src/shims-tsx.d.ts | 13 | ||||
-rw-r--r-- | src/shims-vue.d.ts | 10 | ||||
-rw-r--r-- | src/views/AccountRecovery.vue | 99 | ||||
-rw-r--r-- | src/views/Home.vue | 6 | ||||
-rw-r--r-- | src/views/NewsArchive.vue (renamed from src/views/News.vue) | 13 | ||||
-rw-r--r-- | src/views/Registration.vue | 74 | ||||
-rw-r--r-- | src/views/Support.vue | 5 |
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&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; } |