├── .gitignore
├── .prettierrc.json
├── shared
├── tsconfig.json
├── missing-types.d.ts
└── state.ts
├── server
├── tsconfig.json
├── render.tsx
├── components
│ └── pages
│ │ ├── big-screen
│ │ ├── styles.css
│ │ └── index.tsx
│ │ ├── header
│ │ └── index.tsx
│ │ ├── logged-out
│ │ └── index.tsx
│ │ ├── admin
│ │ ├── topics
│ │ │ └── index.tsx
│ │ ├── index.tsx
│ │ ├── simple-login
│ │ │ └── index.tsx
│ │ └── styles.css
│ │ ├── logged-in
│ │ └── index.tsx
│ │ ├── big-screen-iframe
│ │ └── index.tsx
│ │ └── user-page
│ │ ├── index.tsx
│ │ └── styles.css
├── missing-types.d.ts
├── utils.ts
├── config.ts
├── index.ts
├── big-screen
│ └── index.tsx
├── auth
│ └── index.tsx
├── user
│ └── index.tsx
├── admin
│ └── index.tsx
└── state.ts
├── client
├── tsconfig.json
├── missing-types.d.ts
├── big-screen-iframe
│ └── index.tsx
├── admin
│ └── index.tsx
├── admin-topics
│ └── index.tsx
├── user
│ └── index.tsx
├── components
│ ├── select
│ │ └── index.tsx
│ ├── big-screen
│ │ ├── vs.tsx
│ │ ├── champion-scores.tsx
│ │ ├── iframe.tsx
│ │ ├── audio.tsx
│ │ └── index.tsx
│ ├── color-select
│ │ └── index.tsx
│ ├── vote
│ │ ├── vote-buttons.tsx
│ │ └── index.tsx
│ ├── transition
│ │ └── index.tsx
│ └── admin
│ │ ├── presentation-view
│ │ └── index.tsx
│ │ ├── index.tsx
│ │ ├── bracket
│ │ └── index.tsx
│ │ ├── iframe
│ │ └── index.tsx
│ │ ├── selected-bracket
│ │ └── index.tsx
│ │ ├── champions
│ │ └── index.tsx
│ │ └── vote
│ │ └── index.tsx
├── big-screen
│ └── index.ts
├── ws
│ └── index.ts
└── utils.ts
├── generic-tsconfig.json
├── lib
├── node-external-plugin.js
├── resolve-dirs-plugin.js
├── consts-plugin.js
├── compress-plugin.js
├── asset-plugin.js
├── simple-ts.js
├── css-plugin.js
└── client-bundle-plugin.js
├── missing-types.d.ts
├── config.js
├── CONTRIBUTING.md
├── package.json
├── rollup.config.js
├── start.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .DS_Store
3 | node_modules
4 | .data
5 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../generic-tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["esnext", "dom", "dom.iterable"],
5 | "types": []
6 | },
7 | "include": ["./**/*"]
8 | }
9 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../generic-tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["esnext", "dom"],
5 | "types": ["node"]
6 | },
7 | "references": [{ "path": "../client" }, { "path": "../shared" }]
8 | }
9 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../generic-tsconfig.json",
3 | "compilerOptions": {
4 | "lib": ["esnext", "dom", "dom.iterable"],
5 | "types": []
6 | },
7 | "references": [{ "path": "../shared" }],
8 | "include": ["./**/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/generic-tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "target": "ES2020",
5 | "downlevelIteration": true,
6 | "module": "esnext",
7 | "jsx": "react",
8 | "jsxFactory": "h",
9 | "strict": true,
10 | "moduleResolution": "node",
11 | "outDir": ".data/ts-tmp",
12 | "composite": true,
13 | "declarationMap": true,
14 | "baseUrl": "./",
15 | "rootDir": "./",
16 | "allowSyntheticDefaultImports": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/missing-types.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | ///
14 |
--------------------------------------------------------------------------------
/shared/missing-types.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | ///
14 |
--------------------------------------------------------------------------------
/client/big-screen-iframe/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, render } from 'preact';
14 | import BigScreen from 'client/components/big-screen';
15 |
16 | render(, document.querySelector('.big-screen-container')!);
17 |
--------------------------------------------------------------------------------
/server/render.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import render from 'preact-render-to-string';
14 | import { h, VNode } from 'preact';
15 |
16 | export function renderPage(vnode: VNode) {
17 | return '' + render(vnode);
18 | }
19 |
--------------------------------------------------------------------------------
/client/admin/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, render } from 'preact';
14 | import Admin from 'client/components/admin';
15 | import { keepServerAlive } from 'client/utils';
16 |
17 | keepServerAlive();
18 | render(, document.querySelector('.admin-container')!);
19 |
--------------------------------------------------------------------------------
/client/admin-topics/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, render } from 'preact';
14 | import AdminTopics from 'client/components/admin-topics';
15 | import { keepServerAlive } from 'client/utils';
16 |
17 | keepServerAlive();
18 | render(, document.querySelector('.topics-container')!);
19 |
--------------------------------------------------------------------------------
/lib/node-external-plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | // Check that a node module exists, but treat it as external.
14 | export default function() {
15 | return {
16 | name: 'node-external',
17 | resolveId(id) {
18 | try {
19 | require.resolve(id);
20 | return { id, external: true };
21 | } catch (err) {}
22 | },
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/client/user/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, render } from 'preact';
14 | import Vote from 'client/components/vote';
15 |
16 | declare global {
17 | interface Window {
18 | initialState: Vote['props']['initialState'];
19 | }
20 | }
21 |
22 | render(
23 | ,
24 | document.querySelector('.vote-container')!,
25 | );
26 |
--------------------------------------------------------------------------------
/lib/resolve-dirs-plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { resolve } from 'path';
14 |
15 | export default function resolveDirs(paths) {
16 | return {
17 | name: 'resolve-dirs',
18 | async resolveId(id) {
19 | const foundMatch = paths.some(
20 | path => id === path || id.startsWith(path + '/'),
21 | );
22 | if (!foundMatch) return;
23 | return resolve(await this.resolveId('./' + id, './'));
24 | },
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/missing-types.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | declare module 'asset-url:*' {
14 | const value: string;
15 | export default value;
16 | }
17 |
18 | declare module 'consts:title' {
19 | const value: string;
20 | export default value;
21 | }
22 |
23 | declare module 'consts:subTitle' {
24 | const value: string;
25 | export default value;
26 | }
27 |
28 | declare module 'consts:webSocketOrigin' {
29 | const value: string;
30 | export default value;
31 | }
32 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 |
14 | /** Displayed to the user, and on the big screen, and the
*/
15 | export const title = 'Big Web Quiz';
16 | /** Displayed to the user, and on the big screen */
17 | export const subTitle = 'The over-engineered way to vote on stuff';
18 | /**
19 | * Set WEB_SOCKET_ORIGIN if you want to use a different origin for the web socket.
20 | * You probably don't.
21 | */
22 | export const webSocketOrigin = process.env.WEB_SOCKET_ORIGIN || '';
23 |
--------------------------------------------------------------------------------
/server/components/pages/big-screen/styles.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | html,
14 | body {
15 | background: #000;
16 | margin: 0;
17 | padding: 0;
18 | overflow: hidden;
19 | height: 100%;
20 | }
21 |
22 | html:fullscreen {
23 | cursor: none;
24 |
25 | iframe {
26 | pointer-events: none;
27 | }
28 | }
29 |
30 | iframe {
31 | position: absolute;
32 | top: 50%;
33 | left: 50%;
34 | width: 1920px;
35 | height: 1080px;
36 | border: none;
37 | transform: translate(-50%, -50%);
38 | }
39 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/lib/consts-plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | const moduleStart = 'consts:';
14 |
15 | export default function constsPlugin(consts) {
16 | return {
17 | name: 'consts-plugin',
18 | resolveId(id) {
19 | if (!id.startsWith(moduleStart)) return;
20 | return id;
21 | },
22 | load(id) {
23 | if (!id.startsWith(moduleStart)) return;
24 | const key = id.slice(moduleStart.length);
25 |
26 | if (!(key in consts)) {
27 | this.error(`Cannot find const: ${key}`);
28 | return;
29 | }
30 |
31 | return `export default ${JSON.stringify(consts[key])}`;
32 | },
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/client/components/select/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent, JSX } from 'preact';
14 |
15 | const Select: FunctionalComponent = props => {
16 | const { class: className, ...selectProps } = props;
17 |
18 | return (
19 |
20 |
21 |
22 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Select;
31 |
--------------------------------------------------------------------------------
/client/big-screen/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | const iframe = document.querySelector('iframe') as HTMLIFrameElement;
14 | const { width, height } = iframe.getBoundingClientRect();
15 |
16 | function resizeIframe() {
17 | const xScale = document.documentElement.offsetWidth / width;
18 | const yScale = document.documentElement.offsetHeight / height;
19 | const scale = Math.min(xScale, yScale);
20 | iframe.style.transform = `translate(-50%, -50%) scale(${scale})`;
21 | }
22 |
23 | window.onresize = resizeIframe;
24 | resizeIframe();
25 |
26 | window.onkeydown = (event: KeyboardEvent) => {
27 | if (event.key === 'f') {
28 | document.documentElement.requestFullscreen();
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/server/missing-types.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | ///
14 |
15 | declare module 'client-bundle:*' {
16 | const value: string;
17 | export default value;
18 | export const imports: string[];
19 | }
20 |
21 | declare module 'css:*' {
22 | const value: string;
23 | export default value;
24 | export const inline: string;
25 | }
26 |
27 | // Types for our session
28 | declare namespace Express {
29 | interface Session extends SessionData {
30 | user?: UserSession;
31 | voterId: string;
32 | simpleAdminPassword?: string;
33 | }
34 | }
35 |
36 | interface UserSession {
37 | email: string;
38 | emailVerified: boolean;
39 | name: string;
40 | picture?: string;
41 | id: string;
42 | }
43 |
--------------------------------------------------------------------------------
/server/components/pages/header/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent } from 'preact';
14 |
15 | interface Props {
16 | user?: UserSession;
17 | }
18 |
19 | const Header: FunctionalComponent = ({ user }) => {
20 | if (!user) return ;
21 |
22 | return (
23 |
24 |
33 |
36 |
37 | );
38 | };
39 |
40 | export default Header;
41 |
--------------------------------------------------------------------------------
/client/components/big-screen/vs.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent, RenderableProps } from 'preact';
14 |
15 | interface Props {
16 | colorFrom?: string;
17 | colorTo?: string;
18 | }
19 |
20 | const VS: FunctionalComponent = ({
21 | colorFrom,
22 | colorTo,
23 | children,
24 | }: RenderableProps) => (
25 |
32 |
33 |
34 |
35 |
36 |
{children || 'VS'}
37 |
38 | );
39 |
40 | export default VS;
41 |
--------------------------------------------------------------------------------
/lib/compress-plugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { brotliCompress } from 'zlib';
14 | import { promisify } from 'util';
15 |
16 | const compress = promisify(brotliCompress);
17 |
18 | export default function() {
19 | return {
20 | name: 'compress-plugin',
21 | async generateBundle(options, bundle) {
22 | const compressRe = /\.(js|css|svg|html)$/;
23 |
24 | await Promise.all(
25 | Object.values(bundle).map(async entry => {
26 | if (entry.type !== 'asset') return;
27 | if (!compressRe.test(entry.fileName)) return;
28 | const output = await compress(entry.source);
29 | this.emitFile({
30 | type: 'asset',
31 | source: output,
32 | fileName: entry.fileName + '.br',
33 | });
34 | }),
35 | );
36 | },
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/server/components/pages/logged-out/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent } from 'preact';
14 | import UserPage from '../user-page';
15 | import { palette } from 'shared/state';
16 | import { getHexColor } from 'client/utils';
17 |
18 | const LoggedOut: FunctionalComponent = () => {
19 | return (
20 |
21 |
22 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default LoggedOut;
39 |
--------------------------------------------------------------------------------
/server/components/pages/admin/topics/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent } from 'preact';
14 |
15 | import cssPath from 'css:../styles.css';
16 | import bundleURL from 'client-bundle:client/admin-topics';
17 | import title from 'consts:title';
18 |
19 | const TopicsAdminPage: FunctionalComponent = () => {
20 | return (
21 |
22 |
23 | Topics - Admin - {title}
24 |
25 |
26 |
27 |
28 |
29 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default AdminPage;
48 |
--------------------------------------------------------------------------------
/server/components/pages/admin/simple-login/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent } from 'preact';
14 |
15 | import cssPath from 'css:../styles.css';
16 | import title from 'consts:title';
17 |
18 | const AdminSimpleLogin: FunctionalComponent = () => {
19 | return (
20 |
21 |
22 | Admin - {title}
23 |
24 |
25 |
26 |
27 |
Login
28 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default AdminSimpleLogin;
52 |
--------------------------------------------------------------------------------
/client/components/color-select/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, Component } from 'preact';
14 | import { palette } from 'shared/state';
15 | import { getHexColor } from 'client/utils';
16 |
17 | interface Props {
18 | name: string;
19 | value?: number;
20 | disabled?: boolean;
21 | onChange?: (newVal: number) => void;
22 | }
23 |
24 | export default class ColorSelect extends Component {
25 | private _onClick = (event: Event) => {
26 | const onChange = this.props.onChange;
27 | if (!onChange) return;
28 | const el = event.currentTarget as HTMLInputElement;
29 | onChange(Number(el.value));
30 | };
31 |
32 | render({ name, disabled, value }: Props) {
33 | return (
34 |
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/server/components/pages/user-page/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, FunctionalComponent } from 'preact';
14 | import Header from '../header';
15 | import title from 'consts:title';
16 | import subTitle from 'consts:subTitle';
17 |
18 | import { inline as inlineCSS } from 'css:./styles.css';
19 |
20 | interface Props {
21 | user?: UserSession;
22 | }
23 |
24 | const UserPage: FunctionalComponent = ({ children, user }) => {
25 | return (
26 |
27 |
28 | {title}
29 |
30 |
36 |
42 |
46 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
{title}
59 |
{subTitle}
60 |
61 |
{children}
62 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default UserPage;
71 |
--------------------------------------------------------------------------------
/client/components/admin/presentation-view/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, Component } from 'preact';
14 | import { PresentationView } from 'shared/state';
15 | import { Operation } from 'fast-json-patch';
16 | import { fetchJSON } from 'client/utils';
17 |
18 | interface Props {
19 | mode: PresentationView;
20 | }
21 |
22 | const presentationStates: { [mode in PresentationView]: string } = {
23 | url: 'URL',
24 | bracket: 'Results bracket',
25 | 'champion-scores': 'Champion scores',
26 | };
27 |
28 | export default class AdminPresentationView extends Component {
29 | private _onRadioClick = (event: Event) => {
30 | const el = event.currentTarget as HTMLInputElement;
31 | const newVal = el.value as PresentationView;
32 |
33 | const patches: Operation[] = [
34 | {
35 | op: 'replace',
36 | path: `/presentationView`,
37 | value: newVal,
38 | },
39 | ];
40 |
41 | // Clear the zoom state of the bracket when we go back to the URL
42 | if (newVal === 'url') {
43 | patches.push({
44 | op: 'replace',
45 | path: `/bracketZoom`,
46 | value: null,
47 | });
48 | }
49 |
50 | fetchJSON('/admin/patch', {
51 | method: 'PATCH',
52 | body: patches,
53 | });
54 | };
55 |
56 | render({ mode }: Props) {
57 | return (
58 |
59 |
Presentation state
60 |
74 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/server/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 |
14 | /**
15 | * Google login is a little harder to set up, but it's more secure,
16 | * and is required if you want players to log in to vote.
17 | */
18 | export const useGoogleLogin = false;
19 |
20 | /**
21 | * Do players need to log in to vote?
22 | * Requiring login makes it harder to vote multiple times for a question.
23 | * This requires useGoogleLogin to be true, otherwise it's ignored.
24 | */
25 | export const requirePlayerLogin = false;
26 |
27 | /**
28 | * If you're not using Google login, the admin page is protected by a
29 | * simple password, set in your env file.
30 | */
31 | export const simpleAdminPassword = process.env.ADMIN_PASSWORD!;
32 |
33 | /**
34 | * If you're using Google login, add the email addresses of admin users
35 | * here.
36 | */
37 | export const admins: string[] = [];
38 |
39 | export const port = Number(process.env.PORT) || 8081;
40 |
41 | export const origin = (() => {
42 | if (process.env.ORIGIN) return process.env.ORIGIN;
43 | if (process.env.PROJECT_DOMAIN) {
44 | return `https://${process.env.PROJECT_DOMAIN}.glitch.me`;
45 | }
46 | return `http://localhost:${port}`;
47 | })();
48 |
49 | export const cookieDomain = process.env.COOKIE_DOMAIN || '';
50 |
51 | export const cookieSecret: string = process.env.COOKIE_SECRET!;
52 | if (!cookieSecret) throw Error('No cookie secret set');
53 |
54 | export const oauthClientId: string = process.env.OAUTH_CLIENT!;
55 | if (useGoogleLogin && !oauthClientId) throw Error('No oauth client set');
56 |
57 | export const oauthClientSecret: string = process.env.OAUTH_SECRET!;
58 | if (useGoogleLogin && !oauthClientSecret) throw Error('No oauth secret set');
59 |
60 | if (!useGoogleLogin && requirePlayerLogin) {
61 | throw Error('Google login must be enabled if players are required to log in');
62 | }
63 |
64 | if (!useGoogleLogin && !simpleAdminPassword) {
65 | throw Error('No admin password set');
66 | }
67 |
68 | if (useGoogleLogin && admins.length === 0) {
69 | throw Error('No admins listed');
70 | }
71 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import del from 'del';
14 | import resolve from 'rollup-plugin-node-resolve';
15 | import { terser } from 'rollup-plugin-terser';
16 |
17 | import simpleTS from './lib/simple-ts';
18 | import clientBundlePlugin from './lib/client-bundle-plugin';
19 | import nodeExternalPlugin from './lib/node-external-plugin';
20 | import cssPlugin from './lib/css-plugin';
21 | import assetPlugin from './lib/asset-plugin';
22 | import constsPlugin from './lib/consts-plugin';
23 | import resolveDirsPlugin from './lib/resolve-dirs-plugin';
24 | import compressPlugin from './lib/compress-plugin';
25 | import * as config from './config';
26 |
27 | const isGlitch = 'PROJECT_DOMAIN' in process.env;
28 | const nullPlugin = {};
29 | /**
30 | * This will generate brotli-compressed assets. You don't need to do this
31 | * if you're using a CDN which does this automatically (eg, Cloudflare).
32 | */
33 | const compressAssets = isGlitch;
34 |
35 | function resolveFileUrl({ fileName }) {
36 | return JSON.stringify('/' + fileName);
37 | }
38 |
39 | export default async function({ watch }) {
40 | await del('.data/dist');
41 |
42 | const tsPluginInstance = simpleTS('server', { watch });
43 | const commonPlugins = () => [
44 | tsPluginInstance,
45 | resolveDirsPlugin(['client', 'server', 'shared']),
46 | assetPlugin(),
47 | constsPlugin(config),
48 | ];
49 |
50 | return {
51 | input: 'server/index.ts',
52 | output: {
53 | dir: '.data/dist/',
54 | format: 'cjs',
55 | assetFileNames: 'assets/[name]-[hash][extname]',
56 | exports: 'named',
57 | },
58 | watch: { clearScreen: false },
59 | preserveModules: true,
60 | plugins: [
61 | { resolveFileUrl },
62 | clientBundlePlugin(
63 | {
64 | plugins: [
65 | { resolveFileUrl },
66 | ...commonPlugins(),
67 | resolve(),
68 | terser({ module: true }),
69 | ],
70 | },
71 | {
72 | dir: '.data/dist/',
73 | format: 'esm',
74 | chunkFileNames: 'assets/[name]-[hash].js',
75 | entryFileNames: 'assets/[name]-[hash].js',
76 | },
77 | resolveFileUrl,
78 | ),
79 | cssPlugin(),
80 | ...commonPlugins(),
81 | nodeExternalPlugin(),
82 | compressAssets ? compressPlugin() : nullPlugin,
83 | ],
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/client/components/big-screen/champion-scores.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, Component } from 'preact';
14 | import { Champions } from 'shared/state';
15 | import Transition from '../transition';
16 | import { animate } from 'client/utils';
17 |
18 | const genericAvatarURL =
19 | 'https://cdn.glitch.com/b7996c5b-5a36-4f1b-84db-52a31d101dfc%2Favatar.svg?v=1577974252576';
20 |
21 | interface Props {
22 | champions: Champions;
23 | }
24 |
25 | export default class PresentationChampionScores extends Component {
26 | private _onScoreTransition = async (el: HTMLElement) => {
27 | const outgoing = el.children[0] as HTMLElement;
28 | const incoming = el.children[1] as HTMLElement;
29 |
30 | const moveDistance = 200;
31 | const newIsLarger =
32 | Number(incoming.textContent) > Number(outgoing.textContent);
33 | const duration = 250;
34 | const easing = 'cubic-bezier(0.645, 0.045, 0.355, 1.000)'; // easeInOutCubic
35 |
36 | animate(
37 | outgoing,
38 | {
39 | to: {
40 | transform: `translateY(${moveDistance * (newIsLarger ? -1 : 1)}px)`,
41 | opacity: '0',
42 | },
43 | },
44 | { duration, easing },
45 | );
46 |
47 | return animate(
48 | incoming,
49 | {
50 | from: {
51 | transform: `translateY(${moveDistance * (newIsLarger ? 1 : -1)}px)`,
52 | opacity: '0',
53 | },
54 | },
55 | { duration, easing },
56 | );
57 | };
58 |
59 | render({ champions }: Props) {
60 | return (
61 |
301 | );
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Big Web Quiz
2 |
3 | This exists on:
4 |
5 | - Github https://github.com/GoogleChromeLabs/big-web-quiz
6 | - Glitch https://glitch.com/edit/#!/bwq
7 |
8 | ## What's this all about?
9 |
10 | At Chrome Dev Summit in 2019 [we polled the audience to discover their favourite web feature of 2019](https://www.youtube.com/watch?v=RE4QlXdou8c). Here's the stuff we (hastily) built to make it all happen. You can use it to run your own live polls, present slides, and run contests.
11 |
12 | **Glitch limits projects to 4000 requests per hour** which will limit the number of people you can have taking part to around 200-500. If you need more, you'll need to set up your own server.
13 |
14 | ## Getting started on Glitch
15 |
16 | First, remix [this project](https://glitch.com/edit/#!/bwq) to create your own copy, and fill in the following in your project's `.env` file:
17 |
18 | - `ADMIN_PASSWORD` - This is the password you'll use to access the admin panel. Make this something difficult to guess, and don't like, write it on your forehead.
19 | - `COOKIE_SECRET` - Make this something unique. You don't need to remember it, so a quick mash of the keyboard is enough.
20 |
21 | For example:
22 |
23 | ```sh
24 | ADMIN_PASSWORD=useSomethingDifferent
25 | COOKIE_SECRET=wociamssjcn9834gkj
26 | ```
27 |
28 | But yeah, **don't just copy the above**. If you want things to be more secure, you can use Google for logins. More on that later…
29 |
30 | Once you've done the above, the project should build successfully, and you're ready to go!
31 |
32 | ## URLs
33 |
34 | - `/big-screen/` - This is the page you'll display for everyone to see. I recommend clicking somewhere on this page before presenting, as this will allow sound to play. You don't need the sound, but the sound is _cool_ and synchronising it took me _effort_ so please don't _miss out on that_.
35 | - `/admin/` - This is how you'll control things. Remember how I said this was built hastily? Well, once you see the design of this page you'll see where all the haste was spent.
36 | - `/` - This is the page folks will use to vote.
37 |
38 | ## Creating a vote
39 |
40 | 1. From `/admin/`, go to the "Active vote" section and click "New vote".
41 | 1. Click "Edit" and enter labels for each. This is the label that'll appear to the players and on the big screen.
42 | 1. Ponder briefly at what "Champion" means, then skip over it (it's optional, we'll cover to it later).
43 | 1. Click 'save'.
44 |
45 | And with that, you've created your first vote! No, nothing happens yet. To make stuff happen you need to change the "State":
46 |
47 | - `Staging` - The vote appears in the admin page, but it doesn't appear on the big screen, or to users.
48 | - `Introducing` - The vote labels appear on the big screen, but voting hasn't started yet.
49 | - `Voting` - Users can now vote. The big screen shows the live results.
50 | - `Results` - Users can no longer vote. The big screen shows the final results.
51 | - `Clear vote` - Remove the vote. It's removed from the big screen and admin page.
52 |
53 | Cookies are used to 'prevent' folks from voting multiple times. 'Prevent' is in quotes because it's pretty ineffective. Folks can just use multiple browsers, profiles, or voting bots to get around it. If you want extra protection, you'll need to make users log in. More on that later.
54 |
55 | _But first,_ more fun stuff:
56 |
57 | ## Champions
58 |
59 | 'Champions' are people (or teams, or I dunno, maybe other things). In the "Champions" section of `/admin/` you can add/edit champions. Avatars don't need to be bigger than 270x270, but larger/smaller won't break anything. Non-square avatars might break layout. I dunno. Did I mention this was built hastily?
60 |
61 | Of course, the best place to upload avatars is Glitch's 'assets' CDN.
62 |
63 | Once you have champions, you can assign them to votes. This means they'll be displayed on the big screen while a vote is happening, and that's it really.
64 |
65 | You can also give champions a score. You can display scores on the big screen by changing the "Presentation state" on the `/admin/` page to "Champion scores".
66 |
67 | Champions aren't automatically awarded points for 'winning' votes. Scoring is entirely manual and independent from everything else. But this means you can award points however you want. Maybe give double points to a champion if it's their birthday? Maybe secretly dock points from someone you hate? It's up to you.
68 |
69 | ## Presenting iframes
70 |
71 | Do you want to display the contents of another page on the big screen? No? Well stop reading this section. For everyone else:
72 |
73 | In the "Present iframe" section of `/admin/`, click "Edit", enter a URL, then click "save". It'll now be displayed on the big screen. Click "clear" to get rid of it.
74 |
75 | And that's it! No, wait, that's not it…
76 |
77 | ### Iframe actions
78 |
79 | If you want to be really smart, your iframe can expose 'actions' using [Comlink](https://github.com/GoogleChromeLabs/comlink).
80 |
81 | [Here's an example you can remix](https://glitch.com/edit/#!/bwq-iframe-example?path=script.js). Edit the object passed to `init`. The keys are labels, and the values are functions to call.
82 |
83 | Give it a try by showing `https://bwq-iframe-example.glitch.me/` as an iframe. The admin page will now offer the actions "Go red", "Go green", "Go blue", which you can click to run the functions. This is what we used at Chrome Dev Summit to advance the slides as we were talking about features.
84 |
85 | ## Creating a sports bracket
86 |
87 | First, create some topics that'll be part of the bracket:
88 |
89 | 1. From `/admin/`, go to "Edit topics".
90 | 1. "Add"
91 | 1. Give it a name, and optionally a champion and "Slides URL". You can easily change these later so don't worry too much about it. "Slides URL" just makes it easier to display this URL as an iframe later.
92 | 1. Add more, then click 'Save'.
93 |
94 | Back on `/admin/` in the "Bracket" section, click "Regenerate bracket".
95 |
96 | The bracket can have any number of items, but we optimised it for 16 items (8 and 32 work pretty well too). The current CSS might not quite work for other numbers, but feel free to go in and edit the CSS!
97 |
98 | ## Displaying & editing the bracket
99 |
100 | From `/admin/` you can set the "Presentation state" to "Results bracket", which will display the bracket on the big screen.
101 |
102 | See those `...` buttons in the "Bracket" section of `/admin/`? Tap those to select that area of the bracket. That'll populate the "Selected bracket" section of `/admin/`.
103 |
104 | Clicking "Zoom here" will zoom the big screen into that section of the bracket. Click "Zoom out" in the "Bracket" section if you want to zoom back out.
105 |
106 | You can use the "Selected bracket" section to assign a topic. Then, you'll be able to change its champion, and show its iframe if it has one.
107 |
108 | Clicking "create vote" will populate the "Active vote" section with these two items.
109 |
110 | Clicking "Set as winner" will make this item the winner of this stage. You can change the winner later if you want.
111 |
112 | ## Better logins
113 |
114 | If you want, you can use Google Sign-In for admins and even regular users. This is more secure for admins, but it also prevents most vote cheating, as each vote is tied to a Google login. Of course, if someone has 100 Google logins, they can still vote 100 times, but meh, well done them. If you don't require users to log in, they can [pretty easily write a voting bot](https://medium.com/@jsoverson/how-i-hacked-the-vote-at-chrome-dev-summit-ba6cea7a468e).
115 |
116 | By the way, if you're a government agent and you're thinking of using this system for an election, please stop now. Stop everything. Go away.
117 |
118 | To set up a Google Sign-in project:
119 |
120 | 1. Click the "Configure a project" button on [this page](https://developers.google.com/identity/sign-in/web/sign-in).
121 | 1. Create a new project and give it a name.
122 | 1. For "where are you calling from", pick "Web server".
123 | 1. For "Authorized redirect URIs", enter your Glitch project domain followed by `/auth/oauth2callback`. So, if your Glitch domain is `https://bwq.glitch.me`, you'd enter `https://bwq.glitch.me/auth/oauth2callback`.
124 | 1. Save the "Client ID" and "Client Secret".
125 |
126 | Then in your project's `.env` file add:
127 |
128 | ```sh
129 | OAUTH_CLIENT=client_id_goes_here
130 | OAUTH_SECRET=client_secret_goes_here
131 | ```
132 |
133 | Of course, replace the above values with your "Client ID" and "Client Secret".
134 |
135 | Then, over in `./server/config.ts`:
136 |
137 | 1. Set `useGoogleLogin` to true.
138 | 1. Set `requirePlayerLogin` to true if you want to require voters to log in.
139 | 1. Set `admins` to an array of email addresses of admins.
140 |
141 | Done!
142 |
143 | ## Other config
144 |
145 | ## Environment variables
146 |
147 | - `ADMIN_PASSWORD` - If `useGoogleLogin` is false, this is the password for the admin panel.
148 | - `OAUTH_CLIENT` - For Google logins. See above.
149 | - `OAUTH_SECRET` - For Google logins. See above.
150 | - `COOKIE_SECRET` - A random string to improve cookie security.
151 | - `COOKIE_DOMAIN` - The domain for the cookie. It's useful if you want the cookie to apply to subdomains. You shouldn't need this when using Glitch.
152 | - `WEB_SOCKET_ORIGIN` - Optional origin to use for web sockets. Defaults to the same origin.
153 | - `PORT` - Server port number. Again, you probably don't need to change this.
154 | - `ORIGIN` - This is usually autodetected, but you can set it if autodetection doesn't do the right thing. It works fine on Glitch.
155 | - `STORAGE_ROOT` - Where to store persistant data. Defaults to `./.data`, which is the correct value for Glitch.
156 |
157 | ## Config in JS files
158 |
159 | `./config.js`
160 |
161 | - `title` - The main site title.
162 | - `subTitle` - Um, the site sub title.
163 |
164 | Anything exported from this will be available via `import val from 'consts:propery-name-here';`.
165 |
166 | `./shared/state.ts`
167 |
168 | - `palette` - The gradients available to the app.
169 | - `presetIframes` - This populates the admin panel with shortcuts to display particular iframes.
170 |
171 | `./server/config.ts`
172 |
173 | - `useGoogleLogin` - Enable using Google logins.
174 | - `requireLogin` - Should users have to log in with Google in order to vote?
175 | - `admins` - Who can log in to the admin bits when Google login is enabled.
176 |
177 | ## I wish this app did things differently!
178 |
179 | Change it! Like I said, this was built in a hurry, so there's code in there I'm not proud of, but feel free to dig in and change stuff! That's the beauty of Glitch.
180 |
--------------------------------------------------------------------------------
/client/components/admin/champions/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License.
5 | * You may obtain a copy of the License at
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS,
9 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | * See the License for the specific language governing permissions and
11 | * limitations under the License.
12 | */
13 | import { h, Component } from 'preact';
14 | import { Operation } from 'fast-json-patch';
15 | import { Champions, Champion } from 'shared/state';
16 | import {
17 | fetchJSON,
18 | escapePatchPathComponent,
19 | generateRandomId,
20 | } from 'client/utils';
21 |
22 | const genericAvatarURL =
23 | 'https://cdn.glitch.com/b7996c5b-5a36-4f1b-84db-52a31d101dfc%2Favatar.svg?v=1577974252576';
24 |
25 | interface EditedChampion extends Champion {
26 | saving: boolean;
27 | }
28 |
29 | interface EditedChampions {
30 | [id: string]: EditedChampion;
31 | }
32 |
33 | interface Props {
34 | champions: Champions;
35 | }
36 |
37 | interface State {
38 | editedChampions: EditedChampions;
39 | }
40 |
41 | export default class AdminChampions extends Component {
42 | state: State = {
43 | editedChampions: {},
44 | };
45 |
46 | private _onPlusClick = (event: Event) => this._onScoreButtonClick(event, 1);
47 | private _onMinusClick = (event: Event) => this._onScoreButtonClick(event, -1);
48 |
49 | private _onScoreButtonClick(event: Event, delta: number): void {
50 | const el = (event.currentTarget as HTMLElement).closest(
51 | '.edit-champion-item',
52 | ) as HTMLElement;
53 | const id = el.dataset.id as string;
54 |
55 | fetchJSON('/admin/patch', {
56 | method: 'PATCH',
57 | body: [
58 | {
59 | op: 'replace',
60 | path: `/champions/${escapePatchPathComponent(id)}/score`,
61 | value: (this.props.champions[id]?.score || 0) + delta,
62 | },
63 | ],
64 | });
65 | }
66 |
67 | private _onResetClick = () => {
68 | if (!confirm('Are you sure?')) return;
69 |
70 | fetchJSON('/admin/patch', {
71 | method: 'PATCH',
72 | body: Object.keys(this.props.champions).map(championId => ({
73 | op: 'replace',
74 | path: `/champions/${escapePatchPathComponent(championId)}/score`,
75 | value: 0,
76 | })),
77 | });
78 | };
79 |
80 | private _onAddClick = () => {
81 | this.setState(({ editedChampions }) => ({
82 | editedChampions: {
83 | ...editedChampions,
84 | [generateRandomId()]: {
85 | name: '',
86 | picture: '',
87 | score: 0,
88 | saving: false,
89 | },
90 | },
91 | }));
92 | };
93 |
94 | private _onEditClick = (event: Event) => {
95 | const el = event.currentTarget as HTMLInputElement;
96 | const champEl = el.closest('.edit-champion-item') as HTMLInputElement;
97 | const id = champEl.dataset.id!;
98 |
99 | this.setState(({ editedChampions }) => ({
100 | editedChampions: {
101 | ...editedChampions,
102 | [id]: {
103 | ...this.props.champions[id],
104 | saving: false,
105 | },
106 | },
107 | }));
108 | };
109 |
110 | private _onDeleteClick = (event: Event) => {
111 | const el = event.currentTarget as HTMLInputElement;
112 | const champEl = el.closest('.edit-champion-item') as HTMLInputElement;
113 | const id = champEl.dataset.id!;
114 |
115 | if (
116 | !confirm(`Delete ${this.props.champions[id].name || 'Unnamed champion'}?`)
117 | ) {
118 | return;
119 | }
120 |
121 | fetchJSON('/admin/patch', {
122 | method: 'PATCH',
123 | body: [
124 | {
125 | op: 'remove',
126 | path: `/champions/${escapePatchPathComponent(id)}`,
127 | },
128 | ],
129 | });
130 | };
131 |
132 | private _onSaveClick = async (event: Event) => {
133 | const el = event.currentTarget as HTMLInputElement;
134 | const champEl = el.closest('.edit-champion-item') as HTMLInputElement;
135 | const id = champEl.dataset.id!;
136 |
137 | this.setState(({ editedChampions }) => ({
138 | editedChampions: {
139 | ...editedChampions,
140 | [id]: {
141 | ...editedChampions[id],
142 | saving: true,
143 | },
144 | },
145 | }));
146 |
147 | const keys: Array = ['name', 'picture'];
148 | const isNew = !(id in this.props.champions);
149 | const patches: Operation[] = [];
150 |
151 | if (isNew) {
152 | patches.push({
153 | op: 'add',
154 | path: `/champions/${escapePatchPathComponent(id)}`,
155 | value: { score: 0 },
156 | });
157 | }
158 |
159 | patches.push(
160 | ...(keys.map(key => ({
161 | op: 'replace',
162 | path: `/champions/${escapePatchPathComponent(id)}/${key}`,
163 | value: this.state.editedChampions[id][key],
164 | })) as Operation[]),
165 | );
166 |
167 | await fetchJSON('/admin/patch', {
168 | method: 'PATCH',
169 | body: patches,
170 | });
171 |
172 | this.setState(({ editedChampions }) => {
173 | const newEditedChampions = { ...editedChampions };
174 | delete newEditedChampions[id];
175 | return { editedChampions: newEditedChampions };
176 | });
177 | };
178 |
179 | private _onChampionInput = (event: Event) => {
180 | const el = event.currentTarget as HTMLInputElement;
181 | const champEl = el.closest('.edit-champion-item') as HTMLInputElement;
182 | const id = champEl.dataset.id!;
183 | const nameEl = champEl.querySelector(
184 | '.edit-champion-name',
185 | ) as HTMLInputElement;
186 | const urlEl = champEl.querySelector(
187 | '.edit-champion-url',
188 | ) as HTMLInputElement;
189 |
190 | this.setState(({ editedChampions }) => ({
191 | editedChampions: {
192 | ...editedChampions,
193 | [id]: {
194 | ...editedChampions[id],
195 | name: nameEl.value,
196 | picture: urlEl.value,
197 | },
198 | },
199 | }));
200 | };
201 |
202 | render({ champions }: Props, { editedChampions }: State) {
203 | const allChampions = { ...champions, ...editedChampions };
204 |
205 | return (
206 |
207 |