Skip to content

Commit 069dd2d

Browse files
author
k.golikov
committed
#14 Data URL Generator improvements
1 parent bdb98ad commit 069dd2d

File tree

8 files changed

+137
-44
lines changed

8 files changed

+137
-44
lines changed

src/constants/router/routes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import QrScannerPage from '../../pages/qrScannerPage/QrScannerPage';
1111
import BgGeneratorPage from '../../pages/bgGeneratorPage/BgGeneratorPage';
1212
import JsEvaluatorPage from '../../pages/jsEvaluatorPage/JsEvaluatorPage';
1313
import UserInfoPage from '../../pages/userInfoPage/UserInfoPage';
14-
import RouteWrapper from '../../layouts/RouteWrapper';
1514
import NotFoundPage from '../../pages/notFoundPage/NotFoundPage';
1615
import UnitConverterPage from '../../pages/unitConverterPage/UnitConverterPage';
1716
import SnakeGamePage from '../../pages/snakeGamePage/SnakeGamePage';
@@ -29,6 +28,7 @@ import HtmlEditorPage from '../../pages/htmlEditorPage/HtmlEditorPage';
2928
import Base64Page from '../../pages/base64Page/Base64Page';
3029
import DataUrlPage from '../../pages/dataUrlPage/DataUrlPage';
3130
import DataUrlViewPage from '../../pages/dataUrlViewPage/DataUrlViewPage';
31+
import RouteWrapper from '../../layouts/RouteWrapper';
3232

3333
export interface AppRoute extends Omit<RouteProps, 'element'> {
3434
path: string;

src/context/RouteContext.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createContext, Dispatch, SetStateAction } from 'react';
2+
3+
export interface RouteContextType {
4+
title: string;
5+
}
6+
7+
const RouteContext = createContext<[RouteContextType, Dispatch<SetStateAction<RouteContextType>>]>([] as any);
8+
9+
export default RouteContext;

src/hooks/useRouteContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { useContext } from 'react';
2+
import RouteContext from '../context/RouteContext';
3+
4+
const useRouteContext = () => useContext(RouteContext);
5+
6+
export default useRouteContext;

src/layouts/RouteContextConsumer.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React, { FunctionComponent, useEffect } from 'react';
2+
import useRouteContext from '../hooks/useRouteContext';
3+
4+
const RouteContextConsumer: FunctionComponent = ({ children }) => {
5+
const [{ title }] = useRouteContext();
6+
7+
useEffect(() => {
8+
if (title) {
9+
document.title = title;
10+
}
11+
}, [title]);
12+
13+
return <>{children}</>;
14+
};
15+
16+
export default RouteContextConsumer;

src/layouts/RouteWrapper.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import React, { FunctionComponent, useEffect } from 'react';
2-
3-
interface Props {
4-
title: string;
5-
}
6-
7-
const RouteWrapper: FunctionComponent<Props> = ({ title, children }) => {
8-
useEffect(() => {
9-
if (title) {
10-
document.title = title;
11-
}
12-
}, [title]);
13-
14-
return <>{children}</>;
1+
import React, { FunctionComponent, useState } from 'react';
2+
import RouteContext, { RouteContextType } from '../context/RouteContext';
3+
import RouteContextConsumer from './RouteContextConsumer';
4+
5+
const RouteWrapper: FunctionComponent<RouteContextType> = ({ children, ...contextValue }) => {
6+
const contextState = useState(contextValue);
7+
8+
return (
9+
<RouteContext.Provider value={contextState}>
10+
<RouteContextConsumer>{children}</RouteContextConsumer>
11+
</RouteContext.Provider>
12+
);
1513
};
1614

1715
export default RouteWrapper;

src/pages/dataUrlPage/DataUrlPage.module.scss

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import '../../styles/main';
2+
13
.container {
24
display: flex;
35
flex-direction: column;
@@ -11,7 +13,11 @@
1113

1214
.label {
1315
margin-top: 4px;
14-
width: 120px;
16+
17+
width: 100px;
18+
@include mediaSM {
19+
width: 120px;
20+
}
1521
}
1622

1723
.input {
@@ -28,3 +34,8 @@
2834
margin-bottom: 6px;
2935
}
3036
}
37+
38+
.copyButton {
39+
align-self: flex-start;
40+
margin-top: -22px;
41+
}

src/pages/dataUrlPage/DataUrlPage.tsx

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FunctionComponent, useCallback, useMemo, useState } from 'react';
1+
import React, { ClipboardEventHandler, FunctionComponent, useCallback, useMemo, useState } from 'react';
22
import PageContainer from '../../layouts/pages/pageContainer/PageContainer';
33
import styles from './DataUrlPage.module.scss';
44
import { Button, Col, Input, notification, Space, Switch, Upload } from 'antd';
@@ -17,29 +17,36 @@ import { UploadFile } from 'antd/lib/upload/interface';
1717
import getErrorMessage from '../../utils/getErrorMessage';
1818
import { Base64 } from 'js-base64';
1919
import readFileAsBase64 from '../../utils/readFileAsBase64';
20+
import CopyButton from '../../components/copyButton/CopyButton';
2021

2122
interface DataUrlState {
2223
title: string;
2324
mimeType: string;
2425
encoding: string;
2526
content: string;
2627
isBase64: boolean;
28+
isAutoBase64: boolean;
2729
}
2830

2931
const initialState: DataUrlState = {
3032
title: '',
3133
mimeType: 'text/plain',
3234
encoding: 'utf-8',
3335
content: 'Hello World',
34-
isBase64: false
36+
isBase64: false,
37+
isAutoBase64: false
3538
};
3639

3740
const getDataUrlQueryParams = (state: DataUrlState): DataUrlViewPageQueryParams => {
3841
// data:[<MIME-type>][;charset=<encoding>][;base64],<data>
3942

43+
const isAutoBase64 = state.isAutoBase64 && !state.isBase64;
44+
const isBase64 = state.isBase64 || isAutoBase64;
45+
const content = isAutoBase64 ? Base64.encode(state.content) : state.content;
46+
4047
const charsetPart = state.encoding && `;charset=${state.encoding}`;
41-
const base64Part = state.isBase64 ? ';base64' : '';
42-
const dataPart = `,${state.content}`;
48+
const base64Part = isBase64 ? ';base64' : '';
49+
const dataPart = `,${content}`;
4350

4451
const data = `data:${state.mimeType}${charsetPart}${base64Part}${dataPart}`;
4552

@@ -91,33 +98,60 @@ const DataUrlPage: FunctionComponent = () => {
9198
const encodeBase64Content = useCallback(() => transformContent(Base64.encode), [transformContent]);
9299
const decodeBase64Content = useCallback(() => transformContent(Base64.decode), [transformContent]);
93100

94-
const handleFileChange = useCallback(async (info: UploadChangeParam<UploadFile<unknown>>) => {
95-
const { file } = info;
96-
97-
if (file.status !== 'done') {
98-
return;
99-
}
100-
101-
const { name, type } = file;
102-
103-
const blob = file.originFileObj;
104-
101+
const handleFileChange = useCallback(async (blob: Blob, name: string) => {
105102
const content = (blob && (await readFileAsBase64(blob))) ?? '';
106103

107104
setState({
108105
title: name,
109-
mimeType: type ?? '',
106+
mimeType: blob.type ?? '',
110107
isBase64: true,
108+
isAutoBase64: false,
111109
encoding: '',
112110
content
113111
});
114112
}, []);
115113

114+
const handleUploadedFileChange = useCallback(
115+
async (info: UploadChangeParam<UploadFile<unknown>>) => {
116+
const { file } = info;
117+
118+
if (file.status !== 'done') {
119+
return;
120+
}
121+
122+
const blob = file.originFileObj;
123+
124+
if (!blob) {
125+
return;
126+
}
127+
128+
await handleFileChange(blob, file.name);
129+
},
130+
[handleFileChange]
131+
);
132+
133+
const handleContentPaste = useCallback<ClipboardEventHandler<HTMLTextAreaElement>>(
134+
async (event) => {
135+
const file = event.clipboardData.files[0];
136+
137+
if (file === undefined) {
138+
return;
139+
}
140+
141+
await handleFileChange(file, file.name);
142+
},
143+
[handleFileChange]
144+
);
145+
116146
return (
117147
<PageContainer title="Data URL Generator">
118148
<Col xs={24} lg={18} xl={12} className={styles.container}>
119149
<Space className="mb-2">
120-
<Upload customRequest={dummyAntdUploadRequest} onChange={handleFileChange} showUploadList={false}>
150+
<Upload
151+
customRequest={dummyAntdUploadRequest}
152+
onChange={handleUploadedFileChange}
153+
showUploadList={false}
154+
>
121155
<Button icon={<UploadOutlined />}>Upload file</Button>
122156
</Upload>
123157
</Space>
@@ -141,21 +175,33 @@ const DataUrlPage: FunctionComponent = () => {
141175
value={state.content}
142176
onChange={handleStateChange('content')}
143177
rows={3}
178+
showCount
179+
onPaste={handleContentPaste}
180+
allowClear
144181
/>
145-
<Space className="mt-2">
182+
<Space className="mt-2" wrap>
146183
<Button onClick={encodeBase64Content}>Encode base64</Button>
147184
<Button onClick={decodeBase64Content}>Decode base64</Button>
148185
</Space>
149186
<label className={classNames(styles.formItem, 'mt-2')}>
150187
<Switch checked={state.isBase64} onChange={handleStateChange('isBase64')} />
151188
<span className="ms-3">Base64</span>
152189
</label>
190+
<label className={classNames(styles.formItem, 'mt-2')}>
191+
<Switch
192+
checked={state.isAutoBase64 && !state.isBase64}
193+
onChange={handleStateChange('isAutoBase64')}
194+
disabled={state.isBase64}
195+
/>
196+
<span className="ms-3">Auto convert text to base64</span>
197+
</label>
153198
</div>
154199
</label>
155-
<Link to={dataUrlViewUrl} className="mt-3" ref={dataUrlViewLinkRef}>
200+
<Link to={dataUrlViewUrl} target="_blank" className="mt-3" ref={dataUrlViewLinkRef}>
156201
View the iframe
157202
</Link>
158-
<TextArea readOnly value={displayedDataUrlViewUrl} rows={4} />
203+
<TextArea readOnly value={displayedDataUrlViewUrl} rows={4} showCount />
204+
<CopyButton value={displayedDataUrlViewUrl} className={styles.copyButton} />
159205
</Col>
160206
</PageContainer>
161207
);

src/pages/dataUrlViewPage/DataUrlViewPage.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { FunctionComponent } from 'react';
1+
import React, { FunctionComponent, useEffect } from 'react';
22
import useQuery from '../../hooks/useQuery';
33
import styles from './DataUrlViewPage.module.scss';
44
import { Navigate } from 'react-router-dom';
55
import { routes } from '../../constants/router/routes';
6-
import RouteWrapper from '../../layouts/RouteWrapper';
6+
import useRouteContext from '../../hooks/useRouteContext';
7+
import useStateProducer from '../../hooks/useStateProducer';
78

89
export interface DataUrlViewPageQueryParams {
910
data?: string;
@@ -12,18 +13,24 @@ export interface DataUrlViewPageQueryParams {
1213

1314
const DataUrlViewPage: FunctionComponent = () => {
1415
const { data, title } = useQuery<DataUrlViewPageQueryParams>();
16+
const [, setRouteContext] = useRouteContext();
17+
18+
const produceRouteContext = useStateProducer(setRouteContext);
1519

1620
if (!data) {
1721
return <Navigate to={routes.dataUrl.path} />;
1822
}
1923

24+
useEffect(() => {
25+
produceRouteContext((routeContext) => {
26+
routeContext.title = title ?? routes.dataUrlView.title;
27+
});
28+
}, [title]);
29+
2030
return (
21-
//FIXME title is not displayed
22-
<RouteWrapper title={title ?? routes.dataUrlView.title}>
23-
<div className={styles.container}>
24-
<iframe className={styles.iframe} src={data} />
25-
</div>
26-
</RouteWrapper>
31+
<div className={styles.container}>
32+
<iframe className={styles.iframe} src={data} />
33+
</div>
2734
);
2835
};
2936

0 commit comments

Comments
 (0)