Skip to content

Commit 98d6a4d

Browse files
Sajjan99claude
andcommitted
feat(09-14): rewrite ElectionDetailsScreen for Figma gotchoices#13/#18/#19 parity
- ElectionDetailsBlock pared to immutable core only (title, Authority, Type, Date-time, Core Signature) — revision/tags/keyholderPolicy/ revisionSignature removed (checker fix B3) - Current-revision section rendered ONCE in the detail screen: Revision #N + date, Tags, ElectionTimelineList (Decision 2), Keyholder Policy, Revision Signature, PREVIEW chip - Keyholders section with Sent/Unsent KeyholderCard + chevron - REVISE ELECTION → EditElectionRevision; CLONE ELECTION stub - Conditional Proposed Revision block (only when proposed exists): revision header, tags, timeline, keyholder policy, signing rows (SIGN accent + SHARE warning via CustomButton, checker fix B1), ADJUST REVISION → EditElectionRevision - Ballot template InfoCards with Questions subtitle - More collapsible section + filterAuthoritiesField text input (#19) - Zero new TS errors (baseline 27 preserved) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6d5924a commit 98d6a4d

2 files changed

Lines changed: 152 additions & 85 deletions

File tree

apps/VoteTorrentAuthority/src/screens/elections/ElectionDetailsScreen.tsx

Lines changed: 145 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,37 @@ import { ThemedText } from "../../components/ThemedText";
77
import type { BallotSummary, ElectionDetails, IElectionEngine } from "@votetorrent/vote-core";
88
import { globalStyles } from "../../theme/styles";
99
import { ElectionDetailsBlock } from "./components/ElectionDetailsBlock";
10+
import { ElectionTimelineList } from "./components/ElectionTimelineList";
1011
import { ChipButton } from "../../components/ChipButton";
1112
import { KeyholderCard } from "./components/KeyholderCard";
1213
import { CustomButton } from "../../components/CustomButton";
14+
import { CustomTextInput } from "../../components/CustomTextInput";
1315
import { InfoCard } from "../../components/InfoCard";
14-
import { Timeline } from "./components/Timeline";
1516
import { formatDate } from "../../utils/displayUtils";
1617
import type { NavigationProp } from "../../navigation/types";
1718

1819
/**
19-
* ElectionDetailsScreen — timeline-focal stacked layout per Phase 9 D-07.
20+
* ElectionDetailsScreen — Figma parity #13/#18/#19 per Phase 9 plan 09-14.
2021
*
21-
* Order (top → bottom):
22-
* 1. Header (title + metadata via ElectionDetailsBlock)
23-
* 2. Timeline (vertical, 5 milestones with status dots) — focal section
24-
* 3. Keyholders (compact list)
25-
* 4. Tags (chip row)
26-
* 5. Revision deadline (callout)
27-
* 6. Revise / Clone actions
28-
* 7. Ballot templates list — G2/G12 (09-10): loaded from electionEngine.getBallots()
29-
* via useFocusEffect; one card per template when >=1 exists; empty-state
30-
* (noBallotYet text + CREATE BALLOT TEMPLATE button) shown only when 0 templates.
22+
* Render order (top → bottom):
23+
* 1. ElectionDetailsBlock — immutable core (title, Authority, Type, Date-time, Core Sig)
24+
* 2. Current revision section (Revision #N + date, Tags, Timeline text list,
25+
* Keyholder Policy, Revision Signature, PREVIEW chip)
26+
* 3. Keyholders — Sent/Unsent cards + chevron
27+
* 4. REVISE ELECTION / CLONE ELECTION actions
28+
* 5. Proposed Revision block (conditional — only when electionDetails.proposed exists)
29+
* · revision header, tags, timeline text list, keyholder policy
30+
* · signing rows per keyholder (SIGN accent / SHARE warning CustomButton pills)
31+
* · ADJUST REVISION → EditElectionRevision
32+
* 6. Ballot Templates section (one InfoCard per template with Questions subtitle)
33+
* 7. More section (collapsible) + filter-authorities input
3134
*/
3235
export default function ElectionDetailsScreen() {
3336
const { t } = useTranslation();
3437
const { electionEngine } = useRoute().params as { electionEngine: IElectionEngine };
3538
const [electionDetails, setElectionDetails] = useState<ElectionDetails | null>(null);
3639
const [ballots, setBallots] = useState<BallotSummary[]>([]);
40+
const [moreOpen, setMoreOpen] = useState(false);
3741
const { colors } = useTheme() as ExtendedTheme;
3842
const navigation = useNavigation<NavigationProp>();
3943
const insets = useSafeAreaInsets();
@@ -79,25 +83,57 @@ export default function ElectionDetailsScreen() {
7983
);
8084
}
8185

86+
const { election, current, proposed } = electionDetails;
87+
const revisionSignature = (current as any).signature?.signature as string | undefined;
88+
const revisionDate = Array.isArray(current.revisionTimestamp) && current.revisionTimestamp.length > 0
89+
? (current.revisionTimestamp[0] as unknown as number)
90+
: election.date;
91+
8292
return (
8393
<ScrollView
8494
style={styles.container}
8595
contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}>
86-
{/* Header + immutable metadata */}
96+
97+
{/* 1. Immutable core block (title + Authority/Type/Date + Core Signature) */}
8798
<View style={styles.section}>
8899
<ElectionDetailsBlock electionDetails={electionDetails} />
89100
</View>
90101

91-
{/* Timeline — focal section per D-07 */}
102+
{/* 2. Current revision section — rendered ONCE here */}
92103
<View style={styles.section}>
93-
<ThemedText type="defaultSemiBold">{t("timeline")}</ThemedText>
94-
<Timeline electionDetails={electionDetails} />
104+
<View style={styles.detail}>
105+
<ThemedText type="defaultSemiBold">{t("revision")}: </ThemedText>
106+
<ThemedText>#{current.revision} - {formatDate(revisionDate)}</ThemedText>
107+
</View>
108+
<View style={styles.detail}>
109+
<ThemedText type="defaultSemiBold">{t("tags")}: </ThemedText>
110+
<ThemedText>{current.tags.join(", ")}</ThemedText>
111+
</View>
112+
113+
{/* Timeline — text list per Decision 2 */}
114+
<ThemedText type="defaultSemiBold" style={styles.sectionLabel}>{t("timeline")}</ThemedText>
115+
<ElectionTimelineList timeline={current.timeline} />
116+
117+
<View style={styles.detail}>
118+
<ThemedText type="defaultSemiBold">{t("keyholderPolicy")}: </ThemedText>
119+
<ThemedText>{current.keyholderThreshold} of {current.keyholders.length}</ThemedText>
120+
</View>
121+
122+
{revisionSignature ? (
123+
<View style={styles.detail}>
124+
<ThemedText type="defaultSemiBold">{t("revisionSignature")}: </ThemedText>
125+
<ThemedText numberOfLines={1} ellipsizeMode="middle">{revisionSignature}</ThemedText>
126+
</View>
127+
) : null}
128+
129+
{/* PREVIEW chip — stub */}
130+
<ChipButton label={t("previewBallots")} onPress={() => console.log("preview-stub")} />
95131
</View>
96132

97-
{/* Keyholders */}
133+
{/* 3. Keyholders — Sent/Unsent + chevron */}
98134
<View style={styles.section}>
99135
<ThemedText type="defaultSemiBold">{t("keyholders")}</ThemedText>
100-
{electionDetails.current.keyholders.map((keyholder, index) => (
136+
{current.keyholders.map((keyholder, index) => (
101137
<KeyholderCard
102138
key={keyholder.invite?.name ?? `keyholder-${index}`}
103139
invitationStatus={keyholder}
@@ -106,57 +142,95 @@ export default function ElectionDetailsScreen() {
106142
))}
107143
</View>
108144

109-
{/* Tags chip row */}
110-
<View style={styles.section}>
111-
<ThemedText type="defaultSemiBold">{t("tags")}</ThemedText>
112-
<View style={styles.tagRow}>
113-
{electionDetails.current.tags.map((tag) => (
114-
<ChipButton key={tag} label={tag} />
115-
))}
116-
</View>
117-
</View>
118-
119-
{/* Revision deadline callout */}
120-
<View style={[styles.section, styles.calloutBox, { backgroundColor: colors.card, borderColor: colors.border }]}>
121-
<ThemedText type="defaultSemiBold">{t("revisionDeadline")}</ThemedText>
122-
<ThemedText>{formatDate(electionDetails.election.revisionDeadline)}</ThemedText>
123-
</View>
124-
125-
{/* Revise / Clone actions */}
145+
{/* 4. REVISE / CLONE actions */}
126146
<View style={styles.section}>
127147
<CustomButton
128148
title={t("reviseElection")}
129149
size="thin"
130150
icon="pencil"
131151
backgroundColor={colors.accent}
132-
onPress={() => {}}
152+
onPress={() => navigation.navigate("EditElectionRevision", { electionEngine })}
133153
/>
134154
<CustomButton
135155
title={t("cloneElection")}
136156
size="thin"
137157
icon="copy"
138158
backgroundColor={colors.accent}
139-
onPress={() => {}}
159+
onPress={() => console.log("clone-stub")}
140160
/>
141161
</View>
142162

143-
{/* Ballot Templates section — G1/G2/G12 (09-10):
144-
Show one InfoCard per template when >=1 exists (each navigates EditBallot
145-
with ballotId+electionEngine for upsert). When 0 templates, show only
146-
the empty-state text + CREATE BALLOT TEMPLATE button (G1 label). */}
163+
{/* 5. Proposed Revision block — conditional */}
164+
{electionDetails.proposed && (
165+
<View style={styles.section}>
166+
<ThemedText type="defaultSemiBold">{t("proposedRevisionHeader")}</ThemedText>
167+
168+
<View style={styles.detail}>
169+
<ThemedText type="defaultSemiBold">{t("revision")}: </ThemedText>
170+
<ThemedText>#{proposed!.proposed.revision}</ThemedText>
171+
</View>
172+
<View style={styles.detail}>
173+
<ThemedText type="defaultSemiBold">{t("tags")}: </ThemedText>
174+
<ThemedText>{proposed!.proposed.tags.join(", ")}</ThemedText>
175+
</View>
176+
177+
<ThemedText type="defaultSemiBold" style={styles.sectionLabel}>{t("timeline")}</ThemedText>
178+
<ElectionTimelineList timeline={proposed!.proposed.timeline} />
179+
180+
<View style={styles.detail}>
181+
<ThemedText type="defaultSemiBold">{t("keyholderPolicy")}: </ThemedText>
182+
<ThemedText>{proposed!.proposed.keyholderThreshold} of {proposed!.proposed.keyholders.length}</ThemedText>
183+
</View>
184+
185+
{/* Signing rows — one per proposed keyholder */}
186+
{proposed!.proposed.keyholders.map((holder, idx) => (
187+
<View key={holder.name ?? `proposed-holder-${idx}`} style={styles.signingRow}>
188+
<ThemedText type="defaultSemiBold" style={styles.holderName}>{holder.name}</ThemedText>
189+
<View style={styles.signingPills}>
190+
<CustomButton
191+
title={t("signRevision")}
192+
size="thin"
193+
icon="signature"
194+
backgroundColor={colors.accent}
195+
onPress={() => console.log("sign-stub")}
196+
/>
197+
<CustomButton
198+
title={t("shareRevision")}
199+
size="thin"
200+
icon="share-nodes"
201+
backgroundColor={colors.warning}
202+
onPress={() => console.log("share-stub")}
203+
/>
204+
</View>
205+
</View>
206+
))}
207+
208+
{/* ADJUST REVISION → EditElectionRevision */}
209+
<CustomButton
210+
title={t("adjustRevision")}
211+
size="thin"
212+
icon="pencil"
213+
backgroundColor={colors.accent}
214+
onPress={() => navigation.navigate("EditElectionRevision", { electionEngine })}
215+
/>
216+
</View>
217+
)}
218+
219+
{/* 6. Ballot Templates section */}
147220
<View style={styles.section}>
148221
<ThemedText type="title">{t("ballotTemplates")}</ThemedText>
149222
{ballots.length > 0 ? (
150223
ballots.map((ballot) => (
151224
<InfoCard
152225
key={ballot.id}
153226
title={ballot.authorityId || t("ballotTemplate")}
227+
subtitle={t("questionsLabel") + ": —"}
154228
icon="chevron-right"
155229
onPress={() =>
156230
navigation.navigate("EditBallot", {
157-
electionId: electionDetails.election.id,
158-
electionTitle: electionDetails.election.title,
159-
electionDate: formatDate(electionDetails.election.revisionDeadline),
231+
electionId: election.id,
232+
electionTitle: election.title,
233+
electionDate: formatDate(election.revisionDeadline),
160234
ballotId: ballot.id,
161235
electionEngine,
162236
} as any)
@@ -173,31 +247,49 @@ export default function ElectionDetailsScreen() {
173247
backgroundColor={colors.accent}
174248
onPress={() =>
175249
navigation.navigate("CreateBallot", {
176-
electionId: electionDetails.election.id,
177-
electionTitle: electionDetails.election.title,
178-
electionDate: formatDate(electionDetails.election.revisionDeadline),
250+
electionId: election.id,
251+
electionTitle: election.title,
252+
electionDate: formatDate(election.revisionDeadline),
179253
electionEngine,
180254
} as any)
181255
}
182256
/>
183257
</>
184258
)}
185259
</View>
260+
261+
{/* 7. More section (collapsible) + filter-authorities input */}
262+
<View style={styles.section}>
263+
<ChipButton label={t("more")} onPress={() => setMoreOpen((v) => !v)} />
264+
{moreOpen && (
265+
<CustomTextInput
266+
placeholder={t("filterAuthoritiesField")}
267+
/>
268+
)}
269+
</View>
186270
</ScrollView>
187271
);
188272
}
189273

190274
const localStyles = StyleSheet.create({
191-
tagRow: {
275+
detail: {
192276
flexDirection: "row",
193277
flexWrap: "wrap",
194-
gap: 8,
278+
marginVertical: 2,
279+
},
280+
sectionLabel: {
195281
marginTop: 8,
282+
marginBottom: 2,
196283
},
197-
calloutBox: {
198-
padding: 12,
199-
borderRadius: 8,
200-
borderWidth: 1,
284+
signingRow: {
285+
marginVertical: 6,
286+
},
287+
holderName: {
288+
marginBottom: 4,
289+
},
290+
signingPills: {
291+
flexDirection: "row",
292+
gap: 8,
201293
},
202294
});
203295

apps/VoteTorrentAuthority/src/screens/elections/components/ElectionDetailsBlock.tsx

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ interface ElectionDetailsBlockProps {
1010
}
1111

1212
/**
13-
* Election header + metadata block.
13+
* Immutable-core-only election header block.
1414
*
15-
* Phase 9 plan 09-01: The inline timeline rows that previously lived here have
16-
* been removed in favor of the local `Timeline` component (D-05/D-06/D-08).
17-
* `ElectionDetailsScreen` mounts `Timeline` separately as the focal section.
15+
* Phase 9 plan 09-14 (Checker fix B3): pared down to the immutable core:
16+
* title + Authority + Type + Date-time + Core Signature.
17+
* The revision/tags/keyholder-policy/revision-signature rows have been
18+
* removed from here — they live in ElectionDetailsScreen's current-revision
19+
* section to avoid duplication.
1820
*/
1921
export function ElectionDetailsBlock({ electionDetails }: ElectionDetailsBlockProps) {
2022
const { t } = useTranslation();
21-
const { election, current } = electionDetails;
23+
const { election } = electionDetails;
2224

2325
const typeLabel =
2426
election.type === ElectionType.official
@@ -27,13 +29,7 @@ export function ElectionDetailsBlock({ electionDetails }: ElectionDetailsBlockPr
2729
? t("adhoc")
2830
: String(election.type);
2931

30-
const keyholderCount = current.keyholders?.length ?? 0;
31-
const policyText = keyholderCount
32-
? `${current.keyholderThreshold} of ${keyholderCount}`
33-
: String(current.keyholderThreshold);
34-
3532
const coreSignature = (election as any).signature?.signature as string | undefined;
36-
const revisionSignature = (current as any).signature?.signature as string | undefined;
3733

3834
return (
3935
<View>
@@ -61,27 +57,6 @@ export function ElectionDetailsBlock({ electionDetails }: ElectionDetailsBlockPr
6157
</View>
6258
) : null}
6359
</View>
64-
65-
<View style={styles.section}>
66-
<View style={styles.detail}>
67-
<ThemedText type="defaultSemiBold">{t("revision")}: </ThemedText>
68-
<ThemedText>{current.revision}</ThemedText>
69-
</View>
70-
<View style={styles.detail}>
71-
<ThemedText type="defaultSemiBold">{t("tags")}: </ThemedText>
72-
<ThemedText>{current.tags.join(", ")}</ThemedText>
73-
</View>
74-
<View style={styles.detail}>
75-
<ThemedText type="defaultSemiBold">{t("keyholderPolicy")}: </ThemedText>
76-
<ThemedText>{policyText}</ThemedText>
77-
</View>
78-
{revisionSignature ? (
79-
<View style={styles.detail}>
80-
<ThemedText type="defaultSemiBold">{t("revisionSignature")}: </ThemedText>
81-
<ThemedText numberOfLines={1} ellipsizeMode="middle">{revisionSignature}</ThemedText>
82-
</View>
83-
) : null}
84-
</View>
8560
</View>
8661
);
8762
}

0 commit comments

Comments
 (0)