diff options
Diffstat (limited to 'src')
40 files changed, 1370 insertions, 0 deletions
diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..3fa5037 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,14 @@ +<template> + <div id="q-app"> + <router-view /> + </div> +</template> + +<script> +export default { + name: 'App' +} +</script> + +<style> +</style> diff --git a/src/assets/quasar-logo-full.svg b/src/assets/quasar-logo-full.svg new file mode 100644 index 0000000..281d072 --- /dev/null +++ b/src/assets/quasar-logo-full.svg @@ -0,0 +1,191 @@ +<?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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="67.407623mm" + height="62.908276mm" + viewBox="0 0 238.84591 222.90334" + id="svg3570" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="quasar-logo-full.svg"> + <defs + id="defs3572" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="-39.753589" + inkscape:cy="27.706388" + inkscape:document-units="px" + inkscape:current-layer="g4895-4-4" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1056" + inkscape:window-x="0" + inkscape:window-y="24" + inkscape:window-maximized="1" /> + <metadata + id="metadata3575"> + <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 /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-277.71988,-312.33911)"> + <g + id="g4895-4-4" + transform="translate(1419.0442,398.9018)"> + <g + transform="translate(-29.620665,-4)" + id="g4579-2-20"> + <g + id="g4445-2-0" + transform="translate(12.499948,7.809312)"> + <g + inkscape:export-ydpi="44.860481" + inkscape:export-xdpi="44.860481" + inkscape:export-filename="/home/emanuele/Desktop/logo1.png" + transform="translate(-712.85583,-503.26814)" + id="g4561-6-7-0"> + <g + transform="translate(16.233481,0)" + style="font-style:normal;font-weight:normal;font-size:50.25774765px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#263238;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="flowRoot4513-6-6-08"> + <path + d="m -402.73125,631.46823 q -0.6125,0.0438 -1.3125,0.0875 -0.65625,0 -1.4,0 l -9.31875,0 q -12.81875,0 -12.81875,-8.44375 l 0,-13.475 q 0,-8.26875 12.6,-8.26875 l 9.75625,0 q 12.6,0 12.6,8.26875 l 0,13.475 q 0,5.03125 -4.4625,7.04375 l 3.10625,2.14375 q 1.35625,0.83125 1.35625,1.70625 0,0.875 -0.7,1.3125 -0.65625,0.48125 -1.88125,0.48125 -0.30625,0 -0.7875,-0.13125 -0.4375,-0.0875 -1.05,-0.48125 l -5.6875,-3.71875 z m 5.38125,-21.74375 q 0,-4.76875 -7.9625,-4.76875 l -9.58125,0 q -7.9625,0 -7.9625,4.76875 l 0,13.3875 q 0,4.94375 8.3125,4.94375 l 8.88125,0 q 8.3125,0 8.3125,-4.94375 l 0,-13.3875 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1" + id="path3428" /> + <path + d="m -368.0585,631.64323 q -11.2875,0 -11.2875,-6.9125 l 0,-12.73125 q 0,-1.8375 2.31875,-1.8375 2.31875,0 2.31875,1.8375 l 0,12.775 q 0,3.325 6.475,3.325 l 8.3125,0 q 6.475,0 6.475,-3.325 l 0,-12.775 q 0,-1.8375 2.31875,-1.8375 2.3625,0 2.3625,1.8375 l 0,12.73125 q 0,6.9125 -11.2875,6.9125 l -8.00625,0 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1" + id="path3430" /> + <path + d="m -327.2833,631.64323 q -9.3625,0 -9.3625,-5.81875 l 0,-2.49375 q 0,-5.775 9.3625,-5.775 l 18.59375,0 0,-0.65625 q 0,-3.0625 -5.38125,-3.0625 l -6.16875,0 q -2.1875,0 -2.1875,-1.70625 0,-1.75 2.1875,-1.75 l 6.16875,0 q 9.93125,0 9.93125,6.51875 l 0,8.575 q 0,6.16875 -9.5375,6.16875 l -13.60625,0 z m 13.34375,-3.4125 q 5.25,0 5.25,-2.8875 l 0,-4.76875 -18.24375,0 q -5.11875,0 -5.11875,2.66875 l 0,2.275 q 0,2.7125 5.11875,2.7125 l 12.99375,0 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1" + id="path3432" /> + <path + d="m -262.77031,626.74323 q 0,4.9 -9.975,4.9 l -17.0625,0 q -2.1875,0 -2.1875,-1.70625 0,-1.70625 2.1875,-1.70625 l 17.0625,0 q 5.38125,0 5.38125,-1.4875 l 0,-2.45 q 0,-1.4875 -5.38125,-1.4875 l -9.0125,0 q -9.975,0 -9.975,-4.76875 l 0,-2.05625 q 0,-5.6 10.28125,-5.6 l 5.99375,0 q 2.23125,0 2.23125,1.75 0,0.875 -0.6125,1.3125 -0.56875,0.39375 -1.61875,0.39375 l -5.99375,0 q -5.73125,0 -5.73125,2.14375 l 0,1.925 q 0,1.79375 5.6875,1.79375 l 9.0125,0 q 9.7125,0 9.7125,4.4625 l 0,2.58125 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1" + id="path3434" /> + <path + d="m -241.91709,631.64323 q -9.3625,0 -9.3625,-5.81875 l 0,-2.49375 q 0,-5.775 9.3625,-5.775 l 18.59375,0 0,-0.65625 q 0,-3.0625 -5.38125,-3.0625 l -6.16875,0 q -2.1875,0 -2.1875,-1.70625 0,-1.75 2.1875,-1.75 l 6.16875,0 q 9.93125,0 9.93125,6.51875 l 0,8.575 q 0,6.16875 -9.5375,6.16875 l -13.60625,0 z m 13.34375,-3.4125 q 5.25,0 5.25,-2.8875 l 0,-4.76875 -18.24375,0 q -5.11875,0 -5.11875,2.66875 l 0,2.275 q 0,2.7125 5.11875,2.7125 l 12.99375,0 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1" + id="path3436" /> + <path + d="m -205.62285,617.33698 q 0,-6.95625 11.2875,-6.95625 l 3.36875,0 q 2.23125,0 2.23125,1.79375 0,1.79375 -2.23125,1.79375 l -3.54375,0 q -6.475,0 -6.475,3.28125 l 0,12.775 q 0,1.8375 -2.31875,1.8375 -2.31875,0 -2.31875,-1.8375 l 0,-12.6875 z" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1" + id="path3438" /> + </g> + </g> + </g> + </g> + <g + id="g5443-0-1-5-1-9" + transform="matrix(0.55595317,0,0,0.55595317,-521.93484,-328.66104)" + inkscape:export-filename="/home/emanuele/Desktop/logo1.png" + inkscape:export-xdpi="44.860481" + inkscape:export-ydpi="44.860481"> + <g + inkscape:export-ydpi="3.4165223" + inkscape:export-xdpi="3.4165223" + transform="matrix(0.09527033,0,0,0.09527033,-1695.2716,706.62921)" + id="g8856-6-1-1-9-0-1-9"> + <circle + r="1485" + cy="-1361.2571" + cx="8317.3574" + id="circle8858-1-3-7-6-5-3-0" + style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:50;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:export-xdpi="10.031387" + inkscape:export-ydpi="10.031387" /> + <path + inkscape:export-ydpi="10.031387" + inkscape:export-xdpi="10.031387" + style="opacity:1;fill:#263238;fill-opacity:1;stroke:none;stroke-width:10;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 8560.3823,-1361.3029 a 242.947,242.947 0 0 1 -242.947,242.948 242.947,242.947 0 0 1 -242.947,-242.948 242.947,242.947 0 0 1 242.947,-242.946 242.947,242.947 0 0 1 242.947,242.946 z" + id="path8860-5-4-8-2-9-0-9" + inkscape:connector-curvature="0" /> + <path + id="path8862-5-5-9-1-3-6-3" + d="m 9395.8755,-1984.028 a 1245.372,1245.372 0 0 0 -190.8415,-249.4971 l -280.8618,162.1556 c -87.542,-74.7796 -187.0349,-132.0588 -293.2407,-169.9527 -95.8868,97.1766 -172.0602,205.7604 -226.9672,323.8487 312.6411,-21.2772 635.5313,91.8725 935.2898,326.0721 l 176.7612,-102.0532 a 1245.372,1245.372 0 0 0 -120.1398,-290.5734 z" + clip-path="none" + mask="none" + style="fill:#1976d2;fill-opacity:1" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-514.04855" + inkscape:transform-center-y="-444.04649" /> + <path + inkscape:transform-center-y="265.80217" + inkscape:transform-center-x="-689.63727" + inkscape:connector-curvature="0" + style="fill:#42a5f5;fill-opacity:1" + mask="none" + clip-path="none" + d="m 9395.9474,-738.70387 a 1245.372,1245.372 0 0 0 120.6501,-290.02213 l -280.8618,-162.1557 c 20.99,-113.2034 20.8488,-228.0063 0.563,-338.9302 -132.1008,-34.4521 -264.2238,-46.1283 -393.9448,-34.635 174.7471,260.1165 238.2017,596.32248 185.2582,973.02076 l 176.7612,102.05309 a 1245.372,1245.372 0 0 0 191.5741,-249.33082 z" + id="path8864-4-8-1-2-4-4-4" /> + <path + id="path8866-7-5-5-0-6-4-7" + d="m 8317.501,-115.97954 a 1245.372,1245.372 0 0 0 311.4916,-40.52501 l 0,-324.31131 c 108.5321,-38.42382 207.8837,-95.94755 293.8037,-168.97752 -36.214,-131.6287 -92.1636,-251.88868 -166.9776,-358.48372 -137.894,281.39369 -397.3296,504.44998 -750.0316,646.9487 l 0,204.10623 a 1245.372,1245.372 0 0 0 311.7139,41.24263 z" + clip-path="none" + mask="none" + style="fill:#1976d2;fill-opacity:1" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-117.49007" + inkscape:transform-center-y="639.34029" /> + <path + inkscape:transform-center-y="444.04652" + inkscape:transform-center-x="514.04857" + inkscape:connector-curvature="0" + style="fill:#42a5f5;fill-opacity:1" + mask="none" + clip-path="none" + d="m 7238.9827,-738.57936 a 1245.372,1245.372 0 0 0 190.8415,249.49714 l 280.8618,-162.15566 c 87.5421,74.77965 187.0349,132.05879 293.2407,169.95271 95.8868,-97.17659 172.0602,-205.76036 226.9672,-323.8487 -312.6411,21.27714 -635.5313,-91.87254 -935.2898,-326.07203 l -176.7612,102.0531 a 1245.372,1245.372 0 0 0 120.1398,290.57344 z" + id="path8868-6-7-4-7-2-7-3" /> + <path + id="path8870-5-3-9-3-5-5-1" + d="m 7238.9108,-1983.9035 a 1245.372,1245.372 0 0 0 -120.6501,290.0221 l 280.8618,162.1557 c -20.99,113.2035 -20.8488,228.0063 -0.563,338.9302 132.1008,34.4521 264.2238,46.1283 393.9448,34.635 -174.7471,-260.1165 -238.2017,-596.3225 -185.2582,-973.0207 l -176.7612,-102.0532 a 1245.372,1245.372 0 0 0 -191.5741,249.3309 z" + clip-path="none" + mask="none" + style="fill:#1976d2;fill-opacity:1" + inkscape:connector-curvature="0" + inkscape:transform-center-x="689.63729" + inkscape:transform-center-y="-265.80221" /> + <path + inkscape:transform-center-y="-639.34032" + inkscape:transform-center-x="117.49005" + inkscape:connector-curvature="0" + style="fill:#42a5f5;fill-opacity:1" + mask="none" + clip-path="none" + d="m 8317.3572,-2606.6279 a 1245.372,1245.372 0 0 0 -311.4915,40.525 l -1e-4,324.3113 c -108.5321,38.4239 -207.8837,95.9476 -293.8037,168.9776 36.214,131.6287 92.1637,251.8886 166.9776,358.4837 137.894,-281.3937 397.3296,-504.45 750.0316,-646.9487 l 1e-4,-204.1063 a 1245.372,1245.372 0 0 0 -311.714,-41.2426 z" + id="path8872-6-3-2-1-3-3-7" /> + </g> + </g> + </g> + </g> +</svg> diff --git a/src/assets/sad.svg b/src/assets/sad.svg new file mode 100644 index 0000000..628136f --- /dev/null +++ b/src/assets/sad.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166.7 168.9" width="166.7" height="168.9" isolation="isolate"><defs><clipPath><rect width="166.7" height="168.9"/></clipPath></defs><g clip-path="url(#_clipPath_PPPiEcORhRSYwopEENnaRFzzeYSXmwtt)"><path d="M65.6 135.2C65.6 137.1 64.1 138.6 62.2 138.6 60.4 138.6 58.9 137.1 58.9 135.2 58.9 130.7 61.9 126.7 66.8 124 71.1 121.6 77 120.1 83.3 120.1 89.7 120.1 95.6 121.6 99.9 124 104.7 126.7 107.8 130.7 107.8 135.2 107.8 137.1 106.3 138.6 104.4 138.6 102.6 138.6 101.1 137.1 101.1 135.2 101.1 133.3 99.4 131.3 96.6 129.8 93.3 127.9 88.6 126.8 83.3 126.8 78.1 126.8 73.4 127.9 70 129.8 67.3 131.3 65.6 133.3 65.6 135.2ZM149.2 153.3C149.2 157.6 147.5 161.5 144.6 164.4 141.8 167.2 137.9 168.9 133.6 168.9 129.3 168.9 125.4 167.2 122.6 164.4 120.9 162.8 119.7 160.9 118.9 158.7 114.1 161 109 162.8 103.7 164.1 97.2 165.8 90.4 166.6 83.3 166.6 60.3 166.6 39.5 157.3 24.4 142.2 9.3 127.1 0 106.3 0 83.3 0 60.3 9.3 39.5 24.4 24.4 39.5 9.3 60.3 0 83.3 0 106.4 0 127.2 9.3 142.3 24.4 157.3 39.5 166.7 60.3 166.7 83.3 166.7 94.5 164.5 105.1 160.5 114.9 156.6 124.2 151.1 132.7 144.4 140 147 145.1 149.2 150.2 149.2 153.3ZM130.7 126.3C131.1 125.5 131.8 125 132.5 124.8L132.6 124.7 132.6 124.7 132.7 124.7 132.7 124.7 132.8 124.7 132.9 124.6 132.9 124.6 132.9 124.6 133 124.6 133 124.6C133 124.6 133.1 124.6 133.1 124.6L133.1 124.6 133.2 124.6 133.2 124.6C133.9 124.5 134.6 124.6 135.2 125 135.8 125.3 136.3 125.8 136.6 126.4L136.6 126.4 136.6 126.4 136.6 126.4 136.6 126.4 136.6 126.4 136.6 126.5 136.6 126.5 136.6 126.5 136.6 126.5 136.6 126.5 136.7 126.5C137 127.2 137.7 128.3 138.4 129.6L138.4 129.6 138.5 129.7 138.5 129.7 138.6 129.8 138.6 129.9 138.6 129.9 138.7 130 138.7 130.1 138.7 130.1 138.7 130.1 138.8 130.2 138.8 130.2 138.8 130.3 138.9 130.3 138.9 130.4 138.9 130.4 138.9 130.4 139 130.5 139 130.5 139 130.6 139.1 130.7 139.1 130.7 139.1 130.7 139.2 130.8 139.2 130.8 139.2 130.9C139.8 131.8 140.4 132.9 141 133.9 146.5 127.6 151.1 120.3 154.3 112.4 158 103.4 160 93.6 160 83.3 160 62.1 151.4 43 137.6 29.1 123.7 15.2 104.5 6.7 83.3 6.7 62.2 6.7 43 15.2 29.1 29.1 15.2 43 6.7 62.1 6.7 83.3 6.7 104.5 15.2 123.6 29.1 137.5 43 151.4 62.2 160 83.3 160 89.8 160 96.1 159.2 102.1 157.7 107.8 156.2 113.1 154.2 118.1 151.7L118.1 151.6 118.2 151.6 118.2 151.3 118.2 151.3 118.3 151 118.3 151 118.4 150.7 118.4 150.6 118.5 150.4 118.5 150.3 118.5 150 118.6 149.9 118.6 149.7 118.7 149.6 118.8 149.3C118.9 148.9 119 148.5 119.1 148.2L119.2 148.1 119.3 147.8 119.3 147.7 119.4 147.4 119.4 147.4C119.5 147.1 119.6 146.9 119.7 146.7L119.7 146.6 119.8 146.3 119.9 146.2 120 145.9 120.1 145.9C120.2 145.6 120.3 145.3 120.4 145.1L120.4 145.1 120.6 144.7 120.6 144.6 120.7 144.3 120.8 144.2 120.9 143.9 120.9 143.8 121 143.8 121.1 143.5 121.1 143.4 121.2 143.2 121.3 143 121.4 143C121.6 142.6 121.7 142.2 122 141.8L122 141.7 122.2 141.4 122.2 141.3 122.4 140.9 122.4 140.9 122.6 140.5 122.6 140.5 122.8 140.1 123 139.8 123 139.7 123 139.7 123.4 138.9 123.5 138.9 123.6 138.6 123.7 138.4 123.8 138.3 123.9 138 124 137.9 124.2 137.6 124.2 137.5 124.4 137.2 124.4 137.2 124.6 136.8 124.6 136.8 124.8 136.4 124.8 136.4 125 136.1 125.1 136 125.2 135.7 125.3 135.6 125.4 135.3 125.5 135.2 125.6 135 125.7 134.8 125.8 134.6 125.9 134.4 126.2 134 126.2 133.9 126.4 133.6 126.4 133.6 126.6 133.3 126.6 133.2 126.8 132.9 126.8 132.9 127 132.5 127 132.5 127.3 132.2 127.4 131.9 127.4 131.8 127.6 131.6 127.7 131.5 127.8 131.3 127.9 131.1 128 131 128.1 130.8 128.1 130.6 128.3 130.4 128.3 130.4 128.5 130.1 128.5 130.1 128.7 129.8 128.7 129.8 128.8 129.5 128.8 129.5 128.9 129.4 128.9 129.3 129 129.3 129 129.2 129 129.1 129 129.1 129.1 129 129.1 129 129.2 128.9 129.2 128.9 129.2 128.8 129.2 128.8 129.3 128.8 129.3 128.8 129.3 128.7 129.3 128.7 129.3 128.7 129.3 128.7 129.4 128.6 129.4 128.6 129.4 128.5 129.4 128.5 129.4 128.4 129.5 128.4 129.5 128.4 129.5 128.4 129.5 128.4 129.5 128.3 129.5 128.3 129.6 128.2 129.6 128.2 129.6 128.2 129.6 128.2 129.6 128.1 129.6 128.1 129.7 128.1 129.7 128.1 129.7 128 129.7 128 129.8 127.9 129.8 127.9 129.8 127.9 129.8 127.9 129.8 127.8 129.8 127.8 129.8 127.8 129.8 127.8 129.9 127.7 129.9 127.7 129.9 127.7 129.9 127.7 129.9 127.6 129.9 127.6 130 127.6 130 127.6 130 127.5 130 127.5 130 127.4 130 127.4 130.1 127.4 130.1 127.4 130.1 127.4 130.1 127.4 130.1 127.3 130.1 127.3 130.1 127.3 130.1 127.3 130.2 127.2 130.2 127.2 130.2 127.2 130.2 127.2 130.2 127.1 130.2 127.1 130.2 127.1 130.2 127.1 130.3 127 130.3 127 130.3 127 130.3 127 130.3 127 130.3 127 130.4 126.9 130.4 126.9 130.4 126.9 130.4 126.9 130.4 126.8 130.4 126.8 130.4 126.8 130.4 126.8 130.4 126.8 130.4 126.8 130.5 126.7 130.5 126.7 130.5 126.7 130.5 126.7 130.5 126.7 130.5 126.7 130.5 126.6 130.5 126.6 130.5 126.6 130.5 126.6 130.6 126.5 130.6 126.5 130.6 126.5 130.6 126.5 130.6 126.5 130.6 126.5 130.6 126.4 130.6 126.4 130.7 126.4 130.7 126.4 130.7 126.4 130.7 126.4 130.7 126.3 130.7 126.3 130.7 126.3 130.7 126.3ZM140 159.6C141.5 158.1 142.6 155.8 142.6 153.3 142.6 151 140.1 146 137.4 141.1L137.4 141.1 137.4 141.1 137.4 141.1C137 140.4 136.7 139.8 136.3 139.1L136.2 139 136.2 138.9 136.1 138.9 136.1 138.8 136 138.5 135.9 138.5C135.2 137.2 134.5 136.1 133.9 135L133.8 134.9 133.8 134.8 133.8 134.8 133.7 134.7 133.6 134.6 133.6 134.5 133.4 134.8 133.3 135.1 133.3 135.1 133.1 135.4 133.1 135.4 132.9 135.7 132.7 136 132.7 136 132.5 136.3 132.5 136.3 132.4 136.6 132.2 136.9 132.2 136.9 132 137.2 131.8 137.5 131.8 137.5 131.6 137.9 131.6 137.9 131.4 138.2 131.4 138.2 131.2 138.5 131 138.9 131 138.9 130.8 139.2 130.8 139.2 130.7 139.5 130.7 139.5 130.5 139.9 130.5 139.9 130.3 140.2 130.1 140.5 130.1 140.5 129.9 140.9 129.9 140.9 129.7 141.2 129.7 141.2 129.6 141.5 129.4 141.9 129.2 142.2 129.2 142.2 129 142.6 129 142.6 128.8 142.9 128.6 143.2 128.6 143.2 128.5 143.6 128.3 143.9 128.3 143.9 128.1 144.3 128.1 144.3 127.9 144.6 127.9 144.6 127.8 144.9 127.6 145.2 127.4 145.6 127.3 145.9 127.3 145.9 127.1 146.2 127 146.5 127 146.5 126.8 146.8 126.8 146.8 126.7 147.2 126.7 147.2 126.5 147.5 126.5 147.5 126.4 147.8 126.4 147.8 126.3 148.1 126.1 148.4 126 148.6 126 148.6 125.9 149 125.9 149 125.7 149.3 125.7 149.5 125.7 149.5 125.6 149.8 125.6 149.8 125.4 150 125.4 150 125.3 150.3 125.3 150.3 125.3 150.6 125.3 150.6 125.2 150.8 125.2 150.8 125.1 151.1 125.1 151.1 125 151.3 125 151.3 125 151.6 125 151.6 124.9 151.8 124.9 151.8 124.8 152 124.8 152 124.8 152.2 124.8 152.2 124.8 152.4 124.8 152.4C124.7 152.5 124.7 152.5 124.7 152.6L124.7 152.6 124.7 152.8 124.7 152.8C124.7 152.9 124.7 152.9 124.7 153L124.7 153 124.6 153.2 124.6 153.2 124.6 153.3 124.6 153.4C124.7 155.9 125.7 158.1 127.3 159.7 128.9 161.3 131.1 162.3 133.6 162.3 136.1 162.3 138.3 161.3 140 159.6ZM135.3 72.7C136.2 74.3 135.6 76.3 133.9 77.2 132.3 78 130.3 77.4 129.4 75.8 128.7 74.3 127.6 72.9 126.3 71.9 125 70.8 123.4 70.1 121.8 69.6L121.8 69.6C120.8 69.4 119.8 69.2 118.9 69.2 117.8 69.2 116.8 69.3 115.8 69.5 114 69.9 112.3 68.8 111.8 67 111.5 65.2 112.6 63.5 114.4 63 115.8 62.7 117.4 62.6 118.9 62.6 120.5 62.6 122 62.8 123.4 63.2L123.6 63.2C126.1 63.9 128.4 65.1 130.4 66.7 132.5 68.3 134.1 70.4 135.3 72.7ZM37.2 75.8C36.4 77.4 34.4 78 32.7 77.2 31.1 76.3 30.5 74.3 31.3 72.7 32.5 70.4 34.2 68.3 36.2 66.7 38.2 65.1 40.6 63.9 43.1 63.2L43.2 63.2C44.7 62.8 46.2 62.6 47.7 62.6 49.3 62.6 50.8 62.7 52.3 63 54.1 63.5 55.2 65.2 54.8 67 54.4 68.8 52.6 69.9 50.9 69.5 49.9 69.3 48.8 69.2 47.8 69.2 46.8 69.2 45.8 69.4 44.9 69.6L44.9 69.6C43.2 70.1 41.7 70.8 40.4 71.9 39.1 72.9 38 74.3 37.2 75.8ZM125.2 92.7C125.2 90.7 124.5 88.9 123.3 87.6 122.2 86.5 120.6 85.7 119 85.7 117.3 85.7 115.8 86.5 114.7 87.6 113.5 88.9 112.8 90.7 112.8 92.7 112.8 94.6 113.5 96.4 114.7 97.7 115.8 98.9 117.3 99.6 119 99.6 120.6 99.6 122.2 98.9 123.3 97.7 124.5 96.4 125.2 94.6 125.2 92.7ZM128.2 83.2C130.4 85.6 131.8 89 131.8 92.7 131.8 96.4 130.4 99.7 128.2 102.2 125.8 104.7 122.6 106.3 119 106.3 115.4 106.3 112.1 104.7 109.8 102.2 107.5 99.7 106.1 96.4 106.1 92.7 106.1 89 107.5 85.6 109.8 83.2 112.1 80.6 115.4 79.1 119 79.1 122.6 79.1 125.8 80.6 128.2 83.2ZM53.9 92.7C53.9 90.7 53.2 88.9 52 87.6 50.9 86.5 49.4 85.7 47.7 85.7 46 85.7 44.5 86.5 43.4 87.6 42.2 88.9 41.5 90.7 41.5 92.7 41.5 94.6 42.2 96.4 43.4 97.7 44.5 98.9 46 99.6 47.7 99.6 49.4 99.6 50.9 98.9 52 97.7 53.2 96.4 53.9 94.6 53.9 92.7ZM56.9 83.2C59.2 85.6 60.5 89 60.5 92.7 60.5 96.4 59.2 99.7 56.9 102.2 54.5 104.7 51.3 106.3 47.7 106.3 44.1 106.3 40.9 104.7 38.5 102.2 36.2 99.7 34.8 96.4 34.8 92.7 34.8 89 36.2 85.6 38.5 83.2 40.9 80.6 44.1 79.1 47.7 79.1 51.3 79.1 54.5 80.6 56.9 83.2Z" fill="rgb(1,22,39)" fill-opacity="0.2"/></g></svg> diff --git a/src/components/.gitkeep b/src/components/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/components/.gitkeep diff --git a/src/components/ApiAuth.vue b/src/components/ApiAuth.vue new file mode 100644 index 0000000..d2771c7 --- /dev/null +++ b/src/components/ApiAuth.vue @@ -0,0 +1,113 @@ +<template> + <div> + <div v-if="!$store.getters['user/loggedIn']"> + <div v-if="useLarryLogin" class="group"> + <h5>Larry Login</h5> + <p v-if="authError" class="text-negative"> + <span v-if="typeof authError === 'object'">Email or password {{ authError['email or password'] }}</span> + <span v-else>{{ authError }}</span> + </p> + <div class="group"> + <q-input @keyup.enter="larryLogin" :error="authError !== undefined" v-model="email" name="user" float-label="Email" /> + <q-input @keyup.enter="larryLogin" :error="authError !== undefined" v-model="password" name="pass" type="password" float-label="Password" /> + <q-btn color="positive" @click="larryLogin" icon="fa-sign-in">Larry login</q-btn> + </div> + <div class="text-right"> + <q-btn color="tertiary" small outline @click="useLarryLogin = false">Log in via Clonkspot</q-btn> + </div> + </div> + <div v-else class="group"> + <q-btn color="positive" @click="clonkspotLogin">Log in via Clonkspot</q-btn> + <p class="text-negative" v-if="authFailed">Oups, something went wrong. Please try again</p> + <div class="text-right"> + <q-btn color="tertiary" small outline @click="useLarryLogin = true">Log in via Larry</q-btn> + </div> + </div> + </div> + <div v-else> + <q-card> + <q-card-title class="bg-positive text-white"> + Hi {{ $store.state.user.decodedToken.username }} + </q-card-title> + <q-card-separator /> + <q-card-main> + <p>You have successfully logged in!</p> + <q-btn color="negative" outline small icon="fa-sign-out" @click="$store.commit('user/logout')">Logout</q-btn> + </q-card-main> + </q-card> + </div> + </div> +</template> + +<script> + import { + openURL, + LocalStorage, + } from 'quasar' + + export default { + data () { + return { + email: '', + password: '', + authError: undefined, + token: null, + useLarryLogin: false, + authWindow: null, + authFailed: false, + } + }, + mounted () { + let token = LocalStorage.get.item('authToken') + if (token) { + this.$store.commit('user/setAuthToken', { authToken: token }) + } + }, + methods: { + clonkspotLogin () { + this.authWindow = openURL(`${this.$http.defaults.baseURL}/auth/clonkspot`) + this.checkTokenLoop() + }, + larryLogin () { + let that = this + let params = { + user: { + email: this.email, + password: this.password, + } + } + this.authError = undefined + this.$http.post('/auth/login', params).then((response) => { + that.$store.commit('user/setAuthToken', { authToken: response.data.user.token }) + that.email = '' + that.password = '' + }).catch((error) => { + if (error.response && error.response.data) { + that.authError = error.response.data.errors + } + else if (error.message) { + that.authError = error.message + } + else { + that.authError = 'Failed to log in' + console.error({error}) + } + }) + }, + checkTokenLoop (retryCount) { + let token = LocalStorage.get.item('authToken') + if (token) { + this.$store.commit('user/setAuthToken', { authToken: token }) + this.authWindow.close() + this.authWindow = null + } + else { + setTimeout(this.checkTokenLoop, 500) + } + }, + }, + } +</script> + +<style lang="styl" type="text/stylus" scoped> +</style> diff --git a/src/components/UploadVoter.vue b/src/components/UploadVoter.vue new file mode 100644 index 0000000..91019dd --- /dev/null +++ b/src/components/UploadVoter.vue @@ -0,0 +1,50 @@ +<template> + <div> + <div class="flex row items-center group" v-if="$store.getters['user/loggedIn']"> + <h5 style="margin: 0 1rem"> + <i class="fa" + :class="{'fa-caret-up text-positive': upload.voting.sum > 0, 'fa-caret-down text-negative': upload.voting.sum < 0, 'fa-sort text-dark': upload.voting.sum === 0}"> + </i> + {{ upload.voting.sum }} + </h5> + <span v-if="upload.author.username !== $store.state.user.decodedToken.username"> + <q-btn color="negative" round small icon="fa-thumbs-down" @click="vote(-1)"></q-btn> + <q-btn color="positive" round small icon="fa-thumbs-up" @click="vote(1)"></q-btn> + </span> + </div> + <div class="group" v-else> + <i class="fa" + :class="{'fa-caret-up text-positive': upload.voting.sum > 0, 'fa-caret-down text-negative': upload.voting.sum < 0, 'fa-sort text-dark': upload.voting.sum === 0}"> + </i> + {{ upload.voting.sum }} votes + <em>Please log in to vote</em> + </div> + </div> +</template> + +<script> + import { + QBtn, + } from 'quasar' + + export default { + props: { + upload: Object, + }, + components: { + QBtn, + }, + data () { + return {} + }, + methods: { + vote (impact) { + let that = this + this.$http.put(`/uploads/${this.upload._id}/vote`, {vote: {impact: impact}}).then(response => that.$emit('voted')) + }, + }, + } +</script> + +<style lang="styl" type="text/stylus" scoped> +</style> diff --git a/src/css/app.styl b/src/css/app.styl new file mode 100644 index 0000000..e3e5a1e --- /dev/null +++ b/src/css/app.styl @@ -0,0 +1 @@ +// app global css diff --git a/src/css/themes/common.variables.styl b/src/css/themes/common.variables.styl new file mode 100644 index 0000000..4d659aa --- /dev/null +++ b/src/css/themes/common.variables.styl @@ -0,0 +1,25 @@ +// App Shared Variables +// -------------------------------------------------- +// To customize the look and feel of this app, you can override +// the Stylus variables found in Quasar's source Stylus files. Setting +// variables before Quasar's Stylus will use these variables rather than +// Quasar's default Stylus variable values. Stylus variables specific +// to the themes belong in either the variables.ios.styl or variables.mat.styl files. + +// Check documentation for full list of Quasar variables + + +// App Shared Color Variables +// -------------------------------------------------- +// It's highly recommended to change the default colors +// to match your app's branding. + +$primary = #027be3 +$secondary = #26A69A +$tertiary = #555 + +$neutral = #E0E1E2 +$positive = #21BA45 +$negative = #DB2828 +$info = #31CCEC +$warning = #F2C037 diff --git a/src/css/themes/variables.ios.styl b/src/css/themes/variables.ios.styl new file mode 100644 index 0000000..953b825 --- /dev/null +++ b/src/css/themes/variables.ios.styl @@ -0,0 +1,7 @@ +// App Shared Variables +// -------------------------------------------------- +// Shared Stylus variables go in the common.variables.styl file +@import 'common.variables' + +// iOS only Quasar variables overwrites +// ----------------------------------------- diff --git a/src/css/themes/variables.mat.styl b/src/css/themes/variables.mat.styl new file mode 100644 index 0000000..8169a52 --- /dev/null +++ b/src/css/themes/variables.mat.styl @@ -0,0 +1,7 @@ +// App Shared Variables +// -------------------------------------------------- +// Shared Stylus variables go in the common.variables.styl file +@import 'common.variables' + +// Material only Quasar variables overwrites +// ----------------------------------------- diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js new file mode 100644 index 0000000..b70b80f --- /dev/null +++ b/src/i18n/en/index.js @@ -0,0 +1,7 @@ +// This is just an example, +// so you can safely delete all default props below + +export default { + failed: 'Action failed', + success: 'Action was successful' +} diff --git a/src/i18n/index.js b/src/i18n/index.js new file mode 100644 index 0000000..1efd82d --- /dev/null +++ b/src/i18n/index.js @@ -0,0 +1,5 @@ +import en from './en' + +export default { + en +} diff --git a/src/index.template.html b/src/index.template.html new file mode 100644 index 0000000..43bee86 --- /dev/null +++ b/src/index.template.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="format-detection" content="telephone=no"> + <meta name="msapplication-tap-highlight" content="no"> + <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (htmlWebpackPlugin.options.ctx.mode.cordova) { %>, viewport-fit=cover<% } %>"> + <title><%= htmlWebpackPlugin.options.productName %></title> + <base href="<%= htmlWebpackPlugin.options.appBase %>"> + + <link rel="icon" href="statics/quasar-logo.png" type="image/x-icon"> + <link rel="icon" type="image/png" sizes="32x32" href="statics/icons/favicon-32x32.ico"> + <!--<link rel="icon" type="image/png" sizes="16x16" href="statics/icons/favicon-16x16.png">--> + + <% if (htmlWebpackPlugin.options.ctx.mode.pwa) { %> + <!-- Add to home screen for Android and modern mobile browsers --> + <link rel="manifest" href="manifest.json"> + <meta name="theme-color" content="<%= htmlWebpackPlugin.options.pwaManifest.theme_color %>"> + <!-- Add to home screen for Safari on iOS --> + <meta name="apple-mobile-web-app-capable" content="yes"> + <meta name="apple-mobile-web-app-status-bar-style" content="<%= htmlWebpackPlugin.options.pwaManifest.background_color %>"> + <meta name="apple-mobile-web-app-title" content="<%= htmlWebpackPlugin.options.pwaManifest.name %>"> + <link rel="apple-touch-icon" href="statics/icons/apple-icon-152x152.png"> + <!-- Add to home screen for Windows --> + <meta name="msapplication-TileImage" content="statics/icons/ms-icon-144x144.png"> + <meta name="msapplication-TileColor" content="<%= htmlWebpackPlugin.options.pwaManifest.background_color %>"> + <% } %> + + <%= htmlWebpackPlugin.options.headScripts %> + + <!-- + The following is optional if you DON'T build for PWA. + Preloads/prefetches chunks/assets. + --> + <% if (!['cordova', 'electron'].includes(htmlWebpackPlugin.options.ctx.modeName) && htmlWebpackPlugin.options.ctx.prod) { + for (var chunk of webpack.chunks) { + for (var file of chunk.files) { + if (file.match(/\.(js|css)$/)) { %> + <link rel="<%= chunk.initial ? 'preload' : 'prefetch' %>" href="<%= file %>" as="<%= file.match(/\.css$/)? 'style' : 'script' %>"> + <% }}}} %> + </head> + <body> + <% if (!htmlWebpackPlugin.options.ctx.mode.electron) { %> + <noscript> + This is your fallback content in case JavaScript fails to load. + </noscript> + <% } %> + + <!-- DO NOT touch the following <div> --> + <div id="q-app"></div> + + <!-- DO NOT touch the following --> + <%= htmlWebpackPlugin.options.bodyScripts %> + + <!-- built files will be auto injected here --> + </body> +</html> diff --git a/src/layouts/default.vue b/src/layouts/default.vue new file mode 100644 index 0000000..9276e0b --- /dev/null +++ b/src/layouts/default.vue @@ -0,0 +1,69 @@ +<template> + <q-layout view="lHh Lpr lFf"> + <q-layout-header> + <q-toolbar color="primary" glossy> + + <q-toolbar-title> + Larry UI + <div slot="subtitle">Official source of mods for OpenClonk</div> + </q-toolbar-title> + + <q-toolbar-title v-if="$store.getters['user/loggedIn']"> + Hi {{ $store.state.user.decodedToken.username }} + </q-toolbar-title> + <q-btn flat icon="fa-sign-out" v-if="$store.getters['user/loggedIn']" @click="$store.commit('user/logout')"> + Logout + </q-btn> + </q-toolbar> + + <q-tabs> + <q-route-tab slot="title" + icon="fa-list" + :to="{name: 'upload-list'}" + replace + hide="icon" + label="Browse mods" /> + + <q-route-tab slot="title" + icon="fa-upload" + :to="{name: 'upload-create'}" + replace hide="icon" + label="Upload mod" /> + </q-tabs> + </q-layout-header> + + <q-page-container> + <q-page padding> + <div class="flex row items-end reverse-wrap gutter"> + <div class="col"> + <router-view></router-view> + </div> + <div class="col-auto" v-if="!$store.getters['user/loggedIn']"> + <api-auth></api-auth> + </div> + </div> + </q-page> + </q-page-container> + </q-layout> +</template> + +<script> + + import ApiAuth from 'components/ApiAuth' + + export default { + name: 'LayoutDefault', + components: { + ApiAuth, + }, + data () { + return { + } + }, + methods: { + } + } +</script> + +<style> +</style> diff --git a/src/pages/404.vue b/src/pages/404.vue new file mode 100644 index 0000000..6484cb2 --- /dev/null +++ b/src/pages/404.vue @@ -0,0 +1,16 @@ +<template> + <div class="fixed-center text-center"> + <p> + <img + src="~assets/sad.svg" + style="width:30vw;max-width:150px;" + > + </p> + <p class="text-faded">Sorry, nothing here...<strong>(404)</strong></p> + <q-btn + color="secondary" + style="width:200px;" + @click="$router.push('/')" + >Go back</q-btn> + </div> +</template> diff --git a/src/pages/FetchToken.vue b/src/pages/FetchToken.vue new file mode 100644 index 0000000..6745377 --- /dev/null +++ b/src/pages/FetchToken.vue @@ -0,0 +1,68 @@ +<template> + <div> + <div v-if="!tokenValid"> + <h6 class="text-negative">Received an invalid token!</h6> + <p>{{ tokenData.message }}</p> + </div> + <div v-else> + <h6 class="text-success">you have successfully logged in. You can close this window now!</h6> + </div> + </div> +</template> + +<script> + import jwtDecode from 'jwt-decode' + + export default { + components: { + }, + data () { + return { + tokenValid: false, + } + }, + computed: { + routeToken () { + return this.$route.params.token + }, + tokenData () { + try { + return jwtDecode(this.routeToken) + } + catch (e) { + return { + invalid: true, + message: e.message + } + } + }, + }, + watch: { + tokenData: { + handler: function (val, oldVal) { + console.log({val, oldVal}) + if (val !== oldVal) { + if (val.invalid || !val.username) { + this.tokenValid = false + } + else { + let that = this + this.$http.get('/uploads', {headers: {Authorization: `Bearer ${this.routeToken}`}}).then(response => { + that.tokenValid = true + that.$store.commit('user/setAuthToken', { authToken: that.routeToken }) + window.close() + }).catch(error => { + console.warn(error) + that.tokenValid = false + }) + } + } + }, + immediate: true, + }, + } + } +</script> + +<style lang="styl" type="text/stylus" scoped> +</style> diff --git a/src/pages/UploadCreate.vue b/src/pages/UploadCreate.vue new file mode 100644 index 0000000..53a6a36 --- /dev/null +++ b/src/pages/UploadCreate.vue @@ -0,0 +1,264 @@ +<template> + <div class="flex row"> + <div class="col" v-if="authToken"> + <h5>Upload your own mod</h5> + <q-stepper ref="stepper" style="max-width: 800px" alternative-labels> + <q-step default name="data" title="Enter data" :error="errors.slug || errors.title || errors.description"> + <p>Please enter a title and description for your mod</p> + <div class="text-negative" v-if="errors.slug"> + {{ errors.slug }} + </div> + <q-field :error="errors.title !== undefined" + :error-label="errors.title" + icon="fa-pencil"> + <q-input v-model="title" float-label="Title" /> + </q-field> + <q-field :error="errors.description !== undefined" + :error-label="errors.description" + icon="fa-bars"> + <q-input v-model="description" type="textarea" float-label="Description" /> + </q-field> + <q-stepper-navigation> + <q-btn color="primary" :disabled="!description || !title" @click="$refs.stepper.next()">Next</q-btn> + </q-stepper-navigation> + </q-step> + <q-step name="pic" title="Add image"> + <p>You can upload an image, e.g. a screenshot, that will be used as a preview for your mod</p> + <q-alert color="warning" + enter="bounceInLeft" + appear> + Please note that you must not upload anything protected by copyrights you do not own or don't have explicit permission to upload by the owner + </q-alert> + <dropzone id="picDropzone" + ref="picDropzone" + @vdropzone-success="picUploadSuccess" + :options="picDropzoneOptions"> + </dropzone> + <q-stepper-navigation> + <q-btn color="primary" flat @click="$refs.stepper.previous()">Back</q-btn> + <q-btn color="primary" @click="$refs.stepper.next()">Next</q-btn> + <transition enter="fadeIn" leave="fadeOut"> + <q-btn v-if="pic" + icon-right="fa-trash-o" + color="negative" + outline + @click="$refs.picDropzone.removeAllFiles();pic = null;$refs.picDropzone.enable()"> + Use another image + </q-btn> + </transition> + </q-stepper-navigation> + </q-step> + <q-step name="files" title="Add files"> + <p>Now please upload your mod files (e.g. the .ocs file). You have to upload at least one file</p> + <q-alert color="warning" + enter="bounceInLeft" + appear> + Please note that you must not upload anything protected by copyrights you do not own or don't have explicit permission to upload by the owner + </q-alert> + <dropzone id="filesDropzone" + ref="filesDropzone" + @vdropzone-success="uploadSuccess" + :options="filesDropzoneOptions"> + </dropzone> + <table class="q-table horizontal-separator" v-if="files.length > 0"> + <thead> + <tr> + <th>Filename</th> + <th>ID</th> + <th>Size</th> + <th></th> + <th></th> + </tr> + </thead> + <tbody> + <tr v-for="(file, idx) of files" :key="file._id"> + <td>{{ file.filename }}</td> + <td>{{ file._id }}</td> + <td>{{ file.length | prettyBytes }}</td> + <td>uploaded {{ file.uploadDate|moment("from") }}</td> + <td><q-btn round color="negative" small icon="fa-trash" @click="deleteFile(idx)"></q-btn></td> + </tr> + </tbody> + </table> + <q-stepper-navigation> + <q-btn color="primary" flat @click="$refs.stepper.previous()">Back</q-btn> + <q-btn color="primary" :disabled="!files.length" @click="$refs.stepper.next()">Next</q-btn> + </q-stepper-navigation> + </q-step> + <q-step name="final" title="Finalize upload"> + <q-field :error="errors.copyright" + :error-label="errors.copyright"> + <q-checkbox v-model="copyright" + label="I hereby confirm that I am the copyright owner of all content I upload, or that the copyright owner has granted me permission to do so" /> + </q-field> + <q-stepper-navigation> + <q-btn color="primary" flat @click="$refs.stepper.previous()">Back</q-btn> + <q-btn color="positive" icon="fa-save" @click="postUpload" :disabled="!uploadEnabled">Save mod</q-btn> + </q-stepper-navigation> + </q-step> + <q-step name="done" title="Done"> + <h6 class="text-positive" v-if="savedScenario"> + Successfully saved your mod with the id {{ savedScenario._id }} + </h6> + <q-stepper-navigation> + <q-btn color="warning" icon="fa-refresh" @click="reset">Upload another one</q-btn> + <q-btn color="primary" + icon="fa-eye" + @click="$router.push({name: 'upload-detail', params: {uploadId: savedScenario._id}})"> + Show the saved mod + </q-btn> + </q-stepper-navigation> + </q-step> + <q-inner-loading :visible="saving" /> + </q-stepper> + </div> + <div v-else> + Please log in first + </div> + </div> +</template> + +<script> + import Dropzone from 'vue2-dropzone' + import 'vue2-dropzone/dist/vue2Dropzone.css' + + import { + LocalStorage, + } from 'quasar' + + export default { + components: { + Dropzone, + }, + data () { + return { + files: [], + title: '', + description: '', + slug: '', + errors: {}, + pic: null, + savedScenario: null, + selectedFile: null, + copyright: false, + saving: false, + } + }, + mounted () { + this.files = LocalStorage.get.item('uploadedFiles') || [] + }, + computed: { + authToken () { + return this.$store.state.user.authToken + }, + filesDropzoneOptions () { + return { + url: `${this.$http.defaults.baseURL}/media`, + paramName: 'media', + headers: { Authorization: `Bearer ${this.$store.state.user.authToken}` }, + acceptedFiles: '.ocs,.ocf,.ocd,.ocg,.ocr,.c4d,.c4g,.c4f,.c4r,.c4s,c4v', + dictDefaultMessage: "<p><i class='fa fa-3x fa-cloud-upload'></i></p><p>Drop your mod files here or click to upload</p>", + maxFilesize: 30, // MB + } + }, + picDropzoneOptions () { + return { + url: `${this.$http.defaults.baseURL}/media`, + paramName: 'media', + headers: { Authorization: `Bearer ${this.$store.state.user.authToken}` }, + acceptedFiles: '.png,.jpg', + dictDefaultMessage: "<p><i class='fa fa-3x fa-cloud-upload'></i></p><p>Drop your image here or click to upload</p>", + maxFilesize: 3, // MB + maxFiles: 1, + thumbnailWidth: null, + thumbnailHeight: 300, + createImageThumbnails: true, + thumbnailMethod: 'contain', + } + }, + uploadEnabled () { + return this.files.length > 0 && this.copyright + }, + picSrc () { + if (!this.pic) { + return '' + } + return `${this.$http.defaults.baseURL}/media/${this.pic._id}` + }, + }, + watch: { + files (val, oldVal) { + if (!oldVal || val.length !== oldVal.length) { + LocalStorage.set('uploadedFiles', this.files) + } + }, + }, + methods: { + deleteFile (idx) { + let file = this.files[idx] + this.files.splice(idx, 1) + this.$http.delete(`${this.$http.defaults.baseURL}/media/${file._id}`) + }, + picUploadSuccess (file, response) { + this.pic = response + this.$refs.picDropzone.disable() + }, + uploadSuccess (file, response) { + console.log({success: file, xhr: response}) + this.files.push(response) + this.$refs.filesDropzone.removeFile(file) + }, + postUpload () { + let that = this + this.saving = true + this.errors = {} + if (!this.copyright) { + this.errors.copyright = 'You have to accept the copyright notice' + this.saving = false + that.$refs.stepper.goToStep('final') + return + } + if (this.files.length === 0) { + this.errors.files = 'Please upload at least one file' + this.saving = false + that.$refs.stepper.goToStep('files') + return + } + let params = { + upload: { + title: this.title, + description: this.description, + files: this.files.map(el => el._id), + pic: this.pic, + }, + } + this.$http.post('/uploads', params) + .then((response) => { + that.savedScenario = response.data + that.saving = false + that.$refs.stepper.goToStep('done') + }) + .catch((error) => { + that.errors.slug = (error.response.data.error.errors.slug || {}).message + that.errors.title = (error.response.data.error.errors.title || {}).message + that.errors.description = (error.response.data.error.errors.description || {}).message + that.saving = false + that.$refs.stepper.goToStep('data') + }) + }, + reset () { + this.title = '' + this.description = '' + this.savedScenario = '' + this.errors = {} + this.pic = null + this.files = [] + }, + }, + } +</script> + +<style lang="styl" type="text/stylus" scoped> + .indent + margin: 1rem 3rem 2rem 3rem +</style> diff --git a/src/pages/UploadDetail.vue b/src/pages/UploadDetail.vue new file mode 100644 index 0000000..d742a4a --- /dev/null +++ b/src/pages/UploadDetail.vue @@ -0,0 +1,223 @@ +<template> + <div> + <q-btn class="pull-right" @click="$router.push({name: 'upload-list'})" outline icon="fa-list">Back to list</q-btn> + <div v-if="upload" class="row"> + <q-card class="col-6"> + <q-card-media v-if="upload.pic" overlay-position="top"> + <q-card-title slot="overlay"> + Mod: {{ upload.title }} + <span slot="subtitle">by {{ upload.author.username }}</span> + <span slot="right" class="text-white" style="margin-left: 3rem">updated {{ upload.updatedAt | moment("from") }}</span> + </q-card-title> + <img :src="`${$http.defaults.baseURL}/media/${upload.pic}`"> + </q-card-media> + <q-card-title class="bg-positive text-white" v-else> + {{ upload.title }} + <span slot="subtitle" class="text-light">by {{ upload.author.username }}</span> + <span slot="right" class="text-light" style="margin-left: 3rem">updated {{ upload.updatedAt | moment("from") }}</span> + </q-card-title> + <q-card-separator /> + <q-card-main> + <p class="text-faded description"> + {{ upload.description }} + </p> + </q-card-main> + <q-card-actions> + <q-btn @click="openInOpenclonk" color="positive" outline> + Install mod with OpenClonk + </q-btn> + <q-btn v-if="$store.state.user.decodedToken.username === upload.author.username" + outline + color="negative" + icon="fa-trash-o" + @click="deleteUpload(upload)"> + Delete mod + </q-btn> + </q-card-actions> + <q-card-media v-if="upload.pic" overlay-position="bottom"> + <q-card-title slot="overlay"> + Voting + </q-card-title> + <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150"> + </q-parallax> + </q-card-media> + <q-card-title v-else> + Voting + </q-card-title> + <q-card-main> + <div class="group"> + <upload-voter :upload="upload" @voted="refresh"></upload-voter> + </div> + </q-card-main> + <q-card-media v-if="upload.pic" overlay-position="bottom"> + <q-card-title slot="overlay"> + Dependencies + </q-card-title> + <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150"> + </q-parallax> + </q-card-media> + <q-card-title v-else> + Dependencies + </q-card-title> + <q-card-main> + <q-btn disabled flat v-if="upload.dependency.length === 0"> + No dependencies + </q-btn> + <div class="group" v-else> + <div v-for="d of upload.dependency" + :key="d._id"> + <q-btn @click="$router.push({name: 'upload-detail', params: {uploadId: d._id}})" no-caps outline> + {{ d.title }} + </q-btn> + </div> + </div> + </q-card-main> + <q-card-media v-if="upload.pic" overlay-position="bottom"> + <q-card-title slot="overlay"> + File downloads + </q-card-title> + <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150"> + </q-parallax> + </q-card-media> + <q-card-title v-else> + File downloads + </q-card-title> + <q-card-main> + <q-btn disabled flat v-if="upload.file.length === 0"> + No files + </q-btn> + <div class="group" + v-else + v-for="fid of upload.file" + :key="fid._id"> + <q-btn loader + no-caps + color="primary" + :percentage="(downloadProgresses[fid._id] || {}).percentage" + @click="(event, done) => {downloadMedia(fid._id, fid.filename, done)}"> + {{ fid.filename }} ({{ fid.length|prettyBytes }}) + <span slot="loading">Downloading...</span> + </q-btn> + <span v-if="downloadProgresses[fid._id]"> + <q-transition enter="fadeIn" leave="fadeOut" mode="out-in"> + <span key="sizeDownloaded" v-if="downloadProgresses[fid._id].percentage < 100"> + {{ downloadProgresses[fid._id].loaded|prettyBytes }} / {{ downloadProgresses[fid._id].total|prettyBytes }} + </span> + <span key="downloadDone" v-else><i class="fa fa-check fa-2x text-positive"></i></span> + </q-transition> + </span> + </div> + </q-card-main> + <q-card-media v-if="upload.pic" overlay-position="bottom"> + <q-card-title slot="overlay"> + Other data + </q-card-title> + <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150"> + </q-parallax> + </q-card-media> + <q-card-title v-else> + Other data + </q-card-title> + <q-card-main> + <p>ID: {{ upload._id }}</p> + </q-card-main> + </q-card> + </div> + <div v-else> + <q-spinner size="50"></q-spinner> Loading mod data... + </div> + </div> +</template> + +<script> + import { + Dialog, + openURL, + } from 'quasar' + import UploadVoter from 'components/UploadVoter' + import FileSaver from 'file-saver' + + export default { + components: { + UploadVoter, + }, + computed: { + routeId () { + return this.$route.params.uploadId + }, + }, + watch: { + routeId: { + handler (val, oldVal) { + if (val && val !== oldVal) { + this.refresh() + } + }, + immediate: true, + }, + }, + data () { + return { + upload: null, + downloadProgresses: {}, + } + }, + methods: { + refresh () { + let that = this + this.$http.get(`/uploads/${this.routeId}`).then(response => { + that.upload = response.data + }) + }, + deleteUpload () { + let that = this + Dialog.create({ + title: 'Delete mod?', + message: `Do you really want to delete the mod ${this.upload.title}?<br>This cannot be undone!`, + buttons: [ + 'Cancel', + { + label: '<i class="fa fa-trash-o"></i> Yes, delete!', + color: 'negative', + outline: true, + handler () { + that.$http.delete(`/uploads/${that.routeId}`).then(response => that.$router.push({name: 'upload-list'})) + } + } + ] + }) + }, + downloadMedia (mediaId, filename, done) { + console.log({mediaId, done}) + let that = this + this.$set(this.downloadProgresses, mediaId, {}) + this.$set(this.downloadProgresses[mediaId], 'done', done) + this.$set(this.downloadProgresses[mediaId], 'percentage', 0) + this.$set(this.downloadProgresses[mediaId], 'loaded', 0) + this.$set(this.downloadProgresses[mediaId], 'total', 0) + this.$http.get( + `${this.$http.defaults.baseURL}/media/${mediaId}`, + { + responseType: 'blob', + onDownloadProgress: progressEvent => { that.downloadProgress(mediaId, progressEvent) }, + } + ).then((response) => { + FileSaver.saveAs(response.data, filename) + that.downloadProgresses[mediaId].done() + that.downloadProgresses[mediaId].percentage = 100 + }) + }, + downloadProgress (mediaId, progressEvent) { + this.downloadProgresses[mediaId].percentage = progressEvent.loaded * 100 / progressEvent.total + this.downloadProgresses[mediaId].loaded = progressEvent.loaded + this.downloadProgresses[mediaId].total = progressEvent.total + }, + openInOpenclonk () { + openURL(`openclonk://installmod/${this.upload._id}`) + } + } + } +</script> + +<style lang="styl" type="text/stylus" scoped> +</style> diff --git a/src/pages/UploadList.vue b/src/pages/UploadList.vue new file mode 100644 index 0000000..a8a731c --- /dev/null +++ b/src/pages/UploadList.vue @@ -0,0 +1,119 @@ +<template> + <div> + <transition :enter="showList ? 'fadeInLeft' : 'fadeInRight'" :leave="showList ? 'fadeOutRight' : 'fadeOutLeft'" mode="out-in" duration="500"> + <div v-if="showList"> + <h4>Available Mods</h4> + <q-table + no-data-label="No mods available" + :data="uploads" + :config="tableConfig" + :columns="tableColumns" + @refresh="refresh"> + <template slot='col-action' slot-scope='cell'> + <q-btn color="primary" + small + outline + icon="fa-info-circle" + @click="$router.push({name: 'upload-detail', params: {uploadId: cell.row._id}})"> + Details</q-btn> + <q-btn color="negative" + outline + small + icon="fa-trash-o" + v-if="cell.row.author.username === $store.state.user.decodedToken.username" + @click="deleteUpload(cell.row)"> + Delete</q-btn> + </template> + <template slot='col-voting' slot-scope='cell'> + {{ cell.data.sum }} <i class="fa" :class="{'fa-caret-up text-positive': cell.data.sum > 0, 'fa-caret-down text-negative': cell.data.sum < 0, 'fa-sort text-dark': cell.data.sum === 0}"></i> + </template> + </q-table> + </div> + <router-view></router-view> + </transition> + </div> +</template> + +<script> + import { + Dialog, + } from 'quasar' + import Truncate from 'vue-truncate-collapsed' + import moment from 'moment' + + export default { + components: { + Truncate, + }, + computed: { + showList () { + return this.$route.name === 'upload-list' + } + }, + watch: { + showList: { + handler (val, oldVal) { + if (val && val !== oldVal) { + this.refresh() + } + }, + immediate: true, + } + }, + data () { + return { + response: {}, + uploads: [], + tableConfig: { + refresh: true, + leftStickyColumns: 1, + rightStickyColumns: 3, + rowHeight: '60px', + }, + tableColumns: [ + {label: 'Title', field: 'title', width: '100px'}, + {label: 'Description', field: 'description', width: '500px'}, + {label: 'Author', field: 'author', width: '100px', format: el => el.username}, + {label: 'Voting', field: 'voting', width: '100px'}, + {label: 'Last update', field: 'updatedAt', width: '100px', format: el => moment(el).from()}, + {label: 'Actions', field: 'action', width: '200px'}, + ], + } + }, + methods: { + refresh (done) { + let that = this + this.$http.get('/uploads').then((response) => { + that.response = response + that.uploads = response.data.uploads + if (done) { + done() + } + }) + }, + deleteUpload (upload) { + let that = this + Dialog.create({ + title: 'Delete mod?', + message: `Do you really want to delete the mod ${upload.title}?<br>This cannot be undone!`, + buttons: [ + 'Cancel', + { + label: '<i class="fa fa-trash-o"></i> Yes, delete!', + color: 'negative', + outline: true, + handler () { + that.$http.delete(`/uploads/${upload._id}`).then(response => that.refresh()) + } + } + ] + }) + }, + }, + } +</script> + +<style lang="styl" type="text/stylus" scoped> + .description + max-width: 400px +</style> diff --git a/src/plugins/.gitkeep b/src/plugins/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/plugins/.gitkeep diff --git a/src/plugins/axios.js b/src/plugins/axios.js new file mode 100644 index 0000000..e2957b2 --- /dev/null +++ b/src/plugins/axios.js @@ -0,0 +1,7 @@ +import axios from 'axios' + +export default ({ Vue }) => { + axios.defaults.headers.common['Accept'] = 'application/json' + axios.defaults.baseURL = process.env.API + Vue.prototype.$http = axios +} diff --git a/src/plugins/i18n.js b/src/plugins/i18n.js new file mode 100644 index 0000000..8ddacc5 --- /dev/null +++ b/src/plugins/i18n.js @@ -0,0 +1,13 @@ +import VueI18n from 'vue-i18n' +import messages from 'src/i18n' + +export default ({ app, Vue }) => { + Vue.use(VueI18n) + + // Set i18n instance on app + app.i18n = new VueI18n({ + locale: 'en', + fallbackLocale: 'en', + messages + }) +} diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..d9fe66a --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' + +import routes from './routes' + +Vue.use(VueRouter) + +const Router = new VueRouter({ + /* + * NOTE! Change Vue Router mode from quasar.conf.js -> build -> vueRouterMode + * + * If you decide to go with "history" mode, please also set "build.publicPath" + * to something other than an empty string. + * Example: '/' instead of '' + */ + + // Leave as is and change from quasar.conf.js instead! + mode: process.env.VUE_ROUTER_MODE, + base: process.env.VUE_ROUTER_BASE, + scrollBehavior: () => ({ y: 0 }), + routes +}) + +export default Router diff --git a/src/router/routes.js b/src/router/routes.js new file mode 100644 index 0000000..ece2c57 --- /dev/null +++ b/src/router/routes.js @@ -0,0 +1,37 @@ + +export default [ + { + path: '/', + component: () => import('layouts/default'), + redirect: {name: 'upload-list'}, + children: [ + { + path: 'uploads', + name: 'upload-list', + component: () => import('pages/UploadList'), + children: [ + { + path: ':uploadId', + name: 'upload-detail', + component: () => import('pages/UploadDetail'), + }, + ] + }, + { + path: 'upload-create', + name: 'upload-create', + component: () => import('pages/UploadCreate'), + }, + { + path: 'receive-token/:token', + name: 'receive-token', + component: () => import('pages/FetchToken'), + }, + ] + }, + + { // Always leave this as last one + path: '*', + component: () => import('pages/404') + } +] diff --git a/src/statics/icons/apple-icon-152x152.png b/src/statics/icons/apple-icon-152x152.png Binary files differnew file mode 100644 index 0000000..c918acd --- /dev/null +++ b/src/statics/icons/apple-icon-152x152.png diff --git a/src/statics/icons/favicon-16x16.png b/src/statics/icons/favicon-16x16.png Binary files differnew file mode 100644 index 0000000..177c86e --- /dev/null +++ b/src/statics/icons/favicon-16x16.png diff --git a/src/statics/icons/favicon-32x32.ico b/src/statics/icons/favicon-32x32.ico Binary files differnew file mode 100644 index 0000000..f9a7f2b --- /dev/null +++ b/src/statics/icons/favicon-32x32.ico diff --git a/src/statics/icons/icon-128x128.png b/src/statics/icons/icon-128x128.png Binary files differnew file mode 100644 index 0000000..590e8ce --- /dev/null +++ b/src/statics/icons/icon-128x128.png diff --git a/src/statics/icons/icon-192x192.png b/src/statics/icons/icon-192x192.png Binary files differnew file mode 100644 index 0000000..2a9b50b --- /dev/null +++ b/src/statics/icons/icon-192x192.png diff --git a/src/statics/icons/icon-256x256.png b/src/statics/icons/icon-256x256.png Binary files differnew file mode 100644 index 0000000..8450959 --- /dev/null +++ b/src/statics/icons/icon-256x256.png diff --git a/src/statics/icons/icon-384x384.png b/src/statics/icons/icon-384x384.png Binary files differnew file mode 100644 index 0000000..2d6dd7b --- /dev/null +++ b/src/statics/icons/icon-384x384.png diff --git a/src/statics/icons/icon-512x512.png b/src/statics/icons/icon-512x512.png Binary files differnew file mode 100644 index 0000000..a57354e --- /dev/null +++ b/src/statics/icons/icon-512x512.png diff --git a/src/statics/icons/ms-icon-144x144.png b/src/statics/icons/ms-icon-144x144.png Binary files differnew file mode 100644 index 0000000..b0880e8 --- /dev/null +++ b/src/statics/icons/ms-icon-144x144.png diff --git a/src/statics/quasar-logo.png b/src/statics/quasar-logo.png Binary files differnew file mode 100644 index 0000000..590e8ce --- /dev/null +++ b/src/statics/quasar-logo.png diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..1bc6b7d --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue' +import Vuex from 'vuex' + +import user from './module-user' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + user + } +}) + +export default store diff --git a/src/store/module-user/actions.js b/src/store/module-user/actions.js new file mode 100644 index 0000000..5e5d9c6 --- /dev/null +++ b/src/store/module-user/actions.js @@ -0,0 +1,4 @@ +/* +export const someAction = (state) => { +} +*/ diff --git a/src/store/module-user/getters.js b/src/store/module-user/getters.js new file mode 100644 index 0000000..64241d1 --- /dev/null +++ b/src/store/module-user/getters.js @@ -0,0 +1 @@ +export const loggedIn = (state) => Boolean(state.decodedToken.username) diff --git a/src/store/module-user/index.js b/src/store/module-user/index.js new file mode 100644 index 0000000..b41a219 --- /dev/null +++ b/src/store/module-user/index.js @@ -0,0 +1,12 @@ +import state from './state' +import * as getters from './getters' +import * as mutations from './mutations' +import * as actions from './actions' + +export default { + namespaced: true, + state, + getters, + mutations, + actions +} diff --git a/src/store/module-user/mutations.js b/src/store/module-user/mutations.js new file mode 100644 index 0000000..e8f4799 --- /dev/null +++ b/src/store/module-user/mutations.js @@ -0,0 +1,17 @@ +import axios from 'axios' +import jwtDecode from 'jwt-decode' +import { LocalStorage } from 'quasar' + +export const setAuthToken = (state, { authToken }) => { + state.authToken = authToken + state.decodedToken = jwtDecode(authToken) + axios.defaults.headers.common['Authorization'] = `Bearer ${authToken}` + LocalStorage.set('authToken', authToken) +} + +export const logout = (state) => { + state.authToken = null + state.decodedToken = {} + delete axios.defaults.headers.common['Authorization'] + LocalStorage.remove('authToken') +} diff --git a/src/store/module-user/state.js b/src/store/module-user/state.js new file mode 100644 index 0000000..b938620 --- /dev/null +++ b/src/store/module-user/state.js @@ -0,0 +1,4 @@ +export default { + authToken: null, + decodedToken: {}, +} |
