Skip to content

Commit e0605f2

Browse files
committed
refactor: use React 18+ code, added tests, updated dependencies
1 parent f5e54b9 commit e0605f2

File tree

18 files changed

+1513
-381
lines changed

18 files changed

+1513
-381
lines changed

jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@testing-library/jest-dom';

package.json

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "password.phpmyfaq.de",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"homepage": "https://password.phpmyfaq.de",
55
"description": "Password Hash Generator Tool for phpMyFAQ",
66
"private": true,
@@ -12,26 +12,28 @@
1212
"eject": "react-scripts eject"
1313
},
1414
"dependencies": {
15-
"@testing-library/jest-dom": "^5.16.2",
16-
"@testing-library/react": "^13.4.0",
17-
"@testing-library/user-event": "^14.4.3",
18-
"@types/jest": "^29.2.4",
19-
"@types/node": "^18.11.13",
20-
"@types/react": "^18.0.26",
21-
"@types/react-copy-to-clipboard": "^5.0.2",
22-
"@types/react-dom": "^18.0.9",
23-
"bootstrap": "^5.2.0",
15+
"@types/jest": "^29.5.13",
16+
"@types/node": "^22.7.4",
17+
"@types/react": "^18.3.10",
18+
"@types/react-copy-to-clipboard": "^5.0.7",
19+
"@types/react-dom": "^18.3.0",
20+
"bootstrap": "^5.3.3",
2421
"buffer": "^6.0.3",
2522
"fast-sha256": "^1.3.0",
26-
"react": "^18.2.0",
27-
"react-bootstrap": "^2.7.0",
23+
"react": "^18.3.1",
24+
"react-bootstrap": "^2.10.5",
2825
"react-copy-to-clipboard": "^5.1.0",
29-
"react-dom": "^18.2.0",
26+
"react-dom": "^18.3.1",
3027
"react-scripts": "5.0.1",
31-
"typescript": "~4.9.4"
28+
"typescript": "~5.6.2"
3229
},
3330
"devDependencies": {
34-
"jest-environment-jsdom": "^29.3.1"
31+
"@testing-library/dom": "^10.4.0",
32+
"@testing-library/jest-dom": "^6.5.0",
33+
"@testing-library/react": "^16.0.1",
34+
"@testing-library/user-event": "^14.5.2",
35+
"jest": "^29.7.0",
36+
"jest-environment-jsdom": "^29.7.0"
3537
},
3638
"eslintConfig": {
3739
"extends": "react-app"
@@ -47,5 +49,6 @@
4749
"last 1 firefox version",
4850
"last 1 safari version"
4951
]
50-
}
52+
},
53+
"packageManager": "[email protected]"
5154
}

src/components/App.tsx

Lines changed: 89 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeEvent, FormEvent, FunctionComponent, useState } from 'react';
1+
import React, { ChangeEvent, FormEvent, useState } from 'react';
22
import Toast from 'react-bootstrap/Toast';
33
import CopyToClipboard from 'react-copy-to-clipboard';
44

@@ -10,60 +10,95 @@ import { generateHash } from '../services/generateHash';
1010
import { InputReadonly } from './InputReadonly/InputReadonly';
1111
import { Footer } from './Footer/Footer';
1212

13-
export const App: FunctionComponent = () => {
14-
const title = 'Password Hash Generator Tool for phpMyFAQ';
13+
export const App: React.FC = () => {
14+
const title = 'Password Hash Generator Tool for phpMyFAQ';
1515

16-
const [ salt, setSalt ] = useState('');
17-
const [ userName, setUserName ] = useState('');
18-
const [ password, setPassword ] = useState('');
19-
const [ generatedHash, setGeneratedHash ] = useState('');
20-
const [ showToast, setShowToast] = useState(false);
16+
const [formData, setFormData] = useState({
17+
salt: '',
18+
userName: '',
19+
password: '',
20+
generatedHash: '',
21+
});
22+
const [showToast, setShowToast] = useState(false);
23+
const [toastMessage, setToastMessage] = useState('');
2124

22-
const handleSaltChange = (event: ChangeEvent<HTMLInputElement>) => setSalt(event.target.value);
23-
const handleUserNameChange = (event: ChangeEvent<HTMLInputElement>) => setUserName(event.target.value);
24-
const handlePasswordChange = (event: ChangeEvent<HTMLInputElement>) => setPassword(event.target.value);
25+
// Handle input change
26+
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
27+
const { name, value } = event.target;
28+
setFormData((prevData) => ({
29+
...prevData,
30+
[name]: value,
31+
}));
32+
};
2533

26-
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
27-
event.preventDefault();
28-
const hash = generateHash(userName, password, salt);
29-
setGeneratedHash(hash)
30-
};
34+
// Handle form submit
35+
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
36+
event.preventDefault();
37+
const { userName, password, salt } = formData;
3138

32-
return <>
33-
<div className="position-absolute w-100 d-flex flex-column p-4">
34-
<Toast onClose={() => setShowToast(false)} show={showToast} delay={3000} style={{
35-
position: 'absolute',
36-
top: '1em',
37-
right: '1em',
38-
}} autohide>
39-
<Toast.Header>
40-
<strong className="mr-auto">{title}</strong>
41-
</Toast.Header>
42-
<Toast.Body>Generated hash successfully copied to clipboard!</Toast.Body>
43-
</Toast>
44-
</div>
45-
<div className="App container">
46-
<Header title={title}/>
47-
<div className="row justify-content-md-center">
48-
<div className="col-lg-6 col-sm-12">
49-
<form onSubmit={handleSubmit}>
50-
<Input label={'Your phpMyFAQ Salt'} onChange={handleSaltChange}/>
51-
<Input label={'Your Username'} onChange={handleUserNameChange}/>
52-
<InputPassword label={'New Password'} onChange={handlePasswordChange}/>
53-
<Button type={ButtonType.SUBMIT}>Generate hash!</Button>
54-
{
55-
generatedHash &&
56-
<div>
57-
<InputReadonly label={'Generated Hash'} value={generatedHash}/>
58-
<CopyToClipboard text={generatedHash} onCopy={() => setShowToast(true)}>
59-
<Button type={ButtonType.BUTTON}>Copy to clipboard</Button>
60-
</CopyToClipboard>
61-
</div>
62-
}
63-
</form>
64-
</div>
65-
</div>
66-
<Footer/>
67-
</div>
68-
</>;
69-
}
39+
if (!userName || !password || !salt) {
40+
setToastMessage('Please fill in all fields.');
41+
setShowToast(true);
42+
return;
43+
}
44+
45+
const hash = generateHash(userName, password, salt);
46+
setFormData((prevData) => ({
47+
...prevData,
48+
generatedHash: hash,
49+
}));
50+
setToastMessage('Hash generated successfully!');
51+
setShowToast(true);
52+
};
53+
54+
const { salt, userName, password, generatedHash } = formData;
55+
56+
return (
57+
<>
58+
{/* Toast Notification */}
59+
<div className="position-absolute w-100 d-flex flex-column p-4">
60+
<Toast
61+
onClose={() => setShowToast(false)}
62+
show={showToast}
63+
delay={3000}
64+
autohide
65+
style={{
66+
position: 'absolute',
67+
top: '1em',
68+
right: '1em',
69+
}}
70+
>
71+
<Toast.Header>
72+
<strong className="mr-auto">{title}</strong>
73+
</Toast.Header>
74+
<Toast.Body>{toastMessage}</Toast.Body>
75+
</Toast>
76+
</div>
77+
78+
{/* Main Content */}
79+
<div className="App container">
80+
<Header title={title} />
81+
<div className="row justify-content-md-center">
82+
<div className="col-lg-6 col-sm-12">
83+
<form onSubmit={handleSubmit}>
84+
<Input label="Your phpMyFAQ Salt" onChange={handleInputChange} value={salt} name="salt" required />
85+
<Input label="Your Username" onChange={handleInputChange} value={userName} name="userName" required />
86+
<InputPassword label="New Password" onChange={handleInputChange} value={password} name="password" required />
87+
<Button type={ButtonType.SUBMIT}>Generate hash!</Button>
88+
89+
{generatedHash && (
90+
<div class="mt-4">
91+
<InputReadonly label="Generated Hash" value={generatedHash} />
92+
<CopyToClipboard text={generatedHash} onCopy={() => setToastMessage('Generated hash successfully copied to clipboard!')}>
93+
<Button type={ButtonType.BUTTON}>Copy to clipboard</Button>
94+
</CopyToClipboard>
95+
</div>
96+
)}
97+
</form>
98+
</div>
99+
</div>
100+
<Footer />
101+
</div>
102+
</>
103+
);
104+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import { render, fireEvent, screen } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import { Button, ButtonType } from './Button';
5+
6+
describe('Button Component', () => {
7+
test('renders the button with the correct text', () => {
8+
render(<Button type={ButtonType.BUTTON}>Click Me</Button>);
9+
expect(screen.getByRole('button')).toHaveTextContent('Click Me');
10+
});
11+
12+
test('renders the button with the correct type attribute', () => {
13+
render(<Button type={ButtonType.SUBMIT}>Submit</Button>);
14+
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
15+
});
16+
17+
test('calls onClick handler when clicked', () => {
18+
const handleClick = jest.fn();
19+
render(<Button type={ButtonType.BUTTON} onClick={handleClick}>Click Me</Button>);
20+
fireEvent.click(screen.getByRole('button'));
21+
expect(handleClick).toHaveBeenCalledTimes(1);
22+
});
23+
24+
test('does not call onClick when it is not provided', () => {
25+
render(<Button type={ButtonType.BUTTON}>Click Me</Button>);
26+
fireEvent.click(screen.getByRole('button'));
27+
expect(screen.getByRole('button')).toBeInTheDocument();
28+
});
29+
});

src/components/Button/Button.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import React, { FunctionComponent } from 'react';
1+
import React from 'react';
22

33
export enum ButtonType {
4-
BUTTON = 'button',
5-
SUBMIT = 'submit'
4+
BUTTON = 'button',
5+
SUBMIT = 'submit'
66
}
77

88
interface ButtonProps {
9-
type: ButtonType;
10-
children: string;
11-
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
9+
type: ButtonType;
10+
children: React.ReactNode;
11+
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
1212
}
1313

14-
export const Button: FunctionComponent<ButtonProps> = ({ type, children, onClick }) => {
15-
return <>
16-
<button
17-
className="btn btn-primary btn-lg btn-block mt-4"
18-
onClick={onClick}
19-
typeof={type}
20-
>
21-
{children}
22-
</button>
23-
</>;
24-
}
14+
export const Button: React.FC<ButtonProps> = ({ type, children, onClick }) => {
15+
return (
16+
<button
17+
className="btn btn-primary btn-lg btn-block mt-4"
18+
onClick={onClick}
19+
type={type} // Fixed the attribute name
20+
>
21+
{children}
22+
</button>
23+
);
24+
};

src/components/Footer/Footer.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { FunctionComponent } from 'react';
1+
import React from 'react';
22

3-
export const Footer: FunctionComponent = () => {
4-
return <>
5-
<footer className="my-2 pt-2 text-white text-center text-small">
6-
Made with <span role="img" aria-label="heart"></span>️ and <span role="img" aria-label="coffee">☕️</span>
7-
<br/>
8-
<span role="img">©</span> 2019 - 2023 <a target={'_blank'} rel={'noreferrer'} href={'https://www.rinne.info/'}>
9-
Thorsten Rinne
10-
</a>
11-
</footer>
12-
</>
13-
}
3+
export const Footer: React.FC = () => {
4+
return (
5+
<footer className="my-2 pt-2 text-white text-center text-small">
6+
Made with <span role="img" aria-label="heart"></span> and <span role="img" aria-label="coffee">☕️</span>
7+
<br />
8+
<span role="img" aria-label="copyright">©</span> 2019 - 2024
9+
<a target="_blank" rel="noreferrer" href="https://www.rinne.info/">
10+
Thorsten Rinne
11+
</a>
12+
</footer>
13+
);
14+
};

src/components/Header/Header.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import { FunctionComponent } from 'react';
1+
import React from 'react';
22
import './Header.css';
33

44
interface HeaderProps {
5-
title: string;
5+
title: string;
66
}
77

8-
export const Header: FunctionComponent<HeaderProps> = ({ title }) => {
9-
return <>
10-
<header className="app-header">
11-
<img src="./logo.png" className="phpmyfaq-logo" alt="phpMyFAQ Logo"/>
12-
<h1 className="text-center">
13-
{title}
14-
</h1>
15-
</header>
16-
</>;
17-
}
8+
export const Header: React.FC<HeaderProps> = ({ title }) => {
9+
return (
10+
<header className="app-header">
11+
<img src="./logo.png" className="phpmyfaq-logo" alt="phpMyFAQ company logo" />
12+
<h1 className="text-center">
13+
{title}
14+
</h1>
15+
</header>
16+
);
17+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Input.test.tsx
2+
import React, { useState } from 'react';
3+
import { render, fireEvent, screen } from '@testing-library/react';
4+
import '@testing-library/jest-dom';
5+
import { Input } from './Input';
6+
7+
describe('Input Component', () => {
8+
test('renders the input with the correct label', () => {
9+
render(<Input label="Username" value="" name="username" onChange={() => {}} />);
10+
expect(screen.getByLabelText('Username')).toBeInTheDocument();
11+
});
12+
13+
test('renders the input with the correct value', () => {
14+
render(<Input label="Username" value="JohnDoe" name="username" onChange={() => {}} />);
15+
expect(screen.getByLabelText('Username')).toHaveValue('JohnDoe');
16+
});
17+
18+
test('calls onChange handler when typing', () => {
19+
const TestComponent = () => {
20+
const [inputValue, setInputValue] = useState('');
21+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
22+
setInputValue(e.target.value);
23+
};
24+
25+
return <Input label="Username" value={inputValue} name="username" onChange={handleChange} />;
26+
};
27+
28+
render(<TestComponent />);
29+
fireEvent.change(screen.getByLabelText('Username'), { target: { value: 'JohnDoe' } });
30+
expect(screen.getByLabelText('Username')).toHaveValue('JohnDoe');
31+
});
32+
33+
test('renders required attribute when provided', () => {
34+
render(<Input label="Username" value="" name="username" onChange={() => {}} required />);
35+
expect(screen.getByLabelText('Username')).toBeRequired();
36+
});
37+
});

0 commit comments

Comments
 (0)