summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2019-07-13 14:51:35 -0400
committergumi <git@gumi.ca>2019-07-20 17:53:11 -0400
commitd9376169bc0d778381e5c4ff64ef9900d58ce219 (patch)
tree62b8e298f5fc7b6467d85424fd9db79c30f6a0dd /src
parenta55d1feb915d5961d5aa0bca869210534cb9dc3e (diff)
downloadwebsite-d9376169bc0d778381e5c4ff64ef9900d58ce219.tar.gz
website-d9376169bc0d778381e5c4ff64ef9900d58ce219.tar.bz2
website-d9376169bc0d778381e5c4ff64ef9900d58ce219.tar.xz
website-d9376169bc0d778381e5c4ff64ef9900d58ce219.zip
scrap everything and start over with Vue
Diffstat (limited to 'src')
-rw-r--r--src/App.vue99
-rw-r--r--src/assets/logo-extrasmall.svg190
-rw-r--r--src/assets/logo-small.svg199
-rw-r--r--src/assets/logo.svg355
-rw-r--r--src/assets/page_footer.pngbin0 -> 12787 bytes
-rw-r--r--src/components/Footer.vue23
-rw-r--r--src/components/Logo.vue64
-rw-r--r--src/components/Navigation.vue121
-rw-r--r--src/components/News.vue54
-rw-r--r--src/components/ServerStatus.vue73
-rw-r--r--src/main.ts12
-rw-r--r--src/redirects.ts28
-rw-r--r--src/router.ts79
-rw-r--r--src/shims-tsx.d.ts13
-rw-r--r--src/shims-vue.d.ts7
-rw-r--r--src/views/About.vue25
-rw-r--r--src/views/AccountRecovery.vue498
-rw-r--r--src/views/Home.vue45
-rw-r--r--src/views/News.vue18
-rw-r--r--src/views/NotFound.vue6
-rw-r--r--src/views/Registration.vue390
-rw-r--r--src/views/Support.vue49
22 files changed, 2348 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..219987a
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,99 @@
+<template>
+ <div id="app">
+ <Logo class="header"/>
+ <Navigation class="nav"/>
+ <router-view class="content"/>
+ <Copyright class="footer"/>
+ </div>
+</template>
+
+<style>
+/*
+ we might want to consider Normalize
+*/
+
+:root {
+ background: gray(95);
+}
+
+#app {
+ & > .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;
+
+ & 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: Helvetica, Arial, sans-serif;
+ color: #2c3e50;
+
+ width: 100%;
+ max-width: 1100px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-areas:
+ "logo"
+ "page"
+ "side"
+ "footer";
+}
+
+@media (min-width: 1100px) {
+ #app {
+ grid-column-gap: 0em;
+ grid-row-gap: 0px;
+ grid-template-columns: 2fr 4fr;
+ grid-template-areas:
+ "logo logo"
+ "page side"
+ "footer footer";
+
+ & > .content {
+ background: url(assets/page_footer.png) no-repeat left bottom #E1D6CF;
+ min-width: 890px;
+ padding-bottom: 200px;
+ border-radius: 15px 0 0 15px;
+ }
+ }
+}
+</style>
+
+<script lang="ts">
+import { Component, Vue } from "vue-property-decorator";
+import Navigation from "@/components/Navigation.vue";
+import Logo from "@/components/Logo.vue";
+import Copyright from "@/components/Footer.vue";
+
+@Component({
+ components: {
+ Navigation,
+ Logo,
+ Copyright,
+ },
+})
+export default class AppV extends Vue {}
+</script>
diff --git a/src/assets/logo-extrasmall.svg b/src/assets/logo-extrasmall.svg
new file mode 100644
index 0000000..892b420
--- /dev/null
+++ b/src/assets/logo-extrasmall.svg
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="16"
+ height="16"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.47pre4 r22446"
+ sodipodi:docname="games-tmw_extrasmall.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="games-tmw.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"
+ style="display:inline">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 8 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="16 : 8 : 1"
+ inkscape:persp3d-origin="8 : 5.3333333 : 1"
+ id="perspective30" />
+ <linearGradient
+ id="linearGradient3333">
+ <stop
+ id="stop3335"
+ offset="0"
+ style="stop-color:#fff530;stop-opacity:1;" />
+ <stop
+ id="stop3337"
+ offset="1"
+ style="stop-color:#cdde5b;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3635">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3637" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop3639" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3512">
+ <stop
+ style="stop-color:#d7ff38;stop-opacity:0.81402439;"
+ offset="0"
+ id="stop3514" />
+ <stop
+ style="stop-color:#e7fd6f;stop-opacity:0.85365856;"
+ offset="1"
+ id="stop3516" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3502">
+ <stop
+ style="stop-color:#61c61a;stop-opacity:1;"
+ offset="0"
+ id="stop3504" />
+ <stop
+ style="stop-color:#b8f190;stop-opacity:1;"
+ offset="1"
+ id="stop3506" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3502"
+ id="linearGradient3508"
+ x1="29.612352"
+ y1="31.837559"
+ x2="14.060963"
+ y2="11.381969"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3635"
+ id="radialGradient3641"
+ cx="22.98097"
+ cy="42.431534"
+ fx="22.98097"
+ fy="42.431534"
+ r="21.920311"
+ gradientTransform="matrix(1,0,0,0.1895161,0,34.390074)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3333"
+ id="linearGradient3209"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.59979097,0,0,0.58295118,-6.0173651,-5.0728138)"
+ x1="15.367352"
+ y1="27.140129"
+ x2="34.263458"
+ y2="4.6520872" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3512"
+ id="linearGradient3221"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.43156019,0,0,0.42973373,-1.9508427,-1.4919053)"
+ x1="30.025761"
+ y1="28.536076"
+ x2="17.009012"
+ y2="15.476471" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="7.9999996"
+ inkscape:cx="35.536752"
+ inkscape:cy="-17.15823"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer4"
+ showgrid="false"
+ inkscape:window-width="1440"
+ inkscape:window-height="874"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="shadow"
+ style="display:inline" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="shape"
+ style="opacity:1;display:inline">
+ <path
+ sodipodi:type="arc"
+ style="fill:url(#linearGradient3508);fill-opacity:1;stroke:#2b590a;stroke-width:1.91568542;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+ id="path3305"
+ sodipodi:cx="22.286491"
+ sodipodi:cy="21.609764"
+ sodipodi:rx="10.796005"
+ sodipodi:ry="10.227795"
+ d="m 33.082497,21.609764 a 10.796005,10.227795 0 1 1 -21.592011,0 10.796005,10.227795 0 1 1 21.592011,0 z"
+ transform="matrix(0.57125971,0,0,0.60393683,-4.694779,-5.1116003)" />
+ <path
+ style="opacity:0.54000005;fill:none;stroke:#ffffff;stroke-width:1.12521803;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+ d="M 7.67895,2.7683 C 6.2335718,2.8920259 4.8282446,3.6872517 3.9354603,4.8212443 2.4965599,6.6310659 2.5022672,9.4124624 4.0534211,11.157426 4.6960575,12.101643 5.766661,12.630382 6.8798945,12.97456 8.3260486,13.376248 9.9649919,12.903566 11.089767,12.121675 12.408075,11.065477 13.333431,9.3568119 13.209863,7.6432773 13.099862,6.1815543 12.386274,4.9082599 11.335909,3.9638443 10.26509,3.2443956 9.0123651,2.6029181 7.67895,2.7683 z"
+ id="path3658" />
+ <path
+ style="fill:url(#linearGradient3221);fill-opacity:1;fill-rule:evenodd;stroke:#516600;stroke-width:0.56260902;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.28627451;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 10.597381,6.1018266 c 0,0 2.324875,3.4172059 0.895105,3.6301502 C 9.1552656,10.080073 8.1143657,11.367012 7.4147828,10.975178 6.4718691,10.447058 4.5687383,10.36936 4.530554,8.3893184 5.6858447,7.7519356 5.8732126,6.6488356 5.8732126,6.6488356 c 0,0 0,-1.2929303 -0.099456,-2.1880359 1.687188,0.3830877 2.5190674,1.2858621 2.5190674,1.2858621 L 8.6565217,7.7443173 C 10.446733,7.3464926 10.597381,6.1018266 10.597381,6.1018266 z"
+ id="path3211"
+ sodipodi:nodetypes="csscccccc" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer5"
+ inkscape:label="reflect"
+ style="display:inline">
+ <path
+ style="opacity:0.5;fill:url(#linearGradient3209);fill-opacity:1;stroke:none;display:inline"
+ d="M 7.6437869,3.3309091 C 6.4243439,3.6973562 5.0381202,4.1670895 4.408785,5.1945515 4.0441329,5.8338176 3.3740679,6.5307864 3.4945453,7.2691723 4.6762464,7.7232998 6.0267409,7.4969408 7.2299398,7.2115991 8.8391408,6.7757824 10.373503,5.8781227 11.757865,5.0890623 11.504811,4.5550292 10.971373,4.3801758 10.513113,4.0188392 9.6760025,3.471364 8.6452974,3.2177379 7.6437869,3.3309091 z"
+ id="path3197" />
+ </g>
+</svg>
diff --git a/src/assets/logo-small.svg b/src/assets/logo-small.svg
new file mode 100644
index 0000000..836dd56
--- /dev/null
+++ b/src/assets/logo-small.svg
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="22"
+ height="22"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.47pre4 r22446"
+ sodipodi:docname="games-tmw_small.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="games-tmw.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 11 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="22 : 11 : 1"
+ inkscape:persp3d-origin="11 : 7.3333333 : 1"
+ id="perspective30" />
+ <linearGradient
+ id="linearGradient3333">
+ <stop
+ id="stop3335"
+ offset="0"
+ style="stop-color:#fff530;stop-opacity:1;" />
+ <stop
+ id="stop3337"
+ offset="1"
+ style="stop-color:#cdde5b;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3635">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3637" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop3639" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3512">
+ <stop
+ style="stop-color:#d7ff38;stop-opacity:0.81402439;"
+ offset="0"
+ id="stop3514" />
+ <stop
+ style="stop-color:#e7fd6f;stop-opacity:0.85365856;"
+ offset="1"
+ id="stop3516" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3502">
+ <stop
+ style="stop-color:#61c61a;stop-opacity:1;"
+ offset="0"
+ id="stop3504" />
+ <stop
+ style="stop-color:#b8f190;stop-opacity:1;"
+ offset="1"
+ id="stop3506" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3502"
+ id="linearGradient3508"
+ x1="29.612352"
+ y1="31.837559"
+ x2="14.060963"
+ y2="11.381969"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3512"
+ id="linearGradient3518"
+ x1="30.025761"
+ y1="28.536076"
+ x2="17.009012"
+ y2="15.476471"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.5682046,0,0,0.5671264,-1.9633676,-1.7682373)" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3635"
+ id="radialGradient3641"
+ cx="22.98097"
+ cy="42.431534"
+ fx="22.98097"
+ fy="42.431534"
+ r="21.920311"
+ gradientTransform="matrix(1,0,0,0.1895161,0,34.390074)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3333"
+ id="linearGradient3300"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.7289642,0,0,0.710185,-6.0851333,-5.2522606)"
+ x1="15.367352"
+ y1="27.140129"
+ x2="34.263458"
+ y2="4.6520872" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.9999998"
+ inkscape:cx="36.390218"
+ inkscape:cy="-19.687129"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer4"
+ showgrid="false"
+ inkscape:window-width="1440"
+ inkscape:window-height="878"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="shadow"
+ style="display:inline">
+ <path
+ sodipodi:type="arc"
+ style="opacity:0.25;fill:url(#radialGradient3641);fill-opacity:1;stroke:none;stroke-width:1.02947998;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3633"
+ sodipodi:cx="22.98097"
+ sodipodi:cy="42.431534"
+ sodipodi:rx="21.920311"
+ sodipodi:ry="4.1542525"
+ d="M 44.901281,42.431534 A 21.920311,4.1542525 0 1 1 1.0606594,42.431534 A 21.920311,4.1542525 0 1 1 44.901281,42.431534 z"
+ transform="matrix(0.5018177,0,0,0.6619723,-0.5322577,-8.8384957)" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="shape"
+ style="opacity:1;display:inline">
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;fill:url(#linearGradient3508);fill-opacity:1;stroke:#2b590a;stroke-width:1.39915133;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3305"
+ sodipodi:cx="22.286491"
+ sodipodi:cy="21.609764"
+ sodipodi:rx="10.796005"
+ sodipodi:ry="10.227795"
+ d="M 33.082497,21.609764 A 10.796005,10.227795 0 1 1 11.490486,21.609764 A 10.796005,10.227795 0 1 1 33.082497,21.609764 z"
+ transform="matrix(0.6942883,0,0,0.7357509,-4.4777101,-5.2995125)" />
+ <path
+ style="opacity:0.54000005;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 10.22068,4.1867462 C 8.8350041,4.386853 7.5219944,5.125894 6.5241429,5.9682767 C 5.1755067,7.3037921 4.3938838,9.3789853 4.6061488,11.349841 C 4.7635946,12.764976 5.5272056,14.063931 6.3747614,15.082655 C 7.6056114,16.311882 9.4856568,17.15977 11.333467,17.023642 C 13.002374,16.967932 14.572312,16.129942 15.71875,14.95699 C 16.900612,13.522205 17.692453,11.673966 17.368262,9.787044 C 17.170643,8.1843665 16.254668,6.718546 15.097672,5.6261929 C 13.686675,4.6384742 11.988963,3.9411415 10.22068,4.1867462 z"
+ id="path3658" />
+ <path
+ style="fill:url(#linearGradient3518);fill-opacity:1;fill-rule:evenodd;stroke:#516600;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.28627451"
+ d="M 8.7268857,5.9757681 C 8.7268857,5.9757681 10.16182,6.4770414 9.9465812,7.0499251 C 9.7313399,7.6228101 8.8703799,7.766032 9.2291141,8.1240849 C 9.5878457,8.4821376 9.4443541,9.0550198 10.664047,9.3414637 C 11.883742,9.6279048 13.533914,9.9859576 12.242475,10.344011 C 10.951033,10.702065 11.094527,12.349105 11.668502,12.277493 C 12.242475,12.205884 13.175182,12.277493 13.318677,11.847832 C 13.462168,11.418169 14.466621,9.2698535 14.036141,8.6969684 C 13.605661,8.1240849 14.825357,7.9092526 15.327581,8.7685787 C 15.829808,9.6279048 16.403783,12.563936 15.686315,12.850379 C 14.968848,13.13682 12.959942,14.139368 12.744702,14.49742 C 12.529462,14.855474 12.744702,15.070304 11.955488,14.783862 C 11.166274,14.49742 10.018327,14.3542 9.4443541,14.425811 C 8.8703799,14.49742 8.3681539,13.566483 8.1529141,12.993598 C 7.9376733,12.420715 7.0049677,11.847832 7.1484602,11.274947 C 7.2919528,10.702065 8.2964078,9.9143487 8.1529141,9.5562959 C 8.0094205,9.1982432 6.9332204,9.4846843 7.5789399,8.5537465 C 8.2246604,7.6228101 8.3681539,7.336369 8.2964078,6.906706 C 8.2246604,6.4770414 8.6551399,6.0473785 8.7268857,5.9757681 z"
+ id="path3510" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer5"
+ inkscape:label="reflect"
+ style="display:inline">
+ <path
+ style="opacity:0.5;fill:url(#linearGradient3300);fill-opacity:1;stroke:none;stroke-width:1.08744299;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;display:inline"
+ d="M 10.429785,4.7252497 C 9.0315565,5.0631626 7.6169424,5.6620296 6.6727996,6.6535136 C 5.9289102,7.5012097 5.3463198,8.6045142 5.0588031,9.753835 C 5.4220458,10.382709 6.4051635,10.250861 7.0685898,10.361317 C 10.092997,10.394161 12.940719,9.0592849 15.411421,7.4708417 C 16.293083,7.1547111 15.55194,6.57235 15.089517,6.2113961 C 13.768208,5.1824381 12.151445,4.5136826 10.429785,4.7252497 z"
+ id="path3197" />
+ </g>
+</svg>
diff --git a/src/assets/logo.svg b/src/assets/logo.svg
new file mode 100644
index 0000000..c7d78b0
--- /dev/null
+++ b/src/assets/logo.svg
@@ -0,0 +1,355 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48"
+ height="48"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.47pre4 r22446"
+ sodipodi:docname="games-tmw.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="games-tmw.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective57" />
+ <linearGradient
+ id="linearGradient3333">
+ <stop
+ id="stop3335"
+ offset="0"
+ style="stop-color:#fff530;stop-opacity:1;" />
+ <stop
+ id="stop3337"
+ offset="1"
+ style="stop-color:#cdde5b;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3635">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop3637" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop3639" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3512">
+ <stop
+ style="stop-color:#d7ff38;stop-opacity:0.81402439;"
+ offset="0"
+ id="stop3514" />
+ <stop
+ style="stop-color:#e7fd6f;stop-opacity:0.85365856;"
+ offset="1"
+ id="stop3516" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3502">
+ <stop
+ style="stop-color:#61c61a;stop-opacity:1;"
+ offset="0"
+ id="stop3504" />
+ <stop
+ style="stop-color:#b8f190;stop-opacity:1;"
+ offset="1"
+ id="stop3506" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient3494">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop3496" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop3498" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3416">
+ <stop
+ id="stop3418"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:0.5" />
+ <stop
+ id="stop3420"
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3394">
+ <stop
+ style="stop-color:#c6951d;stop-opacity:1;"
+ offset="0"
+ id="stop3396" />
+ <stop
+ style="stop-color:#976409;stop-opacity:1;"
+ offset="1"
+ id="stop3398" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3394"
+ id="linearGradient3410"
+ gradientUnits="userSpaceOnUse"
+ x1="8.3104382"
+ y1="5.8219342"
+ x2="29.302979"
+ y2="34.737553" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3416"
+ id="radialGradient3434"
+ cx="14.142136"
+ cy="15.990791"
+ fx="14.142136"
+ fy="15.990791"
+ r="14.015866"
+ gradientTransform="matrix(0.8465816,5.8370823e-3,-3.8656116e-3,0.5606492,2.2314781,6.9430182)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3494"
+ id="linearGradient3500"
+ x1="8.4088926"
+ y1="3.9268627"
+ x2="54.908024"
+ y2="66.051247"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3502"
+ id="linearGradient3508"
+ x1="29.612352"
+ y1="31.837559"
+ x2="14.060963"
+ y2="11.381969"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3512"
+ id="linearGradient3518"
+ x1="30.025761"
+ y1="28.536076"
+ x2="17.009012"
+ y2="15.476471"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3416"
+ id="linearGradient3592"
+ gradientUnits="userSpaceOnUse"
+ x1="-2.6749711"
+ y1="-0.9965958"
+ x2="37.763008"
+ y2="38.020546" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3635"
+ id="radialGradient3641"
+ cx="22.98097"
+ cy="42.431534"
+ fx="22.98097"
+ fy="42.431534"
+ r="21.920311"
+ gradientTransform="matrix(1,0,0,0.1895161,0,34.390074)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3333"
+ id="linearGradient3225"
+ x1="15.367352"
+ y1="27.140129"
+ x2="34.263458"
+ y2="4.6520872"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.313708"
+ inkscape:cx="20.102718"
+ inkscape:cy="24.40305"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer2"
+ showgrid="false"
+ inkscape:window-width="1440"
+ inkscape:window-height="878"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="shadow">
+ <path
+ sodipodi:type="arc"
+ style="opacity:0.25;fill:url(#radialGradient3641);fill-opacity:1;stroke:none;stroke-width:1.02947998;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3633"
+ sodipodi:cx="22.98097"
+ sodipodi:cy="42.431534"
+ sodipodi:rx="21.920311"
+ sodipodi:ry="4.1542525"
+ d="M 44.901281,42.431534 A 21.920311,4.1542525 0 1 1 1.0606594,42.431534 A 21.920311,4.1542525 0 1 1 44.901281,42.431534 z"
+ transform="matrix(0.9435484,0,0,1,2.7115264,0.9142137)" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="shape"
+ style="opacity:1;display:inline">
+ <path
+ style="opacity:1;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 4.3415291,15.486136 C 7.8027481,19.4609 7.1810682,24.553825 4.266085,28.225413 C 7.483313,38.279304 18.042428,43.773688 27.595971,41.862056 C 29.019342,37.263337 32.885715,35.257377 38.137944,35.851333 C 47.304266,24.492262 43.614049,15.111729 38.913833,8.9169733 C 31.006428,10.105333 28.717333,3.9785045 28.323975,2.3206819 C 16.783546,0.023689878 8.2035665,5.5359966 4.3415291,15.486136 z"
+ id="path3227"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#fffedb;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 4.3415291,15.486136 C 7.8027481,19.4609 7.1810682,24.553825 4.266085,28.225413 C 7.483313,38.279304 18.042428,43.773688 27.595971,41.862056 C 29.019342,37.263337 32.885715,35.257377 38.137944,35.851333 C 47.304266,24.492262 43.614049,15.111729 38.913833,8.9169733 C 31.006428,10.105333 28.717333,3.9785045 28.323975,2.3206819 C 16.783546,0.023689878 8.2035665,5.5359966 4.3415291,15.486136 z"
+ id="path3520"
+ sodipodi:nodetypes="ccccccc" />
+ <path
+ style="fill:url(#linearGradient3410);fill-opacity:1;stroke:#49331a;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 23.5,2.78125 C 15.258268,2.78125 8.2245183,8.0067813 5.5,15.3125 C 7.0779562,17.282317 7.9450492,19.958199 7.65625,22.84375 C 7.4405701,24.998725 6.6123558,26.923617 5.40625,28.4375 C 8.0608022,35.867598 15.163138,41.187501 23.5,41.1875 C 24.639004,41.1875 25.759203,41.096699 26.84375,40.90625 C 26.847085,40.896652 26.840378,40.884584 26.84375,40.875 C 28.091359,37.329105 31.832729,34.75 36.25,34.75 C 36.754294,34.75 37.234786,34.778974 37.71875,34.84375 C 40.793816,31.440821 42.687501,26.9461 42.6875,22 C 42.6875,17.476754 41.089585,13.312989 38.46875,10.03125 C 38.065656,10.075751 37.666651,10.125 37.25,10.125 C 32.531074,10.125 28.584941,7.1714407 27.625,3.25 C 26.288683,2.9551357 24.924544,2.78125 23.5,2.78125 z"
+ id="path3267" />
+ <path
+ transform="matrix(1.0635871,0,0,1.1293349,-9.1307291e-2,-2.1004164)"
+ d="M 33.082497,21.609764 A 10.796005,10.227795 0 1 1 11.490486,21.609764 A 10.796005,10.227795 0 1 1 33.082497,21.609764 z"
+ sodipodi:ry="10.227795"
+ sodipodi:rx="10.796005"
+ sodipodi:cy="21.609764"
+ sodipodi:cx="22.286491"
+ id="path3612"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#fffedb;stroke-width:0.91243529;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;fill:url(#linearGradient3508);fill-opacity:1;stroke:#2b590a;stroke-width:0.98777395;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3305"
+ sodipodi:cx="22.286491"
+ sodipodi:cy="21.609764"
+ sodipodi:rx="10.796005"
+ sodipodi:ry="10.227795"
+ d="M 33.082497,21.609764 A 10.796005,10.227795 0 1 1 11.490486,21.609764 A 10.796005,10.227795 0 1 1 33.082497,21.609764 z"
+ transform="matrix(0.9824561,0,0,1.0432099,1.7168163,-0.2392754)" />
+ <path
+ transform="matrix(0.8923976,0,0,0.9476078,3.7239038,1.8266635)"
+ d="M 33.082497,21.609764 A 10.796005,10.227795 0 1 1 11.490486,21.609764 A 10.796005,10.227795 0 1 1 33.082497,21.609764 z"
+ sodipodi:ry="10.227795"
+ sodipodi:rx="10.796005"
+ sodipodi:cy="21.609764"
+ sodipodi:cx="22.286491"
+ id="path3658"
+ style="opacity:0.54000005;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.08744299;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ sodipodi:type="arc" />
+ <path
+ style="opacity:0.5;fill:none;fill-opacity:1;stroke:url(#linearGradient3500);stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 6.6243655,15.097557 C 7.2929971,16.089391 7.1645719,16.22477 7.6811519,17.267602 C 9.1076396,20.453763 8.9921082,23.535372 7.5675763,26.773953 C 7.1275642,27.567929 7.1078521,27.78817 6.5918464,28.578235 C 7.1899751,31.317344 10.874526,36.238848 15.826886,38.572485 C 19.067738,39.947074 22.542376,40.692995 26.073223,39.9375 C 26.949973,38.104758 28.605679,36.453405 30.369061,35.342655 C 32.177712,34.326959 35.000246,33.580924 37.169543,33.804917 C 39.916262,31.509403 41.90866,25.066084 41.769418,21.496378 C 41.69795,17.931997 40.040331,13.960623 38.079895,11.055504 C 37.066901,11.198753 33.588819,11.119914 30.829536,9.2250634 C 29.059801,7.9314201 27.354032,5.9018627 26.822588,4.1208755 C 17.617733,2.4458281 9.6479895,7.8797023 6.6243655,15.097557 z"
+ id="path3444"
+ sodipodi:nodetypes="ccccccccccccc" />
+ <path
+ style="fill:url(#linearGradient3518);fill-opacity:1;fill-rule:evenodd;stroke:#516600;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.28627451"
+ d="M 18.814091,13.654813 C 18.814091,13.654813 21.339473,14.538697 20.960665,15.548849 C 20.581858,16.559002 19.066629,16.81154 19.697975,17.442885 C 20.32932,18.074231 20.076782,19.084383 22.223356,19.58946 C 24.36993,20.094536 27.274119,20.725881 25.001276,21.357227 C 22.728432,21.988572 22.98097,24.89276 23.991123,24.766491 C 25.001276,24.640222 26.642773,24.766491 26.895312,24.008877 C 27.14785,23.251263 28.915617,19.463191 28.158002,18.453038 C 27.400388,17.442885 29.546962,17.064078 30.430845,18.579307 C 31.314729,20.094536 32.324882,25.271568 31.062191,25.776644 C 29.7995,26.28172 26.263966,28.049487 25.885159,28.680833 C 25.506352,29.312178 25.885159,29.690985 24.496199,29.185909 C 23.10724,28.680833 21.086934,28.428294 20.076782,28.554563 C 19.066629,28.680833 18.182746,27.039335 17.803939,26.029182 C 17.425131,25.01903 15.783634,24.008877 16.036172,22.998724 C 16.28871,21.988572 18.056477,20.599612 17.803939,19.968267 C 17.551401,19.336921 15.657364,19.841998 16.793786,18.2005 C 17.930208,16.559002 18.182746,16.053926 18.056477,15.296311 C 17.930208,14.538697 18.687822,13.781082 18.814091,13.654813 z"
+ id="path3510" />
+ <g
+ id="g3649">
+ <path
+ id="path3643"
+ d="M 13.435029,6.5458649 L 17.589281,12.291107"
+ style="fill:none;fill-rule:evenodd;stroke:#fffedb;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ style="fill:none;fill-rule:evenodd;stroke:#fffedb;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.38431373"
+ d="M 12.639537,7.0761947 L 16.793789,12.821435"
+ id="path3645" />
+ <path
+ id="path3647"
+ d="M 14.230521,6.0155351 L 18.384773,11.760779"
+ style="fill:none;fill-rule:evenodd;stroke:#fffedb;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.38244515" />
+ </g>
+ <use
+ x="0"
+ y="0"
+ xlink:href="#g3649"
+ id="use3654"
+ transform="matrix(-0.4373301,-0.899301,0.899301,-0.4373301,15.151863,53.562135)"
+ width="48"
+ height="48" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#g3649"
+ id="use3656"
+ transform="matrix(0.5886515,-0.808387,0.808387,0.5886515,21.836477,29.407193)"
+ width="48"
+ height="48" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer5"
+ inkscape:label="reflect"
+ style="display:inline">
+ <path
+ style="opacity:0.5;fill:url(#linearGradient3225);fill-opacity:1;stroke:none;stroke-width:1.08744299;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;display:inline"
+ d="M 22.828585,13.137944 C 20.566096,13.137944 14.4375,15.911703 14.4375,22 C 19.905368,25.156875 30.784419,18.952084 31.5,17.65625 C 28.652873,12.985306 24.431111,13.137944 22.828585,13.137944 z"
+ id="path3197"
+ sodipodi:nodetypes="cccc" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer3"
+ inkscape:label="lights"
+ style="display:inline">
+ <path
+ style="opacity:0.5;fill:url(#linearGradient3592);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ d="M 22.5625,3.28125 C 18.643513,3.4701747 14.85605,5.0095738 11.795,7.435 C 10.657287,8.2039126 9.5071326,9.360189 8.7927268,10.546811 C 7.8103213,12.026406 6.7458216,13.483739 6.125,15.15625 C 8.5667581,18.645823 8.8322967,23.544802 6.7855573,27.303885 C 6.5708419,27.917365 5.6369311,28.549914 6.25,29.21875 C 7.0290316,31.075241 8.3188255,32.788589 9.4402963,34.356807 C 10.131375,35.24334 10.878198,35.916558 11.799391,36.496636 C 13.930472,38.175627 16.340245,39.497753 18.982382,40.162809 C 21.330845,40.739697 23.795126,40.865692 26.1875,40.53125 C 26.83687,40.230436 26.807088,39.349501 27.29844,38.80234 C 28.327098,37.570918 29.536417,36.342968 30.926094,35.53018 C 32.959678,34.603351 35.255852,34.0335 37.5,34.375 C 39.318359,31.933623 41.03358,29.318048 41.683768,26.296179 C 42.892309,21.374781 41.955816,15.92926 39.018807,11.783172 C 38.632107,11.431421 38.484146,10.558671 37.95826,10.528284 C 35.330618,10.743307 32.541211,10.012614 30.487759,8.3710636 C 29.205635,7.273861 27.972055,6.0462846 27.5,4.375 C 27.356458,3.4562774 26.321466,3.4347657 25.589022,3.3888715 C 24.593427,3.2245817 23.568986,3.2323384 22.5625,3.28125 z"
+ id="path3413" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:0.7;fill:url(#radialGradient3434);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:6, 6;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3426"
+ sodipodi:cx="14.142136"
+ sodipodi:cy="15.990791"
+ sodipodi:rx="14.015866"
+ sodipodi:ry="10.669736"
+ d="M 28.158002,15.990791 A 14.015866,10.669736 0 1 1 0.12626934,15.990791 A 14.015866,10.669736 0 1 1 28.158002,15.990791 z"
+ transform="matrix(0.9105415,0.5441243,-0.5441243,0.9105415,9.9661117,-6.2645656)" />
+ </g>
+</svg>
diff --git a/src/assets/page_footer.png b/src/assets/page_footer.png
new file mode 100644
index 0000000..72d315e
--- /dev/null
+++ b/src/assets/page_footer.png
Binary files differ
diff --git a/src/components/Footer.vue b/src/components/Footer.vue
new file mode 100644
index 0000000..c454e3a
--- /dev/null
+++ b/src/components/Footer.vue
@@ -0,0 +1,23 @@
+<template>
+ <footer class="footer">
+ &copy; 2004&mdash;{{ year }} The Mana World
+ </footer>
+</template>
+
+<style scoped>
+.footer {
+ text-align: right;
+ font-size: 8pt;
+ padding: 5px;
+}
+</style>
+
+<script lang="ts">
+import Vue from "vue"
+import Component from "vue-class-component"
+
+@Component
+export default class Copyright extends Vue {
+ year = Reflect.construct(Date, []).getFullYear();
+}
+</script>
diff --git a/src/components/Logo.vue b/src/components/Logo.vue
new file mode 100644
index 0000000..ff81161
--- /dev/null
+++ b/src/components/Logo.vue
@@ -0,0 +1,64 @@
+<template>
+ <router-link tag="div" :to="{ name: 'home' }" class="logo">
+ The Mana World
+ <!--<span>Feel the mana power growing inside you</span>-->
+ <span>A free open source 2D MMORPG in development</span>
+ </router-link>
+</template>
+
+<style scoped>
+/*
+ XXX: I couldn't find the font usued in the original PNG logo so I used something
+ similar-ish
+*/
+@import url('https://fonts.googleapis.com/css?family=Carter+One&display=swap');
+
+.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 */
+ background-size: 12vw 12vw;
+ padding: 2vw 0 0 12vw;
+ font-family: 'Carter One', cursive;
+ font-size: 7vw;
+ text-shadow: 0.03ch 0.06ch #070905;
+ color: #34B039;
+ height: 11vw;
+ cursor: pointer;
+
+ & span {
+ display: none;
+ }
+}
+
+@media (min-width: 800px) {
+ .logo {
+ background-image: url(../assets/logo.svg);
+ background-size: 100px 100px;
+ padding: 10px 0 0 100px;
+ font-size: 3em;
+ height: 100px;
+ position: relative;
+
+ & span {
+ display: inline;
+ position: absolute;
+ font-family: Helvetica;
+ font-size: 0.3em;
+ top: 72px;
+ left: 110px;
+ text-shadow: none;
+ color: #616260;
+ font-style: oblique;
+ }
+ }
+}
+
+@media (max-width: 300px) {
+ .logo {
+ background: url(../assets/logo-extrasmall.svg) no-repeat left top;
+ background-size: 12vw 12vw;
+ font-weight: bold;
+ text-shadow: none;
+ }
+}
+</style>
diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue
new file mode 100644
index 0000000..199290b
--- /dev/null
+++ b/src/components/Navigation.vue
@@ -0,0 +1,121 @@
+<template>
+ <nav class="nav">
+ <ul>
+ <li><router-link :class="{ 'custom-active': isHome }" :to="{ name: 'home' }">Home</router-link></li>
+ <li><router-link :to="{ name: 'registration' }">Create Account</router-link></li>
+ <li><a href="https://wiki.themanaworld.org/index.php/Downloads">Download</a></li>
+ <li><router-link :to="{ name: 'about' }">About</router-link></li>
+ <li><a href="https://wiki.themanaworld.org/index.php/FAQ">FAQ</a></li> <!-- we might want to put FAQ under About, or put About on the wiki -->
+ <li><router-link :class="{ 'custom-active': isSupport }" :to="{ name: 'support' }">Support</router-link></li>
+ <li><a href="https://wiki.themanaworld.org/">Wiki</a></li>
+ <li><a href="https://forums.themanaworld.org/">Forums</a></li>
+ </ul>
+ <!-- TODO: we want a server status component: https://api.themanaworld.org/api/tmwa/server -->
+ <div class="server">
+ <span>Server Status</span>
+ <ServerStatus class="status"/>
+ </div>
+ <ul>
+ <span>Source Code</span>
+ <li><a href="https://github.com/themanaworld">The Mana World</a></li>
+ <li><a href="https://gitlab.com/evol">Evol Online</a></li>
+ <li><a href="https://gitlab.com/manaplus">ManaPlus</a></li>
+ <li><a href="https://github.com/bjorn/tiled">Tiled</a></li>
+ </ul>
+ </nav>
+</template>
+
+<style scoped>
+.nav {
+ background: #BA7A58;
+ color: #2f2e32;
+ border-radius: 0 0 15px 15px;
+ padding: 15px;
+ font-size: 14px;
+
+ & span {
+ text-align: center;
+ display: block;
+ padding: 5px;
+ border-bottom: solid 1px #2f2e32;
+ }
+
+ & a, & a:visited {
+ color: #2f2e32;
+ text-decoration: none;
+ display: block;
+ border: solid 1px #CBA083;
+ padding: 1ch;
+
+ &:hover, &.router-link-exact-active, &.custom-active {
+ background: rgba(255,255,255,0.4);
+ border: solid 1px #2f2e32;
+ font-weight: bold;
+ }
+ }
+
+ & ul, & div {
+ background: #CBA083;
+ margin: 0px;
+ padding: 0px;
+ border-radius: 5px;
+ border: solid 1px #2f2e32;
+ list-style: none;
+ margin-bottom: 13px;
+ }
+
+ & ul li {
+ margin-left: 0.8ch;
+ margin-right: 0.8ch;
+
+ &:first-of-type {
+ margin-top: 0.8ch;
+ }
+
+ &:last-of-type {
+ margin-bottom: 0.8ch;
+ }
+ }
+
+
+ & .server > .status {
+ text-align: center;
+ font-weight: bolder;
+ border: 0;
+ border-radius: 0 0 5px 5px;
+
+ &:hover {
+ background: rgba(255,255,255,0.4);
+ }
+ }
+}
+
+@media (min-width: 1100px) {
+ .nav {
+ border-radius: 0 15px 15px 0;
+ }
+}
+</style>
+
+<script lang="ts">
+import { Component, Vue } from "vue-property-decorator";
+import RouteRecord from "vue-router";
+import ServerStatus from "@/components/ServerStatus.vue";
+
+@Component({
+ components: {
+ ServerStatus,
+ },
+ computed: {
+ // XXX: find a better way to do this
+
+ isSupport() {
+ return this.$route.path.startsWith("/recover");
+ },
+ isHome() {
+ return this.$route.path.startsWith("/news");
+ }
+ }
+})
+export default class NavigationV extends Vue {}
+</script>
diff --git a/src/components/News.vue b/src/components/News.vue
new file mode 100644
index 0000000..8a58514
--- /dev/null
+++ b/src/components/News.vue
@@ -0,0 +1,54 @@
+<template>
+ <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.date">
+ <a :href="'#' + entry.date">{{ entry.title }}</a>
+ <time :datetime="entry.date" class="date">{{ entry.date }}</time>
+ <section class="body" v-html="entry.html"></section>
+ </article>
+ </div>
+</template>
+
+<style scoped>
+.news .entry {
+ margin-bottom: 1em;
+
+ &:nth-of-type(1n + 2) {
+ margin-top: 2em;
+ }
+
+ & > a {
+ text-decoration: none;
+ color: inherit;
+ font-weight: bold;
+ }
+
+ & > .date {
+ float: right;
+ }
+
+ & .body {
+ margin-top: 5px;
+ }
+}
+</style>
+
+<script lang="ts">
+import { Component, Prop, Vue } from "vue-property-decorator";
+import newsEntries from "@/assets/news.json";
+
+interface NewsEntry {
+ title: string;
+ date: string;
+ author: string;
+ html: string;
+}
+
+@Component
+export default class News extends Vue {
+ @Prop({ default: Infinity }) private count!: number;
+ @Prop({ default: 0 }) private from!: number;
+ private entries: NewsEntry[] = (newsEntries as NewsEntry[]).slice(this.from, this.count);
+}
+</script>
diff --git a/src/components/ServerStatus.vue b/src/components/ServerStatus.vue
new file mode 100644
index 0000000..c35e916
--- /dev/null
+++ b/src/components/ServerStatus.vue
@@ -0,0 +1,73 @@
+<template>
+ <aside>
+ <a v-if="Online && Players" target="_blank" href="https://server.themanaworld.org">Online: {{Players}} players</a>
+ <a v-if="Online && !Players" target="_blank" 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>
+ </aside>
+</template>
+
+<style scoped>
+aside :any-link {
+ text-decoration: none;
+ color: green;
+ display: block;
+ padding: 8px;
+
+ &.offline {
+ color: #d42424;
+ }
+}
+</style>
+
+<script lang="ts">
+import Vue from "vue"
+import Component from "vue-class-component"
+
+interface StatusResponse {
+ serverStatus: string;
+ playersOnline?: number;
+}
+
+@Component
+export default class ServerStatus extends Vue {
+ Players = 0;
+ Online = true;
+
+ private async getStatus () {
+ const req = new Request(`${process.env.VUE_APP_API}/tmwa/server`, {
+ mode: "cors",
+ referrer: "no-referrer",
+ });
+
+ try {
+ const raw_response = await fetch(req);
+ const data: StatusResponse = await raw_response.json();
+
+ this.Online = data.serverStatus === "Online";
+ this.Players = data.playersOnline || 0;
+
+ if (Reflect.has(self, "localStorage")) {
+ localStorage.setItem("onlinePlayers", `${this.Players}`);
+ localStorage.setItem("serverOnline", this.Online ? "true": "false");
+ }
+ } catch (err) {
+ // API unreachable (assume it's online anyway)
+ this.Online = true;
+ }
+
+ setTimeout(this.getStatus, 8000);
+ }
+
+ mounted () {
+ // use the last cached value to populate prior to first fetch:
+ if (Reflect.has(self, "localStorage")) {
+ this.Players = +(localStorage.getItem("onlinePlayers") || 99);
+ this.Online = !!(localStorage.getItem("serverOnline") || true);
+ } else {
+ this.Players = 99;
+ }
+
+ this.getStatus();
+ }
+}
+</script>
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..d461edb
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,12 @@
+import Vue from "vue"
+import App from "./App.vue"
+import router from "./router"
+import VS2 from "vue-script2"
+
+Vue.config.productionTip = false
+Vue.use(VS2)
+
+new Vue({
+ router,
+ render: h => h(App)
+}).$mount("#app")
diff --git a/src/redirects.ts b/src/redirects.ts
new file mode 100644
index 0000000..e122c0d
--- /dev/null
+++ b/src/redirects.ts
@@ -0,0 +1,28 @@
+// legacy paths from the PHP website
+
+const redirects = [
+ {
+ path: "/index.php",
+ redirect: { name: "home" },
+ },
+ {
+ path: "/news-feed.php",
+ redirect: { name: "news" },
+ },
+ {
+ path: "/about.php",
+ redirect: { name: "about" },
+ },
+ {
+ path: "/registration.php",
+ redirect: { name: "registration" },
+ },
+ {
+ path: "/downloads.php",
+ redirect: () => {
+ self.location.href = "https://wiki.themanaworld.org/index.php/Downloads";
+ }
+ },
+];
+
+export default redirects;
diff --git a/src/router.ts b/src/router.ts
new file mode 100644
index 0000000..c1902db
--- /dev/null
+++ b/src/router.ts
@@ -0,0 +1,79 @@
+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",
+ 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/shims-tsx.d.ts b/src/shims-tsx.d.ts
new file mode 100644
index 0000000..0291e88
--- /dev/null
+++ b/src/shims-tsx.d.ts
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 0000000..ea641cd
--- /dev/null
+++ b/src/shims-vue.d.ts
@@ -0,0 +1,7 @@
+declare module "*.vue" {
+ import Vue from "vue"
+ export default Vue
+}
+
+// vue-script2 has no types!
+declare module "vue-script2"
diff --git a/src/views/About.vue b/src/views/About.vue
new file mode 100644
index 0000000..6eeac30
--- /dev/null
+++ b/src/views/About.vue
@@ -0,0 +1,25 @@
+<template>
+ <main class="about">
+ <h1>Description</h1>
+ <p>The Mana World (TMW) is a serious effort to create an innovative free and open source MMORPG. TMW uses 2D graphics and aims to create a large and diverse interactive world. It is licensed under the GPL, making sure this game can't ever run away from you.</p>
+ <p>Explore this large, ever expanding world to defeat monsters, help NPCs and team up with friends as you achieve your goals. Get your weapons, armor and equipment through quests, monsters or crafting. Play mini-games, go on complex investigations or slay powerful bosses. Hang out in town, socialize or attend player organized events. Wear your boots and grab your sword, adventure waits for you!</p>
+
+ <h1>Story</h1>
+ <p>Start in the powerful city-state of Tulimshar before heading out into the vast expanses of the desert. Get powerful and explore the small continent of Argaes where magic is born. In The Mana World you are an adventurer and monster slayer defending the people of the world from the threats created during the Great Quake. Even in the icy heights of Nivalis there is a call for your assistance to keep the world safe and to grow your potential.</p>
+ <p>The Mage Council of Tulimshar has monitored events following the Great Quake and feel that something ominous is spreading throughout The Mana World. Monsters seem to of come out of every shadowy corner and petty dieties has begun to make presence in dark places. The council has made the call and you are just one of the many people that will battle the forces of evil, sending them back to the depths they came from.</p>
+ <p>Be it warrior, archer or mage, you have answered the call from the leaders of the world to fight back the darkness that spread after the Great Quake. Starting in the Tonori desert, you battle your way to forests surrounding Hurnscald or the icy mountains surrounding Nivalis. Monsters have nowhere to hide.</p>
+
+ <h1>Contributors</h1>
+ <p>We are volunteer driven and encourage player participation in development. We have a long history of contributors. We try to make contributing to the game easy.</p>
+ <p>
+ <ul>
+ <li><a href="https://wiki.themanaworld.org/index.php/TMW_Team">TMW Team</a></li>
+ <li><a href="https://gitlab.com/groups/evol/-/group_members">Current contributors</a></li>
+ <li><a href="https://wiki.themanaworld.org/index.php/Dev:Contributors">Past contributors</a></li>
+ </ul>
+ </p>
+ </main>
+</template>
+
+<!-- This content very quickly becomes stales so we might want to (somehow)
+ import from the wiki, or redirect to the wiki entirely -->
diff --git a/src/views/AccountRecovery.vue b/src/views/AccountRecovery.vue
new file mode 100644
index 0000000..f02c2e0
--- /dev/null
+++ b/src/views/AccountRecovery.vue
@@ -0,0 +1,498 @@
+<template>
+ <main>
+ <div v-if="step == 1">
+ <h1>Account Recovery</h1>
+ Use this form if you forgot your username or password.
+ If it matches any account we have on file you will receive a message containing the list of your account usernames
+ along with a password reset link, should you wish to reset your password.
+ </div>
+
+ <div v-if="step == -1">
+ <h1>reCAPTCHA could not be loaded</h1>
+ This page requires reCAPTCHA but something prevents it from loading.
+ If you are using an ad blocker or tracker blocker please whitelist this page and refresh to continue.
+ </div>
+
+ <div v-if="step == 1">
+ <h1>Email address</h1>
+ The email address that was used to register your account(s).
+
+ <div class="error notFound" v-if="notFound">
+ <h2>Not found</h2>
+ We were not able to find any accounts associated with this email address.
+ </div>
+
+ <form @submit.prevent="checkEmail">
+ <label for="email">Enter your email address:</label>
+ <input @input="notFound = false" v-model="user.email" type="email" maxlength="39" id="email" ref="email" placeholder="you@mail.com" required>
+ <button type="submit">Next step &rarr;</button>
+ </form>
+ </div>
+
+ <div v-if="step == 2">
+ <h1>Confirm</h1>
+ <label for="c-email">Email address:</label>
+ <input id="c-email" disabled readonly type="email" :value="user.email" placeholder="(no email)">
+ <button @click="confirm">Recover account</button>
+ </div>
+
+ <div v-if="step == 3">
+ <h1>Recovery process started</h1>
+ An email was sent with the list of your accounts.
+
+ <h1>Next steps</h1>
+ If you wish to reset the password of your accounts, click the provided link in the email you received.<br>
+ This link is only valid once: if you wish to reset more than one password you will have to repeat the process.
+
+ <br><br><br>
+ <h1>Can't find the account you were looking for?</h1>
+ Double-check the email address you entered; You might have used a different address when you created the account or you might have omitted to provide an email address.
+
+ <h1>Still need help?</h1>
+ Feel free to <router-link :to="{ name: 'support' }">contact us</router-link> for further assistance.
+ </div>
+
+ <!-- PART TWO: -->
+
+ <div v-if="step == -2">
+ <h1>Expired link</h1>
+ This password reset link has expired or is invalid.<br>
+ Keep in mind that emailed links are only valid for 60 minutes.
+
+ <h1>Start over</h1>
+ You may try again in 5 minutes: <router-link :to="{ name: 'support' }">account recovery</router-link>
+ </div>
+
+ <div v-if="step == 4">
+ <h1>Username</h1>
+ Your password reset link is <em>only</em> valid for account usernames listed in the email that was sent to you.
+
+ <form @submit.prevent="checkUser">
+ <label for="user">Enter a username:</label>
+ <input v-model="user.name" type="text" id="user" ref="user" placeholder="type your username here" minlength="4" maxlength="23" pattern="^[a-zA-Z0-9]{4,23}$" title="4-23 characters, alphanumeric" required>
+ <button type="submit" v-if="user.name.length >= 4">Next step &rarr;</button>
+ </form>
+ </div>
+
+ <div v-if="step == 5">
+ <h1>Password</h1>
+ Please choose a new, hard-to-guess password.
+ <p>It must contain between 8 and 23 characters. Letters and numbers only. Case-sensitive.</p>
+
+ <div v-if="exposed" class="exposed">
+ <h2>WARNING: This password is compromised</h2>
+ This password has previously appeared in a data breach. Please use a more secure alternative.
+ <a href="https://haveibeenpwned.com/Passwords" target="_blank" rel="noopener">verified by haveibeenpwned.com</a>
+ </div>
+
+ <form @submit.prevent="checkPassword">
+ <div class="pass-box">
+ <label for="password">Choose a unique password:</label>
+ <input v-model="user.pwd" :type="visible ? 'text' : 'password'" id="password" ref="password" placeholder="type your password here" minlength="8" maxlength="23" pattern="^[a-zA-Z0-9]{8,23}$" title="8-23 characters, alphanumeric" required>
+ <span @click="visible = !visible"></span>
+ </div>
+ <div class="pass-box">
+ <label for="password2">Confirm your password:</label>
+ <input v-model="user.pwd2" :type="visible ? 'text' : 'password'" id="password2" ref="password2" placeholder="type your password again" minlength="8" maxlength="23" pattern="^[a-zA-Z0-9]{8,23}$" title="8-23 characters, alphanumeric" required>
+ <span @click="visible = !visible"></span>
+ </div>
+ <button type="submit" v-if="user.pwd && user.pwd === user.pwd2">Next step &rarr;</button>
+ </form>
+ </div>
+
+ <div v-if="step == 6">
+ <h1>Confirm</h1>
+ <label for="c-user">Username:</label>
+ <input id="c-user" disabled readonly type="text" :value="user.name">
+
+ <div class="pass-box">
+ <label for="c-pass">Password:</label>
+ <input id="c-pass" disabled readonly :type="visible ? 'text' : 'password'" :value="user.pwd">
+ <span @click="visible = !visible"></span>
+ </div>
+ <button @click="confirm2">Reset my password</button>
+ </div>
+
+ <div v-if="step == 7">
+ <h1>The deed is done</h1>
+ The password of account <q>{{user.name}}</q> has been reset.
+
+ <h1>Next steps</h1>
+ To start playing, <a href="https://wiki.themanaworld.org/index.php/Downloads">download ManaPlus</a> and select the server <i>The Mana World</i>
+ </div>
+
+ <div class="g-recaptcha" id="recaptcha-container"
+ :data-sitekey="recaptcha_key"
+ data-size="invisible">
+ </div>
+
+ <script2 src="https://www.google.com/recaptcha/api.js" unload="Reflect.deleteProperty(self, 'grecaptcha')"/>
+ </main>
+</template>
+
+<script lang="ts">
+import { Vue, Component, Prop } from "vue-property-decorator"
+
+@Component
+export default class Recovery extends Vue {
+ step = 1; // no Begin button here
+ notFound = false; // no accounts found
+ visible = false; // password is visible
+ exposed = false; // password has been breached
+ user = {
+ email: "",
+ name: "",
+ pwd: "",
+ pwd2: "",
+ };
+
+ emailToken = "";
+ recaptcha_key = process.env.VUE_APP_RECAPTCHA;
+
+ async mounted () {
+ if (Reflect.has(this.$route.params, "emailToken")) {
+ let token = this.$route.params.emailToken;
+
+ if (/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/i.test(token)) {
+ this.emailToken = token;
+ this.step = 4;
+ } else {
+ this.step = -2;
+ }
+ }
+
+ // already loaded (user returned to this page)
+ if (Reflect.has(self, "grecaptcha")) {
+ await this.$nextTick();
+ (self as any).grecaptcha.render("recaptcha-container", {
+ sitekey: process.env.VUE_APP_RECAPTCHA,
+ size: "invisible",
+ });
+ (self as any).grecaptcha.reset();
+ }
+ }
+
+ async checkEmail () {
+ this.step = Reflect.has(self, "grecaptcha") ? 2 : -1;
+ // XXX: any actual checks needed here?
+ }
+
+ private sleep (milliseconds: number) {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+ }
+
+ async confirm () {
+ (self as any).grecaptcha.execute();
+ let token: string = "";
+
+ // the recaptcha API doesn't play nice with Vue
+ while (!(token = (self as any).grecaptcha.getResponse())) {
+ await this.sleep(1000);
+ }
+
+ const req = new Request(`${process.env.VUE_APP_API}/tmwa/account`, {
+ method: "PUT",
+ mode: "cors",
+ cache: "no-cache",
+ redirect: "follow",
+ referrer: "no-referrer",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "X-CAPTCHA-TOKEN": token,
+ },
+ body: JSON.stringify({
+ email: this.user.email,
+ }),
+ });
+
+ const raw_response = await fetch(req);
+ const response: string = await raw_response.text();
+
+ switch (raw_response.status) {
+ // TODO: don't use alerts: embed the error message on the page
+ case 200:
+ case 201:
+ this.step = 3;
+ break;
+ case 400:
+ self.alert("API: malformed request");
+ document.location.reload();
+ break;
+ case 403:
+ self.alert("Captcha validation failed.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 404:
+ this.notFound = true;
+ this.step = 1;
+ (self as any).grecaptcha.reset();
+ await this.$nextTick();
+ (this.$refs.email as any).focus();
+ break;
+ case 408:
+ this.step = -2;
+ break;
+ case 429:
+ self.alert("Too many requests.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 500:
+ self.alert("Internal server error.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 502:
+ self.alert("Couldn't reach the server.\nPlease try again later");
+ document.location.reload();
+ break;
+ default:
+ self.alert(`Unknown error: ${raw_response.status}`);
+ document.location.reload();
+ break;
+ }
+ }
+
+ async checkUser () {
+ // TODO: check if the token is valid for this username
+ this.step = Reflect.has(self, "grecaptcha") ? 5 : -1;
+ await this.$nextTick();
+ (this.$refs.password as any).focus();
+ }
+
+ // TODO: this is not compatible with Edge! we must polyfill
+ private async sha1 (text: string) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(text);
+ const buffer = await self.crypto.subtle.digest("SHA-1", data);
+ return this.hexString(buffer);
+ }
+
+ // turns a subtlecrypto arraybuffer into a usable hex string
+ private hexString (buffer: ArrayBuffer) {
+ const byteArray = new Uint8Array(buffer);
+ const hexCodes = Array.from(byteArray).map(value =>
+ value.toString(16).padStart(2, "0"));
+
+ return hexCodes.join("");
+ }
+
+ 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 req = new Request(`https://api.pwnedpasswords.com/range/${hash_prefix}`, {
+ mode: "cors",
+ cache: "force-cache",
+ referrer: "no-referrer",
+ });
+
+ const raw_response = await fetch(req);
+ const response: string = await raw_response.text();
+
+ const found = response.split("\n").some(h => {
+ const [hs, times] = h.split(":");
+ return hash_suffix.toUpperCase() === hs.toUpperCase();
+ });
+
+ if (found) {
+ // reset the animation
+ if (this.exposed) {
+ this.exposed = false;
+ await this.$nextTick();
+ }
+
+ this.exposed = true;
+ await this.$nextTick();
+ (this.$refs.password as any).focus();
+ } else {
+ this.exposed = false;
+ this.step = 6;
+ }
+ }
+
+ async confirm2 () {
+ (self as any).grecaptcha.execute();
+ let token: string = "";
+
+ // the recaptcha API doesn't play nice with Vue
+ while (!(token = (self as any).grecaptcha.getResponse())) {
+ await this.sleep(1000);
+ }
+
+ const req = new Request(`${process.env.VUE_APP_API}/tmwa/account`, {
+ method: "PUT",
+ mode: "cors",
+ cache: "no-cache",
+ redirect: "follow",
+ referrer: "no-referrer",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "X-CAPTCHA-TOKEN": token,
+ },
+ body: JSON.stringify({
+ username: this.user.name,
+ password: this.user.pwd,
+ code: this.emailToken,
+ }),
+ });
+
+ const raw_response = await fetch(req);
+ const response: string = await raw_response.text();
+
+ switch (raw_response.status) {
+ // TODO: don't use alerts: embed the error message on the page
+ case 200:
+ case 201:
+ this.step = 7;
+ break;
+ case 400:
+ self.alert("API: malformed request");
+ document.location.reload();
+ break;
+ case 403:
+ self.alert("Captcha validation failed.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 404:
+ self.alert("You are unauthorized to reset the password of this account.\nOnly accounts listed in the email you received can be reset.");
+ this.$router.replace({ name: "support" });
+ break;
+ case 408:
+ this.step = -2;
+ break;
+ case 429:
+ self.alert("Too many requests.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 500:
+ self.alert("Internal server error.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 502:
+ self.alert("Couldn't reach the server.\nPlease try again later");
+ document.location.reload();
+ break;
+ default:
+ self.alert(`Unknown error: ${raw_response.status}`);
+ document.location.reload();
+ break;
+ }
+ }
+}
+</script>
+
+<style scoped>
+/*
+TODO: share the stylesheet with Registration (DRY)
+*/
+form {
+ margin-top: 20px;
+}
+
+main {
+ & > h1 + div {
+ margin-top: 30px;
+ }
+
+ & label {
+ display: block;
+
+ &:nth-of-type(1n + 2) {
+ margin-top: 1em;
+ }
+ }
+
+ & .pass-box {
+ position: relative;
+
+ &:nth-of-type(1n + 2) {
+ margin-top: 1em;
+ }
+ }
+
+ & input {
+ width: calc(100% - 2ch);
+ border: 1px solid #2f2e32;
+ font-size: 15px;
+ padding: 1ch;
+ margin-top: 0.6ch;
+
+ & + .pass-box {
+ margin-top: 1em;
+ }
+
+ & + span {
+ &::after {
+ content: "👁";
+ font-family: monospace;
+ padding: 0 0.5ch 0 0.5ch;
+ }
+
+ position: absolute;
+ right: -1px;
+ top: auto;
+ bottom: 0;
+ font-size: 1.9em;
+ cursor: pointer;
+ }
+
+ &[type="text"] + span {
+ background: rgba(0, 0, 0, 0.2);
+ }
+ }
+
+ & button {
+ margin-top: 1em;
+ width: 100%;
+ background-color: #34B039;
+ border: 1px solid #2f2e32;
+ display: inline-block;
+ cursor: pointer;
+ color: #ffffff;
+ font-size: 15px;
+ font-weight: bold;
+ padding: 1ch;
+ text-decoration: none;
+
+ &:hover {
+ background-color: #2F9E33;
+ }
+ }
+
+ & > div:nth-of-type(1n + 2) {
+ margin-top: 30px;
+ }
+
+ & .exposed {
+ background: rgba(255, 0, 0, 0.1);
+ border: dashed 6px rgba(255, 0, 0, 0.9);
+ padding: 1em;
+ margin: 1em;
+ animation-name: scary;
+ animation-duration: 2s;
+
+ & a {
+ display: block;
+ margin-top: 0.7em;
+ }
+ }
+
+ & .error {
+ padding: 1em;
+ }
+}
+
+@keyframes scary {
+ from {
+ background-color: rgba(255, 0, 0, 0);
+ border-color: rgba(255, 0, 0, 0);
+ }
+
+ to {
+ background-color: rgba(255, 0, 0, 0.1);
+ border-color: rgba(255, 0, 0, 0.9);
+ }
+}
+</style>
diff --git a/src/views/Home.vue b/src/views/Home.vue
new file mode 100644
index 0000000..0033939
--- /dev/null
+++ b/src/views/Home.vue
@@ -0,0 +1,45 @@
+<template>
+ <main class="home">
+ <h1>The Mana World Project</h1>
+ <p>The Mana World (TMW) is a serious effort to create an innovative free and open source MMORPG. TMW uses 2D graphics and aims to create a large and diverse interactive world. It is licensed under the GPL, making sure this game can't ever run away from you.</p>
+ <div class="read-more">
+ <a href="#">Read More >></a>
+ </div>
+
+ <h1>Recent News</h1>
+ <News count="1"/>
+ <div class="read-more">
+ <router-link :to="{ name: 'news' }">More News >></router-link>
+ </div>
+ </main>
+</template>
+
+<style scoped>
+.read-more {
+ text-align: right;
+ padding-right: 8px;
+
+ & a, & a:visited {
+ color: #2f2e32;
+ text-decoration: none;
+ font-weight: bold;
+ font-size: 0.8em;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
+</style>
+
+<script lang="ts">
+import { Component, Vue } from "vue-property-decorator";
+import News from "@/components/News.vue";
+
+@Component({
+ components: {
+ News,
+ },
+})
+export default class Home extends Vue {}
+</script>
diff --git a/src/views/News.vue b/src/views/News.vue
new file mode 100644
index 0000000..24aa689
--- /dev/null
+++ b/src/views/News.vue
@@ -0,0 +1,18 @@
+<template>
+ <main class="main-content">
+ <h1>News</h1>
+ <News count="Infinity"/>
+ </main>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from "vue-property-decorator";
+import News from "@/components/News.vue";
+
+@Component({
+ components: {
+ News,
+ },
+})
+export default class NewsV extends Vue {}
+</script>
diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue
new file mode 100644
index 0000000..b45a4f2
--- /dev/null
+++ b/src/views/NotFound.vue
@@ -0,0 +1,6 @@
+<template>
+ <main>
+ <h1>Page not found</h1>
+ This page does not exist or has been removed
+ </main>
+</template>
diff --git a/src/views/Registration.vue b/src/views/Registration.vue
new file mode 100644
index 0000000..f2c0ad0
--- /dev/null
+++ b/src/views/Registration.vue
@@ -0,0 +1,390 @@
+<template>
+ <main class="registration">
+ <h1>Account creation</h1>
+ Welcome to The Mana World! With this form you can register for a new game account.<br>
+ Please note that you will also need to down and install ManaPlus, our official game client.
+ <br><br>
+ <button v-if="!step" @click="start">Begin!</button>
+
+ <div v-if="step == -1">
+ <h1>reCAPTCHA could not be loaded</h1>
+ This page requires reCAPTCHA but something prevents it from loading.
+ If you are using an ad blocker or tracker blocker please whitelist this page and refresh to continue.
+ </div>
+
+ <!-- XXX: do we want to add the game rules here? -->
+
+ <div v-if="step == 1">
+ <h1>Email address</h1>
+ We will never give your email address to someone else or send you spam.
+ Providing an email address is entirely optional but it is the only way to request a password reset, should you loose access to your account.
+ If you did not provide an email address you will be unable to perform password resets.
+ <form @submit.prevent="checkEmail">
+ <label for="email">Enter your email (optional):</label>
+ <input v-model="user.email" type="email" maxlength="39" id="email" ref="email" placeholder="your@email.com">
+ <button type="submit">Next step &rarr;</button>
+ </form>
+ </div>
+
+ <div v-if="step == 2">
+ <h1>Username</h1>
+ Your username is used to log in to the game server. It is never shared with other players: only you see this name.
+ <p>It must contain between 4 and 23 characters. Letters and numbers only.</p>
+
+ <div class="error taken" v-if="taken">
+ <h2>Username taken</h2>
+ Please choose another username.
+ </div>
+
+ <form @submit.prevent="checkUser">
+ <label for="user">Choose a username:</label>
+ <input @input="taken = false" v-model="user.name" type="text" id="user" ref="user" placeholder="type your username here" minlength="4" maxlength="23" pattern="^[a-zA-Z0-9]{4,23}$" title="4-23 characters, alphanumeric" required>
+ <button type="submit" v-if="user.name">Next step &rarr;</button>
+ </form>
+ </div>
+
+ <div v-if="step == 3">
+ <h1>Password</h1>
+ Please choose a hard-to-guess password.
+ <p>It must contain between 8 and 23 characters. Letters and numbers only. Case-sensitive.</p>
+
+ <div v-if="exposed" class="exposed">
+ <h2>WARNING: This password is compromised</h2>
+ This password has previously appeared in a data breach. Please use a more secure alternative.
+ <a href="https://haveibeenpwned.com/Passwords" target="_blank" rel="noopener">verified by haveibeenpwned.com</a>
+ </div>
+
+ <form @submit.prevent="checkPassword">
+ <div class="pass-box">
+ <label for="password">Choose a unique password:</label>
+ <input v-model="user.pwd" :type="visible ? 'text' : 'password'" id="password" ref="password" placeholder="type your password here" minlength="8" maxlength="23" pattern="^[a-zA-Z0-9]{8,23}$" title="8-23 characters, alphanumeric" required>
+ <span @click="visible = !visible"></span>
+ </div>
+ <div class="pass-box">
+ <label for="password2">Confirm your password:</label>
+ <input v-model="user.pwd2" :type="visible ? 'text' : 'password'" id="password2" ref="password2" placeholder="type your password again" minlength="8" maxlength="23" pattern="^[a-zA-Z0-9]{8,23}$" title="8-23 characters, alphanumeric" required>
+ <span @click="visible = !visible"></span>
+ </div>
+ <button type="submit" v-if="user.pwd && user.pwd === user.pwd2">Next step &rarr;</button>
+ </form>
+ </div>
+
+ <div v-if="step == 4">
+ <h1>Confirm</h1>
+ <label for="c-email">Email address:</label>
+ <input id="c-email" disabled readonly type="email" :value="user.email" placeholder="(no email)">
+
+ <label for="c-user">Username:</label>
+ <input id="c-user" disabled readonly type="text" :value="user.name">
+
+ <div class="pass-box">
+ <label for="c-pass">Password:</label>
+ <input id="c-pass" disabled readonly :type="visible ? 'text' : 'password'" :value="user.pwd">
+ <span @click="visible = !visible"></span>
+ </div>
+ <button @click="create">Create account</button>
+ </div>
+
+ <div v-if="step == 5">
+ <h1>Thank you</h1>
+ Your account has been successfully created.
+
+ <h1>Next steps</h1>
+ To start playing, <a href="https://wiki.themanaworld.org/index.php/Downloads">download ManaPlus</a> and select the server <i>The Mana World</i>
+ </div>
+
+ <div class="g-recaptcha" id="recaptcha-container"
+ :data-sitekey="recaptcha_key"
+ data-size="invisible">
+ </div>
+
+ <script2 src="https://www.google.com/recaptcha/api.js" unload="Reflect.deleteProperty(self, 'grecaptcha')"/>
+ </main>
+</template>
+
+<script lang="ts">
+import Vue from "vue"
+import Component from "vue-class-component"
+
+@Component({
+ beforeRouteLeave: (to, from, next) => {
+ next();
+ }
+})
+export default class Registration extends Vue {
+ step = 0;
+ visible = false; // password is visible or hidden
+ exposed = false; // password has been leaked
+ taken = false; // username is taken
+ user = {
+ email: "",
+ name: "",
+ pwd: "",
+ pwd2: "",
+ };
+
+ recaptcha_key = process.env.VUE_APP_RECAPTCHA;
+
+ async mounted () {
+ // already loaded (user returned to this page)
+ if (Reflect.has(self, "grecaptcha")) {
+ await this.$nextTick();
+ (self as any).grecaptcha.render("recaptcha-container", {
+ sitekey: process.env.VUE_APP_RECAPTCHA,
+ size: "invisible",
+ });
+ (self as any).grecaptcha.reset();
+ }
+ }
+
+ async start () {
+ this.step = Reflect.has(self, "grecaptcha") ? 1 : -1;
+ await this.$nextTick();
+ (this.$refs.email as any).focus();
+ }
+
+ async checkEmail () {
+ this.step = 2;
+ await this.$nextTick();
+ (this.$refs.user as any).focus();
+ }
+
+ async checkUser () {
+ // TODO: check here whether the username is taken
+ this.step = 3;
+ await this.$nextTick();
+ (this.$refs.password as any).focus();
+ }
+
+ // TODO: this is not compatible with Edge! we must polyfill
+ private async sha1 (text: string) {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(text);
+ const buffer = await self.crypto.subtle.digest("SHA-1", data);
+ return this.hexString(buffer);
+ }
+
+ // turns a subtlecrypto arraybuffer into a usable hex string
+ private hexString (buffer: ArrayBuffer) {
+ const byteArray = new Uint8Array(buffer);
+ const hexCodes = Array.from(byteArray).map(value =>
+ value.toString(16).padStart(2, "0"));
+
+ return hexCodes.join("");
+ }
+
+ 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 req = new Request(`https://api.pwnedpasswords.com/range/${hash_prefix}`, {
+ mode: "cors",
+ cache: "force-cache",
+ referrer: "no-referrer",
+ });
+
+ const raw_response = await fetch(req);
+ const response: string = await raw_response.text();
+
+ const found = response.split("\n").some(h => {
+ const [hs, times] = h.split(":");
+ return hash_suffix.toUpperCase() === hs.toUpperCase();
+ });
+
+ if (found) {
+ // reset the animation
+ if (this.exposed) {
+ this.exposed = false;
+ await this.$nextTick();
+ }
+
+ this.exposed = true;
+ await this.$nextTick();
+ (this.$refs.password as any).focus();
+ } else {
+ this.exposed = false;
+ this.step = 4;
+ }
+ }
+
+ sleep (milliseconds: number) {
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
+ }
+
+ async create () {
+ (self as any).grecaptcha.execute();
+ let token: string = "";
+
+ // the recaptcha API doesn't play nice with Vue
+ while (!(token = (self as any).grecaptcha.getResponse())) {
+ await this.sleep(1000);
+ }
+
+ const req = new Request(`${process.env.VUE_APP_API}/tmwa/account`, {
+ method: "POST",
+ mode: "cors",
+ cache: "no-cache",
+ redirect: "follow",
+ referrer: "no-referrer",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "X-CAPTCHA-TOKEN": token,
+ },
+ body: JSON.stringify({
+ username: this.user.name,
+ password: this.user.pwd,
+ email: this.user.email,
+ }),
+ });
+
+ const raw_response = await fetch(req);
+ const response: string = await raw_response.text();
+
+ switch (raw_response.status) {
+ // TODO: don't use alerts: embed the error message on the page
+ case 201:
+ this.step = 5;
+ break;
+ case 400:
+ self.alert("API: malformed request");
+ document.location.reload();
+ break;
+ case 403:
+ self.alert("Captcha validation failed.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 409:
+ this.taken = true;
+ this.step = 2;
+ await this.$nextTick();
+ (this.$refs.user as any).focus();
+ break;
+ case 429:
+ self.alert("Too many requests.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 500:
+ self.alert("Internal server error.\nPlease try again later");
+ document.location.reload();
+ break;
+ case 502:
+ self.alert("Couldn't reach the server.\nPlease try again later");
+ document.location.reload();
+ break;
+ default:
+ self.alert(`Unknown error: ${raw_response.status}`);
+ document.location.reload();
+ break;
+ }
+ }
+}
+</script>
+
+<style scoped>
+form {
+ margin-top: 20px;
+}
+
+.registration {
+ & label {
+ display: block;
+
+ &:nth-of-type(1n + 2) {
+ margin-top: 1em;
+ }
+ }
+
+ & .pass-box {
+ position: relative;
+
+ &:nth-of-type(1n + 2) {
+ margin-top: 1em;
+ }
+ }
+
+ & input {
+ width: calc(100% - 2ch);
+ border: 1px solid #2f2e32;
+ font-size: 15px;
+ padding: 1ch;
+ margin-top: 0.6ch;
+
+ & + .pass-box {
+ margin-top: 1em;
+ }
+
+ & + span {
+ &::after {
+ content: "👁";
+ font-family: monospace;
+ padding: 0 0.5ch 0 0.5ch;
+ }
+
+ position: absolute;
+ right: -1px;
+ top: auto;
+ bottom: 0;
+ font-size: 1.9em;
+ cursor: pointer;
+ }
+
+ &[type="text"] + span {
+ background: rgba(0, 0, 0, 0.2);
+ }
+ }
+
+ & button {
+ margin-top: 1em;
+ width: 100%;
+ background-color: #34B039;
+ border: 1px solid #2f2e32;
+ display: inline-block;
+ cursor: pointer;
+ color: #ffffff;
+ font-size: 15px;
+ font-weight: bold;
+ padding: 1ch;
+ text-decoration: none;
+
+ &:hover {
+ background-color: #2F9E33;
+ }
+ }
+
+ & > div:nth-of-type(1n + 2) {
+ margin-top: 30px;
+ }
+
+ & .exposed {
+ background: rgba(255, 0, 0, 0.1);
+ border: dashed 6px rgba(255, 0, 0, 0.9);
+ padding: 1em;
+ margin: 1em;
+ animation-name: scary;
+ animation-duration: 2s;
+
+ & a {
+ display: block;
+ margin-top: 0.7em;
+ }
+ }
+
+ & .error {
+ padding: 1em;
+ }
+}
+
+@keyframes scary {
+ from {
+ background-color: rgba(255, 0, 0, 0);
+ border-color: rgba(255, 0, 0, 0);
+ }
+
+ to {
+ background-color: rgba(255, 0, 0, 0.1);
+ border-color: rgba(255, 0, 0, 0.9);
+ }
+}
+</style>
diff --git a/src/views/Support.vue b/src/views/Support.vue
new file mode 100644
index 0000000..458ffe0
--- /dev/null
+++ b/src/views/Support.vue
@@ -0,0 +1,49 @@
+<template>
+ <main class="support">
+ <h1>Support</h1>
+ <p>Please select your issue below. If you cannot find your issue, contact us.</p>
+
+ <h1>Account problems</h1>
+ <ul>
+ <li><router-link to="/recover/password">I forgot my password</router-link></li>
+ <li><router-link to="/recover/username">I forgot my user name</router-link></li>
+ <li><a href="https://forums.themanaworld.org/viewtopic.php?f=20&t=7559">My account is banned</a></li>
+ <li><a href="https://forums.themanaworld.org/viewtopic.php?f=20&t=6472">My account was compromised</a></li>
+ </ul>
+
+ <h1 id="contact">Contact us</h1>
+ <p>On IRC: <a href="https://forums.themanaworld.org/viewforum.php?f=41" target="_blank" rel="noopener">#themanaworld on Freenode</a></p>
+ <p>On Discord: <a href="https://forums.themanaworld.org/viewforum.php?f=65" target="_blank" rel="noopener">The Mana World server</a></p>
+ <p>On the forums: <a href="https://forums.themanaworld.org/viewforum.php?f=3">Support and Bug reports</a></p>
+
+ <h1>Technical contacts</h1>
+ Legal inquiries: <address>legal@themanaworld.org</address>
+ DMCA takedown requests: <address>legal@themanaworld.org</address>
+ GDPR requests: <address>legal@themanaworld.org</address>
+ Security disclosures:
+ <address>
+ security@themanaworld.org
+ <span v-if="PGP">(PGP: <a :href="`http://pgp.mit.edu/pks/lookup?op=get&search=${PGP}`" rel="noopener">{{PGP}}</a>)</span>
+ </address>
+ </main>
+</template>
+
+<style scoped>
+address {
+ font-family: monospace;
+
+ &:not(:last-of-type) {
+ margin-bottom: 1.5em;
+ }
+}
+</style>
+
+<script lang="ts">
+import Vue from "vue"
+import Component from "vue-class-component"
+
+@Component
+export default class Copyright extends Vue {
+ PGP = process.env.VUE_APP_PGP;
+}
+</script>