Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions bounty-3/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Warpspeed Bounty #3 — Poll Creation & Voting UI — Design

## Architecture Overview

```
Chat UI
└─ Action Menu ("+") ──> PollCreationSheet
PollStore.createPoll()
Chat Message (InlinePoll)
┌───────────┴───────────┐
PollVoting PollResults
(not voted) (voted)
ViewVotesSheet
```

The poll feature integrates directly into the group chat message list. Each poll is rendered as an `InlinePoll` card. Before the current user votes, the card shows `PollVoting` (interactive options). After voting, it swaps to `PollResults` (read-only bars with percentages). A "View Votes" button opens the `ViewVotesSheet` bottom sheet listing every voter grouped by option.

## Component Responsibilities

| Component | Role |
|---|---|
| `PollCreationSheet` | Bottom sheet modal — question input, dynamic option list (2–12), single/multi toggle, create action. |
| `InlinePoll` | Container card in the chat — header badge, question, delegates to `PollVoting` or `PollResults`. |
| `PollVoting` | Radio buttons (single) or checkboxes (multi) — selection state, submit vote. |
| `PollResults` | Horizontal result bars, percentages, vote counts, leading-option crown, "View Votes" trigger. |
| `ViewVotesSheet` | Bottom sheet with `SectionList` — each option is a section, voters listed with avatar + timestamp. |

## Data Flow

1. User taps the "+" action button in chat input → `PollCreationSheet` opens.
2. User fills in question + options + toggles multi/single → taps "Create Poll".
3. `PollStore.createPoll()` is called → returns a `Poll` object stored in the MobX store.
4. A chat message containing the `InlinePoll` component is inserted into the message list.
5. User taps options → `PollVoting` manages local selection state → "Vote" button calls `PollStore.castVote()`.
6. `castVote` adds the user's `Vote` record to each selected option's `voters` array and sets `poll.userVote`.
7. Because `poll.userVote` becomes defined, `InlinePoll` automatically swaps to `PollResults`.
8. `PollResults` reads option voters via `getResults()` which computes percentages, leading option, etc.
9. "View Votes" fires a callback that renders `ViewVotesSheet` with the same data, grouped by option.

## State Management (MobX)

`PollStore` is a singleton MobX store with `makeAutoObservable`:

- `polls: Map<string, Poll>` — observable map of all polls in the session.
- `createPoll(data, createdBy)` — generates IDs, builds `PollOption[]`, stores poll.
- `castVote(pollId, optionIds, userId, username)` — validates, pushes `Vote` objects, sets `userVote`.
- `getResults(pollId)` — derived array of `PollResult` with percentages.
- `hasUserVoted(pollId)` — convenience getter.

Votes are **immutable** once cast (one user = one vote). Multi-choice allows selecting multiple options in a single submission.

## Styling Approach

- Dark theme (matching Warpspeed's iOS aesthetic): `#1C1C1E` surfaces, `#2C2C2E` inputs, `#FFFFFF` text, `#8E8E93` secondary.
- Accent: `#0A84FF` (iOS blue) for selected states, buttons.
- 12-color palette (`POLL_COLORS`) for result bars — distinct enough to differentiate options.
- Bottom sheets use `borderTopLeftRadius: 16` / `borderTopRightRadius: 16` with a drag handle indicator.
- All touch targets are ≥44px height for accessibility.
- `SectionList` in `ViewVotesSheet` groups voters by option with a colored dot per section header.

## Edge Cases

- **Multi-choice toggle**: `PollCreationSheet` shows `Switch`; `PollVoting` renders checkboxes when true.
- **Empty poll vote**: "View Votes" shows "No votes yet" empty state.
- **255 char limit**: enforced on each option input; question limit of 500 chars.
- **Min 2 / Max 12 options**: creation sheet disables add/remove at boundaries.
- **Leading option**: `PollResults` shows a crown emoji next to the leading option(s); ties are handled (multiple leaders).
- **User's own vote**: highlighted with blue text + a blue dot indicator on the result bar.
- **Single choice re-selection**: tapping the same radio button deselects it (allows un-selection before submit).

## Future Considerations

- API integration: replace MobX store with socket events, persist polls server-side.
- Poll expiry: add `endsAt` timestamp + auto-close + "Poll closed" state.
- Edit poll: allow creator to add options before voting starts.
- Anonymous polls: hide usernames in results.
- Reactions: add emoji reactions on poll cards.
76 changes: 76 additions & 0 deletions bounty-3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Warpspeed Bounty #3 — Group Chat Poll Creation & Voting UI

**Bounty Amount:** $440
**Category:** React Native / TypeScript / MobX
**Status:** ✅ Reference Implementation

## Overview

A complete, production-ready reference implementation of the Warpspeed Group Chat Poll Creation & Voting feature. Users can create polls directly from the chat input action menu, vote on polls, view real-time results with percentage bars, and inspect voter details per option in a bottom sheet.

## Files

| File | Purpose |
|---|---|
| `types.ts` | All TypeScript interfaces (`Poll`, `PollOption`, `Vote`, `PollResult`) and constants |
| `state/pollStore.ts` | MobX store with `createPoll`, `castVote`, `getResults` |
| `components/PollCreationSheet.tsx` | Bottom sheet for creating polls (question + 2–12 options, single/multi) |
| `components/InlinePoll.tsx` | Poll card rendered inline in chat messages |
| `components/PollVoting.tsx` | Interactive voting (radio / checkbox select, submit) |
| `components/PollResults.tsx` | Result bars with percentages, leading option, vote counts |
| `components/ViewVotesSheet.tsx` | Bottom sheet listing voters grouped by option |
| `DESIGN.md` | Architecture overview, component tree, data flow, styling, edge cases |

## Usage

```tsx
import { pollStore } from './state/pollStore';
import { InlinePoll } from './components/InlinePoll';
import { PollCreationSheet } from './components/PollCreationSheet';

// 1. Show creation sheet
<PollCreationSheet
visible={showCreation}
onClose={() => setShowCreation(false)}
onCreate={(data) => {
const poll = pollStore.createPoll(data, currentUserId);
appendMessage({ type: 'poll', pollId: poll.id });
}}
/>

// 2. Render inline poll in chat
<InlinePoll
poll={pollStore.getPoll(pollId)!}
currentUserId={currentUserId}
currentUsername={currentUsername}
onVote={(pollId, optionIds) =>
pollStore.castVote(pollId, optionIds, currentUserId, currentUsername)
}
onViewVotes={(pollId) => setViewingPollId(pollId)}
/>

// 3. View votes bottom sheet
<ViewVotesSheet
visible={viewingPollId !== null}
poll={viewingPollId ? pollStore.getPoll(viewingPollId) : null}
onClose={() => setViewingPollId(null)}
/>
```

## Key Features

- **Poll Creation**: Bottom sheet with question + 2–12 options (255 char limit each), single/multi-choice toggle
- **Inline Poll**: Chat-native card with question, options, voting controls
- **Direct Voting**: Radio buttons (single) / checkboxes (multi), submit vote
- **Real-Time Results**: Animated bars, percentages, vote counts, leading-option crown
- **View Votes**: SectionList grouped by option with voter avatars and timestamps
- **MobX State Management**: Observable store with auto-computed results
- **Dark Theme**: iOS-style dark mode design system

## Dependencies

- `react-native` (≥0.72)
- `mobx` (≥6.10) + `mobx-react-lite`
- TypeScript (≥5.0)

No third-party UI libraries required — all components use React Native core primitives.
97 changes: 97 additions & 0 deletions bounty-3/components/InlinePoll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Poll } from '../types';
import { PollVoting } from './PollVoting';
import { PollResults } from './PollResults';

interface InlinePollProps {
poll: Poll;
currentUserId: string;
currentUsername: string;
onVote: (pollId: string, optionIds: string[]) => void;
onViewVotes: (pollId: string) => void;
}

export const InlinePoll: React.FC<InlinePollProps> = ({
poll,
currentUserId,
currentUsername,
onVote,
onViewVotes,
}) => {
const hasVoted = poll.userVote !== undefined;

return (
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.badge}>
<Text style={styles.badgeText}>📊 Poll</Text>
</View>
<Text style={styles.createdBy}>
by {poll.createdBy}
</Text>
</View>

<Text style={styles.question}>{poll.question}</Text>

<Text style={styles.subtitle}>
{poll.isMultiChoice ? 'Select all that apply' : 'Select one option'}
</Text>

{hasVoted ? (
<PollResults poll={poll} onViewVotes={onViewVotes} />
) : (
<PollVoting
poll={poll}
currentUserId={currentUserId}
currentUsername={currentUsername}
onVote={onVote}
/>
)}
</View>
);
};

const styles = StyleSheet.create({
container: {
backgroundColor: '#1C1C1E',
borderRadius: 12,
padding: 16,
marginVertical: 8,
borderWidth: 1,
borderColor: '#2C2C2E',
},
header: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
badge: {
backgroundColor: '#2C2C2E',
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 3,
marginRight: 8,
},
badgeText: {
fontSize: 12,
color: '#FFFFFF',
fontWeight: '600',
},
createdBy: {
fontSize: 12,
color: '#8E8E93',
},
question: {
fontSize: 17,
fontWeight: '600',
color: '#FFFFFF',
marginBottom: 6,
lineHeight: 22,
},
subtitle: {
fontSize: 13,
color: '#8E8E93',
marginBottom: 14,
},
});
Loading