Skip to content

Commit 9ef3367

Browse files
authored
feat: add history (#29)
Update the app to keep code history This PR: - Add code history to keep the last N executions - Bump bootstrap version from 4 to 5 - Persist theme selection in local storage - Introduce Modal component
1 parent b89f0a4 commit 9ef3367

25 files changed

+427
-139
lines changed

package-lock.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"github-fork-ribbon-css": "^0.2.3",
4949
"lodash": "^4.17.21",
5050
"luxon": "^1.28.0",
51+
"lz-string": "^1.5.0",
5152
"monaco-editor": "^0.34.0",
5253
"monaco-editor-webpack-plugin": "^7.0.1",
5354
"react": "^17.0.2",
@@ -66,6 +67,7 @@
6667
"@types/jest": "^26.0.15",
6768
"@types/lodash": "^4.14.185",
6869
"@types/luxon": "^3.0.1",
70+
"@types/lz-string": "^1.5.0",
6971
"@types/node": "^12.0.0",
7072
"@types/react": "^17.0.0",
7173
"@types/react-dom": "^18.0.6",

public/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
content="width=device-width, initial-scale=1, shrink-to-fit=no"
88
/>
99
<link rel="shortcut icon" href="favicon.ico" />
10+
1011
<link
11-
href="https://stackpath.bootstrapcdn.com/bootswatch/4.1.3/slate/bootstrap.min.css"
1212
rel="stylesheet"
13-
integrity="sha384-ywjdn7N8uoxzIfGl7jlEBlqw2BNicOSzZDgo7A2ffvbM24Ct9plRp7KwtaIqZ33j"
13+
href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/slate/bootstrap.min.css"
14+
integrity="sha512-3EVe7TjxthzbTGfmRFr7zIvHjDWW7viFDgKOoTJ7S5IIrrKVN5rbPVjj0F7nT6rTyAkURnzwoujxlALvHoO9jw=="
1415
crossorigin="anonymous"
16+
referrerpolicy="no-referrer"
1517
/>
1618

1719
<link

src/components/About/About.spec.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,26 @@ import { AppContext } from 'context/AppContext';
55
import { AppAactions } from 'context/Reducer';
66

77
describe('<About />', () => {
8-
it('render libraries list', () => {
9-
render(<About />);
10-
const listElement = screen.getByTestId('about-libraries-list');
11-
expect(listElement.children.length).toEqual(LIBRARIES.length);
12-
});
13-
14-
it('calls dipatch on button click', () => {
15-
const state = {
16-
display: 'block',
17-
} as AppState;
18-
const dispatch = jest.fn();
8+
const state = {
9+
display: 'block',
10+
} as AppState;
11+
const dispatch = jest.fn();
1912

13+
beforeEach(() => {
2014
render(
21-
// eslint-disable-next-line react/jsx-no-constructed-context-values
2215
<AppContext.Provider value={{ state, dispatch }}>
2316
<About />
2417
</AppContext.Provider>
2518
);
26-
fireEvent.click(screen.getByText(/close/i));
19+
});
20+
21+
it('render libraries list', () => {
22+
const listElement = screen.getByTestId('about-libraries-list');
23+
expect(listElement.children.length).toEqual(LIBRARIES.length);
24+
});
25+
26+
it('calls dipatch on button click', () => {
27+
fireEvent.click(screen.getByTestId('modal-close-btn'));
2728

2829
expect(dispatch).toHaveBeenCalledWith({
2930
type: AppAactions.TOGGLE_ABOUT_MODAL,

src/components/About/About.tsx

Lines changed: 39 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,53 @@ import { useContext } from 'react';
22
import { LIBRARIES } from 'helpers/const';
33
import { AppContext } from 'context/AppContext';
44
import { AppAactions } from 'context/Reducer';
5+
import Modal from 'components/Modal';
56

67
const About: React.FC = () => {
78
const { state, dispatch } = useContext(AppContext);
89

10+
const open = state.display !== 'none';
11+
12+
const handleClose = () => {
13+
dispatch({ type: AppAactions.TOGGLE_ABOUT_MODAL, payload: 'none' });
14+
};
15+
916
return (
10-
<div>
11-
<div
12-
data-testid="about-main-container"
13-
className="modal fade show"
14-
style={{ display: state.display }}
15-
>
16-
<div className="modal-dialog">
17-
<div className="modal-content">
18-
<div className="modal-header">
19-
<h4 className="modal-title">About JS Playground</h4>
20-
</div>
21-
<div className="modal-body">
22-
<p>
23-
JS Playground is an experimental JavaScript PlayGround created
24-
for Education and Testing Purposes
25-
</p>
26-
<div>
27-
This sandbox playground is hooked up directly with
28-
<ul data-testid="about-libraries-list">
29-
{LIBRARIES.map(lib => (
30-
<li key={lib.name}>
31-
<div
32-
style={{
33-
display: 'flex',
34-
flex: 1,
35-
justifyContent: 'space-between',
36-
}}
37-
>
38-
<a
39-
href={lib.url}
40-
target="_blank"
41-
rel="noopener noreferrer"
42-
>
43-
{lib.name}{' '}
44-
<span className="text-sm">v{lib.version}</span>
45-
</a>
46-
<span>Use as {lib.use}</span>
47-
</div>
48-
</li>
49-
))}
50-
</ul>
51-
</div>
52-
<p>Enjoy</p>
53-
<div>
54-
<div className="float-left">
55-
<a
56-
href="https://nyala.dev"
57-
target="_blank"
58-
rel="noopener noreferrer"
59-
>
60-
Khalid Elshafie
61-
</a>
62-
</div>
63-
</div>
64-
</div>
65-
<div className="modal-footer">
66-
<button
67-
type="button"
68-
className="btn btn-primary"
69-
onClick={() =>
70-
dispatch({
71-
type: AppAactions.TOGGLE_ABOUT_MODAL,
72-
payload: 'none',
73-
})
74-
}
17+
<Modal isOpen={open} onClose={handleClose} title="About JS Playground">
18+
<p>
19+
JS Playground is an experimental JavaScript PlayGround created for
20+
Education and Testing Purposes
21+
</p>
22+
<div>
23+
This sandbox playground is hooked up directly with
24+
<ul data-testid="about-libraries-list">
25+
{LIBRARIES.map(lib => (
26+
<li key={lib.name}>
27+
<div
28+
style={{
29+
display: 'flex',
30+
flex: 1,
31+
justifyContent: 'space-between',
32+
}}
7533
>
76-
Close
77-
</button>
78-
</div>
79-
</div>
34+
<a href={lib.url} target="_blank" rel="noopener noreferrer">
35+
{lib.name} <span className="text-sm">v{lib.version}</span>
36+
</a>
37+
<span>Use as {lib.use}</span>
38+
</div>
39+
</li>
40+
))}
41+
</ul>
42+
</div>
43+
<p>Enjoy</p>
44+
<div>
45+
<div className="float-left">
46+
<a href="https://nyala.dev" target="_blank" rel="noopener noreferrer">
47+
Khalid Elshafie
48+
</a>
8049
</div>
8150
</div>
82-
</div>
51+
</Modal>
8352
);
8453
};
8554

src/components/ActionButton/ActionButton.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const ButtonProps: Record<ActionButtonType, ActionButtonTypeProps> = {
1111
className: 'btn btn-success',
1212
toolTip: 'Run Code (CtrCmd + k)',
1313
},
14+
history: {
15+
title: 'History',
16+
icon: 'fas fa-history',
17+
className: 'btn btn-warning',
18+
toolTip: 'Show run history',
19+
},
1420
};
1521

1622
const ActionButton: React.FC<ActionButtonProps> = ({

src/components/ActionButton/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type ActionButtonType = 'execute' | 'clear';
1+
type ActionButtonType = 'execute' | 'clear' | 'history';
22

33
interface ActionButtonProps {
44
type: ActionButtonType;

src/components/App/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ContextMenu from 'components/ContextMenu';
77
import JsonView from 'components/JsonView';
88
import CodeEditor from 'components/CodeEditor';
99
import Console from 'components/Console';
10+
import HistoryModal from 'components/HistoryModal';
1011

1112
const App: React.FC = () => {
1213
const { dispatch } = useContext(AppContext);
@@ -52,6 +53,8 @@ const App: React.FC = () => {
5253
setPosition(null);
5354
}}
5455
/>
56+
57+
<HistoryModal />
5558
</div>
5659
);
5760
};

src/components/Header/Header.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ const Header: React.FC = () => {
2626
};
2727

2828
return (
29-
<nav className="navbar navbar-expand-lg navbar-dark bg-primary">
29+
<nav className="navbar navbar-expand-lg navbar-dark bg-primary px-2">
3030
<a className="navbar-brand" href="/">
3131
JS PlayGround
3232
</a>
3333

34-
<div className="collapse navbar-collapse" id="navbarColor01">
35-
<ul className="navbar-nav mr-auto">
34+
<div className="collapse navbar-collapse">
35+
<ul className="navbar-nav me-auto">
3636
<li className="nav-item">
3737
<button
3838
type="button"
@@ -49,6 +49,7 @@ const Header: React.FC = () => {
4949
</button>
5050
</li>
5151
</ul>
52+
5253
<div className="my-2 app-actions">
5354
<div>
5455
<select
@@ -97,6 +98,12 @@ const Header: React.FC = () => {
9798
onClick={() => dispatch({ type: AppAactions.CLEAR_RESULT })}
9899
/>
99100

101+
<span style={{ marginLeft: 20, marginRight: 20 }} />
102+
<ActionButton
103+
type="history"
104+
onClick={() => dispatch({ type: AppAactions.TOGGLE_HISTORY_MODAL })}
105+
/>
106+
100107
<span style={{ marginLeft: 20, marginRight: 20 }} />
101108
<ActionButton
102109
type="execute"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { fireEvent, render, screen } from '@testing-library/react';
3+
import { AppContext } from 'context/AppContext';
4+
import { AppAactions } from 'context/Reducer';
5+
import HistoryModal from 'components/HistoryModal';
6+
import * as StorageService from 'services/storage';
7+
8+
const getHistorySpy = jest.spyOn(StorageService, 'getHistory');
9+
const mockHistory = [
10+
{
11+
date: 'second',
12+
code: 'const a = 10',
13+
},
14+
{
15+
date: 'first',
16+
code: 'const b = 20',
17+
},
18+
];
19+
20+
describe('<HistoryModal />', () => {
21+
const state = {
22+
historyModalShown: true,
23+
} as AppState;
24+
const mockSetState = jest.fn();
25+
const dispatch = jest.fn();
26+
27+
beforeEach(() => {
28+
getHistorySpy.mockReturnValue(mockHistory);
29+
jest.spyOn(React, 'useState').mockReturnValue([0, mockSetState]);
30+
render(
31+
<AppContext.Provider value={{ state, dispatch }}>
32+
<HistoryModal />
33+
</AppContext.Provider>
34+
);
35+
});
36+
37+
afterEach(jest.clearAllMocks);
38+
39+
it('render history list', () => {
40+
const historyElement = screen.getByTestId('history-accordion');
41+
expect(historyElement.children.length).toEqual(2);
42+
});
43+
44+
it('calls closes the modal on button click', () => {
45+
fireEvent.click(screen.getByTestId('modal-close-btn'));
46+
47+
expect(dispatch).toHaveBeenCalledWith({
48+
type: AppAactions.TOGGLE_HISTORY_MODAL,
49+
});
50+
});
51+
52+
describe('when restoring history', () => {
53+
const historyItemIndex = 0;
54+
beforeEach(async () => {
55+
const restoreButtons = await screen.findAllByRole('button', {
56+
name: /Restore/,
57+
});
58+
fireEvent.click(restoreButtons[historyItemIndex]);
59+
});
60+
61+
it('restore history when restore button click', () => {
62+
expect(dispatch).toHaveBeenNthCalledWith(1, {
63+
type: AppAactions.LOAD_CODE_SAMPLE,
64+
payload: {
65+
codeSample: mockHistory[historyItemIndex].code,
66+
codeSampleName: '',
67+
},
68+
});
69+
});
70+
71+
it('dismiss the modal', () => {
72+
expect(dispatch).toHaveBeenNthCalledWith(2, {
73+
type: AppAactions.TOGGLE_HISTORY_MODAL,
74+
});
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)