Skip to content

Commit 3c80dbf

Browse files
OpenStaxClaudeclaudeRoyEJohnson
authored
Port errata-form page JavaScript files to TypeScript (CORE-1205) (#2774)
* Convert errata-form page JavaScript files to TypeScript - Added comprehensive type definitions for all components - Converted 10 JavaScript files to TypeScript (.js -> .tsx/.ts) - Added proper typing for React components, hooks, and event handlers - Used inline type definitions and avoided 'any' type per guidelines - Maintained existing functionality while improving type safety Files converted: - errata-form.js -> errata-form.tsx - errata-form-context.js -> errata-form-context.tsx - form/form.js -> form/form.tsx - form/ErrorSourceSelector.js -> form/ErrorSourceSelector.tsx - form/ErrorTypeSelector.js -> form/ErrorTypeSelector.tsx - form/InvalidMessage.js -> form/InvalidMessage.tsx - form/FileUploader.js -> form/FileUploader.tsx - form/ErrorLocationSelector/*.js -> form/ErrorLocationSelector/*.tsx 🤖 Generated with [Claude Code](https://claude.ai/code) Lint issues selectedBook can be null Co-Authored-By: Claude <[email protected]> * Testing Some adjustments for better accessibility were suggested by testing * Prettier --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Roy Johnson <[email protected]>
1 parent 3cdf8b9 commit 3c80dbf

21 files changed

+4327
-511
lines changed

src/app/helpers/books.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export type Book = {
88
subjects: string[];
99
title: string;
1010
content_warning_text: string;
11+
assignable_book?: boolean;
12+
kindle_link?: string;
13+
has_faculty_resources?: boolean;
14+
has_student_resources?: boolean;
15+
1116
};
1217

1318
const statesToInclude = ['live', 'new_edition_available', 'coming_soon'];
Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,51 @@
11
import React, {useState} from 'react';
22
import buildContext from '~/components/jsx-helpers/build-context';
33
import fetchBooks from '~/models/books';
4+
import {type Book} from '~/helpers/books';
45
import {useLocation} from 'react-router-dom';
56
import {useDataFromPromise, useDataFromSlug} from '~/helpers/page-data-utils';
67

8+
type ContextValue = {
9+
selectedBook: Book | null;
10+
books: Book[] | null | undefined;
11+
hasError: string | null;
12+
setHasError: (error: string | null) => void;
13+
hideErrors: boolean;
14+
submitting: boolean;
15+
validateBeforeSubmitting: (event: React.FormEvent) => void;
16+
searchParams: URLSearchParams;
17+
title: string | null;
18+
setTitle: (title: string) => void;
19+
};
20+
721
function useSearchParams() {
822
const {search} = useLocation();
923

1024
return React.useMemo(() => new window.URLSearchParams(search), [search]);
1125
}
1226

13-
function useContextValue() {
27+
function useContextValue(): ContextValue {
1428
const searchParams = useSearchParams();
15-
const initialTitle = React.useMemo(() => searchParams.get('book'), [searchParams]);
16-
const [title, setTitle] = useState(initialTitle);
29+
const initialTitle = React.useMemo(
30+
() => searchParams.get('book'),
31+
[searchParams]
32+
);
33+
const [title, setTitle] = useState<string | null>(initialTitle);
1734
const books = useDataFromPromise(fetchBooks);
1835
const selectedBook = React.useMemo(
19-
() => books?.find((b) => b.title === title) || {},
36+
() => books?.find((b) => b.title === title) || null,
2037
[books, title]
2138
);
22-
const bookInfo = useDataFromSlug(selectedBook.slug) || selectedBook;
23-
const [hasError, setHasError] = useState('You have not completed the form');
39+
const bookInfo =
40+
useDataFromSlug<Book>(selectedBook ? selectedBook.slug : null) ||
41+
selectedBook;
42+
const [hasError, setHasError] = useState<string | null>(
43+
'You have not completed the form'
44+
);
2445
const [hideErrors, setHideErrors] = useState(true);
2546
const [submitting, setSubmitting] = useState(false);
2647
const validateBeforeSubmitting = React.useCallback(
27-
(event) => {
48+
(event: React.FormEvent) => {
2849
event.preventDefault();
2950
setHideErrors(false);
3051
if (!hasError) {
@@ -37,7 +58,8 @@ function useContextValue() {
3758
return {
3859
selectedBook: bookInfo,
3960
books,
40-
hasError, setHasError,
61+
hasError,
62+
setHasError,
4163
hideErrors,
4264
submitting,
4365
validateBeforeSubmitting,
@@ -49,7 +71,4 @@ function useContextValue() {
4971

5072
const {useContext, ContextProvider} = buildContext({useContextValue});
5173

52-
export {
53-
useContext as default,
54-
ContextProvider as ErrataFormContextProvider
55-
};
74+
export {useContext as default, ContextProvider as ErrataFormContextProvider};

src/app/pages/errata-form/errata-form.js renamed to src/app/pages/errata-form/errata-form.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React from 'react';
22
import Form from './form/form';
33
import FormSelect from '~/components/form-select/form-select';
4-
import useErrataFormContext, {ErrataFormContextProvider} from './errata-form-context';
4+
import useErrataFormContext, {
5+
ErrataFormContextProvider
6+
} from './errata-form-context';
57
import useUserContext from '~/contexts/user';
68
import linkHelper from '~/helpers/link';
79
import './errata-form.scss';
810

11+
type Book = {
12+
title: string;
13+
};
14+
915
function ErrataForm() {
1016
const {title} = useErrataFormContext();
1117

@@ -26,22 +32,29 @@ function ErrataForm() {
2632
function TitleSelector() {
2733
const {books, setTitle} = useErrataFormContext();
2834
const options = React.useMemo(
29-
() => books?.map((book) => ({label: book.title, value: book.title})),
35+
() =>
36+
books?.map((book: Book) => ({
37+
label: book.title,
38+
value: book.title
39+
})),
3040
[books]
3141
);
3242

3343
return (
3444
<div className="text-content title-selector">
3545
<p>
36-
It looks like you got referred here but they didn&apos;t tell us what
37-
book you were looking at.
46+
It looks like you got referred here but they didn&apos;t tell us
47+
what book you were looking at.
3848
</p>
3949
<FormSelect
50+
name="title-selector"
4051
selectAttributes={{
4152
placeholder: 'Please select one'
4253
}}
4354
onValueUpdate={setTitle}
44-
label="What book were you in, again?" options={options} />
55+
label="What book were you in, again?"
56+
options={options ?? []}
57+
/>
4558
</div>
4659
);
4760
}
@@ -64,7 +77,13 @@ export default function EnsureLoggedIn() {
6477
<main className="errata-form page">
6578
<div className="boxed">
6679
<div>You need to be logged in to submit errata</div>
67-
<a className="btn primary" href={linkHelper.loginLink()} data-local="true">Log in</a>
80+
<a
81+
className="btn primary"
82+
href={linkHelper.loginLink()}
83+
data-local="true"
84+
>
85+
Log in
86+
</a>
6887
</div>
6988
</main>
7089
);

src/app/pages/errata-form/form/ErrorLocationSelector/ErrorLocationSelector.js

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, {useState, useRef, useEffect} from 'react';
2+
import useErrataFormContext from '../../errata-form-context';
3+
import managedInvalidMessage from '../InvalidMessage';
4+
import TocSelector from './toc-selector';
5+
import './ErrorLocationSelector.scss';
6+
7+
type AdditionalLocationInputProps = {
8+
value?: string;
9+
readOnly?: boolean;
10+
updateValue?: (value: string) => void;
11+
required?: boolean;
12+
};
13+
14+
type InputComponentProps = {
15+
defaultValue: string | null;
16+
};
17+
18+
function AdditionalLocationInput({
19+
value,
20+
readOnly = false,
21+
updateValue,
22+
required = true
23+
}: AdditionalLocationInputProps) {
24+
const inputRef = useRef<HTMLInputElement>(null);
25+
const [InvalidMessage, updateInvalidMessage] =
26+
managedInvalidMessage(inputRef);
27+
const syncValue = React.useCallback(
28+
(event: React.ChangeEvent<HTMLInputElement>) =>
29+
updateValue?.(event.target.value),
30+
[updateValue]
31+
);
32+
33+
useEffect(updateInvalidMessage, [required, updateInvalidMessage]);
34+
35+
return (
36+
<React.Fragment>
37+
<label
38+
className="question"
39+
htmlFor="additional_location_information"
40+
>
41+
Additional location information, if applicable
42+
</label>
43+
<InvalidMessage />
44+
<input
45+
id="additional_location_information"
46+
type="text"
47+
name="additional_location_information"
48+
placeholder="Describe where you found the error"
49+
value={value}
50+
onChange={syncValue}
51+
ref={inputRef}
52+
readOnly={readOnly}
53+
required={required}
54+
/>
55+
</React.Fragment>
56+
);
57+
}
58+
59+
function DefaultValue({defaultValue}: InputComponentProps) {
60+
return (
61+
<AdditionalLocationInput
62+
value={defaultValue as string}
63+
readOnly={true}
64+
/>
65+
);
66+
}
67+
68+
function NotDefaultValue({defaultValue}: InputComponentProps) {
69+
const [tocV, updateTocV] = useState<string | null>();
70+
const [addlV, updateAddlV] = useState(defaultValue || '');
71+
const required = () => !tocV && !addlV;
72+
73+
return (
74+
<React.Fragment>
75+
<TocSelector required={required()} updateValue={updateTocV} />
76+
<AdditionalLocationInput
77+
value={addlV}
78+
required={required()}
79+
updateValue={updateAddlV}
80+
/>
81+
</React.Fragment>
82+
);
83+
}
84+
85+
export default function ErrorLocationSelector() {
86+
const {searchParams} = useErrataFormContext();
87+
const defaultValue = searchParams.get('location');
88+
const readOnly = defaultValue && searchParams.get('source');
89+
const Input = readOnly ? DefaultValue : NotDefaultValue;
90+
91+
return <Input defaultValue={defaultValue} />;
92+
}
File renamed without changes.

0 commit comments

Comments
 (0)