├── .env.local ├── src ├── env.d.ts ├── layouts │ └── default.astro ├── components │ ├── slide.astro │ └── team-unlock.astro ├── pages │ ├── admin.astro │ └── index.astro └── scripts │ └── slide-state.ts ├── tsconfig.json ├── .vscode ├── extensions.json └── launch.json ├── partykit.json ├── astro.config.mjs ├── .gitignore ├── README.md ├── package.json ├── public └── favicon.svg └── party └── index.ts /.env.local: -------------------------------------------------------------------------------- 1 | PUBLIC_PARTYKIT_URL=localhost:1999 -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /partykit.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://www.partykit.io/schema.json", 3 | "name": "astro-slides-party", 4 | "main": "party/index.ts", 5 | "compatibilityDate": "2024-04-16" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | 3 | import sentry from '@sentry/astro'; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | integrations: [sentry()], 8 | devToolbar: { 9 | enabled: false, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | 26 | .partykit/ 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slide deck but it's actually a bunch of mini-games 2 | 3 | This is an interactive slide deck that's intended to be playful and require audience participation. 4 | 5 | ## Ideas 6 | 7 | - X people to unlock 8 | - flash poll 9 | - pop quiz 10 | - reactions 11 | - collab space invaders 12 | - encore button 13 | 14 | ## Requirements 15 | 16 | - slide view 17 | - presenter view 18 | - mobile view / audience view 19 | 20 | ## TODO 21 | 22 | - [ ] Attendee view 23 | - [ ] Auth for admin actions protection 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-slides", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.5.10", 14 | "@sentry/astro": "^7.109.0", 15 | "astro": "^4.5.15", 16 | "partysocket": "1.0.1", 17 | "typescript": "^5.4.3", 18 | "xstate": "^5.11.0" 19 | }, 20 | "devDependencies": { 21 | "partykit": "0.0.103" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/layouts/default.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {title} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 38 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/slide.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | slug?: string; 4 | } 5 | 6 | const { slug = '' } = Astro.props; 7 | --- 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 | 43 | -------------------------------------------------------------------------------- /src/pages/admin.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/default.astro'; 3 | --- 4 | 5 | 6 |
7 | 8 | 9 | 10 |
11 |
12 | 13 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/default.astro'; 3 | import Slide from '../components/slide.astro'; 4 | import TeamUnlock from '../components/team-unlock.astro'; 5 | --- 6 | 7 | 8 | 9 |

What happens if I add way too much text and it wraps?

10 |
11 | 12 | 13 |

Another one

14 |
15 | 16 |

Wow

17 |
18 |
19 | 20 | 49 | -------------------------------------------------------------------------------- /party/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Party from 'partykit/server'; 2 | 3 | export default class Server implements Party.Server { 4 | slide = 0; 5 | 6 | // team unlock 7 | unlockers = new Set(); 8 | 9 | constructor(readonly room: Party.Room) {} 10 | 11 | onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) { 12 | // A websocket just connected! 13 | console.log( 14 | `Connected: 15 | id: ${conn.id} 16 | room: ${this.room.id} 17 | url: ${new URL(ctx.request.url).pathname}`, 18 | ); 19 | 20 | // let's send a message to the connection 21 | conn.send( 22 | JSON.stringify({ 23 | type: 'initialize', 24 | value: this.slide, 25 | }), 26 | ); 27 | } 28 | 29 | onMessage(data: string, sender: Party.Connection) { 30 | const message = JSON.parse(data); 31 | 32 | switch (message.type) { 33 | case 'team-unlock-hold': 34 | this.unlockers.add(sender.id); 35 | this.room.broadcast( 36 | JSON.stringify({ 37 | type: 'team-unlock-update', 38 | value: this.unlockers.size, 39 | }), 40 | ); 41 | break; 42 | 43 | case 'team-unlock-release': 44 | this.unlockers.delete(sender.id); 45 | this.room.broadcast( 46 | JSON.stringify({ 47 | type: 'team-unlock-update', 48 | value: this.unlockers.size, 49 | }), 50 | ); 51 | break; 52 | 53 | default: 54 | this.room.broadcast(JSON.stringify(message)); 55 | } 56 | } 57 | } 58 | 59 | Server satisfies Party.Worker; 60 | -------------------------------------------------------------------------------- /src/scripts/slide-state.ts: -------------------------------------------------------------------------------- 1 | import PartySocket from 'partysocket'; 2 | import { createMachine, assign, createActor } from 'xstate'; 3 | 4 | export const partySocket = new PartySocket({ 5 | host: import.meta.env.PUBLIC_PARTYKIT_URL, 6 | room: 'lwj', 7 | }); 8 | 9 | export function send({ type, value }: { type: string; value?: any }) { 10 | partySocket.send(JSON.stringify({ type, value })); 11 | } 12 | 13 | partySocket.addEventListener('message', (event) => { 14 | const message = JSON.parse(event.data); 15 | 16 | switch (message.type) { 17 | case 'initialize': 18 | case 'update-slide': 19 | countActor.send({ type: 'SET', value: message.value }); 20 | break; 21 | 22 | default: 23 | console.log(`Unknown type "${message.type}"`); 24 | } 25 | }); 26 | 27 | const countMachine = createMachine({ 28 | context: { 29 | count: 0, 30 | } as { count: number }, 31 | on: { 32 | INC: { 33 | actions: assign({ 34 | count: ({ context }) => { 35 | const nextCount = context.count + 1; 36 | 37 | send({ 38 | type: 'update-slide', 39 | value: nextCount, 40 | }); 41 | 42 | return nextCount; 43 | }, 44 | }), 45 | }, 46 | DEC: { 47 | actions: assign({ 48 | count: ({ context }) => { 49 | const nextCount = Math.max(context.count - 1, 0); 50 | 51 | send({ 52 | type: 'update-slide', 53 | value: nextCount, 54 | }); 55 | 56 | return nextCount; 57 | }, 58 | }), 59 | }, 60 | SET: { 61 | actions: assign({ 62 | count: ({ event }) => event.value, 63 | }), 64 | }, 65 | }, 66 | }); 67 | 68 | export const countActor = createActor(countMachine).start(); 69 | -------------------------------------------------------------------------------- /src/components/team-unlock.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /* 3 | * 1. Locked state 4 | * - shows a QR code and a short link 5 | * - instruction: X people need to boop this corgi before we begin 6 | * - show the corgi to be booped 7 | * - counters: people on site, # of boops 8 | * 2. Unlocked state 9 | * - slide content 10 | */ 11 | import Slide from './slide.astro'; 12 | --- 13 | 14 | 15 |

TODO: team unlock

16 |

Press and hold the button for a surprise.

17 | 18 |
19 | 20 | 92 | --------------------------------------------------------------------------------