Skip to content

Commit

Permalink
feat: add history (#29)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
abolkog authored Oct 17, 2024
1 parent b89f0a4 commit 9ef3367
Show file tree
Hide file tree
Showing 25 changed files with 427 additions and 139 deletions.
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"github-fork-ribbon-css": "^0.2.3",
"lodash": "^4.17.21",
"luxon": "^1.28.0",
"lz-string": "^1.5.0",
"monaco-editor": "^0.34.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2",
Expand All @@ -66,6 +67,7 @@
"@types/jest": "^26.0.15",
"@types/lodash": "^4.14.185",
"@types/luxon": "^3.0.1",
"@types/lz-string": "^1.5.0",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^18.0.6",
Expand Down
6 changes: 4 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<link rel="shortcut icon" href="favicon.ico" />

<link
href="https://stackpath.bootstrapcdn.com/bootswatch/4.1.3/slate/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-ywjdn7N8uoxzIfGl7jlEBlqw2BNicOSzZDgo7A2ffvbM24Ct9plRp7KwtaIqZ33j"
href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/slate/bootstrap.min.css"
integrity="sha512-3EVe7TjxthzbTGfmRFr7zIvHjDWW7viFDgKOoTJ7S5IIrrKVN5rbPVjj0F7nT6rTyAkURnzwoujxlALvHoO9jw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>

<link
Expand Down
27 changes: 14 additions & 13 deletions src/components/About/About.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@ import { AppContext } from 'context/AppContext';
import { AppAactions } from 'context/Reducer';

describe('<About />', () => {
it('render libraries list', () => {
render(<About />);
const listElement = screen.getByTestId('about-libraries-list');
expect(listElement.children.length).toEqual(LIBRARIES.length);
});

it('calls dipatch on button click', () => {
const state = {
display: 'block',
} as AppState;
const dispatch = jest.fn();
const state = {
display: 'block',
} as AppState;
const dispatch = jest.fn();

beforeEach(() => {
render(
// eslint-disable-next-line react/jsx-no-constructed-context-values
<AppContext.Provider value={{ state, dispatch }}>
<About />
</AppContext.Provider>
);
fireEvent.click(screen.getByText(/close/i));
});

it('render libraries list', () => {
const listElement = screen.getByTestId('about-libraries-list');
expect(listElement.children.length).toEqual(LIBRARIES.length);
});

it('calls dipatch on button click', () => {
fireEvent.click(screen.getByTestId('modal-close-btn'));

expect(dispatch).toHaveBeenCalledWith({
type: AppAactions.TOGGLE_ABOUT_MODAL,
Expand Down
109 changes: 39 additions & 70 deletions src/components/About/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,53 @@ import { useContext } from 'react';
import { LIBRARIES } from 'helpers/const';
import { AppContext } from 'context/AppContext';
import { AppAactions } from 'context/Reducer';
import Modal from 'components/Modal';

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

const open = state.display !== 'none';

const handleClose = () => {
dispatch({ type: AppAactions.TOGGLE_ABOUT_MODAL, payload: 'none' });
};

return (
<div>
<div
data-testid="about-main-container"
className="modal fade show"
style={{ display: state.display }}
>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">About JS Playground</h4>
</div>
<div className="modal-body">
<p>
JS Playground is an experimental JavaScript PlayGround created
for Education and Testing Purposes
</p>
<div>
This sandbox playground is hooked up directly with
<ul data-testid="about-libraries-list">
{LIBRARIES.map(lib => (
<li key={lib.name}>
<div
style={{
display: 'flex',
flex: 1,
justifyContent: 'space-between',
}}
>
<a
href={lib.url}
target="_blank"
rel="noopener noreferrer"
>
{lib.name}{' '}
<span className="text-sm">v{lib.version}</span>
</a>
<span>Use as {lib.use}</span>
</div>
</li>
))}
</ul>
</div>
<p>Enjoy</p>
<div>
<div className="float-left">
<a
href="https://nyala.dev"
target="_blank"
rel="noopener noreferrer"
>
Khalid Elshafie
</a>
</div>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() =>
dispatch({
type: AppAactions.TOGGLE_ABOUT_MODAL,
payload: 'none',
})
}
<Modal isOpen={open} onClose={handleClose} title="About JS Playground">
<p>
JS Playground is an experimental JavaScript PlayGround created for
Education and Testing Purposes
</p>
<div>
This sandbox playground is hooked up directly with
<ul data-testid="about-libraries-list">
{LIBRARIES.map(lib => (
<li key={lib.name}>
<div
style={{
display: 'flex',
flex: 1,
justifyContent: 'space-between',
}}
>
Close
</button>
</div>
</div>
<a href={lib.url} target="_blank" rel="noopener noreferrer">
{lib.name} <span className="text-sm">v{lib.version}</span>
</a>
<span>Use as {lib.use}</span>
</div>
</li>
))}
</ul>
</div>
<p>Enjoy</p>
<div>
<div className="float-left">
<a href="https://nyala.dev" target="_blank" rel="noopener noreferrer">
Khalid Elshafie
</a>
</div>
</div>
</div>
</Modal>
);
};

Expand Down
6 changes: 6 additions & 0 deletions src/components/ActionButton/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const ButtonProps: Record<ActionButtonType, ActionButtonTypeProps> = {
className: 'btn btn-success',
toolTip: 'Run Code (CtrCmd + k)',
},
history: {
title: 'History',
icon: 'fas fa-history',
className: 'btn btn-warning',
toolTip: 'Show run history',
},
};

const ActionButton: React.FC<ActionButtonProps> = ({
Expand Down
2 changes: 1 addition & 1 deletion src/components/ActionButton/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type ActionButtonType = 'execute' | 'clear';
type ActionButtonType = 'execute' | 'clear' | 'history';

interface ActionButtonProps {
type: ActionButtonType;
Expand Down
3 changes: 3 additions & 0 deletions src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ContextMenu from 'components/ContextMenu';
import JsonView from 'components/JsonView';
import CodeEditor from 'components/CodeEditor';
import Console from 'components/Console';
import HistoryModal from 'components/HistoryModal';

const App: React.FC = () => {
const { dispatch } = useContext(AppContext);
Expand Down Expand Up @@ -52,6 +53,8 @@ const App: React.FC = () => {
setPosition(null);
}}
/>

<HistoryModal />
</div>
);
};
Expand Down
13 changes: 10 additions & 3 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ const Header: React.FC = () => {
};

return (
<nav className="navbar navbar-expand-lg navbar-dark bg-primary">
<nav className="navbar navbar-expand-lg navbar-dark bg-primary px-2">
<a className="navbar-brand" href="/">
JS PlayGround
</a>

<div className="collapse navbar-collapse" id="navbarColor01">
<ul className="navbar-nav mr-auto">
<div className="collapse navbar-collapse">
<ul className="navbar-nav me-auto">
<li className="nav-item">
<button
type="button"
Expand All @@ -49,6 +49,7 @@ const Header: React.FC = () => {
</button>
</li>
</ul>

<div className="my-2 app-actions">
<div>
<select
Expand Down Expand Up @@ -97,6 +98,12 @@ const Header: React.FC = () => {
onClick={() => dispatch({ type: AppAactions.CLEAR_RESULT })}
/>

<span style={{ marginLeft: 20, marginRight: 20 }} />
<ActionButton
type="history"
onClick={() => dispatch({ type: AppAactions.TOGGLE_HISTORY_MODAL })}
/>

<span style={{ marginLeft: 20, marginRight: 20 }} />
<ActionButton
type="execute"
Expand Down
77 changes: 77 additions & 0 deletions src/components/HistoryModal/HistoryModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { AppContext } from 'context/AppContext';
import { AppAactions } from 'context/Reducer';
import HistoryModal from 'components/HistoryModal';
import * as StorageService from 'services/storage';

const getHistorySpy = jest.spyOn(StorageService, 'getHistory');
const mockHistory = [
{
date: 'second',
code: 'const a = 10',
},
{
date: 'first',
code: 'const b = 20',
},
];

describe('<HistoryModal />', () => {
const state = {
historyModalShown: true,
} as AppState;
const mockSetState = jest.fn();
const dispatch = jest.fn();

beforeEach(() => {
getHistorySpy.mockReturnValue(mockHistory);
jest.spyOn(React, 'useState').mockReturnValue([0, mockSetState]);
render(
<AppContext.Provider value={{ state, dispatch }}>
<HistoryModal />
</AppContext.Provider>
);
});

afterEach(jest.clearAllMocks);

it('render history list', () => {
const historyElement = screen.getByTestId('history-accordion');
expect(historyElement.children.length).toEqual(2);
});

it('calls closes the modal on button click', () => {
fireEvent.click(screen.getByTestId('modal-close-btn'));

expect(dispatch).toHaveBeenCalledWith({
type: AppAactions.TOGGLE_HISTORY_MODAL,
});
});

describe('when restoring history', () => {
const historyItemIndex = 0;
beforeEach(async () => {
const restoreButtons = await screen.findAllByRole('button', {
name: /Restore/,
});
fireEvent.click(restoreButtons[historyItemIndex]);
});

it('restore history when restore button click', () => {
expect(dispatch).toHaveBeenNthCalledWith(1, {
type: AppAactions.LOAD_CODE_SAMPLE,
payload: {
codeSample: mockHistory[historyItemIndex].code,
codeSampleName: '',
},
});
});

it('dismiss the modal', () => {
expect(dispatch).toHaveBeenNthCalledWith(2, {
type: AppAactions.TOGGLE_HISTORY_MODAL,
});
});
});
});
Loading

0 comments on commit 9ef3367

Please sign in to comment.