diff --git a/.travis.yml b/.travis.yml index b8a4fc7..4138fe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: csharp mono: none sudo: required dist: xenial -dotnet: 2.2 +dotnet: 3.1 script: - dotnet restore diff --git a/pokeR/ClientApp/package-lock.json b/pokeR/ClientApp/package-lock.json index 5d9c2a5..4493fc9 100644 --- a/pokeR/ClientApp/package-lock.json +++ b/pokeR/ClientApp/package-lock.json @@ -78,6 +78,60 @@ "webpack-subresource-integrity": "1.1.0-rc.6" }, "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "optional": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + } + }, "rxjs": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", @@ -108,6 +162,13 @@ "buffer-from": "^1.0.0", "source-map": "^0.6.0" } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "optional": true } } }, @@ -1097,26 +1158,6 @@ "tslib": "^1.9.0" } }, - "@aspnet/signalr": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.1.0.tgz", - "integrity": "sha512-UwWXpfVyB7pfod/GmntaYWMdmmqlfuX7hoig7wMs01xxp53FIBrWGFStUKC3y3S4yu83i38NkzBndBIfOHo5Dw==", - "requires": { - "eventsource": "^1.0.7", - "request": "^2.88.0", - "ws": "^6.0.0" - }, - "dependencies": { - "ws": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz", - "integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==", - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", @@ -1201,12 +1242,24 @@ } } }, - "@ng-bootstrap/ng-bootstrap": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-4.0.1.tgz", - "integrity": "sha512-COQ6MgZ+HD27pGz2sVPB2ttCZozrjHPs0sayuZkleMvzTllYX/eQEPAOiS+yRsXNkDApi5/XGlIFVWBjxTtwoA==", + "@microsoft/signalr": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-3.1.5.tgz", + "integrity": "sha512-S2Y7HhnClipySDwnUVTIKAP5dwMsJrk2fbTVedLn88wcag4q6kb4yzUH44qejJcl6xFmbzU++3KHaH6rvblIrQ==", "requires": { - "tslib": "^1.9.0" + "eventsource": "^1.0.7", + "request": "^2.88.0", + "ws": "^6.0.0" + }, + "dependencies": { + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + } } }, "@ngtools/webpack": { @@ -1292,15 +1345,6 @@ "@types/jasmine": "*" } }, - "@types/jquery": { - "version": "3.3.29", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.29.tgz", - "integrity": "sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==", - "dev": true, - "requires": { - "@types/sizzle": "*" - } - }, "@types/node": { "version": "8.9.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", @@ -1319,21 +1363,6 @@ "integrity": "sha512-4GbNCDs98uHCT/OMv40qQC/OpoPbYn9XdXeTiFwHBBFO6eJhYEPUu2zDKirXSbHlvDV8oZ9l8EQ+HrEx/YS9DQ==", "dev": true }, - "@types/signalr": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/signalr/-/signalr-2.2.35.tgz", - "integrity": "sha512-YVIqZzmiNA8E1maMM57fqz44xJFTWxoNeuw3NfYv/j22lVhVXZeef1rOFo96ENpnrpEa+5ktKpDsGpozfKdTCw==", - "dev": true, - "requires": { - "@types/jquery": "*" - } - }, - "@types/sizzle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", - "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", - "dev": true - }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -1563,8 +1592,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true + "dev": true }, "accepts": { "version": "1.3.5", @@ -1753,8 +1781,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true, - "optional": true + "dev": true }, "array-flatten": { "version": "2.1.2", @@ -1878,8 +1905,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true, - "optional": true + "dev": true }, "async-limiter": { "version": "1.0.0", @@ -2181,7 +2207,6 @@ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "dev": true, - "optional": true, "requires": { "inherits": "~2.0.0" } @@ -2506,15 +2531,13 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true, - "optional": true + "dev": true }, "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, - "optional": true, "requires": { "camelcase": "^2.0.0", "map-obj": "^1.0.0" @@ -3067,7 +3090,6 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", "dev": true, - "optional": true, "requires": { "lru-cache": "^4.0.1", "which": "^1.2.9" @@ -3129,7 +3151,6 @@ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "dev": true, - "optional": true, "requires": { "array-find-index": "^1.0.1" } @@ -4231,7 +4252,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4252,12 +4274,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4272,17 +4296,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4399,7 +4426,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4411,6 +4439,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4425,6 +4454,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4432,12 +4462,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4456,6 +4488,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4536,7 +4569,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4548,6 +4582,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4633,7 +4668,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4669,6 +4705,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4688,6 +4725,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4731,19 +4769,21 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -4773,7 +4813,6 @@ "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "dev": true, - "optional": true, "requires": { "globule": "^1.0.0" } @@ -4872,15 +4911,22 @@ } }, "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz", + "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", "dev": true, - "optional": true, "requires": { "glob": "~7.1.1", - "lodash": "~4.17.10", + "lodash": "~4.17.12", "minimatch": "~3.0.2" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } } }, "graceful-fs": { @@ -5337,18 +5383,16 @@ "dev": true }, "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true, - "optional": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", + "dev": true }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -6178,11 +6222,10 @@ "dev": true }, "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", - "dev": true, - "optional": true + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", + "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==", + "dev": true }, "js-tokens": { "version": "3.0.2", @@ -6626,7 +6669,6 @@ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "dev": true, - "optional": true, "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -6844,7 +6886,6 @@ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, - "optional": true, "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -6859,11 +6900,10 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true } } }, @@ -7147,6 +7187,14 @@ "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", "dev": true }, + "ng2-tooltip-directive": { + "version": "2.9.20", + "resolved": "https://registry.npmjs.org/ng2-tooltip-directive/-/ng2-tooltip-directive-2.9.20.tgz", + "integrity": "sha512-mZzZnBy9ugQgK548n3btp2rBsPIX1YTjVAvmbfKam21El5SnMrure0H0cFa4P34KE+cDaOSAU/FgZOGWXVcF6w==", + "requires": { + "tslib": "^1.9.0" + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -7175,7 +7223,6 @@ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "dev": true, - "optional": true, "requires": { "fstream": "^1.0.0", "glob": "^7.0.3", @@ -7195,8 +7242,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true, - "optional": true + "dev": true } } }, @@ -7249,11 +7295,10 @@ } }, "node-sass": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz", + "integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==", "dev": true, - "optional": true, "requires": { "async-foreach": "^0.1.3", "chalk": "^1.1.1", @@ -7262,7 +7307,7 @@ "get-stdin": "^4.0.1", "glob": "^7.0.3", "in-publish": "^2.0.0", - "lodash": "^4.17.11", + "lodash": "^4.17.15", "meow": "^3.7.0", "mkdirp": "^0.5.1", "nan": "^2.13.2", @@ -7278,15 +7323,13 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "optional": true + "dev": true }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, - "optional": true, "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -7295,19 +7338,23 @@ "supports-color": "^2.0.0" } }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true, - "optional": true + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "optional": true + "dev": true } } }, @@ -7316,7 +7363,6 @@ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, - "optional": true, "requires": { "abbrev": "1" } @@ -7606,7 +7652,6 @@ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, - "optional": true, "requires": { "lcid": "^1.0.0" } @@ -8528,7 +8573,6 @@ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, - "optional": true, "requires": { "indent-string": "^2.1.0", "strip-indent": "^1.0.1" @@ -8800,7 +8844,6 @@ "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", "dev": true, - "optional": true, "requires": { "glob": "^7.0.0", "lodash": "^4.0.0", @@ -8853,7 +8896,6 @@ "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", "dev": true, - "optional": true, "requires": { "js-base64": "^2.1.8", "source-map": "^0.4.2" @@ -8864,7 +8906,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, - "optional": true, "requires": { "amdefine": ">=0.0.4" } @@ -9643,7 +9684,6 @@ "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.0.1" } @@ -9766,7 +9806,6 @@ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, - "optional": true, "requires": { "get-stdin": "^4.0.1" } @@ -9853,14 +9892,13 @@ "dev": true }, "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "dev": true, - "optional": true, "requires": { "block-stream": "*", - "fstream": "^1.0.2", + "fstream": "^1.0.12", "inherits": "2" } }, @@ -10208,8 +10246,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true, - "optional": true + "dev": true }, "trim-right": { "version": "1.0.1", @@ -10222,7 +10259,6 @@ "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "dev": true, - "optional": true, "requires": { "glob": "^7.1.2" } @@ -11109,8 +11145,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true, - "optional": true + "dev": true }, "wide-align": { "version": "1.1.3", @@ -11216,7 +11251,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", "dev": true, - "optional": true, "requires": { "camelcase": "^3.0.0", "cliui": "^3.2.0", @@ -11237,15 +11271,13 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true, - "optional": true + "dev": true }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true, - "optional": true + "dev": true } } }, @@ -11254,7 +11286,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", "dev": true, - "optional": true, "requires": { "camelcase": "^3.0.0" }, @@ -11263,8 +11294,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/pokeR/ClientApp/package.json b/pokeR/ClientApp/package.json index 7d7c17b..d5bf3d2 100644 --- a/pokeR/ClientApp/package.json +++ b/pokeR/ClientApp/package.json @@ -1,10 +1,10 @@ { - "name": "client-app", - "version": "0.0.0", + "name": "poker-web", + "version": "2.0.0", "scripts": { "ng": "ng", "start": "ng serve", - "build": "npm rebuild node-sass && ng build", + "build": "ng build --prod", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" @@ -20,10 +20,10 @@ "@angular/platform-browser": "~7.2.11", "@angular/platform-browser-dynamic": "~7.2.11", "@angular/router": "~7.2.11", - "@aspnet/signalr": "^1.1.0", - "@ng-bootstrap/ng-bootstrap": "^4.0.1", + "@microsoft/signalr": "^3.1.5", "canvas-confetti": "^0.2.0", "core-js": "^2.5.4", + "ng2-tooltip-directive": "^2.9.20", "rxjs": "~6.4.0", "tslib": "^1.9.0", "zone.js": "^0.8.29" @@ -36,7 +36,6 @@ "@types/jasmine": "^2.8.15", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", - "@types/signalr": "^2.2.35", "codelyzer": "~4.5.0", "jasmine-core": "~2.99.1", "jasmine-spec-reporter": "~4.2.1", @@ -45,6 +44,7 @@ "karma-coverage-istanbul-reporter": "~2.0.1", "karma-jasmine": "~1.1.2", "karma-jasmine-html-reporter": "^0.2.2", + "node-sass": "^4.13.1", "protractor": "~5.4.0", "ts-node": "~7.0.0", "tslint": "~5.11.0", diff --git a/pokeR/ClientApp/src/app/app.component.html b/pokeR/ClientApp/src/app/app.component.html index b3d7414..abf69c9 100644 --- a/pokeR/ClientApp/src/app/app.component.html +++ b/pokeR/ClientApp/src/app/app.component.html @@ -1,2 +1,3 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/app.component.scss b/pokeR/ClientApp/src/app/app.component.scss index e69de29..650a65c 100644 --- a/pokeR/ClientApp/src/app/app.component.scss +++ b/pokeR/ClientApp/src/app/app.component.scss @@ -0,0 +1,8 @@ +@import '../theme/variables'; + +.corner-button { + position: fixed; + top: $mid; + right: $mid; + z-index: 30; +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/app.module.ts b/pokeR/ClientApp/src/app/app.module.ts index c55aae0..0db2a46 100644 --- a/pokeR/ClientApp/src/app/app.module.ts +++ b/pokeR/ClientApp/src/app/app.module.ts @@ -6,10 +6,10 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; import { PagesModule } from './pages/pages.module'; -import { NotificationComponent } from './shared-components/notification/notification.component'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { SharedComponentsModule } from './shared-components/shared-components.module'; +import { TooltipModule } from 'ng2-tooltip-directive'; + @NgModule({ declarations: [ AppComponent @@ -20,13 +20,11 @@ import { SharedComponentsModule } from './shared-components/shared-components.mo HttpClientModule, PagesModule, FormsModule, - NgbModule, + TooltipModule, SharedComponentsModule ], providers: [], bootstrap: [AppComponent], - entryComponents: [ - NotificationComponent - ] + entryComponents: [] }) export class AppModule { } diff --git a/pokeR/ClientApp/src/app/models/theme-mode.ts b/pokeR/ClientApp/src/app/models/theme-mode.ts new file mode 100644 index 0000000..07f9e4e --- /dev/null +++ b/pokeR/ClientApp/src/app/models/theme-mode.ts @@ -0,0 +1,5 @@ +export enum ThemeMode { + light, + dark, + automatic +} diff --git a/pokeR/ClientApp/src/app/pages/ingame/ingame.component.html b/pokeR/ClientApp/src/app/pages/ingame/ingame.component.html index 9c80653..ca3ee33 100644 --- a/pokeR/ClientApp/src/app/pages/ingame/ingame.component.html +++ b/pokeR/ClientApp/src/app/pages/ingame/ingame.component.html @@ -1,20 +1,28 @@
-
-
+
+
+ +
- + +
-
-

Room not found 😢

- +
+

Room not found 😢

+
+ + +
- \ No newline at end of file + diff --git a/pokeR/ClientApp/src/app/pages/ingame/ingame.component.scss b/pokeR/ClientApp/src/app/pages/ingame/ingame.component.scss index e69de29..7a75df3 100644 --- a/pokeR/ClientApp/src/app/pages/ingame/ingame.component.scss +++ b/pokeR/ClientApp/src/app/pages/ingame/ingame.component.scss @@ -0,0 +1,54 @@ +@import "../../../theme/variables"; + +.enter-form { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + animation: fan-fade-in 1s 1s ease forwards; + opacity: 0; +} + +.bg-underlay { + z-index: -1; +} + +.contained { + max-width: 800px; +} + +.bg-feather-mask { + box-shadow: 0 0 $thicc $thicc var(--primary-color); + padding: $some; + background-color: var(--primary-color); + border-radius: 40%; +} + +@keyframes fan-fade-in { + from { + opacity: 0; + transform: scale(0.9); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.invalid-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + + h4 { + font-size: 2.3rem; + font-weight: lighter; + } + + .btn { + margin: $some; + } +} diff --git a/pokeR/ClientApp/src/app/pages/ingame/ingame.component.ts b/pokeR/ClientApp/src/app/pages/ingame/ingame.component.ts index a03e871..3a4d3b6 100644 --- a/pokeR/ClientApp/src/app/pages/ingame/ingame.component.ts +++ b/pokeR/ClientApp/src/app/pages/ingame/ingame.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router'; import { Room } from 'src/app/models/entities/room'; import { Observable } from 'rxjs'; import { PokerService } from 'src/app/services/poker.service'; -import { map, flatMap } from 'rxjs/operators'; +import { map, flatMap, tap } from 'rxjs/operators'; @Component({ selector: 'app-ingame', @@ -21,16 +21,21 @@ export class IngameComponent implements OnInit { ngOnInit() { this.loadRoom().subscribe(); - this.route.params.subscribe(p => { - this.roomId = p['id']; - }); } onRoomError = (): boolean => this.isInvalid = true; onJoined = (): boolean => this.isInGame = true; - loadRoom = (): Observable => this.route.params - .pipe(flatMap(p => this.service.getRoom(p['id']))) - .pipe(map(r => this.room = r)) + loadRoom = (): Observable => this.route.params.pipe( + map(params => params['id']), + tap(id => this.roomId = id), + flatMap(id => this.service.getRoom(id)), + map(room => this.room = room) + ) + + reload = () => { + this.service.getRoom(this.roomId).pipe(map(room => this.room = room)).subscribe(); + this.isInvalid = false; + } } diff --git a/pokeR/ClientApp/src/app/pages/landing/landing.component.html b/pokeR/ClientApp/src/app/pages/landing/landing.component.html index c7a769f..21cb94b 100644 --- a/pokeR/ClientApp/src/app/pages/landing/landing.component.html +++ b/pokeR/ClientApp/src/app/pages/landing/landing.component.html @@ -1,15 +1,19 @@ -
-
-
-
-

Planning PokeR

-
- v1.2 -
+
+
+
+
+
+
+
+
+
+
+

Plan your heart out.

+

Created by Alex Griffith, powered by SignalR

- Made by Alex Griffith, - powered by SignalR. -
+
+ +
\ No newline at end of file diff --git a/pokeR/ClientApp/src/app/pages/landing/landing.component.scss b/pokeR/ClientApp/src/app/pages/landing/landing.component.scss index e69de29..90f1cdb 100644 --- a/pokeR/ClientApp/src/app/pages/landing/landing.component.scss +++ b/pokeR/ClientApp/src/app/pages/landing/landing.component.scss @@ -0,0 +1,134 @@ +@import '../../../theme/variables'; + +.landing-container { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: row; + align-items: stretch; + overflow: hidden; + flex-wrap: wrap; + + .promo-pane { + background-color: var(--accent-color); + padding: $thicc; + flex-grow: 1; + position: relative; + overflow: hidden; + z-index: 0; + + .cards { + position: absolute; + pointer-events: none; + transform: rotate(-36deg); + width: 100%; + height: 100%; + position: absolute; + + .card { + width: 340px; + height: 410px; + border-radius: 48px; + display: block; + background: linear-gradient(0deg, transparent, var(--primary-color)); + opacity: 0; + position: absolute; + animation-name: slide-up; + animation-timing-function: ease; + animation-duration: .8s; + animation-fill-mode: forwards; + + &.pos-a { + top: 12%; + left: 47%; + animation-delay: 0.8s; + } + + &.pos-b { + top: 18%; + left: 15%; + animation-delay: 1.1s; + } + + &.pos-c { + top: 41%; + left: 32%; + animation-delay: 1.24s; + } + } + } + + .promo-text { + z-index: 4; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-around; + height: 100%; + + h1 { + font-weight: normal; + font-size: 48px; + margin: 0; + animation: slide-left 0.8s .5s ease forwards; + opacity: 0; + } + + p { + font-size: 20px; + animation: slide-left 0.8s .7s ease forwards; + opacity: 0; + } + } + } + + .content-pane { + flex-grow: 2; + background-color: var(--primary-color); + padding: $thicc; + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; + box-shadow: 0 0 0 0 var(--primary-color); + z-index: 1; + } + + &.exiting { + pointer-events: none; + + .content-pane { + &>* { + transition: opacity .5s ease; + opacity: 0; + } + + transition: box-shadow .5s ease; + box-shadow: 0 0 0 100vw var(--primary-color); + } + } +} + +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(168px); + } + + to { + opacity: 0.35; + transform: translateY(0); + } +} + +@keyframes slide-left { + from { + opacity: 0; + transform: translateX(-168px); + } + + to { + opacity: 2; + transform: translateX(0); + } +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/pages/landing/landing.component.ts b/pokeR/ClientApp/src/app/pages/landing/landing.component.ts index 0a884c5..ccc42ef 100644 --- a/pokeR/ClientApp/src/app/pages/landing/landing.component.ts +++ b/pokeR/ClientApp/src/app/pages/landing/landing.component.ts @@ -1,15 +1,23 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; +import { Subscription, of } from 'rxjs'; +import { delay, tap } from 'rxjs/operators'; +import { Router } from '@angular/router'; @Component({ selector: 'app-landing', templateUrl: './landing.component.html', styleUrls: ['./landing.component.scss'] }) -export class LandingComponent implements OnInit { +export class LandingComponent { + public exiting = false; - constructor() { } - - ngOnInit() { - } + constructor( + private router: Router + ) { } + onExit = (route: any[]): Subscription => of(null).pipe( + tap(() => this.exiting = true), + delay(550), + tap(() => this.router.navigate(route)) + ).subscribe() } diff --git a/pokeR/ClientApp/src/app/services/poker.service.ts b/pokeR/ClientApp/src/app/services/poker.service.ts index 9585078..8354c7a 100644 --- a/pokeR/ClientApp/src/app/services/poker.service.ts +++ b/pokeR/ClientApp/src/app/services/poker.service.ts @@ -2,10 +2,10 @@ import { Injectable, EventEmitter } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { CreateRoomRequest } from '../models/create-room-request'; import { Deck } from '../models/entities/deck'; -import { of, Observable, from, Subject, forkJoin } from 'rxjs'; +import { of, Observable, from, forkJoin } from 'rxjs'; import { map, flatMap } from 'rxjs/operators'; import { Room } from '../models/entities/room'; -import { HubConnection, HubConnectionBuilder, JsonHubProtocol } from '@aspnet/signalr'; +import { HubConnection, HubConnectionBuilder, JsonHubProtocol } from '@microsoft/signalr'; import { JoinRoomRequest } from '../models/join-room-request'; import { User } from '../models/entities/user'; import { ListChange } from '../models/list-change'; @@ -44,32 +44,32 @@ export class PokerService { this.initializeHubWatches().subscribe(); } - public getEmblemUrl = (id: number): string => `api/emblems/${id}/image`; + public getEmblemUrl = (id: number): string => `/api/emblems/${id}/image`; public reset = (): void => this.room = null; public createRoom = (request: CreateRoomRequest): Observable => - this.http.post('api/rooms', request) + this.http.post('/api/rooms', request) public getDecks = (): Observable => this.decks.length ? of(this.decks) : - this.http.get('api/decks').pipe(map(d => this.decks = d)) + this.http.get('/api/decks').pipe(map(d => this.decks = d)) public getEmblems = (): Observable => this.emblems.length ? of(this.emblems) : - this.http.get('api/emblems').pipe(map(e => this.emblems = e)) + this.http.get('/api/emblems').pipe(map(e => this.emblems = e)) public getRoom = (roomId: string): Observable => - this.http.get(`api/rooms/${roomId}`).pipe(map(r => this.room = r)) + this.http.get(`/api/rooms/${roomId}`).pipe(map(r => this.room = r)) public getPlayers = (): Observable> => this.room - ? this.http.get(`api/rooms/${this.room.id}`).pipe(map(r => r.users)) + ? this.http.get(`/api/rooms/${this.room.id}`).pipe(map(r => r.users)) : of(new Array()) public getTagline = (): Observable => this.room - ? this.http.get(`api/rooms/${this.room.id}`).pipe(map(r => r.tagLine)) + ? this.http.get(`/api/rooms/${this.room.id}`).pipe(map(r => r.tagLine)) : of('') public joinRoom = (request: JoinRoomRequest): Observable => @@ -100,7 +100,7 @@ export class PokerService { this.getHub().pipe(flatMap((hub: HubConnection) => from(hub.invoke('switchHost', newHostId)))) public checkAvailability = (id: string): Observable => - this.http.get(`api/rooms/available/${id}`, { observe: 'response', responseType: 'text' as 'json' }) + this.http.get(`/api/rooms/available/${id}`, { observe: 'response', responseType: 'text' as 'json' }) .pipe(map(r => r.body === 'true')) public getCards = (deckId: number): Observable => @@ -118,6 +118,7 @@ export class PokerService { private prepareHub = (): Promise => { this.hub = new HubConnectionBuilder() .withUrl('/notify/room') + .withAutomaticReconnect([0, 1000, 2000, 3000, 5000, 10000, 12000, 15000, 30000, null]) .withHubProtocol(new JsonHubProtocol()) .build(); return this.hub.start().catch(console.error).then(() => { diff --git a/pokeR/ClientApp/src/app/services/theme.service.spec.ts b/pokeR/ClientApp/src/app/services/theme.service.spec.ts new file mode 100644 index 0000000..a223a14 --- /dev/null +++ b/pokeR/ClientApp/src/app/services/theme.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { ThemeService } from './theme.service'; + +describe('ThemeService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: ThemeService = TestBed.get(ThemeService); + expect(service).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/services/theme.service.ts b/pokeR/ClientApp/src/app/services/theme.service.ts new file mode 100644 index 0000000..971551f --- /dev/null +++ b/pokeR/ClientApp/src/app/services/theme.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { ThemeMode } from '../models/theme-mode'; + +const themes = { + dark: { + '--primary-color': '#353749', + '--accent-color': '#53577D', + '--contrast-color': 'white', + '--hover-color': 'rgba(255,255,255,0.1)', + '--active-color': 'rgba(255,255,255,0.2)', + '--font-family': '\'Open Sans\',sans-serif', + '--valid-color': 'green', + '--invalid-color': '#ff5c5c' + }, light: { + '--primary-color': '#E9E9E9', + '--accent-color': '#9FA2C2', + '--contrast-color': '#353749', + '--hover-color': 'rgba(0,0,0,0.1)', + '--active-color': 'rgba(0,0,0,0.2)', + '--font-family': '\'Open Sans\',sans-serif', + '--valid-color': 'green', + '--invalid-color': 'red' + } +}; + +@Injectable({ + providedIn: 'root' +}) +export class ThemeService { + + constructor() { } + + public applyTheme = (m: ThemeMode): void => { + switch (m) { + case ThemeMode.automatic: + if (this.isInDarkMode()) { + this.setVariables(themes.dark); + } else { + this.setVariables(themes.light); + } return; + case ThemeMode.dark: + this.setVariables(themes.dark); + return; + case ThemeMode.light: + this.setVariables(themes.light); + return; + } + } + + private isInDarkMode = (): boolean => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + private setVariables = (map: any) => Object.keys(map) + .forEach(k => document.documentElement.style.setProperty(k, map[k])) +} diff --git a/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.html b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.html new file mode 100644 index 0000000..83ba3fe --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.html @@ -0,0 +1,9 @@ +
+
+
+
+ {{c.text}} +
+
+
+
\ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.scss b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.scss new file mode 100644 index 0000000..4effdb4 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.scss @@ -0,0 +1,52 @@ +@import '../../../theme/variables'; + +.bg-cards { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + perspective: 900px; + pointer-events: none; + overflow: hidden; + + .perspective-container { + width: 100%; + height: 100%; + animation: pan-in 20s ease 0s forwards; + transform-style: preserve-3d; + + .bg-card { + position: absolute; + border: $slim solid var(--accent-color); + border-radius: $slim; + width: 4.5rem; + height: 6rem; + display: flex; + align-items: center; + justify-content: center; + opacity: .5; + transform-style: preserve-3d; + background-color: var(--primary-color); + + .card-label { + color: var(--accent-color); + font-size: 56px; + } + } + } +} + +@keyframes pan-in { + 0% { + transform: translateZ(-10000px); + } + + 5% { + transform: translateZ(-240px); + } + + 100% { + transform: translateZ(0); + } +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.spec.ts new file mode 100644 index 0000000..e5f6263 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BackgroundCardsComponent } from './background-cards.component'; + +describe('BackgroundCardsComponent', () => { + let component: BackgroundCardsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BackgroundCardsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BackgroundCardsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.ts b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.ts new file mode 100644 index 0000000..5c8aa1a --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/background-cards/background-cards.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Card } from 'src/app/models/entities/card'; + +@Component({ + selector: 'app-background-cards', + templateUrl: './background-cards.component.html', + styleUrls: ['./background-cards.component.scss'] +}) +export class BackgroundCardsComponent implements OnChanges { + @Input() cards: Array = []; + cardViews: any[] = []; + constructor() { } + + ngOnChanges(changes: SimpleChanges): void { + const newCards: Array = changes.cards.currentValue; + this.cardViews = this.duplicate(newCards, 2).map(this.getView); + } + + private getView = (c: Card): any => ({ + px: Math.random() * 100, + py: Math.random() * 100, + zBase: Math.random(), + ox: Math.random() * 180 - 90, + oy: Math.random() * 180 - 90, + oz: Math.random() * 180 - 90, + text: c.name + }) + + getStyle = (v: any): any => ({ + 'transform': `translateZ(${v.zBase * 1200 - 600}px) rotateX(${v.ox}deg) rotateY(${v.oy}deg) rotateZ(${v.oz}deg)`, + 'left': `${v.px}%`, + 'top': `${v.py}%`, + 'opacity': v.zBase * 0.7 + 0.3 + }) + + private duplicate = (source: Array, count: number): Array => { + let result = source; + for (let i = 0; i < count; i++) { + result = result.concat(result); + } + return result; + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.html b/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.html index 1d09c07..58c136e 100644 --- a/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.html +++ b/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.html @@ -1,7 +1,9 @@ -
-
- {{c.name}} +
+
+
+ {{c.name}} +
-
\ No newline at end of file +
diff --git a/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.scss b/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.scss index 9a4e4ef..a1d2373 100644 --- a/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.scss +++ b/pokeR/ClientApp/src/app/shared-components/card-list/card-list.component.scss @@ -1,17 +1,51 @@ -.cardTray { - font-size: 1.4rem; - min-width: 2em; - padding-top: .5em; - padding-bottom: .5em; - text-align: center; - transition: font-size .2s ease; -} +@import "../../../theme/variables"; + +.card-list { + left: 50%; + transform: translateX(-50%); + animation: slide-up forwards 1s ease; + width: 100%; + overflow-x: auto; + + & > .card-list__inner { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-end; + + @media screen and (max-width: 768px) { + justify-content: flex-start; + } -.cardTray.selected { - font-size: 2rem; + & > .card { + font-size: 1.4rem; + min-width: 2em; + padding-top: 0.5em; + padding-bottom: 0.5em; + text-align: center; + transition: font-size 0.2s ease; + border: $thinn solid var(--accent-color); + border-radius: $slim; + cursor: pointer; + margin: $mid; + + &.selected { + font-size: 2rem; + } + } + } } .fixed-bottom { - position: fixed; - bottom: 0; -} \ No newline at end of file + position: fixed; + bottom: 0; +} + +@keyframes slide-up { + 0% { + transform: translateY(100%) translateX(-50%); + } + 100% { + transform: translateY(0) translateX(-50%); + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.html b/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.html index d542f60..4a1a860 100644 --- a/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.html +++ b/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.html @@ -2,67 +2,59 @@
-
- -
-
-
- - This room is available.
-
- - This room is taken. - Join it -
-
- - Room ID is required. + +
+ + This room is available.
+
+ + This room is taken. + Join it +
+
+ + Room Number is required. +
-
- -
-
-
- - Name is required. + +
+ + Room Name is required. +
-
- -
- + +
-
- -
-
-
- - Deck is required. + +
+ + Deck is required. +
@@ -79,8 +71,7 @@
-
diff --git a/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.ts b/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.ts index dbbbc9b..ac66714 100644 --- a/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.ts +++ b/pokeR/ClientApp/src/app/shared-components/create-room/create-room.component.ts @@ -1,9 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, EventEmitter, Output } from '@angular/core'; import { CreateRoomRequest } from 'src/app/models/create-room-request'; import { PokerService } from 'src/app/services/poker.service'; import { Deck } from 'src/app/models/entities/deck'; import { map, debounceTime, flatMap } from 'rxjs/operators'; -import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { FormGroup, FormControl, Validators } from '@angular/forms'; @@ -15,6 +14,7 @@ import { FormGroup, FormControl, Validators } from '@angular/forms'; export class CreateRoomComponent implements OnInit { decks: Array = new Array(); isAvailable: boolean; + @Output() exit: EventEmitter = new EventEmitter(); submitAttempted = false; @@ -25,13 +25,15 @@ export class CreateRoomComponent implements OnInit { deck: new FormControl('', Validators.required) }); - constructor(private service: PokerService, private router: Router) { } + constructor(private service: PokerService) { } ngOnInit() { this.service.getDecks().subscribe(d => this.decks = d); this.watchIdChanges().subscribe(); } + join = (): void => this.exit.emit(['/room', this.form.get('roomId').value]); + // tslint:disable-next-line:triple-equals getSelectedDeck = (): Deck => this.decks.find(d => d.id == this.form.get('deck').value); @@ -40,7 +42,7 @@ export class CreateRoomComponent implements OnInit { if (this.form.valid && this.isAvailable) { const snapshot = this.getModel(); this.service.createRoom(snapshot).pipe(map(() => { - this.router.navigate(['/room', snapshot.id]); + this.exit.emit(['/room', snapshot.id]); })).subscribe(); } } @@ -56,7 +58,7 @@ export class CreateRoomComponent implements OnInit { debounceTime(300), flatMap(this.service.checkAvailability), map(r => this.isAvailable = r) - ); + ) getDelayStyle = (i: number) => { 'animation-delay': `${i * 60}ms` diff --git a/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.html b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.html new file mode 100644 index 0000000..78dd6e8 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.html @@ -0,0 +1,20 @@ +
+
+ + Join an existing room + +
+
+ +
+
+
+
+ + Create a new room + +
+
+ +
+
\ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.scss b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.scss new file mode 100644 index 0000000..2d9daed --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.scss @@ -0,0 +1,104 @@ +@import '../../../theme/variables'; + +.actionable { + overflow-y: hidden; + max-height: 100vh; + transition: max-height 1s ease; + margin-top: $slim; + margin-bottom: $slim; + border-radius: $slim; + + .selector-bar { + display: flex; + flex-direction: row; + align-items: center; + font-size: 24px; + margin-top: $mid; + margin-bottom: $mid; + padding: $some $slim $some $slim; + border-radius: $slim; + cursor: pointer; + position: relative; + transition: background-color .2s ease; + + .forward-icon, + .back-icon { + overflow: hidden; + } + + .back-icon { + max-width: 0; + transition: .5s; + margin-right: $slim; + } + + .forward-icon { + transition: .5s; + max-width: 24px; + margin-left: $slim; + } + + .label { + flex-grow: 1; + } + + &::after { + content: ''; + display: block; + position: absolute; + height: $slim; + background-color: var(--accent-color); + width: calc(100% - #{$slim}); + transform-origin: left; + top: 100%; + transform: scaleX(0); + transition: transform 1.4s ease; + } + } + + .content { + max-height: 0; + overflow-y: hidden; + pointer-events: none; + transition: 1.2s; + padding-top: $some; + opacity: 0; + } + + &.active { + .selector-bar { + cursor: inherit; + + .back-icon { + max-width: 24px; + cursor: pointer; + } + + .forward-icon { + max-width: 0; + } + + &::after { + transform: scaleX(1); + } + } + + .content { + pointer-events: initial; + max-height: 100vh; + opacity: 1; + } + } + + &:not(.active) { + &:hover { + .selector-bar { + background-color: var(--hover-color); + } + } + } + + &.inactive { + max-height: 0; + } +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.spec.ts new file mode 100644 index 0000000..26de2d7 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EntrywayComponent } from './entryway.component'; + +describe('EntrywayComponent', () => { + let component: EntrywayComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EntrywayComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EntrywayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.ts b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.ts new file mode 100644 index 0000000..04fb56f --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/entryway/entryway.component.ts @@ -0,0 +1,14 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'app-entryway', + templateUrl: './entryway.component.html', + styleUrls: ['./entryway.component.scss'] +}) +export class EntrywayComponent { + @Output() exit: EventEmitter = new EventEmitter(); + public action: string = null; + constructor() { } + + onExit = (route: any[]): void => this.exit.emit(route); +} diff --git a/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.html b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.html new file mode 100644 index 0000000..679c3b4 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.html @@ -0,0 +1,29 @@ +
+ +
+
+ + +
+ + You can join this room.
+
+ + This room doesn't exist. +
+
+ + Room Number is required. +
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.scss b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.spec.ts new file mode 100644 index 0000000..d2a1185 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FindRoomComponent } from './find-room.component'; + +describe('FindRoomComponent', () => { + let component: FindRoomComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FindRoomComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FindRoomComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.ts b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.ts new file mode 100644 index 0000000..4a8ed7c --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/find-room/find-room.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit, EventEmitter, Output } from '@angular/core'; +import { PokerService } from 'src/app/services/poker.service'; +import { Observable } from 'rxjs'; +import { debounceTime, flatMap, map } from 'rxjs/operators'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-find-room', + templateUrl: './find-room.component.html', + styleUrls: ['./find-room.component.scss'] +}) +export class FindRoomComponent implements OnInit { + @Output() exit: EventEmitter = new EventEmitter(); + public exists: boolean = null; + form: FormGroup = new FormGroup({ + roomId: new FormControl('', Validators.required) + }); + + constructor( + private service: PokerService + ) { } + + ngOnInit() { + this.watchIdChanges().subscribe(); + } + + onSubmit = (): void => { + if (this.form.valid && this.exists) { + const id = this.form.get('roomId').value; + this.exit.emit(['/room', id]); + } + } + + watchIdChanges = (): Observable => this.form.get('roomId').valueChanges.pipe( + debounceTime(300), + flatMap(this.service.checkAvailability), + map(r => this.exists = !r) + ) +} diff --git a/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.html b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.html new file mode 100644 index 0000000..f49a7cb --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.html @@ -0,0 +1,31 @@ +
+
+
+
{{currentTagline}}
+
+
+ + +
+
+ + +
+ + +
+
+
+
+ {{getTimeText()}} +
+
+
+
diff --git a/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.scss b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.scss new file mode 100644 index 0000000..23b93ad --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.scss @@ -0,0 +1,71 @@ +@import "../../../theme/variables"; + +.host-controls { + top: 0; + left: 0; + right: 0; + z-index: 28; + animation: slide-down forwards 1s ease; + + .current-tagline { + text-align: center; + font-size: 1.4rem; + margin-top: 0.3rem; + } + + .control-bar { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + & > .input-group { + margin-bottom: $some; + margin-top: $some; + margin-right: $some; + } + + & > .subject--input { + flex-grow: 10; + } + + & > .countdown--input { + min-width: 120px; + flex-grow: 1; + } + + & > .btn { + margin-bottom: $some; + margin-top: $some; + $size: 46px; + height: $size; + width: $size; + margin-right: $some; + padding: $some; + + & > i::before { + transform: scale(1.3) translateY(-3px); + } + } + } +} + +@keyframes slide-down { + 0% { + transform: translateY(-100%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} + +.countdown { + width: 100%; + & > .bar { + border-radius: $mid; + color: var(--contrast-color); + background-color: var(--accent-color); + text-align: center; + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.spec.ts new file mode 100644 index 0000000..67447c1 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HostControlsComponent } from './host-controls.component'; + +describe('HostControlsComponent', () => { + let component: HostControlsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HostControlsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HostControlsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.ts b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.ts new file mode 100644 index 0000000..0e1dc9d --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/host-controls/host-controls.component.ts @@ -0,0 +1,130 @@ +import { Component, OnInit } from '@angular/core'; +import { PokerService } from 'src/app/services/poker.service'; +import { Observable, Subscription, forkJoin } from 'rxjs'; +import { map, debounceTime, flatMap } from 'rxjs/operators'; +import { User } from 'src/app/models/entities/user'; +import { FormGroup, FormControl } from '@angular/forms'; + +@Component({ + selector: 'app-host-controls', + templateUrl: './host-controls.component.html', + styleUrls: ['./host-controls.component.scss'] +}) +export class HostControlsComponent implements OnInit { + currentTagline: string; + countdownIsActive = false; + remainingTime: number; + maxTime: number; + deadline: Date; + timer: number; + player: User; + formGroup: FormGroup = new FormGroup({ + subject: new FormControl(''), + countdown: new FormControl('') + }); + + constructor(private service: PokerService) { } + + ngOnInit() { + this.player = this.service.player; + this.initialize().subscribe(); + this.monitorGameState().subscribe(); + } + + initialize = (): Observable => this.service.getTagline().pipe(map(t => this.currentTagline = t)); + + monitorGameState = (): Observable => + forkJoin( + this.watchInputChange(), + this.watchTaglineChanges(), + this.watchRoundEnd(), + this.watchCountdownStart(), + this.watchHostChanges() + ) + + getRemainingTime = (): number => this.deadline ? this.deadline.getTime() - Date.now() : 0; + + updateRemainingTime = (): void => { + const time = this.getRemainingTime(); + if (time > 0) { + this.remainingTime = time; + this.timer = window.requestAnimationFrame(this.updateRemainingTime); + } else { + window.cancelAnimationFrame(this.timer); + } + } + + getCountdownValue = (): number => { + const time = this.remainingTime; + if (time > 0 && this.maxTime) { + return time * 100 / this.maxTime; + } + return 0; + } + + getTimeText = (): string => { + let result = ''; + const value = this.remainingTime / 1000; + if (value >= 60) { + result += `${Math.floor(value / 60)}m `; + } + result += `${Math.floor(value % 60)}s`; + return result; + } + + startCountdown = (): void => { + const countdownLength = Number(this.formGroup.get('countdown').value); + if (countdownLength) { + this.service.startTimer(1000 * countdownLength).subscribe(); + this.countdownIsActive = true; + window.setTimeout(() => { + this.service.endRound().subscribe(); + }, 1000 * countdownLength); + } + } + + startNewRound = (): Subscription => this.service.startRound().subscribe(); + + watchInputChange = (): Observable => { + const source = this.formGroup.get('subject').valueChanges; + return forkJoin( + source.pipe(debounceTime(500), flatMap(this.service.storeTagline)), + source.pipe(flatMap(this.service.updateTagline)) + ); + }; + + watchTaglineChanges = (): Observable => + this.service.taglineUpdated.pipe(map(t => { + if (!this.player.isHost) { + this.currentTagline = t; + } + })) + + watchRoundEnd = (): Observable => + this.service.roundEnds.pipe(map(() => { + this.countdownIsActive = false; + this.remainingTime = null; + this.maxTime = null; + this.formGroup.get('countdown').setValue(''); + window.cancelAnimationFrame(this.timer); + })) + + watchCountdownStart = (): Observable => + this.service.timerStarts.pipe(map(t => { + if (t) { + this.maxTime = t; + this.remainingTime = t; + this.deadline = new Date(Date.now() + t); + this.countdownIsActive = true; + this.timer = window.requestAnimationFrame(this.updateRemainingTime); + } + })) + + watchHostChanges = (): Observable => + this.service.hostChanges.pipe(map(d => { + const playerMatch = d.collection.find(c => c.id === this.player.id); + if (playerMatch) { + this.player = playerMatch; + } + })) +} diff --git a/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.html b/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.html index 06603df..13c9cc5 100644 --- a/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.html +++ b/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.html @@ -1,35 +1,29 @@ -
-
-
-
- -
- -
+ +
+ +
+ class="invalid-feedback helper"> Name is required.
-
-
+
+ +
+ [src]="getEmblemUrl(e.id)" class="miniCardBack">
-
+
Select an emblem.
- - \ No newline at end of file + diff --git a/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.scss b/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.scss index 9297dde..77f57e9 100644 --- a/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.scss +++ b/pokeR/ClientApp/src/app/shared-components/join-room/join-room.component.scss @@ -1,3 +1,5 @@ +@import '../../../theme/forms'; + .miniCardBack { border-radius: 0.5rem; width: 4.5rem; @@ -5,6 +7,8 @@ object-fit: cover; object-position: center; transition: all ease 0.2s; + margin: $mid; + cursor: pointer; } .miniCardBack.selected { @@ -17,3 +21,35 @@ opacity: 0.6; transform: scale(0.9); } + +.join-form { + display: flex; + flex-direction: column; + align-items: center; + + .input-group { + width: 100%; + } +} + +.emblem-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.faux-label { + font-weight: 700; + color: var(--contrast-color); + + &.inactive { + color: var(--accent-color); + } +} + +.faux-group { + .helper { + bottom: -16px; + top: initial; + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.html b/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.html index b835691..cf10f92 100644 --- a/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.html +++ b/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.html @@ -1,3 +1,3 @@ -
+
-
\ No newline at end of file +
diff --git a/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.scss b/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.scss index bebe7aa..03b7d77 100644 --- a/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.scss +++ b/pokeR/ClientApp/src/app/shared-components/notification-feed/notification-feed.component.scss @@ -1,9 +1,12 @@ -.fixed-tr { - position: fixed; - top: 0; - right: 0; -} +@import "../../../theme/variables"; -.z-top { - z-index: 30; -} \ No newline at end of file +.notification-feed { + z-index: 26; + position: fixed; + bottom: 80px; + left: 50%; + transform: translateX(-50%); + width: 100%; + padding-left: $some; + padding-right: $some; +} diff --git a/pokeR/ClientApp/src/app/shared-components/notification/notification.component.html b/pokeR/ClientApp/src/app/shared-components/notification/notification.component.html index 29c9985..fcfbdcc 100644 --- a/pokeR/ClientApp/src/app/shared-components/notification/notification.component.html +++ b/pokeR/ClientApp/src/app/shared-components/notification/notification.component.html @@ -1,5 +1,5 @@ -
- diff --git a/pokeR/ClientApp/src/app/shared-components/notification/notification.component.scss b/pokeR/ClientApp/src/app/shared-components/notification/notification.component.scss index 350d99b..0dd390e 100644 --- a/pokeR/ClientApp/src/app/shared-components/notification/notification.component.scss +++ b/pokeR/ClientApp/src/app/shared-components/notification/notification.component.scss @@ -1,32 +1,40 @@ -@keyframes slideIn { - from { - transform: translateX(105%); - } - to { - transform: translateX(0); - } +@keyframes fadeInUp { + 0% { + opacity: 0; + max-height: 0; + overflow: hidden; + } + 50% { + opacity: 0; + max-height: 30px; + overflow: hidden; + } + 100% { + opacity: 1; + } } -@keyframes fadeOutUp { - 0% { - opacity: 1; - } - 50% { - opacity: 0; - max-height: 200px; - overflow: hidden; - } - 100% { - opacity: 0; - max-height: 0; - overflow: hidden; - } +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } } -.animIn { - animation: slideIn .5s ease; -} +.notification { + &.in { + // using separate keyframes instead of reversing because js checks animation name + animation: fadeInUp 0.3s ease forwards; + } + + &.out { + animation: fadeOut 0.3s ease forwards; + } -.animOut { - animation: fadeOutUp .7s ease forwards; -} \ No newline at end of file + & > .notification__content { + text-align: center; + color: var(--accent-color); + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/notification/notification.component.ts b/pokeR/ClientApp/src/app/shared-components/notification/notification.component.ts index e601a11..a9466a1 100644 --- a/pokeR/ClientApp/src/app/shared-components/notification/notification.component.ts +++ b/pokeR/ClientApp/src/app/shared-components/notification/notification.component.ts @@ -23,13 +23,13 @@ export class NotificationComponent implements OnInit { ngOnInit() { setTimeout(() => { this.dismiss(); - }, 10000); + }, 5000); } dismiss = (): boolean => this.animOut = true; transitionEnd = (e: AnimationEvent) => { - if (e.animationName === 'fadeOutUp') { + if (e.animationName === 'fadeOut') { try { this.selfRef.destroy(); } catch { diff --git a/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.html b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.html new file mode 100644 index 0000000..3e3e473 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.html @@ -0,0 +1,12 @@ +
+
+
+
+ + {{ label }} +
+
+ +
+
+
diff --git a/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.scss b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.scss new file mode 100644 index 0000000..ad42c6c --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.scss @@ -0,0 +1,120 @@ +@import "../../../theme/variables"; +$tab-width: 24px; + +.pane-drawer { + position: fixed; + height: 100%; + z-index: 30; + pointer-events: none; + animation: slide-in-left 1s forwards ease; + + & > .shade { + position: fixed; + pointer-events: none; + opacity: 0; + background-color: black; + transition: opacity 0.2s ease; + width: 100vw; + height: 100%; + } + + & > .pane { + background-color: var(--accent-color); + width: 100%; + height: 100%; + padding: $some; + color: var(--contrast-color); + position: relative; + transform: translateX(-100%); + transition: all 0.2s ease; + pointer-events: initial; + + & > .pane__content { + width: 100%; + height: 100%; + } + + & > .tab { + width: $tab-width; + background-color: var(--accent-color); + cursor: pointer; + writing-mode: vertical-lr; + transform: translateY(-50%); + right: -1 * $tab-width; + top: 50%; + position: absolute; + padding-top: $mid; + padding-bottom: $mid; + transition: right 0.2s ease; + + & > .tab__icon::before { + transition: transform 0.2s ease; + } + } + } + + &.active { + & > .shade { + pointer-events: initial; + opacity: 0.4; + } + + & > .pane { + transform: translateX(0); + padding-right: $some + $tab-width; + + & > .tab { + right: 0; + + & > .tab__icon::before { + transform: rotate(180deg); + } + } + } + } + + &.right { + right: 0; + animation: slide-in-right 1s forwards ease; + + & > .pane { + transform: translateX(100%); + + & > .tab { + writing-mode: vertical-rl; + right: unset; + left: -1 * $tab-width; + } + } + + &.active { + & > .pane { + padding-right: $some; + padding-left: $some + $tab-width; + + & > .tab { + left: 0; + right: unset; + } + } + } + } +} + +@keyframes slide-in-left { + 0% { + transform: translateX(-1 * $tab-width); + } + 100% { + transform: translateX(0); + } +} + +@keyframes slide-in-right { + 0% { + transform: translateX($tab-width); + } + 100% { + transform: translateX(0); + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.spec.ts new file mode 100644 index 0000000..53f1d0b --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PaneDrawerComponent } from './pane-drawer.component'; + +describe('PaneDrawerComponent', () => { + let component: PaneDrawerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PaneDrawerComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PaneDrawerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.ts b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.ts new file mode 100644 index 0000000..f3814c0 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/pane-drawer/pane-drawer.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit, Input } from '@angular/core'; + +@Component({ + selector: 'app-pane-drawer', + templateUrl: './pane-drawer.component.html', + styleUrls: ['./pane-drawer.component.scss'] +}) +export class PaneDrawerComponent { + @Input() label: string; + @Input() direction: string = 'left'; + open = false; + togglePanel = () => this.open = !this.open; + + constructor() { } +} diff --git a/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.html b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.html new file mode 100644 index 0000000..4080bf1 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.html @@ -0,0 +1,9 @@ +
+
+ +
+
+ {{user.currentCard?.name}} +
+
diff --git a/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.scss b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.scss new file mode 100644 index 0000000..04e735a --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.scss @@ -0,0 +1,122 @@ +@import "../../../theme/variables"; + +.playCard { + width: 160px; + height: 220px; + position: relative; + transform-style: preserve-3d; + border-radius: 5px; + transition: transform 1s ease, opacity 0.2s ease, top 0.3s ease, left 0.3s ease; + transform-origin: -25% 50%; + position: absolute; +} + +.pre-play { + transform: translateY(80%) translateZ(60px) !important; + opacity: 0; +} + +.card-face { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: $some; +} + +.card-back { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + border-radius: $some; +} + +@media #{$query-ie} { + .card-face-front { + opacity: 1; + } + .card-face-back { + opacity: 0; + } +} + +.card-face-back { + background-color: var(--primary-color); + color: var(--contrast-color); + box-shadow: 0 0 0 $slim inset var(--accent-color); + transform: rotateY(180deg) rotateZ(180deg); + + font-size: 3.5rem; + display: flex; + justify-content: center; + align-items: center; +} + +.card-flipped:not(.card-exiting) { + transform: rotateY(180deg) rotateZ(-180deg) !important; + + @media #{$query-ie} { + .card-face-front { + animation: crossFadeOut 1s ease forwards; + } + + .card-face-back { + animation: crossFadeIn 1s ease forwards; + } + } +} + +.card-exiting { + animation: withdraw 0.3s ease forwards; + + &.card-flipped { + animation: withdraw-revealed 0.1s ease forwards; + } +} + +@keyframes crossFadeIn { + 0%, + 40% { + opacity: 0; + } + + 60%, + 100% { + opacity: 1; + } +} + +@keyframes crossFadeOut { + 0%, + 40% { + opacity: 1; + } + + 60%, + 100% { + opacity: 0; + } +} + +@keyframes withdraw { + 0% { + transform: translateY(0); + opacity: 1; + } + 100% { + transform: translateY(200%); + opacity: 0; + } +} + +@keyframes withdraw-revealed { + 0% { + transform: rotateY(180deg) rotateZ(-180deg) translateY(0) !important; + opacity: 1; + } + 100% { + transform: rotateY(180deg) rotateZ(-180deg) translateY(200%) !important; + opacity: 0; + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.spec.ts new file mode 100644 index 0000000..898862e --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PlayfieldCardComponent } from './playfield-card.component'; + +describe('PlayfieldCardComponent', () => { + let component: PlayfieldCardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PlayfieldCardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PlayfieldCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.ts b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.ts new file mode 100644 index 0000000..295335d --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/playfield-card/playfield-card.component.ts @@ -0,0 +1,77 @@ +import { Component, Input, ComponentRef, AfterViewInit, OnDestroy } from '@angular/core'; +import { PokerService } from 'src/app/services/poker.service'; +import { User } from 'src/app/models/entities/user'; + +@Component({ + selector: 'app-playfield-card', + templateUrl: './playfield-card.component.html', + styleUrls: ['./playfield-card.component.scss'] +}) +export class PlayfieldCardComponent implements AfterViewInit, OnDestroy { + isRevealed = false; + isExiting = false; + initialized = false; + @Input() user: User; + @Input() public selfRef: ComponentRef; + private rotation: number; + private x: number = null; + private y: number = null; + trackerInterval: number; + + constructor(private service: PokerService) { + this.rotation = (Math.random() * 16) - 8; + } + + ngAfterViewInit(): void { + window.setTimeout(() => this.initialized = true, 10); + this.trackTarget(); + this.trackerInterval = window.setInterval(this.trackTarget, 200); + } + + ngOnDestroy(): void { + this.stopTracking(); + } + + public reveal = (timeout?: number): any => { + console.log(`revealing ${this.user.currentCardId} with delay of ${timeout}`); + timeout ? window.setTimeout(() => this.isRevealed = true, timeout) : this.isRevealed = true; + } + + public withdraw = (): void => { + this.stopTracking(); + this.isExiting = true; + }; + + getEmblemUrl = (id: number): string => this.service.getEmblemUrl(id); + + onAnimationEnd = (e: AnimationEvent) => { + if (e.animationName === 'withdraw' || e.animationName === 'withdraw-revealed') { + try { + this.selfRef.destroy(); + } catch { + } + } + } + + getStyle = (): any => ({ + 'transform': `rotateZ(${this.rotation}deg)`, + 'top': this.y != null ? `${this.y}px` : 'initial', + 'left': this.x != null ? `${this.x}px` : 'initial' + }) + + private trackTarget = (): void => { + const tracker = document.getElementById(`card-${this.user.id}`); + if (tracker) { + const rect = tracker.getBoundingClientRect(); + this.x = rect.left; + this.y = rect.top; + } + } + + private stopTracking = (): void => { + if (this.trackerInterval) { + window.clearInterval(this.trackerInterval); + this.trackerInterval = null; + } + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.html b/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.html index 83ee8fc..77364af 100644 --- a/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.html +++ b/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.html @@ -1,15 +1,12 @@
-
-
-
- -
-
- {{u.currentCard?.name}} -
+
+
- {{u.displayName}} + {{u.displayName}}
+
+
+
diff --git a/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.scss b/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.scss index 247accf..27645a1 100644 --- a/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.scss +++ b/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.scss @@ -1,83 +1,33 @@ -.cardScene { - perspective: 1200px; -} - -.playCard { - width: 160px; - height: 220px; - position: relative; - transform-style: preserve-3d; - border-radius: 5px; - overflow: hidden; -} - -.card-face { - position: absolute; - width: 100%; - height: 100%; - //backface-visibility: hidden; -} - -.card-back { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center; -} - -.card-face-front { - opacity: 1; -} - -.card-face-back { - opacity: 0; - transform: rotateY(180deg); -} - -.card-flipped { - animation: flipCard 1s ease forwards; +@import "../../../theme/variables"; - .card-face-front { - animation: crossFadeOut 1s ease forwards; - } - - .card-face-back { - animation: crossFadeIn 1s ease forwards; - } +.cardScene { + perspective: 500px; + margin-top: 24px; } -@keyframes flipCard { - from { - transform: rotateY(0); - } +.cardholder { + display: flex; + flex-direction: column; + margin: $some; + align-items: center; + text-align: center; - to { - transform: rotateY(180deg); + .card--aligner { + width: 160px; + height: 220px; } } -@keyframes crossFadeIn { - - 0%, - 40% { - opacity: 0; - } - - 60%, - 100% { - opacity: 1; - } +.overlayCardScene { + perspective: 500px; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; } -@keyframes crossFadeOut { - - 0%, - 40% { - opacity: 1; - } - - 60%, - 100% { - opacity: 0; - } +.card--label { + margin-top: 20px; } diff --git a/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.ts b/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.ts index 36a7322..fecb23b 100644 --- a/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.ts +++ b/pokeR/ClientApp/src/app/shared-components/playfield/playfield.component.ts @@ -1,26 +1,37 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewChild, ViewContainerRef, ComponentFactoryResolver, ComponentFactory, ComponentRef, OnDestroy } from '@angular/core'; import { PokerService } from 'src/app/services/poker.service'; import { User } from 'src/app/models/entities/user'; import { Observable, forkJoin } from 'rxjs'; import { map } from 'rxjs/operators'; import { Card } from 'src/app/models/entities/card'; import { ConfettiService } from '../confetti/confetti.service'; +import { PlayfieldCardComponent } from '../playfield-card/playfield-card.component'; @Component({ selector: 'app-playfield', templateUrl: './playfield.component.html', styleUrls: ['./playfield.component.scss'] }) -export class PlayfieldComponent implements OnInit { +export class PlayfieldComponent implements OnInit, OnDestroy { + @ViewChild('overlayCardHolder', { read: ViewContainerRef }) notificationHolder: ViewContainerRef; lastState: Array = new Array(); isRevealed = false; + cardComponents: Array> = []; - constructor(private service: PokerService, private confetti: ConfettiService) { } + constructor( + private service: PokerService, + private confetti: ConfettiService, + private resolver: ComponentFactoryResolver + ) { } ngOnInit() { this.watchState().subscribe(); } + ngOnDestroy(): void { + this.cardComponents.forEach(c => c.destroy()); + } + watchState = (): Observable => forkJoin( this.watchPlays(), @@ -31,7 +42,7 @@ export class PlayfieldComponent implements OnInit { watchPlays = (): Observable => this.service.cardPlays.pipe(map(p => { - this.lastState = p.collection; + this.updateState(p.collection); this.animateCardPlay(p.delta.currentCard, p.delta.emblemId, p.delta.displayName); })) @@ -42,13 +53,13 @@ export class PlayfieldComponent implements OnInit { watchRoundStart = (): Observable => this.service.roundStarts.pipe(map(() => { - this.lastState = new Array(); + this.updateState(new Array()); this.isRevealed = false; })) watchParts = (): Observable => this.service.userLeaves.pipe(map(c => { - this.lastState = this.lastState.filter(u => u.id !== c.delta.id); + this.updateState(this.lastState.filter(u => u.id !== c.delta.id)); })) getActiveCards = (): Array => this.lastState.filter((u: User) => u.currentCard !== null); @@ -62,9 +73,44 @@ export class PlayfieldComponent implements OnInit { console.log('party time!'); this.showConfetti(); } + + this.cardComponents.forEach((componentRef, index) => componentRef.instance.reveal(index * 70)); } getEmblemUrl = (id: number): string => this.service.getEmblemUrl(id); private showConfetti = (): void => this.confetti.pop(); + + private updateState = (newState: Array): void => { + const previousState = this.lastState; + this.lastState = newState; + + // need to add cards from new set for users that hadn't played yet or card value has changed + const cardsToAdd = newState + .filter(n => n.currentCardId) + .filter(n => !previousState.find(p => p.id === n.id && p.currentCardId === n.currentCardId)); + + // need to remove cards for users who parted or card value has changed + const cardsToRemove = previousState.filter(p => !newState.find(n => p.id === n.id && p.currentCardId === n.currentCardId)); + + console.log('updating', cardsToAdd, cardsToRemove); + cardsToRemove.forEach(this.removeCardComponent); + cardsToAdd.forEach(this.createCardComponent); + } + + private createCardComponent = (user: User) => { + const factory: ComponentFactory = this.resolver.resolveComponentFactory(PlayfieldCardComponent); + const container = this.notificationHolder; + const component = container.createComponent(factory); + component.instance.user = user; + component.instance.isRevealed = this.isRevealed; + component.instance.selfRef = component; + this.cardComponents.push(component); + } + + private removeCardComponent = (user: User): void => { + const components = this.cardComponents.filter(r => r.instance.user.id === user.id); + components.forEach(c => c.instance.withdraw()); + this.cardComponents = this.cardComponents.filter(c => !components.includes(c)); + } } diff --git a/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.html b/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.html index 3d640c3..6d0941d 100644 --- a/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.html +++ b/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.html @@ -1,15 +1,15 @@ -
-
You are joining
+
+ You are joining

{{room?.name}}

{{room?.tagLine}}
-
-
+
+
{{room?.deck?.name}}
-
+
- {{room?.users?.length}} user(s) + {{room?.users?.length}} {{room?.users?.length == 1 ? 'participant' : 'participants'}}
@@ -18,4 +18,4 @@
{{room?.tagLine}}
-
\ No newline at end of file +
diff --git a/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.scss b/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.scss index e69de29..cc05808 100644 --- a/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.scss +++ b/pokeR/ClientApp/src/app/shared-components/room-info/room-info.component.scss @@ -0,0 +1,26 @@ +@import '../../../theme/variables'; + +.join-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: $thicc; +} + +h2 { + margin: 0; + font-size: 64px; + font-weight: 100; +} + +.info-list { + display: flex; + flex-direction: row; + justify-content: space-around; + color: var(--accent-color); + + &>div { + margin-left: $mid; + margin-right: $mid; + } +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/roster/roster.component.html b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.html new file mode 100644 index 0000000..c9abaa6 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.html @@ -0,0 +1,17 @@ + +
+
+
+ + +
+
+ {{u.displayName}} + +
+
+
+
diff --git a/pokeR/ClientApp/src/app/shared-components/roster/roster.component.scss b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.scss new file mode 100644 index 0000000..c2033f8 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.scss @@ -0,0 +1,49 @@ +@import "../../../theme/variables"; + +.drop-hover { + transform: scale(1.1); +} + +.user-animate { + transition: all 0.2s ease; +} + +.draggable { + cursor: grab; + &:active { + cursor: grabbing; + } +} + +.forbidden { + cursor: not-allowed; +} + +.player-list { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + + & > .player { + display: flex; + padding: $mid; + flex-direction: row; + align-items: center; + justify-content: flex-start; + font-size: 1.3rem; + + &:not(.played) { + opacity: 0.6; + } + + & > .icon { + margin-right: $mid; + } + + .host-icon { + margin-left: $mid; + } + } +} diff --git a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.spec.ts similarity index 60% rename from pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.spec.ts rename to pokeR/ClientApp/src/app/shared-components/roster/roster.component.spec.ts index 57d82cf..e2e01a0 100644 --- a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.spec.ts +++ b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.spec.ts @@ -1,20 +1,20 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RoundStatusComponent } from './round-status.component'; +import { RosterComponent } from './roster.component'; describe('RoundStatusComponent', () => { - let component: RoundStatusComponent; - let fixture: ComponentFixture; + let component: RosterComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ RoundStatusComponent ] + declarations: [ RosterComponent ] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(RoundStatusComponent); + fixture = TestBed.createComponent(RosterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/pokeR/ClientApp/src/app/shared-components/roster/roster.component.ts b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.ts new file mode 100644 index 0000000..7a06c60 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/roster/roster.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit } from '@angular/core'; +import { PokerService } from 'src/app/services/poker.service'; +import { User } from 'src/app/models/entities/user'; +import { Observable, forkJoin } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'app-roster', + templateUrl: './roster.component.html', + styleUrls: ['./roster.component.scss'] +}) +export class RosterComponent implements OnInit { + users: Array = new Array(); + player: User; + hoveredUser: User; + + constructor(private service: PokerService) { } + + ngOnInit() { + this.player = this.service.player; + this.initialize().subscribe(); + this.monitorGameState().subscribe(); + } + initialize = (): Observable => this.service.getPlayers().pipe(map(p => this.users = p)) + + monitorGameState = (): Observable => + forkJoin( + this.watchPlays(), + this.watchParts(), + this.watchRoundStart(), + this.watchJoins(), + this.watchPlayerChanges(), + this.watchHostChanges() + ) + + watchParts = (): Observable => + this.service.userLeaves.pipe(map(c => { + this.users = c.collection; + })) + + watchPlays = (): Observable => + this.service.cardPlays.pipe(map(c => { + this.users = c.collection; + })) + + watchRoundStart = (): Observable => + this.service.roundStarts.pipe(map(() => { + this.users.forEach(u => { + u.currentCard = null; + u.currentCardId = null; + }); + })) + + watchJoins = (): Observable => + this.service.userJoins.pipe(map(c => { + this.users = c.collection; + })) + + watchPlayerChanges = (): Observable => + this.service.playerChanges.pipe(map(p => { + this.player = p; + })) + + watchHostChanges = (): Observable => + this.service.hostChanges.pipe(map(d => { + this.users = d.collection; + const playerMatch = d.collection.find(c => c.id === this.player.id); + if (playerMatch) { + this.player = playerMatch; + } + })) + + allowDrop = ($event: any, u: User) => { + $event.preventDefault(); + this.onDragEnter(u); + } + + setKing = (id: string): void => { + if (this.player.isHost && id !== this.getCurrentLeaderId()) { + console.log('Kinging ' + id); + this.service.changeHost(id).subscribe(); + } + } + + private getCurrentLeaderId = (): string => { + const leader = this.users.find(u => u.isHost); + return leader ? leader.id : null; + } + + onDragEnter = (u: User) => this.hoveredUser = u; + + onDragLeave = () => this.hoveredUser = null; +} diff --git a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.html b/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.html deleted file mode 100644 index 9b5a03d..0000000 --- a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.html +++ /dev/null @@ -1,45 +0,0 @@ -
-
-
- {{currentTagline}} -
- - -
- - -
-
-
-
-
-
- - -
-
- - {{u.displayName}} -
-
-
-
- - {{getTimeText()}} - -
-
-
\ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.scss b/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.scss deleted file mode 100644 index b84cb2c..0000000 --- a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.scss +++ /dev/null @@ -1,18 +0,0 @@ -.drop-hover { - transform: scale(1.1); -} - -.user-animate { - transition: all 0.2s ease; -} - -.draggable { - cursor: grab; - &:active { - cursor: grabbing; - } -} - -.forbidden { - cursor: not-allowed; -} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.ts b/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.ts deleted file mode 100644 index 3a50e3b..0000000 --- a/pokeR/ClientApp/src/app/shared-components/round-status/round-status.component.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { Component, OnInit, EventEmitter } from '@angular/core'; -import { PokerService } from 'src/app/services/poker.service'; -import { User } from 'src/app/models/entities/user'; -import { Observable, forkJoin, Subscription } from 'rxjs'; -import { map, debounceTime } from 'rxjs/operators'; - -@Component({ - selector: 'app-round-status', - templateUrl: './round-status.component.html', - styleUrls: ['./round-status.component.scss'] -}) -export class RoundStatusComponent implements OnInit { - users: Array = new Array(); - currentTagline: string; - player: User; - countdownInput: number; - countdownIsActive = false; - remainingTime: number; - maxTime: number; - deadline: Date; - readyToStart = false; - - hoveredUser: User; - - timer: number; - - private textChanges: EventEmitter = new EventEmitter(); - - constructor(private service: PokerService) { } - - ngOnInit() { - this.player = this.service.player; - this.initialize().subscribe(); - this.monitorGameState().subscribe(); - } - - getRemainingUsers = (): number => this.users.filter(u => u.currentCardId == null).length; - - initialize = (): Observable => - forkJoin( - this.service.getPlayers().pipe(map(p => this.users = p)), - this.service.getTagline().pipe(map(t => this.currentTagline = t)) - ).pipe(map(() => { })) - - onTextChange = (newText: string): void => { - this.textChanges.emit(newText); - this.service.updateTagline(newText).subscribe(); - } - - getRemainingTime = (): number => this.deadline ? this.deadline.getTime() - Date.now() : 0; - - updateRemainingTime = (): void => { - const time = this.getRemainingTime(); - if (time > 0) { - this.remainingTime = time; - this.timer = window.requestAnimationFrame(this.updateRemainingTime); - } else { - window.cancelAnimationFrame(this.timer); - } - } - - getCountdownValue = (): number => { - const time = this.remainingTime; - if (time > 0 && this.maxTime) { - return time * 100 / this.maxTime; - } - return 0; - } - - getTimeText = (): string => { - let result = ''; - const value = this.remainingTime / 1000; - if (value >= 60) { - result += `${Math.floor(value / 60)}m `; - } - result += `${Math.floor(value % 60)}s`; - return result; - } - - startCountdown = (): void => { - if (this.countdownInput !== 0) { - this.service.startTimer(1000 * this.countdownInput).subscribe(); - this.countdownIsActive = true; - window.setTimeout(() => { - this.service.endRound().subscribe(); - }, 1000 * this.countdownInput); - } - } - - startNewRound = (): Subscription => this.service.startRound().subscribe(); - - monitorGameState = (): Observable => - forkJoin( - this.watchPlays(), - this.watchParts(), - this.watchRoundStart(), - this.watchJoins(), - this.watchInputChange(), - this.watchTaglineChanges(), - this.watchRoundEnd(), - this.watchCountdownStart(), - this.watchPlayerChanges(), - this.watchHostChanges() - ).pipe(map(() => { })) - - watchInputChange = (): Observable => this.textChanges - .pipe(debounceTime(500)) - .pipe(map(t => this.service.storeTagline(t).subscribe())) - - watchTaglineChanges = (): Observable => - this.service.taglineUpdated.pipe(map(t => { - if (!this.player.isHost) { - this.currentTagline = t; - } - })) - - watchParts = (): Observable => - this.service.userLeaves.pipe(map(c => { - this.users = c.collection; - })) - - watchPlays = (): Observable => - this.service.cardPlays.pipe(map(c => { - this.users = c.collection; - })) - - watchRoundStart = (): Observable => - this.service.roundStarts.pipe(map(() => { - this.readyToStart = false; - this.users.forEach(u => { - u.currentCard = null; - u.currentCardId = null; - }); - })) - - watchRoundEnd = (): Observable => - this.service.roundEnds.pipe(map(() => { - this.countdownIsActive = false; - this.remainingTime = null; - this.maxTime = null; - this.countdownInput = null; - window.cancelAnimationFrame(this.timer); - this.readyToStart = true; - })) - - watchJoins = (): Observable => - this.service.userJoins.pipe(map(c => { - this.users = c.collection; - })) - - watchCountdownStart = (): Observable => - this.service.timerStarts.pipe(map(t => { - if (t) { - this.maxTime = t; - this.remainingTime = t; - this.deadline = new Date(Date.now() + t); - this.countdownIsActive = true; - this.timer = window.requestAnimationFrame(this.updateRemainingTime); - } - })) - - watchPlayerChanges = (): Observable => - this.service.playerChanges.pipe(map(p => { - this.player = p; - })) - - watchHostChanges = (): Observable => - this.service.hostChanges.pipe(map(d => { - this.users = d.collection; - const playerMatch = d.collection.find(c => c.id === this.player.id); - if (playerMatch) { - this.player = playerMatch; - } - })) - - allowDrop = ($event: any, u: User) => { - $event.preventDefault(); - this.onDragEnter(u); - } - - setKing = (id: string): void => { - if (this.player.isHost && id !== this.getCurrentLeaderId()) { - console.log('Kinging ' + id); - this.service.changeHost(id).subscribe(); - } - } - - private getCurrentLeaderId = (): string => { - const leader = this.users.find(u => u.isHost); - return leader ? leader.id : null; - } - - onDragEnter = (u: User) => this.hoveredUser = u; - - onDragLeave = () => this.hoveredUser = null; -} diff --git a/pokeR/ClientApp/src/app/shared-components/shared-components.module.ts b/pokeR/ClientApp/src/app/shared-components/shared-components.module.ts index eb1bdea..e428eec 100644 --- a/pokeR/ClientApp/src/app/shared-components/shared-components.module.ts +++ b/pokeR/ClientApp/src/app/shared-components/shared-components.module.ts @@ -7,11 +7,18 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RoomInfoComponent } from './room-info/room-info.component'; import { JoinRoomComponent } from './join-room/join-room.component'; import { CardListComponent } from './card-list/card-list.component'; -import { PlayfieldComponent } from './playfield/playfield.component'; -import { RoundStatusComponent } from './round-status/round-status.component'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { PlayfieldComponent } from './playfield/playfield.component' import { AppRoutingModule } from '../app-routing.module'; import { ConfettiComponent } from './confetti/confetti.component'; +import { EntrywayComponent } from './entryway/entryway.component'; +import { ThemeSwitcherComponent } from './theme-switcher/theme-switcher.component'; +import { FindRoomComponent } from './find-room/find-room.component'; +import { BackgroundCardsComponent } from './background-cards/background-cards.component'; +import { RosterComponent } from './roster/roster.component'; +import { PaneDrawerComponent } from './pane-drawer/pane-drawer.component'; +import { HostControlsComponent } from './host-controls/host-controls.component'; +import { TooltipModule } from 'ng2-tooltip-directive'; +import { PlayfieldCardComponent } from './playfield-card/playfield-card.component'; @NgModule({ declarations: [ @@ -22,12 +29,19 @@ import { ConfettiComponent } from './confetti/confetti.component'; JoinRoomComponent, CardListComponent, PlayfieldComponent, - RoundStatusComponent, - ConfettiComponent + RosterComponent, + ConfettiComponent, + EntrywayComponent, + ThemeSwitcherComponent, + FindRoomComponent, + BackgroundCardsComponent, + PaneDrawerComponent, + HostControlsComponent, + PlayfieldCardComponent ], imports: [ CommonModule, FormsModule, - NgbModule, + TooltipModule, AppRoutingModule, ReactiveFormsModule ], exports: [ @@ -37,8 +51,17 @@ import { ConfettiComponent } from './confetti/confetti.component'; JoinRoomComponent, CardListComponent, PlayfieldComponent, - RoundStatusComponent, - ConfettiComponent + RosterComponent, + ConfettiComponent, + EntrywayComponent, + ThemeSwitcherComponent, + BackgroundCardsComponent, + PaneDrawerComponent, + HostControlsComponent + ], + entryComponents: [ + NotificationComponent, + PlayfieldCardComponent ] }) export class SharedComponentsModule { } diff --git a/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.html b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.html new file mode 100644 index 0000000..41c8301 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.html @@ -0,0 +1,22 @@ +
+
+ + + +
+
+
+ + Automatic +
+
+ + Dark +
+
+ + Light +
+
+
\ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.scss b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.scss new file mode 100644 index 0000000..cc5f962 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.scss @@ -0,0 +1,64 @@ +@import '../../../theme/variables'; + +.switcher { + color: var(--contrast-color); + position: relative; + z-index: 30; + + .switcher-icon { + font-size: 18px; + cursor: pointer; + } + + .switcher-menu { + position: absolute; + pointer-events: none; + transition: all .3s ease; + opacity: 0; + transform: translateX(20px); + top: 0; + right: 0; + display: flex; + flex-direction: column; + background-color: var(--primary-color); + border-radius: $slim; + z-index: 35; + + .switcher-option { + display: flex; + flex-direction: row; + align-items: center; + padding: $mid; + border-radius: $slim; + cursor: pointer; + box-shadow: 0 0 0 0 inset var(--accent-color); + transition: all .3s ease; + user-select: none; + + i.mdi { + margin-right: $mid; + } + + &:hover { + background-color: var(--hover-color); + } + + &:active { + background-color: var(--active-color); + transition: all .1s ease; + } + + &.active { + box-shadow: 0 0 0 $slim inset var(--accent-color); + } + } + } + + &:hover { + .switcher-menu { + opacity: 1; + transform: translateX(0); + pointer-events: initial; + } + } +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.spec.ts b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.spec.ts new file mode 100644 index 0000000..4fdae9d --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ThemeSwitcherComponent } from './theme-switcher.component'; + +describe('ThemeSwitcherComponent', () => { + let component: ThemeSwitcherComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ThemeSwitcherComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ThemeSwitcherComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.ts b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.ts new file mode 100644 index 0000000..6040a99 --- /dev/null +++ b/pokeR/ClientApp/src/app/shared-components/theme-switcher/theme-switcher.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit } from '@angular/core'; +import { ThemeMode } from 'src/app/models/theme-mode'; +import { ThemeService } from 'src/app/services/theme.service'; + +@Component({ + selector: 'app-theme-switcher', + templateUrl: './theme-switcher.component.html', + styleUrls: ['./theme-switcher.component.scss'] +}) +export class ThemeSwitcherComponent implements OnInit { + public currentTheme: ThemeMode = ThemeMode.automatic; + public modes = ThemeMode; + + constructor( + private themeService: ThemeService + ) { } + + ngOnInit() { + this.themeService.applyTheme(this.currentTheme); + } + + setTheme = (theme: ThemeMode): void => { + this.currentTheme = theme; + this.themeService.applyTheme(theme); + } +} diff --git a/pokeR/ClientApp/src/index.html b/pokeR/ClientApp/src/index.html index 389a46d..5ae9ee8 100644 --- a/pokeR/ClientApp/src/index.html +++ b/pokeR/ClientApp/src/index.html @@ -10,9 +10,9 @@ - + @@ -26,4 +26,4 @@ - + \ No newline at end of file diff --git a/pokeR/ClientApp/src/styles.scss b/pokeR/ClientApp/src/styles.scss index bcd6147..edf5485 100644 --- a/pokeR/ClientApp/src/styles.scss +++ b/pokeR/ClientApp/src/styles.scss @@ -1,4 +1,12 @@ -/* You can add global styles to this file, and also import other style files */ +@import "./theme/variables"; +@import "./theme/forms"; +@import "./theme/flex"; + +* { + font-family: var(--font-family); + color: var(--contrast-color); + transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; +} .full-height { min-height: 100vh; @@ -20,19 +28,20 @@ opacity: 0.3; } -.invalid-feedback, -.valid-feedback { - display: initial; - animation: fallDown 0.4s ease forwards; +.d-none { + display: none; +} + +body { + margin: 0; + background-color: var(--primary-color); } -@keyframes fallDown { - from { - opacity: 0; - transform: translateY(-100%); - } - to { - opacity: 1; - transform: translateY(0); - } +.container { + padding-left: $some; + padding-right: $some; + max-width: 840px; + margin-left: auto; + margin-right: auto; + width: 100%; } diff --git a/pokeR/ClientApp/src/theme/_flex.scss b/pokeR/ClientApp/src/theme/_flex.scss new file mode 100644 index 0000000..6ac2b67 --- /dev/null +++ b/pokeR/ClientApp/src/theme/_flex.scss @@ -0,0 +1,15 @@ +.d-flex { + display: flex; +} + +.flex-row { + flex-direction: row; +} + +.justify-content-end { + justify-content: flex-end; +} + +.flex-wrap { + flex-wrap: wrap; +} diff --git a/pokeR/ClientApp/src/theme/_form-reset.scss b/pokeR/ClientApp/src/theme/_form-reset.scss new file mode 100644 index 0000000..3a4ca1a --- /dev/null +++ b/pokeR/ClientApp/src/theme/_form-reset.scss @@ -0,0 +1,191 @@ +/* ---------------------------------------------------------------------------------------------------- + +SCSS Form Reset Helpers - Forked from: https://gist.github.com/anthonyshort/552543 + +Intended usage: +- MIXINS: for very specific use cases, when you dont want to reset absolutly all the forms, very verbose output. +- PLACEHOLDER SELECTORS: use as extending classes. Less verbose, more generic overrides. + +A couple of things to watch out for: + +- IE8: If a text input doesn't have padding on all sides or none the text won't be centered. +- The default border sizes on text inputs in all UAs seem to be slightly different. You're better off using custom borders. +- You NEED to set the font-size and family on all form elements +- Search inputs need to have their appearance reset and the box-sizing set to content-box to match other UAs +- You can style the upload button in webkit using ::-webkit-file-upload-button +- ::-webkit-file-upload-button selectors can't be used in the same selector as normal ones. FF and IE freak out. +- IE: You don't need to fake inline-block with labels and form controls in IE. They function as inline-block. +- By turning off ::-webkit-search-decoration, it removes the extra whitespace on the left on search inputs + +----------------------------------------------------------------------------------------------------*/ + +@mixin form-reset-general { + display: inline-block; + margin: 0; + border: 0; + padding: 0; + width: auto; + vertical-align: middle; // IE6,IE7 + white-space: normal; + line-height: inherit; + background: none; + + /* Browsers have different default form fonts */ + color: inherit; + font-size: inherit; + font-family: inherit; + } + + %form-reset-general { + @include form-reset-general; + } + + @mixin form-reset-special-box-sizing { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + + %form-reset-special-box-sizing { + @include form-reset-special-box-sizing; + } + + @mixin form-reset-input-specific { + &:focus { + outline: 0; + } + + &[type=checkbox], + &[type=radio] { + width: 13px; + height: 13px; + } + + /* Make webkit render the search input like a normal text field */ + &[type=search] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + } + + /* Turn off the recent search for webkit. It adds about 15px padding on the left */ + @at-root { + ::-webkit-search-decoration { + display: none; + } + } + + /* Fix IE7 display bug */ + &[type="reset"], + &[type="button"], + &[type="submit"] { + overflow: visible; + } + } + + @mixin form-reset-input { + @include form-reset-general; + @include form-reset-input-specific; + + /* These elements are usually rendered a certain way by the browser */ + &[type=reset], + &[type=button], + &[type=submit], + &[type=checkbox], + &[type=radio] { + @include form-reset-special-box-sizing; + } + } + + %form-reset-input { + @extend %form-reset-general; + @include form-reset-input-specific; + + /* These elements are usually rendered a certain way by the browser */ + &[type=reset], + &[type=button], + &[type=submit], + &[type=checkbox], + &[type=radio] { + @extend %form-reset-special-box-sizing; + } + } + + @mixin form-reset-label { + @include form-reset-general; + } + + %form-reset-label { + @extend %form-reset-general; + } + + @mixin form-reset-select-specific { + /* Move the label to the top */ + &[multiple] { + vertical-align: top; + } + } + + @mixin form-reset-select { + @include form-reset-general; + @include form-reset-special-box-sizing; + @include form-reset-select-specific; + } + + %form-reset-select { + @extend %form-reset-general; + @extend %form-reset-special-box-sizing; + @include form-reset-select-specific; + } + + @mixin form-reset-button-specific { + background: none; + border: 0; + outline: none; + + &::-moz-focus-inner { + border: 0; + padding: 0; + } + &:hover, + &:active, + &:focus { + outline: 0; + } + + /* IE8 and FF freak out if this rule is within another selector */ + @at-root { + ::-webkit-file-upload-button { + padding: 0; + border: 0; + background: none; + } + } + } + + @mixin form-reset-button { + @include form-reset-general; + @include form-reset-button-specific; + } + + %form-reset-button { + @extend %form-reset-general; + @include form-reset-button-specific; + } + + @mixin form-reset-textarea-specific { + /* Move the label to the top */ + vertical-align: top; + + /* Turn off scroll bars in IE unless needed */ + overflow: auto; + } + + @mixin form-reset-textarea { + @include form-reset-general; + @include form-reset-textarea-specific; + } + + %form-reset-textarea { + @extend %form-reset-general; + @include form-reset-textarea-specific; + } \ No newline at end of file diff --git a/pokeR/ClientApp/src/theme/_forms.scss b/pokeR/ClientApp/src/theme/_forms.scss new file mode 100644 index 0000000..34929a4 --- /dev/null +++ b/pokeR/ClientApp/src/theme/_forms.scss @@ -0,0 +1,169 @@ +@import './form-reset'; +@import './variables'; + +@mixin box-input { + border: $slim solid var(--accent-color); + border-radius: $slim; + padding-left: $mid; + padding-right: $mid; + padding-bottom: $mid; + padding-top: $mid; + + &:focus { + border-color: var(--contrast-color); + } + + &.ng-invalid.ng-touched, + &.is-invalid.ng-touched { + border-color: var(--invalid-color) !important; + } + + &.ng-valid:focus, + &.is-valid:focus { + border-color: var(--valid-color); + } +} + +@mixin button { + border: $slim solid var(--accent-color); + padding-left: $thicc; + padding-right: $thicc; + padding-top: $some; + padding-bottom: $some; + border-radius: $slim; + cursor: pointer; + box-shadow: 0 0 0 0 inset var(--accent-color); + transition: background-color .2s ease, box-shadow .2s ease, color .2s ease; + + &:hover { + background-color: var(--hover-color); + } + + &:active { + background-color: var(--active-color); + box-shadow: 0 0 0 $slim inset var(--accent-color); + } + + &:disabled, + &.disabled { + cursor: not-allowed; + opacity: 0.5; + } +} + +.input-group { + display: flex; + flex-direction: column; + align-items: stretch; + margin-top: $thicc; + margin-bottom: $thicc*1.5; + position: relative; + + &:hover { + .floating-label { + color: var(--contrast-color); + } + } + + .floating-label { + position: absolute; + pointer-events: none; + transition: .2s ease all; + color: var(--accent-color); + left: $slim + $mid; + top: 11px; + z-index: 3; + font-weight: 700; + + &::after { + content: ''; + display: block; + position: absolute; + top: -20px; + left: -8px; + right: -8px; + bottom: 0; + background-color: var(--primary-color); + z-index: -1; + transform: scaleX(0); + transition: transform .2s ease, top .2s ease; + } + } + + .form-control:focus~.floating-label, + .form-control:not(:focus):valid~.floating-label { + transform: translateY(-22px) scale(0.8); + + &::after { + transform: scaleX(1); + top: 0; + } + } + + .form-control:focus~.floating-label { + color: var(--contrast-color); + } + + .helper { + position: absolute; + top: 48px; + font-size: 14px; + display: initial; + animation: fallDown 0.4s ease forwards; + + &.invalid-feedback, + &.invalid-feedback>i { + color: var(--invalid-color); + } + } + + &.form-control:focus~.helper.valid-feedback { + color: var(--valid-color); + + &>i { + color: var(--valid-color); + } + } +} + +@keyframes fallDown { + from { + opacity: 0; + transform: translateY(-100%); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +input { + @extend %form-reset-input; + @include box-input(); +} + +label { + @extend %form-reset-label; + +} + +select { + @extend %form-reset-select; + @include box-input(); + + option { + color: var(--contrast-color); + background-color: var(--primary-color); + } +} + +button { + @extend %form-reset-button; + @include button(); +} + +textarea { + @extend %form-reset-textarea; + @include box-input(); +} \ No newline at end of file diff --git a/pokeR/ClientApp/src/theme/_variables.scss b/pokeR/ClientApp/src/theme/_variables.scss new file mode 100644 index 0000000..87d3f3a --- /dev/null +++ b/pokeR/ClientApp/src/theme/_variables.scss @@ -0,0 +1,8 @@ +$thinn: 1px; +$realslim: 2px; +$slim: 4px; +$mid: 8px; +$some: 12px; +$thicc: 24px; + +$query-ie: "all and (-ms-high-contrast: none), (-ms-high-contrast: active)"; diff --git a/pokeR/Data/DbSeeder.cs b/pokeR/Data/DbSeeder.cs index 2a302e2..0a64352 100644 --- a/pokeR/Data/DbSeeder.cs +++ b/pokeR/Data/DbSeeder.cs @@ -11,9 +11,9 @@ namespace PokeR.Data public class DbSeeder { private RoomContext db; - private readonly IHostingEnvironment environment; + private readonly IWebHostEnvironment environment; - public DbSeeder(RoomContext db, IHostingEnvironment environment) + public DbSeeder(RoomContext db, IWebHostEnvironment environment) { this.db = db; this.environment = environment; diff --git a/pokeR/Dockerfile b/pokeR/Dockerfile index 6f6b5f3..7376269 100644 --- a/pokeR/Dockerfile +++ b/pokeR/Dockerfile @@ -1,21 +1,33 @@ -FROM armutcom/aspnet-core:spa-latest AS base +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +RUN apt-get update -yq \ + && apt-get install curl gnupg -yq \ + && curl -sL https://deb.nodesource.com/setup_10.x | bash \ + && apt-get install nodejs -yq WORKDIR /app EXPOSE 80 EXPOSE 443 ENV ASPNETCORE_ENVIRONMENT=Production -FROM armutcom/aspnet-core:2.2.3-sdk-2.2.105-node-8.11.2-build-bionic AS build +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +RUN apt-get update -yq \ + && apt-get install curl gnupg -yq \ + && curl -sL https://deb.nodesource.com/setup_10.x | bash \ + && apt-get install nodejs -yq WORKDIR /src COPY ["pokeR/PokeR.csproj", "pokeR/"] RUN dotnet restore "pokeR/PokeR.csproj" COPY . . +WORKDIR "/src/pokeR/ClientApp" +RUN npm install +RUN npm install -g @angular/cli +RUN npm rebuild node-sass WORKDIR "/src/pokeR" -RUN dotnet build "PokeR.csproj" -c Release -o /app +RUN dotnet build "PokeR.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "PokeR.csproj" -c Release -o /app +RUN dotnet publish "PokeR.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app -COPY --from=publish /app . +COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "PokeR.dll"] \ No newline at end of file diff --git a/pokeR/Hubs/RoomHub.cs b/pokeR/Hubs/RoomHub.cs index 1d77ac4..58ac9ea 100644 --- a/pokeR/Hubs/RoomHub.cs +++ b/pokeR/Hubs/RoomHub.cs @@ -87,7 +87,7 @@ public async Task SwitchHost(Guid Id) await Notify(Clients.Group(room.Id), $"{newHost.DisplayName} is now the host."); await Notify(Clients.Client(newHost.ConnectionId), "You are now the host."); await Clients.Client(newHost.ConnectionId).SendAsync("Self", newHost); - + await Clients.Group(room.Id).SendAsync("HostChange", new ListChange(newHost, await GetRoomUsers(room.Id))); } @@ -95,6 +95,7 @@ public async Task PlayCard(int cardId) { var user = await GetUser(); user.CurrentCardId = cardId; + user.LastPlayed = DateTime.Now; await db.SaveChangesAsync(); await Clients.Group(user.RoomId).SendAsync("CardPlayed", new ListChange(user, await GetRoomUsers(user.RoomId))); @@ -167,7 +168,9 @@ private async Task CloseRoom(string roomId) private async Task> GetRoomUsers(string roomId) => await db.Users .Include(u => u.CurrentCard) - .Where(u => u.RoomId == roomId).ToListAsync(); + .Where(u => u.RoomId == roomId) + .OrderBy(u => u.LastPlayed) + .ToListAsync(); private async Task GetRoomId() => await db.Users .Where(u => u.ConnectionId == Context.ConnectionId) diff --git a/pokeR/Models/Entities/User.cs b/pokeR/Models/Entities/User.cs index 80c2979..60113c7 100644 --- a/pokeR/Models/Entities/User.cs +++ b/pokeR/Models/Entities/User.cs @@ -21,6 +21,8 @@ public class User public int? CurrentCardId { get; set; } + public DateTime? LastPlayed { get; set; } + public virtual Room Room { get; set; } public virtual Emblem Emblem { get; set; } [ForeignKey("CurrentCardId")] diff --git a/pokeR/PokeR.csproj b/pokeR/PokeR.csproj index 105541e..fdb908f 100644 --- a/pokeR/PokeR.csproj +++ b/pokeR/PokeR.csproj @@ -1,7 +1,8 @@ - + - netcoreapp2.2 + netcoreapp3.1 + OutOfProcess true Latest false @@ -12,15 +13,19 @@ false Linux 2655f33f-2971-4c41-bf5e-0ad9afe6f19a + 2.0.0 - - - - - - + + + + + + + + + diff --git a/pokeR/Startup.cs b/pokeR/Startup.cs index dcd4664..f63a420 100644 --- a/pokeR/Startup.cs +++ b/pokeR/Startup.cs @@ -1,10 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using PokeR.Data; @@ -25,9 +25,8 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc() - .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) - .AddJsonOptions(options => + services.AddControllers() + .AddNewtonsoftJson(options => { options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); @@ -44,14 +43,14 @@ public void ConfigureServices(IServiceCollection services) }); services.AddTransient(); - services.AddSignalR().AddJsonProtocol(options => + services.AddSignalR().AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, DbSeeder seeder, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DbSeeder seeder, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { @@ -69,16 +68,12 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, DbSeeder loggerFactory.AddFile("Logs/pokeR-{Date}.log"); - app.UseSignalR(config => - { - config.MapHub("/notify/room"); - }); + app.UseRouting(); - app.UseMvc(routes => + app.UseEndpoints(endpoints => { - routes.MapRoute( - name: "default", - template: "{controller}/{action=Index}/{id?}"); + endpoints.MapHub("/notify/room"); + endpoints.MapControllerRoute("default", "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa =>