@@ -51,26 +51,143 @@ const STATUS_ORDER = {
5151 skipped : 4 ,
5252} ;
5353
54+ const CHECK_CATEGORY = {
55+ // Paper formatting
56+ "Page numbering" : "Paper formatting" ,
57+ "Title page" : "Paper formatting" ,
58+ "Margins" : "Paper formatting" ,
59+ "Body line spacing" : "Paper formatting" ,
60+ "Heading line spacing" : "Paper formatting" ,
61+ "Body paragraph spacing" : "Paper formatting" ,
62+ "Heading paragraph spacing" : "Paper formatting" ,
63+ "Body first-line indents" : "Paper formatting" ,
64+ "Body alignment" : "Paper formatting" ,
65+ "Font" : "Paper formatting" ,
66+ "Unconverted markup symbols" : "Paper formatting" ,
67+ // References
68+ "References page" : "References" ,
69+ "References heading alignment" : "References" ,
70+ "References line spacing" : "References" ,
71+ "Reference hanging indent" : "References" ,
72+ "Reference DOI/URL" : "References" ,
73+ "Reference short link" : "References" ,
74+ "Unapproved source" : "References" ,
75+ "Reference authors" : "References" ,
76+ "Reference year format" : "References" ,
77+ "Reference title capitalization" : "References" ,
78+ "Reference italics" : "References" ,
79+ "Reference punctuation" : "References" ,
80+ "Reference DOI format" : "References" ,
81+ "Reference link verification" : "References" ,
82+ // Citations
83+ "Inline citations" : "Citations" ,
84+ "Uncited references" : "Citations" ,
85+ "Unmatched citations" : "Citations" ,
86+ "Personal communication" : "Citations" ,
87+ "Citation ampersand" : "Citations" ,
88+ "Citation et al. format" : "Citations" ,
89+ "Citation no-date format" : "Citations" ,
90+ "Citation page format" : "Citations" ,
91+ "Citation multiple sources" : "Citations" ,
92+ "Citation year suffix" : "Citations" ,
93+ "Secondary citations" : "Citations" ,
94+ } ;
95+
96+ const CATEGORY_ORDER = [ "Paper formatting" , "References" , "Citations" ] ;
97+
5498function sortChecks ( checks ) {
5599 return [ ...checks ] . sort ( ( a , b ) => STATUS_ORDER [ a . status ] - STATUS_ORDER [ b . status ] ) ;
56100}
57101
58- function SummaryCell ( { count, label, href } ) {
59- const Tag = href ? "a" : "div" ;
102+ function categorySlug ( label ) {
103+ return label . toLowerCase ( ) . replace ( / \s + / g, "-" ) ;
104+ }
105+
106+ function getCategoryLinksForStatuses ( checks , statuses ) {
107+ const groups = new Map ( ) ;
108+ for ( const check of checks ) {
109+ if ( ! statuses . includes ( check . status ) ) continue ;
110+ const cat = CHECK_CATEGORY [ check . rule ] ?? "Other" ;
111+ if ( ! groups . has ( cat ) ) groups . set ( cat , { count : 0 , primaryStatus : check . status } ) ;
112+ const g = groups . get ( cat ) ;
113+ g . count ++ ;
114+ if ( STATUS_ORDER [ check . status ] < STATUS_ORDER [ g . primaryStatus ] ) g . primaryStatus = check . status ;
115+ }
116+ const links = [ ] ;
117+ for ( const cat of [ ...CATEGORY_ORDER , "Other" ] ) {
118+ if ( groups . has ( cat ) ) {
119+ const { count, primaryStatus } = groups . get ( cat ) ;
120+ links . push ( { label : cat , count, href : `#${ primaryStatus } -${ categorySlug ( cat ) } ` } ) ;
121+ }
122+ }
123+ return links ;
124+ }
125+
126+ function CheckList ( { checks, status } ) {
127+ const categories = useMemo ( ( ) => {
128+ const groups = new Map ( ) ;
129+ for ( const check of checks ) {
130+ const cat = CHECK_CATEGORY [ check . rule ] ?? "Other" ;
131+ if ( ! groups . has ( cat ) ) groups . set ( cat , [ ] ) ;
132+ groups . get ( cat ) . push ( check ) ;
133+ }
134+ const ordered = [ ] ;
135+ for ( const cat of [ ...CATEGORY_ORDER , "Other" ] ) {
136+ if ( groups . has ( cat ) ) ordered . push ( { label : cat , checks : groups . get ( cat ) } ) ;
137+ }
138+ return ordered ;
139+ } , [ checks ] ) ;
140+
141+ return (
142+ < >
143+ { categories . map ( ( { label, checks : catChecks } ) => (
144+ < React . Fragment key = { label } >
145+ < div id = { `${ status } -${ categorySlug ( label ) } ` } className = "check-category-label" >
146+ < span > { label } </ span >
147+ < a href = "#top" className = "check-category-top" > Back to top</ a >
148+ </ div >
149+ { catChecks . map ( ( check ) => (
150+ < CheckCard key = { check . rule } check = { check } />
151+ ) ) }
152+ </ React . Fragment >
153+ ) ) }
154+ </ >
155+ ) ;
156+ }
157+
158+ function SummaryCell ( { label, links, colorClass } ) {
60159 return (
61- < Tag className = { `summary-cell${ href ? " summary-cell-link" : "" } ` } href = { href || undefined } >
160+ < div className = { `summary-cell summary-cell-- ${ colorClass } ` } >
62161 < dt > { label } </ dt >
63- < dd > { count } </ dd >
64- </ Tag >
162+ < dd >
163+ { links . length === 0 ? (
164+ < span className = "summary-cell-zero" > —</ span >
165+ ) : (
166+ < ul className = "summary-cat-links" role = "list" >
167+ { links . map ( ( { label : catLabel , count, href } ) => (
168+ < li key = { catLabel } >
169+ < a href = { href } className = "summary-cat-link" >
170+ < span className = "summary-cat-name" > { catLabel } </ span >
171+ < span className = "summary-cat-count" > { count } </ span >
172+ </ a >
173+ </ li >
174+ ) ) }
175+ </ ul >
176+ ) }
177+ </ dd >
178+ </ div >
65179 ) ;
66180}
67181
68- function Summary ( { report } ) {
69- const { failed, warn = 0 , review, passed } = report . summary ;
182+ function Summary ( { report, checks } ) {
183+ const { failed, warn = 0 , review } = report . summary ;
70184 const issueCount = failed + warn ;
71185 const isReady = issueCount === 0 ;
72186 const issueText = issueCount === 1 ? "issue" : "issues" ;
73- const failHref = failed > 0 ? "#fail-section" : warn > 0 ? "#warn-section" : null ;
187+
188+ const failLinks = useMemo ( ( ) => getCategoryLinksForStatuses ( checks , [ "fail" , "warn" ] ) , [ checks ] ) ;
189+ const reviewLinks = useMemo ( ( ) => getCategoryLinksForStatuses ( checks , [ "review" ] ) , [ checks ] ) ;
190+ const passLinks = useMemo ( ( ) => getCategoryLinksForStatuses ( checks , [ "pass" ] ) , [ checks ] ) ;
74191
75192 return (
76193 < section className = "summary" aria-labelledby = "summary-heading" >
@@ -99,9 +216,9 @@ function Summary({ report }) {
99216 </ button >
100217 </ div >
101218 < dl className = "summary-grid" aria-label = "Check totals" >
102- < SummaryCell count = { issueCount } label = "Failed" href = { failHref } />
103- < SummaryCell count = { review } label = "Review" href = { review > 0 ? "# review-section" : null } />
104- < SummaryCell count = { passed } label = "Passed" href = { passed > 0 ? "# pass-section" : null } />
219+ < SummaryCell label = "Failed" links = { failLinks } colorClass = "fail" />
220+ < SummaryCell label = "Review" links = { reviewLinks } colorClass = " review" />
221+ < SummaryCell label = "Passed" links = { passLinks } colorClass = " pass" />
105222 </ dl >
106223 </ section >
107224 ) ;
@@ -611,26 +728,20 @@ function Report({ report }) {
611728 < p className = "print-report-title" > APA Formatting Report</ p >
612729 < p className = "print-report-meta" > { report . file } — Checked { printDate } — APA Coach v{ APP_INFO . version } </ p >
613730 </ div >
614- < Summary report = { report } />
731+ < Summary report = { report } checks = { report . checks } />
615732 < section className = "checks" aria-label = "APA checks" >
616733 { failChecks . length > 0 && (
617734 < section id = "fail-section" className = "check-group" aria-labelledby = "fail-heading" >
618735 < h3 id = "fail-heading" className = "check-group-heading" > Issues to fix (Required)</ h3 >
619736 < p className = "fail-intro" > These items should be corrected before you submit your paper.</ p >
620- { failChecks . map ( ( check ) => (
621- < CheckCard key = { check . rule } check = { check } />
622- ) ) }
623- < a href = "#top" className = "back-to-top" > ↑ Back to top</ a >
737+ < CheckList checks = { failChecks } status = "fail" />
624738 </ section >
625739 ) }
626740 { warnChecks . length > 0 && (
627741 < section id = "warn-section" className = "check-group" aria-labelledby = "warn-heading" >
628742 < h3 id = "warn-heading" className = "check-group-heading" > Warnings — verify before submitting</ h3 >
629743 < p className = "warn-intro" > These items may indicate a problem. Open each link and confirm it leads to the source you intended.</ p >
630- { warnChecks . map ( ( check ) => (
631- < CheckCard key = { check . rule } check = { check } />
632- ) ) }
633- < a href = "#top" className = "back-to-top" > ↑ Back to top</ a >
744+ < CheckList checks = { warnChecks } status = "warn" />
634745 </ section >
635746 ) }
636747 { reviewChecks . length > 0 && (
@@ -639,28 +750,20 @@ function Report({ report }) {
639750 < p className = "review-intro" >
640751 These items may not require changes, but are worth double-checking.
641752 </ p >
642- { reviewChecks . map ( ( check ) => (
643- < CheckCard key = { check . rule } check = { check } />
644- ) ) }
645- < a href = "#top" className = "back-to-top" > ↑ Back to top</ a >
753+ < CheckList checks = { reviewChecks } status = "review" />
646754 </ section >
647755 ) }
648756 { passChecks . length > 0 && (
649757 < section id = "pass-section" className = "check-group" aria-labelledby = "pass-heading" >
650758 < h3 id = "pass-heading" className = "check-group-heading" > Looks good (Passed)</ h3 >
651759 < p className = "pass-intro" > These items meet APA expectations.</ p >
652- { passChecks . map ( ( check ) => (
653- < CheckCard key = { check . rule } check = { check } />
654- ) ) }
655- < a href = "#top" className = "back-to-top" > ↑ Back to top</ a >
760+ < CheckList checks = { passChecks } status = "pass" />
656761 </ section >
657762 ) }
658763 { skippedChecks . length > 0 && (
659764 < section id = "skipped-section" className = "check-group" aria-labelledby = "skipped-heading" >
660765 < h3 id = "skipped-heading" className = "check-group-heading" > Not checked (Offline)</ h3 >
661- { skippedChecks . map ( ( check ) => (
662- < CheckCard key = { check . rule } check = { check } />
663- ) ) }
766+ < CheckList checks = { skippedChecks } status = "skipped" />
664767 </ section >
665768 ) }
666769 </ section >
0 commit comments