├── .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 |
10 |
--------------------------------------------------------------------------------
/src/components/slide.astro:
--------------------------------------------------------------------------------
1 | ---
2 | export interface Props {
3 | slug?: string;
4 | }
5 |
6 | const { slug = '' } = Astro.props;
7 | ---
8 |
9 |
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 |
--------------------------------------------------------------------------------