--- sessionId: session-261005-184808-15gg --- # Requirements ### Overview & Goals Provide immediate, clear visual feedback to users regarding their previous voting decisions on any visited proposal and its associated amendments. By leveraging `ProposalService.getUserVotes`, the application eliminates cognitive friction, prevents redundant vote attempts, and enhances user confidence by highlighting previously selected voting choices across proposal and amendment action controls. ### Scope - **In Scope**: - Fetching user vote records on proposal load using `ProposalService.getUserVotes(proposalId)`. - Handling authenticated and unauthenticated states gracefully without blocking proposal content. - Updating `ProposalVotingCard.vue` to highlight the user's previous proposal vote (`Approve`, `Reject`, or `Abstain`) with active colored styling and accessible state. - Updating `AmendmentCarousel.vue` to color the quick-action approval (`Confirm`) and disapproval (`Cancel`) icons according to the user's vote on each amendment. - Synchronizing vote indicators immediately upon casting a new proposal or amendment vote. - **Out of Scope**: - Modifying backend OpenAPI endpoints or database schemas. - Adding vote retraction/deletion workflows beyond standard vote casting. ### User Stories - **As a governance participant**, I want to see my existing votes highlighted when I visit a proposal so that I immediately know whether I have already participated and what position I took. - **As a voter evaluating amendments**, I want the approval/disapproval icons on each amendment card to reflect my past votes so that I can quickly review my stance without opening each amendment individually. ### Functional Requirements - **Data Retrieval**: - When navigating to a proposal, `ProposalDetail.vue` must call `ProposalService.getUserVotes(proposalId)` to fetch all proposal and amendment votes cast by the current user. - Unauthenticated requests (e.g. 401 responses) or users with no recorded votes must be handled gracefully without throwing errors or breaking page rendering. - **Proposal Voting Card States**: - If the user voted `VoteChoice.YES` (`yes` / `Positive`), the "Approve" button and icon must display active emerald styling. - If the user voted `VoteChoice.NO` (`no` / `Negative`), the "Reject" button and icon must display active rose styling. - If the user voted `VoteChoice.ABSTAIN` (`abstain` / `Abstain`), the "Abstain" button and icon must display active amber styling. - If the user has not voted, all buttons retain their default neutral resting styling with hover accents. - **Amendment Quick-Action States**: - For each proposed amendment, if the user voted in favor (`inFavor === true`), the `Confirm` button/icon is colored emerald. - If the user voted against (`inFavor === false`), the `Cancel` button/icon is colored rose. - If unvoted, both icons retain neutral slate styling (`text-slate-400`). - **Reactive Synchronization**: - Casting a vote on either the proposal or an amendment must promptly re-fetch or optimistically update the vote state so the visual indicators reflect the change without requiring a page reload. ### Non-Functional Requirements - **Accessibility**: Include `aria-pressed="true"` or descriptive `aria-label` / `title` attributes on active buttons to inform screen readers of the user's recorded stance. - **Efficiency**: Fetch proposal and amendment votes in a single network request (`getUserVotes`) to avoid N+1 queries. # Technical Design ### Current Implementation - `ProposalService.getUserVotes(proposalId: number): CancelablePromise` is available in `src/generated/typescript/services/ProposalService.ts` and returns: - `proposalVote?: ProposalVote | null` containing `choice: VoteChoice` (`'yes' | 'no' | 'abstain'`). - `amendmentVotes: Array` containing `amendmentId: number` and `vote?: AmendmentVote | null` with `inFavor: boolean`. - `src/views/ProposalView.vue` loads `proposal` and passes it to `src/components/ProposalDetail.vue`. - `ProposalDetail.vue` renders: - `` - `` - Currently, neither component fetches `getUserVotes` or accepts user vote props; buttons in `ProposalVotingCard.vue` and `AmendmentCarousel.vue` display static unselected styling regardless of user voting history. ### Key Decisions 1. **Centralized Vote State in `ProposalDetail.vue`**: - *Decision*: Fetch `getUserVotes` in `ProposalDetail.vue` and pass relevant slice data as props down to `ProposalVotingCard.vue` and `AmendmentCarousel.vue`. - *Rationale*: Prevents duplicate API requests while ensuring both components remain pure presentational/reactive consumers synchronized to the single source of truth. 2. **Resilient Unauthenticated Handling**: - *Decision*: Wrap `ProposalService.getUserVotes` in a try/catch block; if 401 or network error occurs, silently default `userVotes` to `null`. - *Rationale*: Allows guest users or users without wallet connection to browse proposals seamlessly without console spam or UI disruption. 3. **Semantic Tailwind Color Tokens**: - *Decision*: Use Tailwind `emerald` (positive/approve), `rose` (negative/reject), and `amber` (abstain) classes with subtle background and border tints for active states. - *Rationale*: Matches existing design tokens across `Chip.vue`, `AmendmentCarousel.vue`, and `VotingControls.vue` while delivering high-contrast clarity. ### Proposed Changes #### 1. `src/components/ProposalDetail.vue` - Import `UserProposalVotes`, `UserAmendmentVote`, `VoteChoice` from generated typescript models. - Add reactive state `const userVotes = ref(null);`. - Implement `fetchUserVotes(proposalId: number)`: - Invoke `ProposalService.getUserVotes(proposalId)`. - Store response in `userVotes.value`. - On error (401/404/network), reset `userVotes.value = null`. - Call `fetchUserVotes` in `onMounted` and within the `watch(() => props.proposal?.id)` handler. - Pass `current-vote="userVotes?.proposalVote?.choice"` to ``. - Pass `:amendment-votes="userVotes?.amendmentVotes"` to ``. - Update `recordVote` to await `voteProposal` and immediately call `fetchUserVotes(props.proposal.id)`. #### 2. `src/components/ProposalVotingCard.vue` - Accept prop `currentVote?: VoteChoice | 'yes' | 'no' | 'abstain' | null`. - Compute active helper booleans: - `isApproved = computed(() => props.currentVote === VoteChoice.YES || props.currentVote === 'yes')` - `isRejected = computed(() => props.currentVote === VoteChoice.NO || props.currentVote === 'no')` - `isAbstained = computed(() => props.currentVote === VoteChoice.ABSTAIN || props.currentVote === 'abstain')` - Apply conditional classes: - Approve button: `:class="isApproved ? 'border-emerald-600 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-500' : 'border-slate-300 bg-white text-slate-700 hover:border-green-600 hover:bg-green-600 hover:text-white'"` - Reject button: `:class="isRejected ? 'border-rose-600 bg-rose-50 text-rose-700 ring-1 ring-rose-500' : 'border-slate-300 bg-white text-slate-700 hover:border-red-600 hover:bg-red-600 hover:text-white'"` - Abstain button: `:class="isAbstained ? 'border-amber-500 bg-amber-50 text-amber-700 ring-1 ring-amber-500' : 'border-slate-300 bg-white text-slate-700 hover:border-amber-500 hover:bg-amber-500 hover:text-slate-950'"` - Pass `aria-pressed` based on active vote status. #### 3. `src/components/AmendmentCarousel.vue` - Add prop `amendmentVotes?: UserAmendmentVote[]`. - Create a computed lookup map or helper function `getAmendmentVote(amendmentId: number): boolean | null | undefined`: - Returns `true` if `vote?.inFavor === true`, `false` if `vote?.inFavor === false`, or `null`/`undefined` if unvoted. - In the template for proposed amendments: - Highlight Confirm button with `text-emerald-600 bg-emerald-50 ring-1 ring-emerald-400` when `getAmendmentVote(amendment.id) === true`. - Highlight Cancel button with `text-rose-600 bg-rose-50 ring-1 ring-rose-400` when `getAmendmentVote(amendment.id) === false`. - Emit a `voted` event when `confirm` or `reject` completes so parent `ProposalDetail` can refresh `getUserVotes`. ### Architecture Diagram ```mermaid graph TD PV[ProposalView.vue] --> PD[ProposalDetail.vue] PD -->|1. getUserVotes proposalId| PS[ProposalService API] PS -->|UserProposalVotes| PD PD -->|currentVote| PVC[ProposalVotingCard.vue] PD -->|amendmentVotes| AC[AmendmentCarousel.vue] PVC -->|@vote -> voteProposal + refresh| PD AC -->|@voted -> voteAmendment + refresh| PD ``` ### File Structure - `src/components/ProposalDetail.vue` *(Modified)*: Adds user votes fetching, state management, and props dispatch. - `src/components/ProposalVotingCard.vue` *(Modified)*: Receives active proposal vote prop and renders highlight styles for Approve, Reject, and Abstain. - `src/components/AmendmentCarousel.vue` *(Modified)*: Receives amendment votes prop, applies active color styles on Confirm/Cancel icons, and notifies parent on vote. ### Risks & Mitigations - **Unauthenticated / Expired Tokens**: `getUserVotes` will return 401 when tokens are missing or invalid. *Mitigation*: Catch 401 errors gracefully and treat user votes as empty without throwing alerts. - **Race Condition on Voting**: Multiple quick clicks on vote actions. *Mitigation*: Await API resolution and update state reactivity to ensure visual indicators match persisted server state. # Testing ### Validation Approach Verify the functionality by loading proposals in authenticated and unauthenticated states, testing all vote positions on both proposal and amendment components, and confirming real-time UI updates upon casting votes. ### Key Scenarios 1. **Proposal Vote Highlighting**: - Navigate to a proposal where the user previously voted "Approve" -> Verify the Approve button in `ProposalVotingCard` is highlighted in emerald with active border/background. - Navigate to a proposal where the user previously voted "Reject" -> Verify the Reject button is highlighted in rose. - Navigate to a proposal where the user previously voted "Abstain" -> Verify the Abstain button is highlighted in amber. - Navigate to an unvoted proposal -> Verify all 3 buttons are rendered in their neutral resting state. 2. **Amendment Vote Highlighting**: - For an amendment where the user voted in favor (`inFavor: true`) -> Verify the checkmark (`Confirm`) icon is highlighted in emerald. - For an amendment where the user voted against (`inFavor: false`) -> Verify the cross (`Cancel`) icon is highlighted in rose. - For unvoted amendments -> Verify both icons render in neutral slate color (`text-slate-400`). 3. **Dynamic Voting State Updates**: - Click "Approve" on an unvoted proposal -> Verify the Approve button immediately shifts to active highlighted state upon API confirmation. - Click "Confirm" on a proposed amendment -> Verify the checkmark icon immediately transitions to the active emerald state. 4. **Guest / Unauthenticated Experience**: - View proposals with no token in `localStorage` -> Verify the proposal details, comments, and amendment carousel render properly with default unselected action buttons and no console runtime errors. ### Edge Cases - **No Amendments on Proposal**: Verify `amendmentVotes` handles empty arrays safely without runtime errors. - **Token Expiry**: When token expires mid-session, API failures are caught cleanly, leaving icons in fallback neutral states. # Delivery Steps ### ✓ Step 1: Integrate user vote retrieval and state management in ProposalDetail ProposalDetail fetches and manages user vote records for the active proposal and its amendments, maintaining synchronized reactive state. - Implement `fetchUserVotes` in `src/components/ProposalDetail.vue` using `ProposalService.getUserVotes(props.proposal.id)`. - Gracefully handle unauthorized (401), not found (404), or network error states when a user is unauthenticated or has no recorded votes. - Add reactive state `userVotes` typed as `UserProposalVotes | null` to track active proposal and amendment votes. - Trigger `fetchUserVotes` on component mount and watch `proposal.id` changes to keep user vote state updated when navigating proposals. - Pass the proposal vote selection (`userVotes?.proposalVote?.choice`) to `ProposalVotingCard` and the amendment vote mapping to `AmendmentCarousel`. ### ✓ Step 2: Render active vote indicators in ProposalVotingCard ProposalVotingCard visually indicates the user's previously cast vote with distinct color and container styling for Approve, Reject, and Abstain options. - Add a `currentVote` prop to `src/components/ProposalVotingCard.vue` accepting `VoteChoice | 'Positive' | 'Negative' | 'Abstain' | null | undefined`. - Update button styles to reflect active selection states: - **Approve**: Highlight with emerald border/background (`border-emerald-500 bg-emerald-50 text-emerald-700`) and emerald `Confirm` icon. - **Reject**: Highlight with rose border/background (`border-rose-500 bg-rose-50 text-rose-700`) and rose `Cancel` icon. - **Abstain**: Highlight with amber border/background (`border-amber-500 bg-amber-50 text-amber-700`) and amber `Abstain` icon. - Add `aria-pressed` attributes to buttons for assistive technology feedback. - Update `recordVote` in `ProposalDetail.vue` to refresh or optimistically update user vote state immediately after casting a vote. ### ✓ Step 3: Highlight previous votes in AmendmentCarousel and synchronize state AmendmentCarousel displays colored approval and disapproval quick-action icons corresponding to the user's recorded amendment votes. - Update `src/components/AmendmentCarousel.vue` to accept amendment vote data (e.g. `userVotes?: UserAmendmentVote[]` or a vote lookup map) from `ProposalDetail.vue`. - Derive helper methods/computed maps to resolve a user's vote (`inFavor: true`, `inFavor: false`, or unvoted) for each amendment ID. - Update the quick-action button styles in proposed amendment cards: - **Approve (in favor)**: Render active emerald styling (`text-emerald-600 bg-emerald-50 border border-emerald-200`) on the `Confirm` icon button. - **Reject (against)**: Render active rose styling (`text-rose-600 bg-rose-50 border border-rose-200`) on the `Cancel` icon button. - **Unvoted**: Maintain neutral resting styling (`text-slate-400 hover:text-emerald-500` / `hover:text-rose-500`). - Update `confirm` and `reject` voting handlers in `AmendmentCarousel.vue` to emit vote events or trigger vote refreshes to ensure immediate UI synchronization.