├── src
├── index.ts
├── styles
│ └── tailwind.css
├── utils
│ ├── utils.ts
│ └── utils.spec.ts
├── components
│ └── poll-party
│ │ ├── store.ts
│ │ ├── readme.md
│ │ ├── poll-party.css
│ │ └── poll-party.tsx
├── index.html
└── components.d.ts
├── assets
├── poll.png
├── results.png
├── source.png
└── poll-party.gif
├── partykit.json
├── tailwind.config.js
├── tsconfig.json
├── docs
└── index.html
├── LICENSE
├── stencil.config.ts
├── partykit
└── polls.ts
├── package.json
├── .gitignore
├── README.md
└── stencil-readme.md
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 |
--------------------------------------------------------------------------------
/assets/poll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/partykit/sketch-polls/HEAD/assets/poll.png
--------------------------------------------------------------------------------
/partykit.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "partykit/polls.ts",
3 | "name": "poll-party",
4 | }
--------------------------------------------------------------------------------
/assets/results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/partykit/sketch-polls/HEAD/assets/results.png
--------------------------------------------------------------------------------
/assets/source.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/partykit/sketch-polls/HEAD/assets/source.png
--------------------------------------------------------------------------------
/src/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/assets/poll-party.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/partykit/sketch-polls/HEAD/assets/poll-party.gif
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | export function format(first: string, middle: string, last: string): string {
2 | return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/poll-party/store.ts:
--------------------------------------------------------------------------------
1 | import { createLocalStore } from "stencil-store-storage";
2 |
3 | export const key = "poll-party";
4 | export const defaultValues = {
5 | hasVoted: [],
6 | };
7 | const local = createLocalStore(key, defaultValues);
8 | const state = local.state;
9 |
10 | export default state;
11 |
--------------------------------------------------------------------------------
/src/components/poll-party/readme.md:
--------------------------------------------------------------------------------
1 | # poll-party
2 |
3 |
4 |
5 |
6 |
7 |
8 | ## Properties
9 |
10 | | Property | Attribute | Description | Type | Default |
11 | | -------- | --------- | ----------- | -------- | ----------- |
12 | | `host` | `host` | | `string` | `undefined` |
13 | | `party` | `party` | | `string` | `null` |
14 |
15 |
16 | ----------------------------------------------
17 |
18 | *Built with [StencilJS](https://stenciljs.com/)*
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "allowUnreachableCode": false,
5 | "declaration": false,
6 | "experimentalDecorators": true,
7 | "lib": [
8 | "dom",
9 | "es2017"
10 | ],
11 | "moduleResolution": "node",
12 | "module": "esnext",
13 | "target": "es2017",
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "jsx": "react",
17 | "jsxFactory": "h"
18 | },
19 | "include": [
20 | "src"
21 | ],
22 | "exclude": [
23 | "node_modules"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { format } from './utils';
2 |
3 | describe('format', () => {
4 | it('returns empty string for no names defined', () => {
5 | expect(format(undefined, undefined, undefined)).toEqual('');
6 | });
7 |
8 | it('formats just first names', () => {
9 | expect(format('Joseph', undefined, undefined)).toEqual('Joseph');
10 | });
11 |
12 | it('formats first and last names', () => {
13 | expect(format('Joseph', undefined, 'Publique')).toEqual('Joseph Publique');
14 | });
15 |
16 | it('formats first, middle and last names', () => {
17 | expect(format('Joseph', 'Quincy', 'Publique')).toEqual('Joseph Quincy Publique');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | poll-party demo
7 |
8 |
9 |
10 |
11 |
12 |
13 | What is the best animal?
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | poll-party demo
5 |
6 |
7 |
8 |
9 |
10 | poll-party demo
11 |
12 |
13 | What is the best animal?
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | View source to see:
23 |
24 |
25 |
26 | - How the component is imported in a script tag
27 | - How to set the PartyKit host
28 | - How to set up the poll-party with question and options.
29 |
30 |
31 |
32 | poll-party on GitHub →
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 PartyKit, inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/stencil.config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from "@stencil/core";
2 | import { sass } from "@stencil/sass";
3 | import tailwind, {
4 | tailwindHMR,
5 | setPluginConfigurationDefaults,
6 | } from "stencil-tailwind-plugin";
7 | import tailwindcss from "tailwindcss";
8 | import tailwindConf from "./tailwind.config";
9 | import autoprefixer from "autoprefixer";
10 |
11 | setPluginConfigurationDefaults({
12 | tailwindConf,
13 | tailwindCssPath: "./src/styles/tailwind.css",
14 | postcss: {
15 | plugins: [tailwindcss(), autoprefixer()],
16 | },
17 | });
18 |
19 | export const config: Config = {
20 | namespace: "poll-party",
21 | plugins: [sass(), tailwind(), tailwindHMR()],
22 | devServer: {
23 | reloadStrategy: "pageReload",
24 | },
25 | outputTargets: [
26 | {
27 | type: "dist",
28 | esmLoaderPath: "../loader",
29 | },
30 | {
31 | type: "dist-custom-elements",
32 | },
33 | {
34 | type: "docs-readme",
35 | },
36 | {
37 | type: "www",
38 | serviceWorker: null, // disable service workers
39 | },
40 | ],
41 | testing: {
42 | browserHeadless: "new",
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/partykit/polls.ts:
--------------------------------------------------------------------------------
1 | import type * as Party from "partykit/server";
2 |
3 | // room ID is completely arbitrary for this partyserver.
4 | // For the client, room is the object-hash of the questions and options.
5 | // But for us, it's just a string, and what we'll store a tally of id/votes against.
6 |
7 | type Votes = {
8 | [option: string]: number;
9 | };
10 |
11 | export default class PollParty implements Party.Server {
12 | votes: Votes = {};
13 |
14 | constructor(public party: Party.Party) {}
15 |
16 | async onStart() {
17 | this.votes = (await this.party.storage.get("votes")) || {};
18 | }
19 |
20 | async onConnect(connection: Party.Connection) {
21 | const msg = {
22 | type: "sync",
23 | votes: this.votes,
24 | };
25 | connection.send(JSON.stringify(msg));
26 | }
27 |
28 | async onMessage(message: string) {
29 | const msg = JSON.parse(message);
30 | if (msg.type === "vote") {
31 | const { option } = msg;
32 | this.votes[option] = (parseInt(`${this.votes[option]}`) || 0) + 1;
33 | this.party.broadcast(JSON.stringify({ type: "sync", votes: this.votes }));
34 | await this.party.storage.put("votes", this.votes);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* tslint:disable */
3 | /**
4 | * This is an autogenerated file created by the Stencil compiler.
5 | * It contains typing information for all components that exist in this project.
6 | */
7 | import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
8 | export namespace Components {
9 | interface PollParty {
10 | "host": string;
11 | "party": string | null;
12 | }
13 | }
14 | declare global {
15 | interface HTMLPollPartyElement extends Components.PollParty, HTMLStencilElement {
16 | }
17 | var HTMLPollPartyElement: {
18 | prototype: HTMLPollPartyElement;
19 | new (): HTMLPollPartyElement;
20 | };
21 | interface HTMLElementTagNameMap {
22 | "poll-party": HTMLPollPartyElement;
23 | }
24 | }
25 | declare namespace LocalJSX {
26 | interface PollParty {
27 | "host"?: string;
28 | "party"?: string | null;
29 | }
30 | interface IntrinsicElements {
31 | "poll-party": PollParty;
32 | }
33 | }
34 | export { LocalJSX as JSX };
35 | declare module "@stencil/core" {
36 | export namespace JSX {
37 | interface IntrinsicElements {
38 | "poll-party": LocalJSX.PollParty & JSXBase.HTMLAttributes;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "poll-party",
3 | "version": "0.0.1",
4 | "description": "Live poll web components using PartyKit",
5 | "main": "dist/index.cjs.js",
6 | "module": "dist/index.js",
7 | "es2015": "dist/esm/index.mjs",
8 | "es2017": "dist/esm/index.mjs",
9 | "types": "dist/types/index.d.ts",
10 | "collection": "dist/collection/collection-manifest.json",
11 | "collection:main": "dist/collection/index.js",
12 | "unpkg": "dist/poll-party/poll-party.esm.js",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/partykit/sketch-polls"
16 | },
17 | "files": [
18 | "dist/",
19 | "loader/"
20 | ],
21 | "scripts": {
22 | "build": "stencil build --docs",
23 | "start": "stencil build --dev --watch --serve",
24 | "test": "stencil test --spec --e2e",
25 | "test.watch": "stencil test --spec --e2e --watchAll",
26 | "generate": "stencil generate"
27 | },
28 | "dependencies": {
29 | "@stencil/core": "^4.0.0",
30 | "@types/object-hash": "^3.0.3",
31 | "object-hash": "^3.0.0",
32 | "partykit": "^0.0.0-b9cd337",
33 | "partysocket": "^0.0.0-185f8c9",
34 | "stencil-store-storage": "^0.2.0"
35 | },
36 | "devDependencies": {
37 | "@stencil/sass": "^3.0.5",
38 | "@types/jest": "^27.5.2",
39 | "@types/node": "^16.18.11",
40 | "jest": "^27.5.1",
41 | "jest-cli": "^27.5.1",
42 | "puppeteer": "^20.7.3",
43 | "stencil-tailwind-plugin": "^1.8.0",
44 | "tailwindcss": "^3.3.3"
45 | },
46 | "license": "MIT",
47 | "bugs": {
48 | "url": "https://github.com/partykit/sketch-polls/issues"
49 | },
50 | "homepage": "https://github.com/partykit/sketch-polls#readme",
51 | "keywords": [
52 | "web components",
53 | "partykit"
54 | ],
55 | "author": "Matt Webb"
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/poll-party/poll-party.css:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
5 | :host .poll-party.styled {
6 | @apply w-80 divide-y divide-gray-200 border border-gray-200 rounded-lg bg-white shadow font-sans;
7 | }
8 |
9 | :host .poll-party.styled header {
10 | @apply p-4 flex flex-row justify-between items-center gap-2;
11 | }
12 |
13 | :host .poll-party.styled header h1 {
14 | @apply font-bold;
15 | }
16 |
17 | :host .poll-party.styled header .total {
18 | @apply text-sm px-3 py-1 bg-gray-200 rounded-full whitespace-nowrap;
19 | }
20 |
21 | :host .poll-party.styled form {
22 | @apply divide-y divide-gray-200;
23 | }
24 |
25 | :host .poll-party.styled form .options {
26 | @apply p-4 flex flex-col gap-3
27 | }
28 |
29 | :host .poll-party.styled form .options label {
30 | @apply flex justify-start items-center gap-2;
31 | }
32 |
33 | :host .poll-party.styled form button {
34 | @apply p-4 w-full font-bold bg-blue-100 hover:bg-blue-200 disabled:bg-gray-100 disabled:text-gray-400 rounded-b-lg;
35 | }
36 |
37 | :host .poll-party.styled .results {
38 | @apply p-4 flex flex-col gap-6;
39 | }
40 |
41 | :host .poll-party.styled .results table {
42 | @apply w-full
43 | }
44 |
45 | :host .poll-party.styled .results table .bar {
46 | @apply w-full h-3;
47 | }
48 |
49 | :host .poll-party.styled .results table .bar-inner {
50 | @apply h-3 bg-blue-400 rounded-r;
51 | }
52 |
53 | :host .poll-party.styled .results table td {
54 | @apply pb-2;
55 | }
56 |
57 | :host .poll-party.styled .results table td:not(:last-child){
58 | @apply pr-3 whitespace-nowrap;
59 | }
60 |
61 | :host .poll-party.styled .results table td:last-child{
62 | @apply w-full;
63 | }
64 |
65 | :host .poll-party.styled .results a {
66 | @apply text-sm underline text-gray-400 hover:text-gray-700 cursor-pointer;
67 | }
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project
2 | .partykit
3 | .stencil
4 | dist/
5 | www/
6 | loader/
7 |
8 | # Logs
9 | logs
10 | *.log
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | lerna-debug.log*
15 | .pnpm-debug.log*
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 | *.lcov
32 |
33 | # nyc test coverage
34 | .nyc_output
35 |
36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 | bower_components
41 |
42 | # node-waf configuration
43 | .lock-wscript
44 |
45 | # Compiled binary addons (https://nodejs.org/api/addons.html)
46 | build/Release
47 |
48 | # Dependency directories
49 | node_modules/
50 | jspm_packages/
51 |
52 | # Snowpack dependency directory (https://snowpack.dev/)
53 | web_modules/
54 |
55 | # TypeScript cache
56 | *.tsbuildinfo
57 |
58 | # Optional npm cache directory
59 | .npm
60 |
61 | # Optional eslint cache
62 | .eslintcache
63 |
64 | # Optional stylelint cache
65 | .stylelintcache
66 |
67 | # Microbundle cache
68 | .rpt2_cache/
69 | .rts2_cache_cjs/
70 | .rts2_cache_es/
71 | .rts2_cache_umd/
72 |
73 | # Optional REPL history
74 | .node_repl_history
75 |
76 | # Output of 'npm pack'
77 | *.tgz
78 |
79 | # Yarn Integrity file
80 | .yarn-integrity
81 |
82 | # dotenv environment variable files
83 | .env
84 | .env.development.local
85 | .env.test.local
86 | .env.production.local
87 | .env.local
88 |
89 | # parcel-bundler cache (https://parceljs.org/)
90 | .cache
91 | .parcel-cache
92 |
93 | # Next.js build output
94 | .next
95 | out
96 |
97 | # Nuxt.js build / generate output
98 | .nuxt
99 | dist
100 |
101 | # Gatsby files
102 | .cache/
103 | # Comment in the public line in if your project uses Gatsby and not Next.js
104 | # https://nextjs.org/blog/next-9-1#public-directory-support
105 | # public
106 |
107 | # vuepress build output
108 | .vuepress/dist
109 |
110 | # vuepress v2.x temp and cache directory
111 | .temp
112 | .cache
113 |
114 | # Docusaurus cache and generated files
115 | .docusaurus
116 |
117 | # Serverless directories
118 | .serverless/
119 |
120 | # FuseBox cache
121 | .fusebox/
122 |
123 | # DynamoDB Local files
124 | .dynamodb/
125 |
126 | # TernJS port file
127 | .tern-port
128 |
129 | # Stores VSCode versions used for testing VSCode extensions
130 | .vscode-test
131 |
132 | # yarn v2
133 | .yarn/cache
134 | .yarn/unplugged
135 | .yarn/build-state.yml
136 | .yarn/install-state.gz
137 | .pnp.*
138 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sketch-polls
2 |
3 | This web component adds a new 'poll-party' element which is used to add a live poll to any web page, given a PartyKit server to connect to.
4 |
5 | Built using [Stencil](https://stenciljs.com/).
6 |
7 | Check out the [live demo](https://partykit.github.io/sketch-polls/).
8 |
9 | 
10 |
11 | ## Experimental!
12 |
13 | This component was created during [Matt](https://interconnected.org)'s summer 2023 residency. The purpose is to experiment with multiplayer interactions, and simultaneously see what PartyKit can do. It's called a sketch because it's lightweight and quick, and because we learn something in making it.
14 |
15 | ## Usage
16 |
17 | The web component allows you to create a live poll straight from HTML, from otherwise static websites. You import the component, and give it a question and some options.
18 |
19 | 
20 |
21 | You also have to provide a host. That's where your PartyKit back-end will run. See below.
22 |
23 | The poll comes to life and looks like this:
24 |
25 | 
26 |
27 | You can vote. It records the fact that you've voted in localStorage on your browser, and sends your option to the PartyKit server.
28 |
29 | The results look like this:
30 |
31 | 
32 |
33 | ...and they update in realtime as other people vote.
34 |
35 | To create a new poll: change the HTML. The PartyKit server doesn't know about the question or options specifically -- it stores the votes against a hash of the poll text. So if you change the question or options, it's a new poll.
36 |
37 | ### Importing the component
38 |
39 | The component is published [on npm as poll-party](https://www.npmjs.com/package/poll-party).
40 |
41 | In production, add this script tag to your HTML head:
42 |
43 | ``
44 |
45 | ### The Partykit back-end
46 |
47 | In development: use `127.0.0.1:1999` and, from this repo, run:
48 |
49 | `npx partykit dev`
50 |
51 | In production: use the host of your own PartyKit server (you'll be given it when you run `npx partykit deploy`) or use: `poll-party.genmon.partykit.dev`.
52 |
53 | If you'd like to add features (e.g. poll expiry dates) start by building on the server in `partykit/polls.ts`.
54 |
55 | ## To do
56 |
57 | - [ ] Add a mini front-end on the server to see all current polls
58 | - [ ] The component doesn't show an error if it can't connect to PartyKit: it should, as votes won't be counted
59 | - [ ] If the poll-party element has a `styles="false"` attribute, it should not use the default styles, and instead rely on the host page to style it
60 |
61 | ## Using StencilJS
62 |
63 | Follow these instructions to start developing a new component.
64 |
65 | From an empty directory:
66 |
67 | `npm init stencil` (select 'component')
68 |
69 | The project was named 'poll-party' and then the files moved to the top-level directory.
70 |
71 | We also use local storage, so:
72 |
73 | `npm i stencil-store-storage`
74 |
75 | `npm install partykit@beta partysocket@beta`
76 |
77 | ...for PartyKit.
78 |
79 | We want to use Tailwind CSS, so use [stencil-tailwind-plugin](https://www.npmjs.com/package/stencil-tailwind-plugin).
80 |
81 | Install:
82 |
83 | ```
84 | npm install -D stencil-tailwind-plugin tailwindcss
85 | npm install @stencil/sass --save-dev
86 | tailwindcss init
87 | ```
88 |
89 | Then copy the `stencil.config.ts` from this repo, and also copy `src/styles/tailwind.css` into place (with the top three `@tailwind` lines).
90 |
91 | Finally delete the directory `src/components/my-component` and run `stencil generate` to create a new component called `poll-party` (or whatever).
92 |
93 | `npm run build` will create the `dist` etc directory.
94 |
95 | During development, use `npm start` to run the test server and look at `index.html` from your `src/` directory.
96 |
97 | Don't forget to also run `npx partykit dev` for the server.
98 |
--------------------------------------------------------------------------------
/stencil-readme.md:
--------------------------------------------------------------------------------
1 | [](https://stenciljs.com)
2 |
3 | # Stencil Component Starter
4 |
5 | This is a starter project for building a standalone Web Component using Stencil.
6 |
7 | Stencil is also great for building entire apps. For that, use the [stencil-app-starter](https://github.com/ionic-team/stencil-app-starter) instead.
8 |
9 | # Stencil
10 |
11 | Stencil is a compiler for building fast web apps using Web Components.
12 |
13 | Stencil combines the best concepts of the most popular frontend frameworks into a compile-time rather than run-time tool. Stencil takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and generates 100% standards-based Web Components that run in any browser supporting the Custom Elements v1 spec.
14 |
15 | Stencil components are just Web Components, so they work in any major framework or with no framework at all.
16 |
17 | ## Getting Started
18 |
19 | To start building a new web component using Stencil, clone this repo to a new directory:
20 |
21 | ```bash
22 | git clone https://github.com/ionic-team/stencil-component-starter.git my-component
23 | cd my-component
24 | git remote rm origin
25 | ```
26 |
27 | and run:
28 |
29 | ```bash
30 | npm install
31 | npm start
32 | ```
33 |
34 | To build the component for production, run:
35 |
36 | ```bash
37 | npm run build
38 | ```
39 |
40 | To run the unit tests for the components, run:
41 |
42 | ```bash
43 | npm test
44 | ```
45 |
46 | Need help? Check out our docs [here](https://stenciljs.com/docs/my-first-component).
47 |
48 |
49 | ## Naming Components
50 |
51 | When creating new component tags, we recommend _not_ using `stencil` in the component name (ex: ``). This is because the generated component has little to nothing to do with Stencil; it's just a web component!
52 |
53 | Instead, use a prefix that fits your company or any name for a group of related components. For example, all of the Ionic generated web components use the prefix `ion`.
54 |
55 |
56 | ## Using this component
57 |
58 | There are three strategies we recommend for using web components built with Stencil.
59 |
60 | The first step for all three of these strategies is to [publish to NPM](https://docs.npmjs.com/getting-started/publishing-npm-packages).
61 |
62 | ### Script tag
63 |
64 | - Put a script tag similar to this `` in the head of your index.html
65 | - Then you can use the element anywhere in your template, JSX, html etc
66 |
67 | ### Node Modules
68 | - Run `npm install my-component --save`
69 | - Put a script tag similar to this `` in the head of your index.html
70 | - Then you can use the element anywhere in your template, JSX, html etc
71 |
72 | ### In a stencil-starter app
73 | - Run `npm install my-component --save`
74 | - Add an import to the npm packages `import my-component;`
75 | - Then you can use the element anywhere in your template, JSX, html etc
76 |
--------------------------------------------------------------------------------
/src/components/poll-party/poll-party.tsx:
--------------------------------------------------------------------------------
1 | import { Component, Prop, State, h, Element, Host } from "@stencil/core";
2 | import PartySocket from "partysocket";
3 | import state from "./store";
4 | import hash from "object-hash";
5 |
6 | type Poll = {
7 | question: string;
8 | options: {
9 | [key: string]: string;
10 | };
11 | };
12 |
13 | type Votes = {
14 | [key: string]: number;
15 | };
16 |
17 | @Component({
18 | tag: "poll-party",
19 | styleUrl: "poll-party.css",
20 | shadow: true,
21 | })
22 | export class PollParty {
23 | @Element() hostEl: HTMLDivElement;
24 | @Prop() host: string;
25 | @Prop() party: string | null = null;
26 | @State() room: string; // derived from poll
27 | @State() poll: Poll;
28 | @State() votes: Votes = {};
29 | @State() socket: PartySocket;
30 |
31 | // For the form
32 | @State() selectedOption: string | null = null;
33 |
34 | async componentWillLoad() {
35 | // Build the poll from elements in the DOM. There should be an
36 | // element called 'question' and a number of elements called 'option'.
37 | // Each option element has an id attr and a text node.
38 | const options: { [key: string]: string } = Object.fromEntries(
39 | Array.from(this.hostEl.querySelectorAll("option")).map((el) => [
40 | el.id,
41 | el.innerHTML,
42 | ])
43 | );
44 | const poll: Poll = {
45 | question: this.hostEl.querySelector("question").innerHTML,
46 | options,
47 | };
48 |
49 | this.poll = poll;
50 | this.room = hash(poll);
51 |
52 | this.socket = new PartySocket({
53 | host: this.host,
54 | party: this.party,
55 | room: this.room,
56 | });
57 |
58 | this.socket.addEventListener("message", async (e) => {
59 | const msg = JSON.parse(e.data);
60 | if (msg.type === "sync") {
61 | this.votes = msg.votes;
62 | }
63 | });
64 | }
65 |
66 | async componentDidLoad() {
67 | // Nothing
68 | }
69 |
70 | async submitVote(e) {
71 | console.log("submitting vote");
72 | e.preventDefault();
73 | //const formData = new FormData(e.target);
74 | //const option = formData.get("option") as string;
75 | const option = this.selectedOption;
76 | this.socket.send(
77 | JSON.stringify({
78 | type: "vote",
79 | option: option,
80 | })
81 | );
82 | // add this.name to state.hasVoted (a list)
83 | state.hasVoted = [...state.hasVoted, this.room];
84 | // Update the poll results locally. This will be overwritten when the socket
85 | // sends a sync message
86 | this.votes = {
87 | ...this.votes,
88 | [option]: (this.votes[option] || 0) + 1,
89 | };
90 | this.selectedOption = null;
91 | }
92 |
93 | async resetPoll() {
94 | // remove this.room from state.hasVoted (a list)
95 | state.hasVoted = state.hasVoted.filter((name) => name !== this.room);
96 | }
97 |
98 | render() {
99 | if (!this.poll) {
100 | return Loading...
;
101 | }
102 |
103 | const hasVoted = state.hasVoted.find((room) => room === this.room)
104 | ? true
105 | : false;
106 |
107 | const totalVotes = Object.values(this.votes).reduce(
108 | (acc, curr) => acc + curr,
109 | 0
110 | );
111 | const maxVotes = Math.max(...Object.values(this.votes));
112 |
113 | return (
114 |
115 |
116 |
122 | {hasVoted ? (
123 |
124 |
125 | {Object.entries(this.poll.options).map(([option, desc]) => {
126 | const votes = this.votes[option] || 0;
127 | return (
128 |
129 | | {desc} |
130 |
131 | {votes}
132 | |
133 |
134 |
142 | |
143 |
144 | );
145 | })}
146 |
147 |
150 |
151 | ) : (
152 |
172 | )}
173 |
174 |
175 | );
176 | }
177 | }
178 |
--------------------------------------------------------------------------------