AI agent reference for working on this codebase. Read this before making changes.
Kanbanish is a real-time collaborative kanban board with retrospective workflow support. Single-page React app backed by Firebase Realtime Database. No backend server — all state is synced directly through Firebase.
- Live site: https://www.kanbanish.com (deployed to GitHub Pages)
- Repo: https://github.com/crittermike/kanbanish
| Layer | Technology |
|---|---|
| Framework | React 19 (JSX, no TypeScript) |
| Build | Vite |
| Database | Firebase Realtime Database |
| Auth | Firebase Anonymous Auth (no login UI) |
| Drag & Drop | react-dnd (HTML5 backend) |
| Icons | react-feather |
| Testing | Vitest + React Testing Library + jest-dom |
| Linting | ESLint (flat config) with react, react-hooks, import, jsx-a11y |
| Styles | Pure CSS with custom properties (no Tailwind, no CSS-in-JS) |
| CI/CD | GitHub Actions (test on PR, auto-deploy on push to main) |
src/
├── index.jsx # Entry point (renders App)
├── App.jsx # Root: template URL handling, view gating (Dashboard vs Board)
├── setupTests.js # Test setup (imports @testing-library/jest-dom)
│
├── data/
│ └── boardTemplates.js # BOARD_TEMPLATES array — all template definitions (columns, icons, tags)
│
├── context/
│ └── BoardContext.jsx # State provider + Firebase listeners (~450 lines, orchestrates hooks)
│
├── components/
│ ├── Board.jsx # Main board layout: header, settings panel, columns (600+ lines)
│ ├── Dashboard.jsx # Landing page: create board, pick template, recent boards
│ ├── Column.jsx # Single column with cards
│ ├── Card.jsx # Individual card (content, votes, reactions, comments)
│ ├── CardGroup.jsx # Grouped cards container
│ ├── CardHoverActions.jsx # Hover action buttons on cards
│ ├── CardReactions.jsx # Emoji reactions display
│ ├── CardCreationIndicator.jsx # Shows who's typing
│ ├── Comments.jsx # Comment thread on cards/groups
│ ├── EmojiPicker.jsx # Emoji selection popover
│ ├── VoteCounter.jsx # Per-card vote display
│ ├── BaseVoteCounter.jsx # Shared vote counter logic
│ ├── TotalVoteCounter.jsx # Aggregate vote display
│ ├── UserCounter.jsx # Active users indicator
│ ├── VotingControls.jsx # Vote up/down buttons
│ ├── WorkflowControls.jsx # Retrospective phase stepper
│ ├── ResultsView.jsx # Results phase display
│ ├── PollVoting.jsx # 1-5 star poll input
│ ├── PollResults.jsx # Poll results display
│ ├── HealthCheckVoting.jsx # Health check voting input
│ ├── HealthCheckResults.jsx # Health check results display
│ └── modals/
│ ├── ExportBoardModal.jsx
│ ├── NewBoardTemplateModal.jsx
│ └── VoteLimitModal.jsx
│
├── hooks/
│ ├── usePoll.js # Poll voting operations (submitPollVote, getPollStats)
│ ├── useHealthCheck.js # Health check voting (submitHealthCheckVote, getHealthCheckStats)
│ ├── useBoardSettings.js # Board settings CRUD (updateVotingEnabled, etc.)
│ ├── usePresence.js # User presence tracking, card creation activity indicators
│ ├── useVoting.js # Vote operations (resetAllVotes, upvoteGroup, downvoteGroup, vote counting)
│ ├── useGroups.js # Card grouping (moveCard, createCardGroup, ungroupCards, removeAllGrouping)
│ ├── useWorkflow.js # Retrospective workflow phase transitions
│ ├── useCardOperations.jsx # Card CRUD, voting, reactions, comments
│ ├── useGroupOperations.jsx # Group-level reactions and comments
│ ├── useVoteCounterVisibility.jsx # Intersection observer for vote counters
│ └── useRecentBoards.js # localStorage-backed recent boards list (add, remove, pin)
│
├── utils/
│ ├── firebase.js # Firebase init + re-exports (database, auth, signInAnonymously, get, ref)
│ ├── boardUtils.js # addColumn(), addCard(), createBoardFromTemplate() helpers
│ ├── helpers.js # Re-export barrel (imports from ids, emoji, urlSettings, linkify)
│ ├── ids.js # generateId() — unique ID generation
│ ├── emoji.js # COMMON_EMOJIS array, getEmojiKeywords()
│ ├── urlSettings.js # parseBool(), parseUrlSettings()
│ ├── linkify.js # linkifyText() — URL detection in text
│ ├── workflowUtils.js # WORKFLOW_PHASES enum, phase permission checks
│ └── retrospectiveModeUtils.js # Reveal phase logic
│
└── styles/
├── index.css # CSS entry point, imports all component CSS
└── components/
├── variables.css # CSS custom properties (dark/light themes)
├── base.css
├── header.css
├── buttons.css
├── columns.css
├── cards.css
├── card-groups.css
├── modals.css
├── emoji-reactions.css
├── template-select.css
├── results.css
├── workflow.css
├── poll.css
├── health-check.css
└── state-utilities.css
All application state is managed through BoardContext.jsx (~450 lines), which serves as the orchestration layer. It:
- Initializes Firebase connection and anonymous auth
- Subscribes to Firebase Realtime Database via
onValuelisteners - Delegates domain operations to 7 extracted hooks (see Hook Architecture below)
- Exposes all board state and operations to the component tree via context
- Manages board creation, joining, URL settings parsing
Domain logic is extracted from BoardContext into focused hooks. Each hook:
- Receives dependencies as parameters (not from context — avoids circular deps)
- Returns operation functions wrapped in
useCallback - BoardContext calls each hook, destructures the return, and spreads into context value
- The context value shape is unchanged — consumers still use
useBoardContext()
Hook call order in BoardContext (order matters for inter-hook dependencies):
// 1. usePresence — no hook dependencies
const { activeUsers, ... } = usePresence({ boardId, user });
// 2. useBoardSettings — no hook dependencies
const { updateBoardSettings, ... } = useBoardSettings({ boardId, user, ... });
// 3. usePoll — no hook dependencies
const { submitPollVote, getPollStats } = usePoll({ boardId, user, ... });
// 4. useHealthCheck — no hook dependencies
const { submitHealthCheckVote, ... } = useHealthCheck({ boardId, user, ... });
// 5. useVoting — depends on activeUsers from usePresence
const { resetAllVotes, ... } = useVoting({ boardId, user, columns, activeUsers, ... });
// 6. useGroups — no hook dependencies
const { moveCard, ..., removeAllGrouping } = useGroups({ boardId, user, columns });
// 7. useWorkflow — depends on updateBoardSettings AND removeAllGrouping
const { startGroupingPhase, ... } = useWorkflow({ updateBoardSettings, columns, ..., removeAllGrouping });| Hook | Responsibility | Key exports |
|---|---|---|
usePresence |
User presence tracking, card creation indicators | activeUsers, usersAddingCards, startCardCreation, stopCardCreation |
useBoardSettings |
Board settings CRUD | updateBoardSettings + 6 setting-specific wrappers |
usePoll |
Poll voting | submitPollVote, getPollStats |
useHealthCheck |
Health check voting | submitHealthCheckVote, getHealthCheckStats |
useVoting |
Vote operations and counting | resetAllVotes, getTotalVotes, getUserVoteCount, upvoteGroup, downvoteGroup |
useGroups |
Card grouping | moveCard, createCardGroup, ungroupCards, removeAllGrouping |
useWorkflow |
Retrospective phase transitions | Phase transition functions (startGroupingPhase, etc.) |
There is no routing library. The board ID comes from the ?board= query parameter. If none is provided, a new board is created.
URL (?board=xyz) → BoardContext (Firebase listener) → Components (read from context)
User action → Component → Context method → Firebase set()/remove() → Firebase listener fires → State updates → Re-render
All Firebase writes use set() and remove() — never update(). This is an intentional pattern throughout the codebase.
App.jsxgates between Dashboard and Board views based on?board=URL paramApp.jsxalso handles?template=<id>URL param: auto-creates a board from a matching template and redirects to it (see URL Settings below)App.jsxuseswindow.history.pushStatefor URL management (no React Router)Board.jsxis the main layout: header bar, settings panel, and columns grid- Components consume context via
useBoardContext()hook - Domain operations are extracted into hooks: 7 in
hooks/called by BoardContext, plususeCardOperationsanduseGroupOperationsused directly by components
Board settings can be pre-configured via URL parameters:
?voting=true&downvotes=false&multivote=true&votes=5&retro=true&sort=votes&theme=dark
Parsed in urlSettings.js (re-exported via helpers.js) → parseUrlSettings(). Applied on board creation.
Additionally, ?template=<id> creates a board from a predefined template and redirects to it (bypasses the template selection dialog). Template IDs match the id field in src/data/boardTemplates.js (e.g., ?template=lean-coffee, ?template=retro, ?template=big-orca). This is handled in App.jsx, not urlSettings.js.
boards/{boardId}/
title: string
created: timestamp
owner: string (uid)
columns/{columnId}/
title: string
cards/{cardId}/
content: string
votes: number
created: timestamp
createdBy: string (uid)
groupId?: string
voters/{userId}: number (+1 or -1)
reactions/{emoji}/
count: number
users/{userId}: boolean
comments/{commentId}/
content: string
timestamp: number
createdBy: string (uid)
groups/{groupId}/
name: string
created: timestamp
expanded: boolean
votes: number
voters/{userId}: number
cardIds: string[]
reactions/... (same structure as cards)
comments/... (same structure as cards)
settings/
votingEnabled: boolean
downvotingEnabled: boolean
multipleVotesAllowed: boolean
votesPerUser: number
sortByVotes: boolean
retrospectiveMode: boolean
workflowPhase: string
resultsViewIndex: number
presence/{userId}/
lastSeen: timestamp
uid: string
cardCreationActivity/{userId}/
columnId: string
lastUpdated: timestamp
uid: string
poll/votes/{userId}: number (1-5)
healthCheck/votes/{questionId}/{userId}: number (1-5)
users/{userId}/preferences/
darkMode: boolean
- Column IDs are prefixed with an alphabet character for sort order:
a_xxx,b_xxx,c_xxx - Card/comment authorship tracked via
createdByfield (Firebase anonymous UID) - Voting uses a
votersmap with+1/-1values;votesfield is the running total - Reactions track both count and per-user state for toggling
- Groups hold a
cardIdsarray; cards reference their group viagroupId
When retrospectiveMode is enabled, the board operates as a guided retrospective with phases:
HEALTH_CHECK → HEALTH_CHECK_RESULTS → CREATION → GROUPING → INTERACTIONS → INTERACTION_REVEAL → RESULTS → POLL → POLL_RESULTS
Defined in workflowUtils.js as the WORKFLOW_PHASES enum. Each phase restricts what actions are available (e.g., voting only in INTERACTIONS, card creation only in CREATION). Phase permission logic is in the canPerformAction() and related functions in workflowUtils.js.
The INTERACTION_REVEAL phase uses reveal logic in retrospectiveModeUtils.js to progressively show votes/reactions.
- Pure CSS with custom properties for theming
- All styles in
src/styles/components/— one file per component/concern variables.cssdefines the theme: dark mode is default, light mode via.light-modeclass on<html>- Theme toggle writes preference to Firebase at
users/{uid}/preferences/darkMode - No CSS modules, no BEM — just descriptive class names
Dark theme is :root defaults. Light theme overrides via html.light-mode. Key variables:
--bg-primary,--bg-secondary,--bg-card--text-primary,--text-secondary--border-color,--accent-color
- Framework: Vitest with jsdom environment, globals enabled
- Setup file:
src/setupTests.js(imports@testing-library/jest-dom) - Config:
vitest.config.js— outputsvitest.results.json - Test files: Co-located with components as
*.test.jsx
npm test # Run all tests once
npm run test:watch # Run tests in watch mode- Tests use React Testing Library (
render,screen,fireEvent,waitFor) BoardContextis typically mocked in tests with a custom provider wrapper- Firebase is mocked — no real database calls in tests
Key rules:
react-hooks/exhaustive-deps: error (not warning)no-unused-vars: error, but vars prefixed with_are ignored- Import ordering enforced (alphabetical, grouped by builtin → external → internal)
- jsx-a11y plugin for accessibility
- 2-space indentation
- LF line endings
- UTF-8 encoding
- No trailing whitespace
- Enforced by
.editorconfig
.vscode/settings.json configures ESLint auto-fix on save.
npm run lint # Check for lint errors
npm run lint:fix # Auto-fix lint errors
npm run lint:check # Same as lint (alias)- Triggers on: push to main, PRs targeting main
- Runs on: Ubuntu latest, Node 20
- Steps: checkout → install → lint → test
- Triggers on: push to main
- Builds with Vite and deploys to GitHub Pages
- Base path handled by
vite.config.js(uses/kanbanish/for GH Pages,/otherwise)
| Script | Command | Purpose |
|---|---|---|
dev / start |
vite --port 3000 |
Development server |
build |
vite build |
Production build to build/ |
preview |
vite preview |
Preview production build |
test |
vitest run |
Run tests once |
test:watch |
vitest |
Tests in watch mode |
lint |
eslint . |
Check lint errors |
lint:fix |
eslint . --fix |
Auto-fix lint errors |
lint:check |
eslint . |
Alias for lint |
- Firebase config is hardcoded in
src/utils/firebase.js— not using environment variables. The project is "big-orca" on Firebase. - No TypeScript — all files are
.jsx. No type checking beyond ESLint. - No routing library — board ID is managed via query params manually.
- If it needs new Firebase operations → add a new hook in
src/hooks/or extend an existing one. Wire it intoBoardContext.jsx. - If it only needs existing state → consume
useBoardContext()directly in your component - If it's a new UI element → create component in
src/components/, add corresponding CSS insrc/styles/components/ - If it needs workflow phase awareness → check
workflowUtils.jsfor phase permissions - Add tests as
ComponentName.test.jsx(components) orhookName.test.js(hooks) alongside the source - Run
npm run lint:fix && npm test && npm run buildbefore committing
Templates are defined in src/data/boardTemplates.js as a BOARD_TEMPLATES array (default export). Each template has { id, name, description, columns, icon, tags }. To add a template:
- Add a new entry to the
BOARD_TEMPLATESarray insrc/data/boardTemplates.js - The template is automatically available in the template selection modal and via
?template=<id>URL param - The
idfield is the slug used in URLs (e.g.,lean-coffee→?template=lean-coffee)
Firebase paths are referenced in the domain hooks (usePresence, useVoting, useGroups, etc.) and in BoardContext.jsx (for the main onValue listener). When changing the schema:
- Update the
onValuelistener inBoardContext.jsxthat reads the data - Update the write function(s) in the relevant hook that use
set()/remove() - Ensure the data model section above is updated
- Mock
BoardContextwith a wrapper that provides test values - Mock Firebase modules (
src/utils/firebase.js) - Use React Testing Library idioms: query by role/text, not implementation details
- Run
npm testto verify
window.locationreplacement: When replacingwindow.locationin tests (delete window.location; window.location = {...}), also mockwindow.history.pushStateas a no-op — jsdom throwsDOMExceptionwhenpushStateis called with a replaced location object.- Firebase auth mocks: When testing code that uses
auth.onAuthStateChanged, the callback should fire asynchronously (Promise.resolve().then(() => cb(...))) to avoidReferenceErrorfrom variables referenced before the return value is assigned. - Always run
npm run buildin addition to tests — Vite's production build (Rollup) catches import/export errors that Vitest (with its more lenient module resolution) does not. For example, importing a symbol not exported by a module will pass tests but fail the build.