Skip to content

Commit f367833

Browse files
committed
Added file uploader screen
1 parent 0a4717f commit f367833

File tree

8 files changed

+295
-20
lines changed

8 files changed

+295
-20
lines changed

src/components/style/upload.css

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
.page {
2+
position: fixed;
3+
top: 0;
4+
left: 0;
5+
bottom: 0;
6+
right: 0;
7+
display: flex;
8+
flex-direction: column;
9+
z-index: 3;
10+
background: white;
11+
}
12+
13+
.title {
14+
font-size: 18px;
15+
margin-top: 20px;
16+
text-align: center;
17+
}
18+
19+
.result {
20+
padding-left: 20px;
21+
padding-right: 20px;
22+
font-size: 13px;
23+
margin-top: 15px;
24+
color: var(--darkGrey);
25+
}
26+
27+
.files {
28+
flex: 1;
29+
padding: 20px;
30+
overflow-y: auto;
31+
}
32+
33+
.file {
34+
margin-top: 15px;
35+
}
36+
37+
.file:first-child {
38+
margin-top: 0;
39+
}
40+
41+
.file-title {
42+
font-size: 14px;
43+
}
44+
45+
.info {
46+
font-size: 10px;
47+
color: var(--grey);
48+
}
49+
50+
.progress {
51+
position: relative;
52+
border-radius: 10px;
53+
margin-top: 5px;
54+
height: 3px;
55+
background: var(--lightGrey);
56+
}
57+
58+
.progress:after {
59+
content: '';
60+
position: absolute;
61+
width: var(--progress);
62+
background-color: var(--lightBlue);
63+
border-radius: 10px;
64+
height: 3px;
65+
}
66+
67+
.progress_done:after {
68+
background: var(--green);
69+
}
70+
71+
.error {
72+
margin-top: 5px;
73+
font-size: 12px;
74+
color: var(--red);
75+
}
76+
77+
.button {
78+
background-color: var(--blue);
79+
text-align: center;
80+
color: white;
81+
padding: 10px;
82+
}
83+
84+
.button:active {
85+
background-color: var(--lightBlue);
86+
}

src/components/uploader.tsx

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import * as React from 'react'
2+
import { createBook } from 'services/book'
3+
import bookDataParser from 'utils/book-data'
4+
const s = require('./style/upload.css')
5+
6+
export interface FileUploaderProvider {
7+
upload(files: File[]): Promise<void>
8+
}
9+
10+
export const FileUploaderContext = React.createContext<FileUploaderProvider>(null)
11+
12+
const titles = {
13+
UPLOAD: 'Загрузка',
14+
HAS_ERRORS: 'Загружено с ошибками',
15+
FINISH: 'Загружено',
16+
WAITS: 'Ожидает',
17+
PARSE: 'Парсинг',
18+
}
19+
20+
interface IFileLine {
21+
id: string
22+
title: string
23+
progress: number | string
24+
error?: string
25+
}
26+
27+
interface State {
28+
type: string
29+
files: IFileLine[]
30+
}
31+
32+
export class FileUploader extends React.Component<any, State> implements FileUploaderProvider {
33+
state: State = {
34+
type: null,
35+
files: [],
36+
// files: [
37+
// {
38+
// id: 'stephen.king.temnaya.bashnya',
39+
// title: 'stephen.king.temnaya.bashnya',
40+
// progress: 100,
41+
// },
42+
// {
43+
// id: 'oldos',
44+
// title: 'Олдос Хаксли. О дивный новый мир',
45+
// progress: 70,
46+
// error: 'The storage is full',
47+
// },
48+
// {
49+
// id: 'yod',
50+
// title: 'Элиезер Юдковский. Гарри Поттер и методы рационального мышления',
51+
// progress: 'PARSE',
52+
// },
53+
// {
54+
// id: 'sam',
55+
// title: 'Сэмюел Дилэни. Вавилон-17',
56+
// progress: 'WAITS',
57+
// },
58+
// ],
59+
}
60+
61+
render() {
62+
return (
63+
<FileUploaderContext.Provider value={this.ctx}>
64+
{this.state.type && this.renderScreen()}
65+
{this.props.children}
66+
</FileUploaderContext.Provider>
67+
)
68+
}
69+
70+
renderScreen() {
71+
const { type, files } = this.state
72+
73+
return (
74+
<div className={s.page}>
75+
<div className={s.title}>{titles[type]}</div>
76+
{type === 'HAS_ERRORS' && (
77+
<div className={s.result}>
78+
{files.length} загружено, {files.filter(f => f.error).length} с ошибкой
79+
</div>
80+
)}
81+
82+
<div className={s.files}>
83+
{files.map(file => (
84+
<FileLine key={file.id} file={file}></FileLine>
85+
))}
86+
</div>
87+
88+
{type !== 'UPLOAD' && (
89+
<div className={s.button} onClick={this.finish}>
90+
Продолжить
91+
</div>
92+
)}
93+
</div>
94+
)
95+
}
96+
97+
createInitialState(files: File[]) {
98+
return new Promise(resolve =>
99+
this.setState({ type: 'UPLOAD', files: files.map(f => ({ id: f.name, title: f.name, progress: 0 })) }, resolve),
100+
)
101+
}
102+
103+
upload = async (files: File[]) => {
104+
await this.createInitialState(files)
105+
106+
for (let i = 0; i < files.length; i++) {
107+
try {
108+
await this.uploadBook(i, files[i])
109+
} catch (e) {
110+
this.setData(i, { error: e?.responseText || e?.toString() })
111+
}
112+
}
113+
114+
const type = this.state.files.some(f => f.error) ? 'HAS_ERRORS' : 'FINISH'
115+
116+
this.setState({ type })
117+
}
118+
119+
finish = () => this.setState({ type: null, files: [] })
120+
121+
async uploadBook(index: number, f: File) {
122+
this.setProgress(index, 'PARSE')
123+
124+
const { author, image, imageName, title, file, fileName } = await bookDataParser(f)
125+
126+
this.setData(index, { title: author ? `${author}. ${title}` : title })
127+
128+
await createBook({ file: file || f, author, image, imageName, title, fileName }, ev =>
129+
this.setProgress(index, (ev.loaded / ev.total) * 100),
130+
)
131+
132+
this.setProgress(index, 101)
133+
}
134+
135+
setProgress(index: number, progress: number | string) {
136+
const file = this.state.files[index]
137+
progress = typeof progress === 'number' ? Math.round(progress) : progress
138+
139+
if (progress !== file.progress) {
140+
this.state.files[index] = { ...file, progress }
141+
142+
this.setState({ files: [...this.state.files] })
143+
}
144+
}
145+
146+
setData(index: number, data: Partial<IFileLine>) {
147+
this.state.files[index] = { ...this.state.files[index], ...data }
148+
149+
this.setState({ files: [...this.state.files] })
150+
}
151+
152+
ctx: FileUploaderProvider = { upload: this.upload }
153+
}
154+
155+
const FileLine = React.memo(({ file }: { file: IFileLine }) => {
156+
return (
157+
<div className={s.file}>
158+
<div className={s.fileTitle}>{file.title}</div>
159+
{!file.error && titles[file.progress] && <div className={s.info}>{titles[file.progress]}</div>}
160+
{!file.error && !titles[file.progress] && (
161+
<div
162+
className={file.progress > 100 ? `${s.progress} ${s.progress_done}` : s.progress}
163+
style={{ '--progress': `${file.progress}%` } as any}
164+
></div>
165+
)}
166+
{file.error && <div className={s.error}>{file.error}</div>}
167+
</div>
168+
)
169+
})

src/containers/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import * as React from 'react'
22

33
import { Confirm } from 'components/confirm'
4+
import { FileUploader } from 'components/uploader'
45
const s = require('theme/global.css')
56

67
export default function App({ children }) {
78
return (
89
<div className={s.ios}>
9-
<Confirm>{children}</Confirm>
10+
<Confirm>
11+
<FileUploader>{children}</FileUploader>
12+
</Confirm>
1013
</div>
1114
)
1215
}

src/pages/home-page/home-page.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { RouteComponentProps } from 'react-router'
33

44
import { EventBus } from 'utils/event-bus'
55
import { Book } from 'models/book'
6-
import { createBook } from 'services/book'
76
import { RsqlFetcher } from 'components/rsql'
87

8+
import { FileUploaderContext } from 'components/uploader'
99
import ListTab from 'components/list-tab'
1010
import { InlineSvg } from 'components/inline-svg'
1111
import { FileInput } from 'components/file-input'
@@ -62,6 +62,8 @@ const queries = [
6262
]
6363

6464
export class HomePage extends React.Component<Props> {
65+
static contextType = FileUploaderContext
66+
6567
rsqlRef = React.createRef<RsqlFetcher>()
6668

6769
refresh = () => this.rsqlRef.current.fetchData(false)
@@ -136,13 +138,7 @@ export class HomePage extends React.Component<Props> {
136138
private createBook = async (files: File[]) => {
137139
this.props.history.replace('/')
138140

139-
for (let i = 0; i < files.length; i++) {
140-
try {
141-
await createBook(files[i])
142-
} catch (e) {
143-
window.alert(e)
144-
}
145-
}
141+
await this.context.upload(files)
146142

147143
this.refresh()
148144
}

src/services/book.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
import request from 'utils/request'
2-
import bookDataParser from 'utils/book-data'
32

43
const BOOKS_ENDPOINT = '/api/book'
54

6-
export async function createBook(f: File) {
7-
const { author, image, imageName, title, file, fileName } = await bookDataParser(f)
5+
interface CreateBookParams {
6+
file: File | Blob
7+
fileName: string
8+
author: string
9+
title: string
10+
image?: File
11+
imageName?: string
12+
}
813

14+
export async function createBook(
15+
{ file, fileName, author, title, image, imageName }: CreateBookParams,
16+
onProgress?: (ev: ProgressEvent) => void,
17+
) {
918
const body = new FormData()
1019
body.append('author', author)
1120
body.append('title', title)
12-
body.append('file', file || f, fileName)
21+
body.append('file', file, fileName)
1322

1423
if (image) {
1524
body.append('image', image, imageName)
1625
body.append('image-name', imageName)
1726
}
1827
const options = { method: 'POST', body, headers: {} }
19-
await request(BOOKS_ENDPOINT, options)
28+
await request(BOOKS_ENDPOINT, options, onProgress)
2029
}
2130

2231
export async function deleteBook(MD5: string) {

src/theme/global.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ body {
22
color: var(--black);
33
font-size: var(--fontM);
44
font-family: -apple-system, Helvetica, sans-serif;
5+
margin: 0;
6+
padding: 0;
57
}
68

79
* {

src/theme/variables.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55

66
--backgroundColor: #fff;
77
--blue: #007aff;
8+
--lightBlue: #03a9f4;
89
--black: #1f1f21;
910
--darkGrey: #555;
1011
--grey: #929292;
12+
--lightGrey: #eceff1;
1113
--red: #eb5757;
14+
--green: #8bc34a;
1215

1316
--border: 1px solid #ccc;
1417

0 commit comments

Comments
 (0)