Refactor upload details, Update dependencies (#376)

This commit is contained in:
Piotr Rogowski 2022-01-15 17:15:08 +01:00 committed by GitHub
parent 0229830b1b
commit 83963dd8c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 658 additions and 610 deletions

View File

@ -30,4 +30,4 @@ There are many ways in which you can participate in the project and every bit of
## Development ## Development
Please see [Developer guide](https://github.com/speedy-tuner/speedy-tuner-cloud/blob/master/DEVELOPMENT.md) Before you begin please see [Development guide](https://github.com/speedy-tuner/speedy-tuner-cloud/blob/master/DEVELOPMENT.md).

807
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,18 +35,18 @@
"@sentry/react": "^6.16.1", "@sentry/react": "^6.16.1",
"@sentry/tracing": "^6.16.1", "@sentry/tracing": "^6.16.1",
"@speedy-tuner/ini": "^0.2.2", "@speedy-tuner/ini": "^0.2.2",
"@speedy-tuner/types": "^0.2.1", "@speedy-tuner/types": "^0.3.0",
"antd": "^4.18.2", "antd": "^4.18.3",
"firebase": "^9.6.1", "firebase": "^9.6.3",
"mlg-converter": "^0.5.1", "mlg-converter": "^0.5.1",
"nanoid": "^3.1.30", "nanoid": "^3.1.32",
"pako": "^2.0.4", "pako": "^2.0.4",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-markdown": "^7.1.2", "react-markdown": "^7.1.2",
"react-perfect-scrollbar": "^1.5.8", "react-perfect-scrollbar": "^1.5.8",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-router-dom": "^5.2.1", "react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3", "react-scripts": "^4.0.3",
"uplot": "^1.6.18", "uplot": "^1.6.18",
"uplot-react": "^1.1.1" "uplot-react": "^1.1.1"
@ -59,7 +59,7 @@
"@types/pako": "^1.0.3", "@types/pako": "^1.0.3",
"@types/react": "^17.0.38", "@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.21", "@types/react-redux": "^7.1.22",
"@types/react-router-dom": "^5.3.2", "@types/react-router-dom": "^5.3.2",
"eslint-plugin-modules-newline": "^0.0.6", "eslint-plugin-modules-newline": "^0.0.6",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/icons/icon.ico" /> <link rel="icon" href="%PUBLIC_URL%/icons/icon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1" />
<meta name="theme-color" content="#222629" /> <meta name="theme-color" content="#222629" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

View File

@ -14,7 +14,7 @@ import {
AppState, AppState,
ConfigState, ConfigState,
StatusState, StatusState,
} from '@speedy-tuner/types'; } from '../types/state';
const { Footer } = Layout; const { Footer } = Layout;

View File

@ -17,7 +17,6 @@ import {
} from 'antd'; } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { import {
AppState,
Dialogs as DialogsType, Dialogs as DialogsType,
Dialog as DialogType, Dialog as DialogType,
Config as ConfigType, Config as ConfigType,
@ -27,8 +26,11 @@ import {
ScalarConstant as ScalarConstantType, ScalarConstant as ScalarConstantType,
ConstantTypes, ConstantTypes,
Tune as TuneType, Tune as TuneType,
UIState,
} from '@speedy-tuner/types'; } from '@speedy-tuner/types';
import {
AppState,
UIState,
} from '../../types/state';
import SmartSelect from './Dialog/SmartSelect'; import SmartSelect from './Dialog/SmartSelect';
import SmartNumber from './Dialog/SmartNumber'; import SmartNumber from './Dialog/SmartNumber';
import TextField from './Dialog/TextField'; import TextField from './Dialog/TextField';

View File

@ -8,12 +8,14 @@ import {
} from '../firebase'; } from '../firebase';
import { TuneDbData } from '../types/dbData'; import { TuneDbData } from '../types/dbData';
const TUNES_PATH = 'publicTunes';
const genericError = (error: Error) => notification.error({ message: 'Database Error', description: error.message }); const genericError = (error: Error) => notification.error({ message: 'Database Error', description: error.message });
const useDb = () => { const useDb = () => {
const getData = async (tuneId: string, collection: string) => { const getData = async (tuneId: string) => {
try { try {
const tune = (await getDoc(fireStoreDoc(db, collection, tuneId))).data() as TuneDbData; const tune = (await getDoc(fireStoreDoc(db, TUNES_PATH, tuneId))).data() as TuneDbData;
return Promise.resolve(tune); return Promise.resolve(tune);
} catch (error) { } catch (error) {
@ -25,9 +27,9 @@ const useDb = () => {
} }
}; };
const updateData = async (tuneId: string, collection: string, data: TuneDbData) => { const updateData = async (tuneId: string, data: TuneDbData) => {
try { try {
await setDoc(fireStoreDoc(db, collection, tuneId), data, { merge: true }); await setDoc(fireStoreDoc(db, TUNES_PATH, tuneId), data, { merge: true });
return Promise.resolve(); return Promise.resolve();
} catch (error) { } catch (error) {
@ -40,8 +42,8 @@ const useDb = () => {
}; };
return { return {
getTune: (tuneId: string): Promise<TuneDbData> => getData(tuneId, 'tunes'), getTune: (tuneId: string): Promise<TuneDbData> => getData(tuneId),
updateData: (tuneId: string, data: TuneDbData): Promise<void> => updateData(tuneId, 'tunes', data), updateData: (tuneId: string, data: TuneDbData): Promise<void> => updateData(tuneId, data),
}; };
}; };

View File

@ -1,11 +1,16 @@
import { notification } from 'antd'; import { notification } from 'antd';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { UploadTask } from 'firebase/storage';
import { import {
storage, storage,
storageRef, storageRef,
getBytes, getBytes,
deleteObject,
uploadBytesResumable,
} from '../firebase'; } from '../firebase';
const TUNES_PATH = 'public/users';
const genericError = (error: Error) => notification.error({ message: 'Database Error', description: error.message }); const genericError = (error: Error) => notification.error({ message: 'Database Error', description: error.message });
const useServerStorage = () => { const useServerStorage = () => {
@ -23,8 +28,32 @@ const useServerStorage = () => {
} }
}; };
const removeFile = async (path: string) => {
try {
await deleteObject(storageRef(storage, `${TUNES_PATH}/${path}`));
return Promise.resolve();
} catch (error) {
Sentry.captureException(error);
console.error(error);
genericError(error as Error);
return Promise.reject(error);
}
};
const uploadFile = (path: string, file: File, data: Uint8Array) =>
uploadBytesResumable(storageRef(storage, `${TUNES_PATH}/${path}`), data, {
customMetadata: {
name: file.name,
size: `${file.size}`,
},
});
return { return {
getFile: (path: string): Promise<ArrayBuffer> => getFile(path), getFile: (path: string): Promise<ArrayBuffer> => getFile(path),
removeFile: (path: string): Promise<void> => removeFile(path),
uploadFile: (path: string, file: File, data: Uint8Array): UploadTask => uploadFile(path, file, data),
}; };
}; };

View File

@ -25,11 +25,13 @@ import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PerfectScrollbar from 'react-perfect-scrollbar'; import PerfectScrollbar from 'react-perfect-scrollbar';
import { import {
AppState,
UIState,
Config, Config,
Logs, Logs,
} from '@speedy-tuner/types'; } from '@speedy-tuner/types';
import {
AppState,
UIState,
} from '../types/state';
import { import {
loadCompositeLogs, loadCompositeLogs,
loadToothLogs, loadToothLogs,

View File

@ -29,8 +29,6 @@ import PerfectScrollbar from 'react-perfect-scrollbar';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import MlgParserWorker from 'worker-loader!../workers/mlgParser.worker'; import MlgParserWorker from 'worker-loader!../workers/mlgParser.worker';
import { import {
AppState,
UIState,
Config, Config,
OutputChannel, OutputChannel,
Logs, Logs,
@ -49,6 +47,10 @@ import {
isExpression, isExpression,
stripExpression, stripExpression,
} from '../utils/tune/expression'; } from '../utils/tune/expression';
import {
AppState,
UIState,
} from '../types/state';
const { TabPane } = Tabs; const { TabPane } = Tabs;
const { Content } = Layout; const { Content } = Layout;

View File

@ -19,6 +19,7 @@ import {
Tooltip, Tooltip,
Typography, Typography,
Upload, Upload,
Form,
} from 'antd'; } from 'antd';
import { import {
PlusOutlined, PlusOutlined,
@ -46,18 +47,15 @@ import {
} from './auth/notifications'; } from './auth/notifications';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { Routes } from '../routes'; import { Routes } from '../routes';
import {
storage,
storageRef,
uploadBytesResumable,
deleteObject,
} from '../firebase';
import useBrowserStorage from '../hooks/useBrowserStorage'; import useBrowserStorage from '../hooks/useBrowserStorage';
import TuneParser from '../utils/tune/TuneParser'; import TuneParser from '../utils/tune/TuneParser';
import TriggerLogsParser from '../utils/logs/TriggerLogsParser'; import TriggerLogsParser from '../utils/logs/TriggerLogsParser';
import LogParser from '../utils/logs/LogParser'; import LogParser from '../utils/logs/LogParser';
import { TuneDbData } from '../types/dbData'; import { TuneDbData } from '../types/dbData';
import useDb from '../hooks/useDb'; import useDb from '../hooks/useDb';
import useServerStorage from '../hooks/useServerStorage';
const { Item } = Form;
enum MaxFiles { enum MaxFiles {
TUNE_FILES = 1, TUNE_FILES = 1,
@ -92,7 +90,7 @@ const containerStyle = {
const newTuneIdKey = 'newTuneId'; const newTuneIdKey = 'newTuneId';
const maxFileSizeMB = 10; const maxFileSizeMB = 10;
const descriptionEditorHeight = 260; const descriptionEditorHeight = 260;
const rowProps = { gutter: 10, style: { marginBottom: 10 } }; const rowProps = { gutter: 10 };
const tuneIcon = () => <ToolOutlined />; const tuneIcon = () => <ToolOutlined />;
const logIcon = () => <FundOutlined />; const logIcon = () => <FundOutlined />;
@ -100,18 +98,14 @@ const toothLogIcon = () => <SettingOutlined />;
const iniIcon = () => <FileTextOutlined />; const iniIcon = () => <FileTextOutlined />;
const nanoidCustom = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10); const nanoidCustom = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 10);
const baseUploadPath = 'public/users';
const UploadPage = () => { const UploadPage = () => {
const [newTuneId, setNewTuneId] = useState<string>(); const [newTuneId, setNewTuneId] = useState<string>();
const [isUserAuthorized, setIsUserAuthorized] = useState(false); const [isUserAuthorized, setIsUserAuthorized] = useState(false);
const [shareUrl, setShareUrl] = useState<string>(); const [shareUrl, setShareUrl] = useState<string>();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isPublished, setIsPublished] = useState(false); const [isPublished, setIsPublished] = useState(false);
const [isPublic, setIsPublic] = useState(true);
const [isListed, setIsListed] = useState(true);
const [tuneFile, setTuneFile] = useState<UploadedFile | null | false>(null); const [tuneFile, setTuneFile] = useState<UploadedFile | null | false>(null);
const [logFiles, setLogFiles] = useState<UploadedFile>({}); const [logFiles, setLogFiles] = useState<UploadedFile>({});
const [toothLogFiles, setToothLogFiles] = useState<UploadedFile>({}); const [toothLogFiles, setToothLogFiles] = useState<UploadedFile>({});
@ -121,21 +115,9 @@ const UploadPage = () => {
const history = useHistory(); const history = useHistory();
const { storageSet, storageGet, storageDelete } = useBrowserStorage(); const { storageSet, storageGet, storageDelete } = useBrowserStorage();
const { updateData, getTune } = useDb(); const { updateData, getTune } = useDb();
const { removeFile, uploadFile } = useServerStorage();
// details const requiredRules = [{ required: true, message: 'This field is required!' }];
const [readme, setReadme] = useState('# My Tune\n\ndescription'); const [readme, setReadme] = useState('# My Tune\n\ndescription');
const [make, setMake] = useState<string>();
const [model, setModel] = useState<string>();
const [displacement, setDisplacement] = useState<string>();
const [year, setYear] = useState<number>();
const [hp, setHp] = useState<number>();
const [stockHp, setStockHp] = useState<number>();
const [engineCode, setEngineCode] = useState<string>();
const [cylinders, setCylinders] = useState<number>();
const [aspiration, setAspiration] = useState<string>();
const [fuel, setFuel] = useState<string>();
const [injectors, setInjectors] = useState<string>();
const [coils, setCoils] = useState<string>();
const noop = () => { }; const noop = () => { };
@ -149,39 +131,31 @@ const UploadPage = () => {
const genericError = (error: Error) => notification.error({ message: 'Error', description: error.message }); const genericError = (error: Error) => notification.error({ message: 'Error', description: error.message });
const removeFile = async (path: string) => { const publish = async (values: any) => {
try {
return await deleteObject(storageRef(storage, path));
} catch (error) {
return Promise.reject(error);
}
};
const publish = async () => {
setIsLoading(true); setIsLoading(true);
await updateData(newTuneId!, { await updateData(newTuneId!, {
updatedAt: new Date(), updatedAt: new Date(),
isPublished: true, isPublished: true,
isPublic, isPublic: values.isPublic,
isListed, isListed: values.isListed,
details: { details: {
readme: readme || null, readme: readme || null,
make: make || null, make: values.make || null,
model: model || null, model: values.model || null,
displacement: displacement || null, displacement: values.displacement || null,
year: year || null, year: values.year || null,
hp: hp || null, hp: values.hp || null,
stockHp: stockHp || null, stockHp: values.stockHp || null,
engineCode: engineCode || null, engineCode: values.engineCode || null,
cylinders: cylinders || null, cylindersCount: values.cylindersCount || null,
aspiration: aspiration || null, aspiration: values.aspiration || null,
fuel: fuel || null, fuel: values.fuel || null,
injectors: injectors || null, injectorsSize: values.injectorsSize || null,
coils: coils || null, coils: values.coils || null,
}, },
}); });
setIsPublished(true);
setIsLoading(false); setIsLoading(false);
setIsPublished(true);
storageDelete(newTuneIdKey); storageDelete(newTuneIdKey);
}; };
@ -205,12 +179,7 @@ const UploadPage = () => {
try { try {
const buffer = await (file as File).arrayBuffer(); const buffer = await (file as File).arrayBuffer();
const compressed = pako.deflate(new Uint8Array(buffer)); const compressed = pako.deflate(new Uint8Array(buffer));
const uploadTask = uploadBytesResumable(storageRef(storage, path), compressed, { const uploadTask = uploadFile(path, file as File, compressed);
customMetadata: {
name: (file as File).name,
size: `${(file as File).size}`,
},
});
uploadTask.on( uploadTask.on(
'state_changed', 'state_changed',
@ -232,35 +201,35 @@ const UploadPage = () => {
}; };
const tuneFileData = () => ({ const tuneFileData = () => ({
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/${nanoid()}.msq.gz`, path: `${currentUser!.uid}/tunes/${newTuneId}/${nanoid()}.msq.gz`,
}); });
const logFileData = (file: UploadFile) => { const logFileData = (file: UploadFile) => {
const { name } = file; const { name } = file;
const extension = name.split('.').pop(); const extension = name.split('.').pop();
return { return {
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/logs/${nanoid()}.${extension}.gz`, path: `${currentUser!.uid}/tunes/${newTuneId}/logs/${nanoid()}.${extension}.gz`,
}; };
}; };
const toothLogFilesData = () => ({ const toothLogFilesData = () => ({
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/tooth-logs/${nanoid()}.csv.gz`, path: `${currentUser!.uid}/tunes/${newTuneId}/tooth-logs/${nanoid()}.csv.gz`,
}); });
const customIniFileData = () => ({ const customIniFileData = () => ({
path: `${baseUploadPath}/${currentUser!.uid}/tunes/${newTuneId}/${nanoid()}.ini.gz`, path: `${currentUser!.uid}/tunes/${newTuneId}/${nanoid()}.ini.gz`,
}); });
const uploadTune = async (options: UploadRequestOption) => { const uploadTune = async (options: UploadRequestOption) => {
const found = await getTune(newTuneId!); const found = await getTune(newTuneId!);
if (found) { if (!found) {
const tuneData: TuneDbData = { const tuneData: TuneDbData = {
userUid: currentUser!.uid, userUid: currentUser!.uid,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
isPublished: false, isPublished: false,
isPublic, isPublic: true,
isListed, isListed: true,
tuneFile: null, tuneFile: null,
logFiles: [], logFiles: [],
toothLogFiles: [], toothLogFiles: [],
@ -495,22 +464,24 @@ const UploadPage = () => {
)} )}
</Row>} </Row>}
<Row style={{ marginTop: 10 }}> <Row style={{ marginTop: 10 }}>
{!isPublished ? <Button <Item style={{ width: '100%' }}>
type="primary" {!isPublished ? <Button
block type="primary"
disabled={isLoading} block
onClick={publish} loading={isLoading}
> htmlType="submit"
Publish >
</Button> : <Button Publish
type="primary" </Button> : <Button
block type="primary"
onClick={() => { block
window.location.href = shareUrl as string; onClick={() => {
}} window.location.href = shareUrl as string;
> }}
Open >
</Button>} Open
</Button>}
</Item>
</Row> </Row>
</> </>
); );
@ -518,24 +489,85 @@ const UploadPage = () => {
const detailsSection = ( const detailsSection = (
<> <>
<Divider> <Divider>
<Space> <Space>Details</Space>
Upload Custom INI
<Typography.Text type="secondary">(.ini)</Typography.Text>
</Space>
</Divider> </Divider>
<Upload <Row {...rowProps}>
listType="picture-card" <Col span={12}>
customRequest={uploadCustomIni} <Item name="make" rules={requiredRules}>
data={customIniFileData} <Input addonBefore="Make"/>
onRemove={removeCustomIniFile} </Item>
iconRender={iniIcon} </Col>
disabled={isPublished} <Col span={12}>
onPreview={noop} <Item name="model" rules={requiredRules}>
accept=".ini" <Input addonBefore="Model"/>
> </Item>
{!customIniFile && uploadButton} </Col>
</Upload> </Row>
<Divider> <Row {...rowProps}>
<Col span={12}>
<Item name="year" rules={requiredRules}>
<InputNumber addonBefore="Year" style={{ width: '100%' }} min={1886} max={2222} />
</Item>
</Col>
<Col span={12}>
<Item name="displacement" rules={requiredRules}>
<InputNumber addonBefore="Displacement" addonAfter="l" min={0} max={100} />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item name="hp">
<InputNumber addonBefore="HP" style={{ width: '100%' }} min={0} />
</Item>
</Col>
<Col span={12}>
<Item name="stockHp">
<InputNumber addonBefore="Stock HP" style={{ width: '100%' }} min={0} />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item name="engineCode">
<Input addonBefore="Engine code"/>
</Item>
</Col>
<Col span={12}>
<Item name="cylindersCount">
<InputNumber addonBefore="No of cylinders" style={{ width: '100%' }} min={0} />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item name="aspiration">
<Select placeholder="Aspiration" style={{ width: '100%' }}>
<Select.Option value="na">Naturally aspirated</Select.Option>
<Select.Option value="turbocharger">Turbocharged</Select.Option>
<Select.Option value="supercharger">Supercharged</Select.Option>
</Select>
</Item>
</Col>
<Col span={12}>
<Item name="fuel">
<Input addonBefore="Fuel" />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item name="injectorsSize">
<InputNumber addonBefore="Injectors size" addonAfter="cc" min={0} />
</Item>
</Col>
<Col span={12}>
<Item name="coils">
<Input addonBefore="Coils" />
</Item>
</Col>
</Row>
<Divider style={{ marginTop: 40 }}>
<Space> <Space>
README README
<Typography.Text type="secondary">(markdown)</Typography.Text> <Typography.Text type="secondary">(markdown)</Typography.Text>
@ -543,13 +575,15 @@ const UploadPage = () => {
</Divider> </Divider>
<Tabs defaultActiveKey="source" className="upload-readme"> <Tabs defaultActiveKey="source" className="upload-readme">
<Tabs.TabPane tab="Edit" key="source" style={{ height: descriptionEditorHeight }}> <Tabs.TabPane tab="Edit" key="source" style={{ height: descriptionEditorHeight }}>
<Input.TextArea <Item name="readme">
rows={10} <Input.TextArea
showCount rows={10}
value={readme} showCount
onChange={(e) => setReadme(e.target.value)} value={readme}
maxLength={3_000} onChange={(e) => setReadme(e.target.value)}
/> maxLength={3_000}
/>
</Item>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="Preview" key="preview" style={{ height: descriptionEditorHeight }}> <Tabs.TabPane tab="Preview" key="preview" style={{ height: descriptionEditorHeight }}>
<div className="markdown-preview" style={{ height: '100%' }}> <div className="markdown-preview" style={{ height: '100%' }}>
@ -559,72 +593,15 @@ const UploadPage = () => {
</div> </div>
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
<Divider>
<Space>Details</Space>
</Divider>
<Row {...rowProps}>
<Col span={12}>
<Input addonBefore="Make" value={make} onChange={(e) => setMake(e.target.value)} />
</Col>
<Col span={12}>
<Input addonBefore="Model" value={model} onChange={(e) => setModel(e.target.value)} />
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<InputNumber addonBefore="Year" value={year} onChange={setYear} style={{ width: '100%' }} min={1886} />
</Col>
<Col span={12}>
<Input addonBefore="Displacement" addonAfter="l" value={displacement} onChange={(e) => setDisplacement(e.target.value)} />
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<InputNumber addonBefore="HP" value={hp} onChange={setHp} style={{ width: '100%' }} min={0} />
</Col>
<Col span={12}>
<InputNumber addonBefore="Stock HP" value={stockHp} onChange={setStockHp} style={{ width: '100%' }} min={0} />
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Input addonBefore="Engine code" value={engineCode} onChange={(e) => setEngineCode(e.target.value)} />
</Col>
<Col span={12}>
<InputNumber addonBefore="No of cylinders" value={cylinders} onChange={setCylinders} style={{ width: '100%' }} min={0} />
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Select placeholder="Aspiration" value={aspiration} onChange={setAspiration} style={{ width: '100%' }}>
<Select.Option value="NA">Naturally aspirated</Select.Option>
<Select.Option value="turbo">Turbo</Select.Option>
<Select.Option value="compressor">Compressor</Select.Option>
</Select>
</Col>
<Col span={12}>
<Input addonBefore="Fuel type" value={fuel} onChange={(e) => setFuel(e.target.value)} />
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Input addonBefore="Injectors" value={injectors} onChange={(e) => setInjectors(e.target.value)} />
</Col>
<Col span={12}>
<Input addonBefore="Coils" value={coils} onChange={(e) => setCoils(e.target.value)} />
</Col>
</Row>
<Divider> <Divider>
Visibility Visibility
</Divider> </Divider>
<Space direction="vertical" size="large"> <Item name="isPublic" label="Public:" valuePropName="checked">
<Space> <Switch disabled />
Public:<Switch disabled checked={isPublic} onChange={setIsPublic} /> </Item>
</Space> <Item name="isListed" label="Listed:" valuePropName="checked">
<Space> <Switch />
Listed:<Switch checked={isListed} onChange={setIsListed} /> </Item>
</Space>
</Space>
</> </>
); );
@ -669,11 +646,25 @@ const UploadPage = () => {
> >
{Object.keys(toothLogFiles).length < MaxFiles.TOOTH_LOG_FILES && uploadButton} {Object.keys(toothLogFiles).length < MaxFiles.TOOTH_LOG_FILES && uploadButton}
</Upload> </Upload>
<Space style={{ marginTop: 30 }}> <Divider>
Show details: <Space>
<Switch checked={showDetails} onChange={setShowDetails} /> Upload Custom INI
</Space> <Typography.Text type="secondary">(.ini)</Typography.Text>
{showDetails && detailsSection} </Space>
</Divider>
<Upload
listType="picture-card"
customRequest={uploadCustomIni}
data={customIniFileData}
onRemove={removeCustomIniFile}
iconRender={iniIcon}
disabled={isPublished}
onPreview={noop}
accept=".ini"
>
{!customIniFile && uploadButton}
</Upload>
{detailsSection}
{shareUrl && tuneFile && shareSection} {shareUrl && tuneFile && shareSection}
</> </>
); );
@ -696,25 +687,34 @@ const UploadPage = () => {
return ( return (
<div style={containerStyle}> <div style={containerStyle}>
<Divider> <Form
<Space> onFinish={publish}
Upload Tune initialValues={{
<Typography.Text type="secondary">(.msq)</Typography.Text> readme: '# My Tune\n\ndescription',
</Space> isPublic: true,
</Divider> isListed: true,
<Upload }}
listType="picture-card"
customRequest={uploadTune}
data={tuneFileData}
onRemove={removeTuneFile}
iconRender={tuneIcon}
disabled={isPublished}
onPreview={noop}
accept=".msq"
> >
{tuneFile === null && uploadButton} <Divider>
</Upload> <Space>
{tuneFile && optionalSection} Upload Tune
<Typography.Text type="secondary">(.msq)</Typography.Text>
</Space>
</Divider>
<Upload
listType="picture-card"
customRequest={uploadTune}
data={tuneFileData}
onRemove={removeTuneFile}
iconRender={tuneIcon}
disabled={isPublished}
onPreview={noop}
accept=".msq"
>
{tuneFile === null && uploadButton}
</Upload>
{tuneFile && optionalSection}
</Form>
</div> </div>
); );
}; };

View File

@ -2,15 +2,15 @@ export interface TuneDataDetails {
readme?: string | null; readme?: string | null;
make?: string | null; make?: string | null;
model?: string | null; model?: string | null;
displacement?: string | null; displacement?: number | null;
year?: number | null; year?: number | null;
hp?: number | null; hp?: number | null;
stockHp?: number | null; stockHp?: number | null;
engineCode?: string | null; engineCode?: string | null;
cylinders?: number | null; cylindersCount?: number | null;
aspiration?: string | null; aspiration?: string | null;
fuel?: string | null; fuel?: string | null;
injectors?: string | null; injectorsSize?: number | null;
coils?: string | null; coils?: string | null;
} }