Merge branch 'master' of https://github.com/noisymime/hyper-tuner-cloud
This commit is contained in:
commit
2f9146b3fa
|
@ -23,7 +23,7 @@ jobs:
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- name: Sentry Release
|
- name: Sentry Release
|
||||||
uses: getsentry/action-release@v1.2.0
|
uses: getsentry/action-release@v1.2.1
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"cSpell.words": [
|
"editor.codeActionsOnSave": {
|
||||||
"hypertuner",
|
"source.fixAll.eslint": true
|
||||||
"kbar",
|
},
|
||||||
"pocketbase",
|
"editor.formatOnSave": true,
|
||||||
"prefs",
|
"cSpell.words": [
|
||||||
"rusefi",
|
"hypertuner",
|
||||||
"typegen",
|
"kbar",
|
||||||
"vite",
|
"pocketbase",
|
||||||
"vitejs"
|
"prefs",
|
||||||
]
|
"rusefi",
|
||||||
|
"typegen",
|
||||||
|
"vite",
|
||||||
|
"vitejs"
|
||||||
|
],
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[less]": {
|
||||||
|
"editor.defaultFormatter": "vscode.css-language-features"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@
|
||||||
|
|
||||||
### Speeduino
|
### Speeduino
|
||||||
|
|
||||||
|
[https://tunes.speeduino.com](https://tunes.speeduino.com)
|
||||||
|
|
||||||
- Source code: [noisymime/speeduino](https://github.com/noisymime/speeduino)
|
- Source code: [noisymime/speeduino](https://github.com/noisymime/speeduino)
|
||||||
- Documentation: [wiki.speeduino.com](https://wiki.speeduino.com)
|
- Documentation: [wiki.speeduino.com](https://wiki.speeduino.com)
|
||||||
|
|
||||||
|
|
57
index.html
57
index.html
|
@ -1,28 +1,35 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
<head>
|
||||||
<link rel="icon" href="/icons/icon.ico" />
|
<meta charset="utf-8" />
|
||||||
<link rel="apple-touch-icon" href="/icons/icon.png" />
|
<link rel="icon" href="/icons/icon.ico" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="apple-touch-icon" href="/icons/icon.png" />
|
||||||
<link href="css/all.min.css" rel="stylesheet">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<link rel="preconnect" href="https://apis.google.com" crossorigin>
|
||||||
<link rel="preconnect" href="https://apis.google.com" crossorigin>
|
|
||||||
<meta property="og:title" content="Speeduino Online Tunes (Powered by HyperTuner Cloud)">
|
<meta name="description" content="<%- metaDescription %>">
|
||||||
<meta name="twitter:image:alt" content="Speeduino Online Tunes">
|
<title>
|
||||||
<meta name="description" content="Share your Speeduino tune and logs" />
|
<%- metaTitle %>
|
||||||
<meta property="og:description" content="The best way to share your Speeduino tunes and logs">
|
</title>
|
||||||
<meta property="og:site_name" content="The best way to share your Speeduino tunes and logs">
|
|
||||||
<meta property="og:image" content="https://tunes.speeduino.com/img/screen2.png">
|
<meta property="og:title" content="<%- metaTitle %>">
|
||||||
<meta property="og:url" content="https://tunes.speeduino.com">
|
<meta property="og:site_name" content="<%- metaTitle %>">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta property="og:description" content="<%- metaDescription %>">
|
||||||
<meta name="description" content="Speeduino Online Tunes - Share your tunes and logs" />
|
<meta property="og:image" content="<%- metaImage %>">
|
||||||
<title>Speeduino Tune Viewer</title>
|
<meta property="og:url" content="<%- metaUrl %>">
|
||||||
</head>
|
|
||||||
<body style="background-color: #191C1E">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<meta name="twitter:image:alt" content="<%- metaTitle %>">
|
||||||
<div id="root"></div>
|
|
||||||
<!-- Vite entrypoint -->
|
<meta name="theme-color" content="<%- metaThemeColor %>">
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
</head>
|
||||||
</body>
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!-- Vite entrypoint -->
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
|
@ -17,45 +17,45 @@
|
||||||
"lint": "tsc && eslint --max-warnings=0 src",
|
"lint": "tsc && eslint --max-warnings=0 src",
|
||||||
"lint:fix": "eslint --fix src",
|
"lint:fix": "eslint --fix src",
|
||||||
"analyze": "npm run build && open stats.html",
|
"analyze": "npm run build && open stats.html",
|
||||||
"typegen": "pocketbase-typegen --json ../pocketbase/pb_schema.json --out src/@types/pocketbase-types.ts"
|
"typegen": "pocketbase-typegen --json ../cloud-backend/pb_schema.json --out src/@types/pocketbase-types.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hyper-tuner/ini": "git+https://github.com/hyper-tuner/ini.git",
|
"@hyper-tuner/ini": "git+https://github.com/hyper-tuner/ini.git",
|
||||||
"@hyper-tuner/types": "git+https://github.com/hyper-tuner/types.git",
|
"@hyper-tuner/types": "git+https://github.com/hyper-tuner/types.git",
|
||||||
"@reduxjs/toolkit": "^1.8.6",
|
"@reduxjs/toolkit": "^1.9.0",
|
||||||
"@sentry/react": "^7.17.3",
|
"@sentry/react": "^7.18.0",
|
||||||
"@sentry/tracing": "^7.17.3",
|
"@sentry/tracing": "^7.18.0",
|
||||||
"antd": "^4.23.6",
|
"antd": "^4.24.1",
|
||||||
"kbar": "^0.1.0-beta.36",
|
"kbar": "^0.1.0-beta.37",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"mlg-converter": "^0.8.0",
|
"mlg-converter": "^0.8.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.1.0",
|
||||||
"pocketbase": "^0.8.0-rc1",
|
"pocketbase": "^0.8.0-rc1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-ga4": "^1.4.1",
|
"react-ga4": "^1.4.1",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-perfect-scrollbar": "^1.5.8",
|
"react-perfect-scrollbar": "^1.5.8",
|
||||||
"react-redux": "^8.0.4",
|
"react-redux": "^8.0.5",
|
||||||
"react-router-dom": "^6.4.2",
|
"react-router-dom": "^6.4.3",
|
||||||
"uplot": "^1.6.22",
|
"uplot": "^1.6.22",
|
||||||
"uplot-react": "^1.1.1",
|
"uplot-react": "^1.1.1",
|
||||||
"vite": "^3.2.2"
|
"vite": "^3.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hyper-tuner/eslint-config": "git+https://github.com/hyper-tuner/eslint-config.git",
|
"@hyper-tuner/eslint-config": "git+https://github.com/hyper-tuner/eslint-config.git",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/react": "^18.0.24",
|
"@types/react": "^18.0.25",
|
||||||
"@types/react-dom": "^18.0.8",
|
"@types/react-dom": "^18.0.8",
|
||||||
"@types/react-redux": "^7.1.24",
|
"@types/react-redux": "^7.1.24",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||||
"@typescript-eslint/parser": "^5.42.0",
|
"@typescript-eslint/parser": "^5.42.1",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.27.0",
|
||||||
"eslint-plugin-flowtype": "^8.0.3",
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
|
@ -64,11 +64,11 @@
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.31.10",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"pocketbase-typegen": "^1.0.11",
|
"pocketbase-typegen": "^1.0.13",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.3",
|
"rollup-plugin-visualizer": "^5.8.3",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-pwa": "^0.13.1"
|
"vite-plugin-pwa": "^0.13.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,57 +7,77 @@ export type RecordIdString = string
|
||||||
export type UserIdString = string
|
export type UserIdString = string
|
||||||
|
|
||||||
export type BaseRecord = {
|
export type BaseRecord = {
|
||||||
id: RecordIdString
|
id: RecordIdString
|
||||||
created: IsoDateString
|
created: IsoDateString
|
||||||
updated: IsoDateString
|
updated: IsoDateString
|
||||||
collectionId: string
|
collectionId: string
|
||||||
collectionName: string
|
collectionName: string
|
||||||
|
expand?: { [key: string]: any }
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Collections {
|
export enum Collections {
|
||||||
IniFiles = 'iniFiles',
|
IniFiles = 'iniFiles',
|
||||||
Tunes = 'tunes',
|
Stargazers = 'stargazers',
|
||||||
Users = 'users',
|
Tunes = 'tunes',
|
||||||
|
Users = 'users',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IniFilesRecord = {
|
export type IniFilesRecord = {
|
||||||
signature: string
|
signature: string
|
||||||
file: string
|
file: string
|
||||||
ecosystem: 'speeduino' | 'rusefi'
|
ecosystem: 'speeduino' | 'rusefi'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IniFilesResponse = IniFilesRecord & BaseRecord
|
||||||
|
|
||||||
|
export type StargazersRecord = {
|
||||||
|
user: RecordIdString
|
||||||
|
tune: RecordIdString
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StargazersResponse = StargazersRecord & BaseRecord
|
||||||
|
|
||||||
export type TunesRecord = {
|
export type TunesRecord = {
|
||||||
author: RecordIdString
|
author: RecordIdString
|
||||||
tuneId: string
|
tuneId: string
|
||||||
signature: string
|
signature: string
|
||||||
vehicleName: string
|
stars?: number
|
||||||
engineMake: string
|
vehicleName: string
|
||||||
engineCode: string
|
engineMake: string
|
||||||
displacement: number
|
engineCode: string
|
||||||
cylindersCount: number
|
displacement: number
|
||||||
aspiration: 'na' | 'turbocharged' | 'supercharged'
|
cylindersCount: number
|
||||||
compression?: number
|
aspiration: 'na' | 'turbocharged' | 'supercharged'
|
||||||
fuel?: string
|
compression?: number
|
||||||
ignition?: string
|
fuel?: string
|
||||||
injectorsSize?: number
|
ignition?: string
|
||||||
year?: number
|
injectorsSize?: number
|
||||||
hp?: number
|
year?: number
|
||||||
stockHp?: number
|
hp?: number
|
||||||
readme: string
|
stockHp?: number
|
||||||
textSearch: string
|
readme: string
|
||||||
visibility: 'public' | 'unlisted'
|
textSearch: string
|
||||||
tuneFile: string
|
visibility: 'public' | 'unlisted'
|
||||||
customIniFile?: string
|
tuneFile: string
|
||||||
logFiles?: string[]
|
customIniFile?: string
|
||||||
toothLogFiles?: string[]
|
logFiles?: string[]
|
||||||
|
toothLogFiles?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TunesResponse = TunesRecord & BaseRecord
|
||||||
|
|
||||||
export type UsersRecord = {
|
export type UsersRecord = {
|
||||||
avatar?: string
|
avatar?: string
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
verified: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UsersResponse = UsersRecord & BaseRecord
|
||||||
|
|
||||||
export type CollectionRecords = {
|
export type CollectionRecords = {
|
||||||
iniFiles: IniFilesRecord
|
iniFiles: IniFilesRecord
|
||||||
tunes: TunesRecord
|
stargazers: StargazersRecord
|
||||||
users: UsersRecord
|
tunes: TunesRecord
|
||||||
|
users: UsersRecord
|
||||||
}
|
}
|
||||||
|
|
11
src/App.tsx
11
src/App.tsx
|
@ -34,12 +34,12 @@ import Info from './pages/Info';
|
||||||
import Hub from './pages/Hub';
|
import Hub from './pages/Hub';
|
||||||
import { FormRoles } from './pages/auth/Login';
|
import { FormRoles } from './pages/auth/Login';
|
||||||
import useServerStorage from './hooks/useServerStorage';
|
import useServerStorage from './hooks/useServerStorage';
|
||||||
import { TunesRecordFull } from './types/dbData';
|
|
||||||
import TuneParser from './utils/tune/TuneParser';
|
import TuneParser from './utils/tune/TuneParser';
|
||||||
import standardDialogs from './data/standardDialogs';
|
import standardDialogs from './data/standardDialogs';
|
||||||
import help from './data/help';
|
import help from './data/help';
|
||||||
import {
|
import {
|
||||||
iniLoadingError,
|
iniLoadingError,
|
||||||
|
tuneNotFound,
|
||||||
tuneParsingError,
|
tuneParsingError,
|
||||||
} from './pages/auth/notifications';
|
} from './pages/auth/notifications';
|
||||||
import { divider } from './data/constants';
|
import { divider } from './data/constants';
|
||||||
|
@ -51,6 +51,7 @@ import {
|
||||||
import 'uplot/dist/uPlot.min.css';
|
import 'uplot/dist/uPlot.min.css';
|
||||||
import 'react-perfect-scrollbar/dist/css/styles.css';
|
import 'react-perfect-scrollbar/dist/css/styles.css';
|
||||||
import './css/App.less';
|
import './css/App.less';
|
||||||
|
import { TunesResponse } from './@types/pocketbase-types';
|
||||||
|
|
||||||
const Tune = lazy(() => import('./pages/Tune'));
|
const Tune = lazy(() => import('./pages/Tune'));
|
||||||
const Logs = lazy(() => import('./pages/Logs'));
|
const Logs = lazy(() => import('./pages/Logs'));
|
||||||
|
@ -82,7 +83,7 @@ const App = ({ ui, tuneData }: { ui: UIState, tuneData: TuneDataState }) => {
|
||||||
const tuneId = tunePathMatch?.params.tuneId;
|
const tuneId = tunePathMatch?.params.tuneId;
|
||||||
const { fetchINIFile, fetchTuneFile } = useServerStorage();
|
const { fetchINIFile, fetchTuneFile } = useServerStorage();
|
||||||
|
|
||||||
const loadTune = async (data: TunesRecordFull | null) => {
|
const loadTune = async (data: TunesResponse | null) => {
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
store.dispatch({ type: 'config/load', payload: null });
|
store.dispatch({ type: 'config/load', payload: null });
|
||||||
store.dispatch({ type: 'tune/load', payload: null });
|
store.dispatch({ type: 'tune/load', payload: null });
|
||||||
|
@ -120,7 +121,9 @@ const App = ({ ui, tuneData }: { ui: UIState, tuneData: TuneDataState }) => {
|
||||||
store.dispatch({ type: 'tune/load', payload: tune });
|
store.dispatch({ type: 'tune/load', payload: tune });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
iniLoadingError((error as Error));
|
iniLoadingError((error as Error));
|
||||||
navigate(Routes.HUB);
|
navigate(generatePath(Routes.TUNE_ROOT, {
|
||||||
|
tuneId: tuneId!,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -149,7 +152,7 @@ const App = ({ ui, tuneData }: { ui: UIState, tuneData: TuneDataState }) => {
|
||||||
|
|
||||||
getTune(tuneId).then(async (tune) => {
|
getTune(tuneId).then(async (tune) => {
|
||||||
if (!tune) {
|
if (!tune) {
|
||||||
console.warn('Tune not found');
|
tuneNotFound();
|
||||||
navigate(Routes.HUB);
|
navigate(Routes.HUB);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Tooltip,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
StarOutlined,
|
||||||
|
StarFilled,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Colors } from '../utils/colors';
|
||||||
|
import { TuneDataState } from '../types/state';
|
||||||
|
import useDb from '../hooks/useDb';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { Routes } from '../routes';
|
||||||
|
|
||||||
|
const StarButton = ({ tuneData }: { tuneData: TuneDataState }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { currentUserToken } = useAuth();
|
||||||
|
const { toggleStar, isStarredByMe } = useDb();
|
||||||
|
const [currentStars, setCurrentStars] = useState(tuneData.stars);
|
||||||
|
const [isCurrentlyStarred, setIsCurrentlyStarred] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const toggleStarClick = async () => {
|
||||||
|
if (!currentUserToken) {
|
||||||
|
navigate(Routes.LOGIN);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { stars, isStarred } = await toggleStar(currentUserToken, tuneData.id);
|
||||||
|
setCurrentStars(stars);
|
||||||
|
setIsCurrentlyStarred(isStarred);
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
setIsLoading(false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUserToken) {
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
isStarredByMe(currentUserToken, tuneData.id).then((isStarred) => {
|
||||||
|
setIsCurrentlyStarred(isStarred);
|
||||||
|
setIsLoading(false);
|
||||||
|
}).catch((error) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentUserToken, tuneData.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<Tooltip
|
||||||
|
title="You must be signed in to star a tune"
|
||||||
|
placement="bottom"
|
||||||
|
trigger={currentUserToken ? 'none' : 'hover'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={isCurrentlyStarred ? <StarFilled style={{ color: Colors.YELLOW }} /> : <StarOutlined />}
|
||||||
|
onClick={toggleStarClick}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
<Space style={{ marginLeft: 10 }}>
|
||||||
|
<div>{isCurrentlyStarred ? 'Starred' : 'Star'}</div>
|
||||||
|
<Badge
|
||||||
|
count={currentStars}
|
||||||
|
style={{ backgroundColor: Colors.TEXT, marginTop: -4 }}
|
||||||
|
showZero
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StarButton;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import {
|
||||||
Col,
|
Col,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Grid,
|
Grid,
|
||||||
Menu,
|
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Typography,
|
Typography,
|
||||||
Radio,
|
Radio,
|
||||||
|
@ -304,9 +303,7 @@ const TopBar = ({
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{tuneData?.tuneId && <Dropdown
|
{tuneData?.tuneId && <Dropdown
|
||||||
overlay={
|
menu={{ items: downloadItems }}
|
||||||
<Menu triggerSubMenuAction="click" items={downloadItems} />
|
|
||||||
}
|
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
>
|
>
|
||||||
|
@ -315,7 +312,7 @@ const TopBar = ({
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>}
|
</Dropdown>}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={<Menu items={userMenuItems} />}
|
menu={{ items: userMenuItems }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,9 +11,11 @@ import {
|
||||||
formatError,
|
formatError,
|
||||||
} from '../pocketbase';
|
} from '../pocketbase';
|
||||||
import { buildRedirectUrl } from '../utils/url';
|
import { buildRedirectUrl } from '../utils/url';
|
||||||
import { Collections } from '../@types/pocketbase-types';
|
import {
|
||||||
|
Collections,
|
||||||
|
UsersResponse,
|
||||||
|
} from '../@types/pocketbase-types';
|
||||||
import { Routes } from '../routes';
|
import { Routes } from '../routes';
|
||||||
import { UsersRecordFull } from '../types/dbData';
|
|
||||||
|
|
||||||
// TODO: this should be imported from pocketbase but currently is not exported
|
// TODO: this should be imported from pocketbase but currently is not exported
|
||||||
export type AuthProviderInfo = {
|
export type AuthProviderInfo = {
|
||||||
|
@ -39,10 +41,11 @@ export enum OAuthProviders {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AuthValue {
|
interface AuthValue {
|
||||||
currentUser: UsersRecordFull | null,
|
currentUser: UsersResponse | null,
|
||||||
signUp: (email: string, password: string, username: string) => Promise<UsersRecordFull>,
|
currentUserToken: string | null,
|
||||||
login: (email: string, password: string) => Promise<UsersRecordFull>,
|
signUp: (email: string, password: string, username: string) => Promise<UsersResponse>,
|
||||||
refreshUser: () => Promise<UsersRecordFull | null>,
|
login: (email: string, password: string) => Promise<UsersResponse>,
|
||||||
|
refreshUser: () => Promise<UsersResponse | null>,
|
||||||
sendEmailVerification: () => Promise<void>,
|
sendEmailVerification: () => Promise<void>,
|
||||||
confirmEmailVerification: (token: string) => Promise<void>,
|
confirmEmailVerification: (token: string) => Promise<void>,
|
||||||
confirmResetPassword: (token: string, password: string) => Promise<void>,
|
confirmResetPassword: (token: string, password: string) => Promise<void>,
|
||||||
|
@ -61,13 +64,15 @@ const users = client.collection(Collections.Users);
|
||||||
|
|
||||||
const AuthProvider = (props: { children: ReactNode }) => {
|
const AuthProvider = (props: { children: ReactNode }) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
const [currentUser, setCurrentUser] = useState<UsersRecordFull | null>(null);
|
const [currentUser, setCurrentUser] = useState<UsersResponse | null>(null);
|
||||||
|
const [currentUserToken, setCurrentUserToken] = useState<string | null>(null);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
currentUser,
|
currentUser,
|
||||||
|
currentUserToken,
|
||||||
signUp: async (email: string, password: string, username: string) => {
|
signUp: async (email: string, password: string, username: string) => {
|
||||||
try {
|
try {
|
||||||
const user = await users.create({
|
const user = await users.create<UsersResponse>({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
passwordConfirm: password,
|
passwordConfirm: password,
|
||||||
|
@ -83,7 +88,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
|
||||||
},
|
},
|
||||||
login: async (email: string, password: string) => {
|
login: async (email: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
const authResponse = await users.authWithPassword(email, password);
|
const authResponse = await users.authWithPassword<UsersResponse>(email, password);
|
||||||
return Promise.resolve(authResponse.record);
|
return Promise.resolve(authResponse.record);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(new Error(formatError(error)));
|
return Promise.reject(new Error(formatError(error)));
|
||||||
|
@ -91,7 +96,7 @@ const AuthProvider = (props: { children: ReactNode }) => {
|
||||||
},
|
},
|
||||||
refreshUser: async () => {
|
refreshUser: async () => {
|
||||||
try {
|
try {
|
||||||
const authResponse = await users.authRefresh();
|
const authResponse = await users.authRefresh<UsersResponse>();
|
||||||
return Promise.resolve(authResponse.record);
|
return Promise.resolve(authResponse.record);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
client.authStore.clear();
|
client.authStore.clear();
|
||||||
|
@ -159,13 +164,15 @@ const AuthProvider = (props: { children: ReactNode }) => {
|
||||||
return Promise.reject(new Error(formatError(error)));
|
return Promise.reject(new Error(formatError(error)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}), [currentUser]);
|
}), [currentUser, currentUserToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentUser(client.authStore.model as UsersRecordFull | null);
|
setCurrentUser(client.authStore.model as UsersResponse | null);
|
||||||
|
setCurrentUserToken(client.authStore.token);
|
||||||
|
|
||||||
const storeUnsubscribe = client.authStore.onChange((_token, model) => {
|
const storeUnsubscribe = client.authStore.onChange((token, model) => {
|
||||||
setCurrentUser(model as UsersRecordFull | null);
|
setCurrentUser(model as UsersResponse | null);
|
||||||
|
setCurrentUserToken(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -1,23 +1,32 @@
|
||||||
@keyframes wiggle {
|
@keyframes wiggle {
|
||||||
0%, 7% {
|
|
||||||
|
0%,
|
||||||
|
7% {
|
||||||
transform: rotateZ(0);
|
transform: rotateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
15% {
|
15% {
|
||||||
transform: rotateZ(-15deg);
|
transform: rotateZ(-15deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
20% {
|
20% {
|
||||||
transform: rotateZ(10deg);
|
transform: rotateZ(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
25% {
|
25% {
|
||||||
transform: rotateZ(-10deg);
|
transform: rotateZ(-10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
30% {
|
30% {
|
||||||
transform: rotateZ(6deg);
|
transform: rotateZ(6deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
35% {
|
35% {
|
||||||
transform: rotateZ(-4deg);
|
transform: rotateZ(-4deg);
|
||||||
}
|
}
|
||||||
40%, 100% {
|
|
||||||
|
40%,
|
||||||
|
100% {
|
||||||
transform: rotateZ(0);
|
transform: rotateZ(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
// ant design
|
// ant design
|
||||||
.ant-upload-list-picture-card
|
.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-delete,
|
||||||
.ant-upload-list-item-actions
|
.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-eye {
|
||||||
.anticon-delete,
|
|
||||||
.ant-upload-list-picture-card
|
|
||||||
.ant-upload-list-item-actions
|
|
||||||
.anticon-eye {
|
|
||||||
color: @text;
|
color: @text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +13,7 @@
|
||||||
--shadow: @shadow-2;
|
--shadow: @shadow-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
reach-portal > div {
|
reach-portal>div {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,38 @@ import {
|
||||||
client,
|
client,
|
||||||
formatError,
|
formatError,
|
||||||
ClientResponseError,
|
ClientResponseError,
|
||||||
|
API_URL,
|
||||||
} from '../pocketbase';
|
} from '../pocketbase';
|
||||||
import {
|
|
||||||
IniFilesRecordFull,
|
|
||||||
TunesRecordFull,
|
|
||||||
TunesRecordPartial,
|
|
||||||
} from '../types/dbData';
|
|
||||||
import { databaseGenericError } from '../pages/auth/notifications';
|
import { databaseGenericError } from '../pages/auth/notifications';
|
||||||
import {
|
import {
|
||||||
Collections,
|
Collections,
|
||||||
|
IniFilesResponse,
|
||||||
TunesRecord,
|
TunesRecord,
|
||||||
|
TunesResponse,
|
||||||
} from '../@types/pocketbase-types';
|
} from '../@types/pocketbase-types';
|
||||||
|
|
||||||
|
type Partial<T> = {
|
||||||
|
[A in keyof T]?: T[A];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TunesRecordPartial = Partial<TunesRecord>;
|
||||||
|
|
||||||
|
type TunesResponseList = {
|
||||||
|
items: TunesResponse[];
|
||||||
|
totalItems: number;
|
||||||
|
}
|
||||||
|
|
||||||
const tunesCollection = client.collection(Collections.Tunes);
|
const tunesCollection = client.collection(Collections.Tunes);
|
||||||
const iniFilesCollection = client.collection(Collections.IniFiles);
|
|
||||||
|
const customEndpoint = `${API_URL}/api/custom`;
|
||||||
|
|
||||||
|
const headers = (token: string) => ({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
});
|
||||||
|
|
||||||
const useDb = () => {
|
const useDb = () => {
|
||||||
const updateTune = async (id: string, data: TunesRecordPartial) => {
|
const updateTune = async (id: string, data: TunesRecordPartial): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await tunesCollection.update(id, data);
|
await tunesCollection.update(id, data);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -31,11 +46,11 @@ const useDb = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTune = async (data: TunesRecord) => {
|
const createTune = async (data: TunesRecord): Promise<TunesResponse> => {
|
||||||
try {
|
try {
|
||||||
const record = await tunesCollection.create(data);
|
const record = await tunesCollection.create<TunesResponse>(data);
|
||||||
|
|
||||||
return Promise.resolve(record as TunesRecordFull);
|
return Promise.resolve(record);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
databaseGenericError(new Error(formatError(error)));
|
databaseGenericError(new Error(formatError(error)));
|
||||||
|
@ -44,54 +59,41 @@ const useDb = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTune = async (tuneId: string) => {
|
const getTune = async (tuneId: string): Promise<TunesResponse | null> => {
|
||||||
try {
|
const response = await fetch(`${customEndpoint}/tunes/byTuneId/${tuneId}`);
|
||||||
const tune = await tunesCollection.getFirstListItem(
|
|
||||||
`tuneId = "${tuneId}"`,
|
|
||||||
{
|
|
||||||
expand: 'author',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.resolve(tune as TunesRecordFull);
|
if (response.ok) {
|
||||||
} catch (error) {
|
return response.json();
|
||||||
if ((error as ClientResponseError).isAbort) {
|
|
||||||
return Promise.reject(new Error('Cancelled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((error as ClientResponseError).status === 404) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.captureException(error);
|
|
||||||
databaseGenericError(new Error(formatError(error)));
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.captureException(response);
|
||||||
|
databaseGenericError(new Error(response.statusText));
|
||||||
|
|
||||||
|
return Promise.reject(response.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIni = async (signature: string) => {
|
const getIni = async (signature: string): Promise<IniFilesResponse | null> => {
|
||||||
try {
|
const response = await fetch(`${customEndpoint}/iniFiles/bySignature/${signature}`);
|
||||||
const ini = await iniFilesCollection.getFirstListItem(`signature = "${signature}"`);
|
|
||||||
|
|
||||||
return Promise.resolve(ini as IniFilesRecordFull);
|
if (response.ok) {
|
||||||
} catch (error) {
|
return response.json();
|
||||||
if ((error as ClientResponseError).isAbort) {
|
|
||||||
return Promise.reject(new Error('Cancelled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((error as ClientResponseError).status === 404) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
Sentry.captureException(error);
|
|
||||||
databaseGenericError(new Error(formatError(error)));
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.captureException(response);
|
||||||
|
databaseGenericError(new Error(response.statusText));
|
||||||
|
|
||||||
|
return Promise.reject(response.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchTunes = async (search: string, page: number, perPage: number) => {
|
const searchTunes = async (search: string, page: number, perPage: number): Promise<TunesResponseList> => {
|
||||||
const phrases = search.length > 0 ? search.replace(/ +(?= )/g, '').split(' ') : [];
|
const phrases = search.length > 0 ? search.replace(/ +(?= )/g, '').split(' ') : [];
|
||||||
const filter = phrases
|
const filter = phrases
|
||||||
.filter((phrase) => phrase.length > 1)
|
.filter((phrase) => phrase.length > 1)
|
||||||
|
@ -99,14 +101,14 @@ const useDb = () => {
|
||||||
.join(' && ');
|
.join(' && ');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const list = await tunesCollection.getList(page, perPage, {
|
const list = await tunesCollection.getList<TunesResponse>(page, perPage, {
|
||||||
sort: '-updated',
|
sort: '-stars,-updated',
|
||||||
filter,
|
filter,
|
||||||
expand: 'author',
|
expand: 'author',
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
items: list.items as TunesRecordFull[],
|
items: list.items,
|
||||||
totalItems: list.totalItems,
|
totalItems: list.totalItems,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -121,16 +123,16 @@ const useDb = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserTunes = async (userId: string, page: number, perPage: number) => {
|
const getUserTunes = async (userId: string, page: number, perPage: number): Promise<TunesResponseList> => {
|
||||||
try {
|
try {
|
||||||
const list = await tunesCollection.getList(page, perPage, {
|
const list = await tunesCollection.getList<TunesResponse>(page, perPage, {
|
||||||
sort: '-updated',
|
sort: '-updated',
|
||||||
filter: `author = "${userId}"`,
|
filter: `author = "${userId}"`,
|
||||||
expand: 'author',
|
expand: 'author',
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
items: list.items as TunesRecordFull[],
|
items: list.items,
|
||||||
totalItems: list.totalItems,
|
totalItems: list.totalItems,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -145,13 +147,13 @@ const useDb = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const autocomplete = async (attribute: string, search: string) => {
|
const autocomplete = async (attribute: string, search: string): Promise<TunesResponse[]> => {
|
||||||
try {
|
try {
|
||||||
const items = await tunesCollection.getFullList(10, {
|
const items = await tunesCollection.getFullList<TunesResponse>(10, {
|
||||||
filter: `${attribute} ~ "${search}"`,
|
filter: `${attribute} ~ "${search}"`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve(items as TunesRecordFull[]);
|
return Promise.resolve(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as ClientResponseError).isAbort) {
|
if ((error as ClientResponseError).isAbort) {
|
||||||
return Promise.reject(new Error('Cancelled'));
|
return Promise.reject(new Error('Cancelled'));
|
||||||
|
@ -164,14 +166,60 @@ const useDb = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleStar = async (currentUserToken: string, tune: string): Promise<{ stars: number, isStarred: boolean }> => {
|
||||||
|
const response = await fetch(`${customEndpoint}/stargazers/toggleStar`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers(currentUserToken),
|
||||||
|
body: JSON.stringify({ tune }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const { stars, isStarred } = await response.json();
|
||||||
|
|
||||||
|
return Promise.resolve({ stars, isStarred });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return Promise.resolve({ stars: 0, isStarred: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.captureException(response);
|
||||||
|
databaseGenericError(new Error(response.statusText));
|
||||||
|
|
||||||
|
return Promise.reject(response.status);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isStarredByMe = async (currentUserToken: string, tune: string): Promise<boolean> => {
|
||||||
|
const response = await fetch(`${customEndpoint}/stargazers/starredByMe/${tune}`, {
|
||||||
|
headers: headers(currentUserToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const { isStarred } = await response.json();
|
||||||
|
|
||||||
|
return Promise.resolve(isStarred);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sentry.captureException(response);
|
||||||
|
databaseGenericError(new Error(response.statusText));
|
||||||
|
|
||||||
|
return Promise.reject(response.status);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateTune: (tuneId: string, data: TunesRecordPartial): Promise<void> => updateTune(tuneId, data),
|
updateTune,
|
||||||
createTune: (data: TunesRecord): Promise<TunesRecordFull> => createTune(data),
|
createTune,
|
||||||
getTune: (tuneId: string): Promise<TunesRecordFull | null> => getTune(tuneId),
|
getTune,
|
||||||
getIni: (tuneId: string): Promise<IniFilesRecordFull | null> => getIni(tuneId),
|
getIni,
|
||||||
searchTunes: (search: string, page: number, perPage: number): Promise<{ items: TunesRecordFull[]; totalItems: number }> => searchTunes(search, page, perPage),
|
searchTunes,
|
||||||
getUserTunes: (userId: string, page: number, perPage: number): Promise<{ items: TunesRecordFull[]; totalItems: number }> => getUserTunes(userId, page, perPage),
|
getUserTunes,
|
||||||
autocomplete: (attribute: string, search: string): Promise<TunesRecordFull[]> => autocomplete(attribute, search),
|
autocomplete,
|
||||||
|
toggleStar,
|
||||||
|
isStarredByMe,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
Typography,
|
Typography,
|
||||||
Badge,
|
Badge,
|
||||||
|
Grid,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
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 Pako from 'pako';
|
import Pako from 'pako';
|
||||||
|
@ -54,7 +54,6 @@ import {
|
||||||
} from '../components/Tune/SideBar';
|
} from '../components/Tune/SideBar';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
const { Step } = Steps;
|
|
||||||
|
|
||||||
const edgeUnknown = 'Unknown';
|
const edgeUnknown = 'Unknown';
|
||||||
const badgeStyle = { backgroundColor: Colors.TEXT };
|
const badgeStyle = { backgroundColor: Colors.TEXT };
|
||||||
|
@ -76,7 +75,7 @@ const Diagnose = ({
|
||||||
loadedToothLogs: ToothLogsState;
|
loadedToothLogs: ToothLogsState;
|
||||||
tuneData: TuneDataState | null;
|
tuneData: TuneDataState | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { lg } = useBreakpoint();
|
const { lg } = Grid.useBreakpoint();
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [fileSize, setFileSize] = useState<string>();
|
const [fileSize, setFileSize] = useState<string>();
|
||||||
|
@ -277,25 +276,29 @@ const Diagnose = ({
|
||||||
className="logs-progress"
|
className="logs-progress"
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Steps current={step} direction={lg ? 'horizontal' : 'vertical'}>
|
<Steps
|
||||||
<Step
|
current={step}
|
||||||
title="Downloading"
|
direction={lg ? 'horizontal' : 'vertical'}
|
||||||
subTitle={fileSize}
|
items={[
|
||||||
description={
|
{
|
||||||
fetchError ? fetchError!.message : <Space>
|
title: 'Downloading',
|
||||||
<GlobalOutlined />{edgeLocation}
|
subTitle: fileSize,
|
||||||
</Space>
|
description: (
|
||||||
}
|
fetchError ? fetchError!.message : <Space>
|
||||||
/>
|
<GlobalOutlined />{edgeLocation}
|
||||||
<Step
|
</Space>
|
||||||
title="Decoding"
|
),
|
||||||
description="Parsing CSV"
|
},
|
||||||
/>
|
{
|
||||||
<Step
|
title: 'Decoding',
|
||||||
title="Rendering"
|
description: 'Parsing CSV',
|
||||||
description="Putting pixels on your screen"
|
},
|
||||||
/>
|
{
|
||||||
</Steps>
|
title: 'Rendering',
|
||||||
|
description: 'Putting pixels on your screen',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,12 +36,12 @@ import {
|
||||||
isClipboardSupported,
|
isClipboardSupported,
|
||||||
} from '../utils/clipboard';
|
} from '../utils/clipboard';
|
||||||
import { isEscape } from '../utils/keyboard/shortcuts';
|
import { isEscape } from '../utils/keyboard/shortcuts';
|
||||||
import {
|
|
||||||
TunesRecordFull,
|
|
||||||
UsersRecordFull,
|
|
||||||
} from '../types/dbData';
|
|
||||||
import { formatTime } from '../utils/time';
|
import { formatTime } from '../utils/time';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import {
|
||||||
|
TunesResponse,
|
||||||
|
UsersResponse,
|
||||||
|
} from '../@types/pocketbase-types';
|
||||||
|
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
@ -52,7 +52,7 @@ const Hub = () => {
|
||||||
const { xs } = useBreakpoint();
|
const { xs } = useBreakpoint();
|
||||||
const { searchTunes } = useDb();
|
const { searchTunes } = useDb();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [dataSource, setDataSource] = useState<TunesRecordFull[]>([]);
|
const [dataSource, setDataSource] = useState<TunesResponse[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
@ -73,11 +73,10 @@ const Hub = () => {
|
||||||
...tune,
|
...tune,
|
||||||
key: tune.tuneId,
|
key: tune.tuneId,
|
||||||
year: tune.year,
|
year: tune.year,
|
||||||
authorUsername: (tune.expand.author as unknown as UsersRecordFull).username,
|
authorUsername: (tune.expand!.author as unknown as UsersResponse).username,
|
||||||
displacement: `${tune.displacement}l`,
|
displacement: `${tune.displacement}l`,
|
||||||
aspiration: aspirationMapper[tune.aspiration],
|
aspiration: aspirationMapper[tune.aspiration],
|
||||||
published: formatTime(tune.updated),
|
updated: formatTime(tune.updated),
|
||||||
stars: 0,
|
|
||||||
}));
|
}));
|
||||||
setDataSource(mapped as any);
|
setDataSource(mapped as any);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -114,14 +113,14 @@ const Hub = () => {
|
||||||
const columns: ColumnsType<any> = [
|
const columns: ColumnsType<any> = [
|
||||||
{
|
{
|
||||||
title: 'Tunes',
|
title: 'Tunes',
|
||||||
render: (tune: TunesRecordFull) => (
|
render: (tune: TunesResponse) => (
|
||||||
<>
|
<>
|
||||||
<Title level={5}>{tune.vehicleName}</Title>
|
<Title level={5}>{tune.vehicleName}</Title>
|
||||||
<Space direction="vertical">
|
<Space direction="vertical">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
<Link to={generatePath(Routes.USER_ROOT, { userId: tune.author })}>
|
<Link to={generatePath(Routes.USER_ROOT, { userId: tune.author })}>
|
||||||
{tune.authorUsername}
|
{(tune as any).authorUsername}
|
||||||
</Link>, {tune.published}
|
</Link>, {tune.updated}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>{tune.engineMake}, {tune.engineCode}, {tune.displacement}, {tune.cylindersCount} cylinders, {tune.aspiration}</Text>
|
<Text>{tune.engineMake}, {tune.engineCode}, {tune.displacement}, {tune.cylindersCount} cylinders, {tune.aspiration}</Text>
|
||||||
<Text code>{tune.signature}</Text>
|
<Text code>{tune.signature}</Text>
|
||||||
|
@ -171,7 +170,7 @@ const Hub = () => {
|
||||||
dataIndex: 'authorUsername',
|
dataIndex: 'authorUsername',
|
||||||
key: 'authorUsername',
|
key: 'authorUsername',
|
||||||
responsive: ['sm'],
|
responsive: ['sm'],
|
||||||
render: (userName: string, record: TunesRecordFull) => (
|
render: (userName: string, record: TunesResponse) => (
|
||||||
<Link to={generatePath(Routes.USER_ROOT, { userId: record.author })}>
|
<Link to={generatePath(Routes.USER_ROOT, { userId: record.author })}>
|
||||||
{userName}
|
{userName}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -185,8 +184,8 @@ const Hub = () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Published',
|
title: 'Published',
|
||||||
dataIndex: 'published',
|
dataIndex: 'updated',
|
||||||
key: 'published',
|
key: 'updated',
|
||||||
responsive: ['sm'],
|
responsive: ['sm'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -198,7 +197,7 @@ const Hub = () => {
|
||||||
{
|
{
|
||||||
dataIndex: 'tuneId',
|
dataIndex: 'tuneId',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (tuneId: string, record: TunesRecordFull) => {
|
render: (tuneId: string, record: TunesResponse) => {
|
||||||
const isOwner = currentUser?.id === record.author;
|
const isOwner = currentUser?.id === record.author;
|
||||||
const size = isOwner ? 'small' : 'middle';
|
const size = isOwner ? 'small' : 'middle';
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,8 @@ import Loader from '../components/Loader';
|
||||||
import { Routes } from '../routes';
|
import { Routes } from '../routes';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { formatTime } from '../utils/time';
|
import { formatTime } from '../utils/time';
|
||||||
import { UsersRecordFull } from '../types/dbData';
|
import { UsersResponse } from '../@types/pocketbase-types';
|
||||||
|
import StarButton from '../components/StarButton';
|
||||||
|
|
||||||
const { Item } = Form;
|
const { Item } = Form;
|
||||||
const rowProps = { gutter: 10 };
|
const rowProps = { gutter: 10 };
|
||||||
|
@ -66,12 +67,13 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="small-container">
|
<div className="small-container">
|
||||||
|
<StarButton tuneData={tuneData} />
|
||||||
<Divider>Details</Divider>
|
<Divider>Details</Divider>
|
||||||
<Form>
|
<Form>
|
||||||
<Row {...rowProps}>
|
<Row {...rowProps}>
|
||||||
<Col {...colProps}>
|
<Col {...colProps}>
|
||||||
<Item>
|
<Item>
|
||||||
<Input value={(tuneData.expand.author as unknown as UsersRecordFull).username} addonBefore="Author" />
|
<Input value={(tuneData.expand!.author as unknown as UsersResponse).username} addonBefore="Author" />
|
||||||
</Item>
|
</Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...colProps}>
|
<Col {...colProps}>
|
||||||
|
|
|
@ -21,14 +21,14 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
Badge,
|
Badge,
|
||||||
Typography,
|
Typography,
|
||||||
|
Grid,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { CheckboxValueType } from 'antd/lib/checkbox/Group';
|
import { CheckboxValueType } from 'antd/es/checkbox/Group';
|
||||||
import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Result as ParserResult } from 'mlg-converter/dist/types';
|
import { Result as ParserResult } from 'mlg-converter/dist/types';
|
||||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||||
|
@ -70,7 +70,6 @@ import {
|
||||||
} from '../components/Tune/SideBar';
|
} from '../components/Tune/SideBar';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
const { Step } = Steps;
|
|
||||||
const edgeUnknown = 'Unknown';
|
const edgeUnknown = 'Unknown';
|
||||||
const minCanvasHeightInner = 500;
|
const minCanvasHeightInner = 500;
|
||||||
const badgeStyle = { backgroundColor: Colors.TEXT };
|
const badgeStyle = { backgroundColor: Colors.TEXT };
|
||||||
|
@ -93,7 +92,7 @@ const Logs = ({
|
||||||
loadedLogs: LogsState;
|
loadedLogs: LogsState;
|
||||||
tuneData: TuneDataState | null;
|
tuneData: TuneDataState | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { lg } = useBreakpoint();
|
const { lg } = Grid.useBreakpoint();
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [fileSize, setFileSize] = useState<string>();
|
const [fileSize, setFileSize] = useState<string>();
|
||||||
|
@ -384,29 +383,33 @@ const Logs = ({
|
||||||
className="logs-progress"
|
className="logs-progress"
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Steps current={step} direction={lg ? 'horizontal' : 'vertical'}>
|
<Steps
|
||||||
<Step
|
current={step}
|
||||||
title="Downloading"
|
direction={lg ? 'horizontal' : 'vertical'}
|
||||||
subTitle={fileSize}
|
items={[
|
||||||
description={
|
{
|
||||||
fetchError ? fetchError!.message : <Space>
|
title: 'Downloading',
|
||||||
<GlobalOutlined />{edgeLocation}
|
subTitle: fileSize,
|
||||||
</Space>
|
description: (
|
||||||
}
|
fetchError ? fetchError!.message : <Space>
|
||||||
status={fetchError && 'error'}
|
<GlobalOutlined />{edgeLocation}
|
||||||
/>
|
</Space>
|
||||||
<Step
|
),
|
||||||
title="Decoding"
|
status: fetchError && 'error',
|
||||||
description={parseError ? parseError!.message : 'Reading ones and zeros'}
|
},
|
||||||
subTitle={parseElapsed}
|
{
|
||||||
status={parseError && 'error'}
|
title: 'Decoding',
|
||||||
/>
|
description: parseError ? parseError!.message : 'Reading ones and zeros',
|
||||||
<Step
|
subTitle: parseElapsed,
|
||||||
title="Rendering"
|
status: parseError && 'error',
|
||||||
description="Putting pixels on your screen"
|
},
|
||||||
subTitle={samplesCount && `${samplesCount} samples`}
|
{
|
||||||
/>
|
title: 'Rendering',
|
||||||
</Steps>
|
description: 'Putting pixels on your screen',
|
||||||
|
subTitle: samplesCount && `${samplesCount} samples`,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -56,7 +56,7 @@ import { Routes } from '../routes';
|
||||||
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 LogValidator from '../utils/logs/LogValidator';
|
import LogValidator from '../utils/logs/LogValidator';
|
||||||
import useDb from '../hooks/useDb';
|
import useDb, { TunesRecordPartial } from '../hooks/useDb';
|
||||||
import useServerStorage from '../hooks/useServerStorage';
|
import useServerStorage from '../hooks/useServerStorage';
|
||||||
import { buildFullUrl } from '../utils/url';
|
import { buildFullUrl } from '../utils/url';
|
||||||
import Loader from '../components/Loader';
|
import Loader from '../components/Loader';
|
||||||
|
@ -66,11 +66,10 @@ import {
|
||||||
} from '../utils/form';
|
} from '../utils/form';
|
||||||
import { aspirationMapper } from '../utils/tune/mappers';
|
import { aspirationMapper } from '../utils/tune/mappers';
|
||||||
import { copyToClipboard } from '../utils/clipboard';
|
import { copyToClipboard } from '../utils/clipboard';
|
||||||
import { TunesRecord } from '../@types/pocketbase-types';
|
|
||||||
import {
|
import {
|
||||||
TunesRecordFull,
|
TunesRecord,
|
||||||
TunesRecordPartial,
|
TunesResponse,
|
||||||
} from '../types/dbData';
|
} from '../@types/pocketbase-types';
|
||||||
import { removeFilenameSuffix } from '../pocketbase';
|
import { removeFilenameSuffix } from '../pocketbase';
|
||||||
|
|
||||||
const { Item, useForm } = Form;
|
const { Item, useForm } = Form;
|
||||||
|
@ -124,7 +123,7 @@ const UploadPage = () => {
|
||||||
const [isPublished, setIsPublished] = useState(false);
|
const [isPublished, setIsPublished] = useState(false);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const [readme, setReadme] = useState(defaultReadme);
|
const [readme, setReadme] = useState(defaultReadme);
|
||||||
const [existingTune, setExistingTune] = useState<TunesRecordFull>();
|
const [existingTune, setExistingTune] = useState<TunesResponse>();
|
||||||
|
|
||||||
const [defaultTuneFileList, setDefaultTuneFileList] = useState<UploadFile[]>([]);
|
const [defaultTuneFileList, setDefaultTuneFileList] = useState<UploadFile[]>([]);
|
||||||
const [defaultLogFilesList, setDefaultLogFilesList] = useState<UploadFile[]>([]);
|
const [defaultLogFilesList, setDefaultLogFilesList] = useState<UploadFile[]>([]);
|
||||||
|
@ -136,6 +135,8 @@ const UploadPage = () => {
|
||||||
const [logFiles, setLogFiles] = useState<File[]>([]);
|
const [logFiles, setLogFiles] = useState<File[]>([]);
|
||||||
const [toothLogFiles, setToothLogFiles] = useState<File[]>([]);
|
const [toothLogFiles, setToothLogFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
|
const [customIniRequired, setCustomIniRequired] = useState(false);
|
||||||
|
|
||||||
const shareSupported = 'share' in navigator;
|
const shareSupported = 'share' in navigator;
|
||||||
const { currentUser, refreshUser } = useAuth();
|
const { currentUser, refreshUser } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -151,7 +152,7 @@ const UploadPage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = (await autocomplete(attribute, search))
|
const options = (await autocomplete(attribute, search))
|
||||||
.map((record) => record[attribute]);
|
.map((record) => (record as any)[attribute]);
|
||||||
|
|
||||||
// TODO: order by occurrence (more common - higher in the list)
|
// TODO: order by occurrence (more common - higher in the list)
|
||||||
const unique = [...new Set(options)].map((value) => ({ value }));
|
const unique = [...new Set(options)].map((value) => ({ value }));
|
||||||
|
@ -242,7 +243,7 @@ const UploadPage = () => {
|
||||||
fuel,
|
fuel,
|
||||||
ignition,
|
ignition,
|
||||||
year,
|
year,
|
||||||
].filter((field) => field !== null && `${field}`.length > 1)
|
].filter((field) => field !== null && `${field}`.length > 1 && field !== 'null')
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.replace(/[^\w.\-\d ]/g, ''),
|
.replace(/[^\w.\-\d ]/g, ''),
|
||||||
};
|
};
|
||||||
|
@ -262,7 +263,7 @@ const UploadPage = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingTune) {
|
if (existingTune) {
|
||||||
// clear old multi files first
|
// always clear old multi files first since there is no other way to handle this
|
||||||
const tempFormData = new FormData();
|
const tempFormData = new FormData();
|
||||||
tempFormData.append('logFiles', '');
|
tempFormData.append('logFiles', '');
|
||||||
tempFormData.append('toothLogFiles', '');
|
tempFormData.append('toothLogFiles', '');
|
||||||
|
@ -330,9 +331,17 @@ const UploadPage = () => {
|
||||||
try {
|
try {
|
||||||
await fetchINIFile(signature);
|
await fetchINIFile(signature);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setCustomIniRequired(true);
|
||||||
signatureNotSupportedWarning((e as Error).message);
|
signatureNotSupportedWarning((e as Error).message);
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: true,
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomIniRequired(false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: true,
|
result: true,
|
||||||
message: '',
|
message: '',
|
||||||
|
@ -410,6 +419,10 @@ const UploadPage = () => {
|
||||||
validationMessage = (e as Error).message;
|
validationMessage = (e as Error).message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
setCustomIniRequired(false);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: valid,
|
result: valid,
|
||||||
message: validationMessage,
|
message: validationMessage,
|
||||||
|
@ -422,11 +435,11 @@ const UploadPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLogFile = async (file: UploadFile) => {
|
const removeLogFile = async (file: UploadFile) => {
|
||||||
setLogFiles((prev) => prev.filter((f) => f.name !== file.name));
|
setLogFiles((prev) => prev.filter((f) => removeFilenameSuffix(f.name) !== file.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeToothLogFile = async (file: UploadFile) => {
|
const removeToothLogFile = async (file: UploadFile) => {
|
||||||
setToothLogFiles((prev) => prev.filter((f) => f.name !== file.name));
|
setToothLogFiles((prev) => prev.filter((f) => removeFilenameSuffix(f.name) !== file.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCustomIniFile = async (file: UploadFile) => {
|
const removeCustomIniFile = async (file: UploadFile) => {
|
||||||
|
@ -453,7 +466,7 @@ const UploadPage = () => {
|
||||||
setTuneFile(await fetchFile(oldTune.id, oldTune.tuneFile));
|
setTuneFile(await fetchFile(oldTune.id, oldTune.tuneFile));
|
||||||
setDefaultTuneFileList([{
|
setDefaultTuneFileList([{
|
||||||
uid: oldTune.tuneFile,
|
uid: oldTune.tuneFile,
|
||||||
name: oldTune.tuneFile,
|
name: removeFilenameSuffix(oldTune.tuneFile),
|
||||||
status: 'done',
|
status: 'done',
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
@ -462,7 +475,7 @@ const UploadPage = () => {
|
||||||
setCustomIniFile(await fetchFile(oldTune.id, oldTune.customIniFile));
|
setCustomIniFile(await fetchFile(oldTune.id, oldTune.customIniFile));
|
||||||
setDefaultCustomIniFileList([{
|
setDefaultCustomIniFileList([{
|
||||||
uid: oldTune.customIniFile,
|
uid: oldTune.customIniFile,
|
||||||
name: oldTune.customIniFile,
|
name: removeFilenameSuffix(oldTune.customIniFile),
|
||||||
status: 'done',
|
status: 'done',
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
@ -472,7 +485,7 @@ const UploadPage = () => {
|
||||||
tempLogFiles.push(await fetchFile(oldTune.id, fileName));
|
tempLogFiles.push(await fetchFile(oldTune.id, fileName));
|
||||||
setDefaultLogFilesList((prev) => [...prev, {
|
setDefaultLogFilesList((prev) => [...prev, {
|
||||||
uid: fileName,
|
uid: fileName,
|
||||||
name: fileName,
|
name: removeFilenameSuffix(fileName),
|
||||||
status: 'done',
|
status: 'done',
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
@ -483,7 +496,7 @@ const UploadPage = () => {
|
||||||
tempToothLogFiles.push(await fetchFile(oldTune.id, fileName));
|
tempToothLogFiles.push(await fetchFile(oldTune.id, fileName));
|
||||||
setDefaultToothLogFilesList((prev) => [...prev, {
|
setDefaultToothLogFilesList((prev) => [...prev, {
|
||||||
uid: fileName,
|
uid: fileName,
|
||||||
name: fileName,
|
name: removeFilenameSuffix(fileName),
|
||||||
status: 'done',
|
status: 'done',
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
@ -550,6 +563,14 @@ const UploadPage = () => {
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const publishButtonText = () => {
|
||||||
|
if (customIniRequired) {
|
||||||
|
return 'Custom INI file required!';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEditMode ? 'Update' : 'Publish';
|
||||||
|
};
|
||||||
|
|
||||||
const publishButton = (
|
const publishButton = (
|
||||||
<Row style={{ marginTop: 10 }} {...rowProps}>
|
<Row style={{ marginTop: 10 }} {...rowProps}>
|
||||||
<Col {...colProps}>
|
<Col {...colProps}>
|
||||||
|
@ -572,8 +593,9 @@ const UploadPage = () => {
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
icon={isEditMode ? <EditOutlined /> : <CheckOutlined />}
|
icon={isEditMode ? <EditOutlined /> : <CheckOutlined />}
|
||||||
|
disabled={customIniRequired}
|
||||||
>
|
>
|
||||||
{isEditMode ? 'Update' : 'Publish'}
|
{publishButtonText()}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -21,9 +21,9 @@ import { formatTime } from '../utils/time';
|
||||||
import useDb from '../hooks/useDb';
|
import useDb from '../hooks/useDb';
|
||||||
import { aspirationMapper } from '../utils/tune/mappers';
|
import { aspirationMapper } from '../utils/tune/mappers';
|
||||||
import {
|
import {
|
||||||
TunesRecordFull,
|
TunesResponse,
|
||||||
UsersRecordFull,
|
UsersResponse,
|
||||||
} from '../types/dbData';
|
} from '../@types/pocketbase-types';
|
||||||
|
|
||||||
const tunePath = (tuneId: string) => generatePath(Routes.TUNE_TUNE, { tuneId });
|
const tunePath = (tuneId: string) => generatePath(Routes.TUNE_TUNE, { tuneId });
|
||||||
|
|
||||||
|
@ -35,22 +35,22 @@ const Profile = () => {
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [isTunesLoading, setIsTunesLoading] = useState(false);
|
const [isTunesLoading, setIsTunesLoading] = useState(false);
|
||||||
const [tunesDataSource, setTunesDataSource] = useState<TunesRecordFull[]>([]);
|
const [tunesDataSource, setTunesDataSource] = useState<TunesResponse[]>([]);
|
||||||
const [username, setUsername] = useState();
|
const [username, setUsername] = useState<string>();
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setIsTunesLoading(true);
|
setIsTunesLoading(true);
|
||||||
try {
|
try {
|
||||||
const { items, totalItems } = await getUserTunes(route?.params.userId!, page, pageSize);
|
const { items, totalItems } = await getUserTunes(route?.params.userId!, page, pageSize);
|
||||||
setTotal(totalItems);
|
setTotal(totalItems);
|
||||||
setUsername((items[0].expand.author as UsersRecordFull).username);
|
setUsername((items[0]!.expand!.author as UsersResponse).username);
|
||||||
const mapped = items.map((tune) => ({
|
const mapped = items.map((tune) => ({
|
||||||
...tune,
|
...tune,
|
||||||
key: tune.tuneId,
|
key: tune.tuneId,
|
||||||
year: tune.year,
|
year: tune.year,
|
||||||
displacement: `${tune.displacement}l`,
|
displacement: `${tune.displacement}l`,
|
||||||
aspiration: aspirationMapper[tune.aspiration],
|
aspiration: aspirationMapper[tune.aspiration],
|
||||||
published: formatTime(tune.updated),
|
updated: formatTime(tune.updated),
|
||||||
}));
|
}));
|
||||||
setTunesDataSource(mapped as any);
|
setTunesDataSource(mapped as any);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -88,7 +88,7 @@ const Profile = () => {
|
||||||
</>}
|
</>}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text italic>{tune.published}</Typography.Text>
|
<Typography.Text italic>{tune.updated}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
|
@ -39,7 +39,7 @@ import { usernameRules } from '../../utils/form';
|
||||||
import { formatTime } from '../../utils/time';
|
import { formatTime } from '../../utils/time';
|
||||||
import useDb from '../../hooks/useDb';
|
import useDb from '../../hooks/useDb';
|
||||||
import { aspirationMapper } from '../../utils/tune/mappers';
|
import { aspirationMapper } from '../../utils/tune/mappers';
|
||||||
import { TunesRecordFull } from '../../types/dbData';
|
import { TunesResponse } from '../../@types/pocketbase-types';
|
||||||
|
|
||||||
const { Item } = Form;
|
const { Item } = Form;
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ const Profile = () => {
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [isTunesLoading, setIsTunesLoading] = useState(false);
|
const [isTunesLoading, setIsTunesLoading] = useState(false);
|
||||||
const [tunesDataSource, setTunesDataSource] = useState<TunesRecordFull[]>([]);
|
const [tunesDataSource, setTunesDataSource] = useState<TunesResponse[]>([]);
|
||||||
|
|
||||||
const goToEdit = (tuneId: string) => navigate(generatePath(Routes.UPLOAD_WITH_TUNE_ID, {
|
const goToEdit = (tuneId: string) => navigate(generatePath(Routes.UPLOAD_WITH_TUNE_ID, {
|
||||||
tuneId,
|
tuneId,
|
||||||
|
@ -106,7 +106,7 @@ const Profile = () => {
|
||||||
year: tune.year,
|
year: tune.year,
|
||||||
displacement: `${tune.displacement}l`,
|
displacement: `${tune.displacement}l`,
|
||||||
aspiration: aspirationMapper[tune.aspiration],
|
aspiration: aspirationMapper[tune.aspiration],
|
||||||
published: formatTime(tune.updated),
|
updated: formatTime(tune.updated),
|
||||||
}));
|
}));
|
||||||
setTunesDataSource(mapped as any);
|
setTunesDataSource(mapped as any);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -132,7 +132,7 @@ const Profile = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -163,11 +163,11 @@ const Profile = () => {
|
||||||
fields={[
|
fields={[
|
||||||
{
|
{
|
||||||
name: 'username',
|
name: 'username',
|
||||||
value: currentUser!.username,
|
value: currentUser?.username,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
value: currentUser!.email,
|
value: currentUser?.email,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
@ -222,7 +222,7 @@ const Profile = () => {
|
||||||
</>}
|
</>}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text italic>{tune.published}</Typography.Text>
|
<Typography.Text italic>{tune.updated}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
|
@ -42,7 +42,7 @@ const logInFailed = (err: Error) => notification.error({
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const restrictedPage = () => notification.error({
|
const restrictedPage = () => notification.warning({
|
||||||
message: 'Restricted page',
|
message: 'Restricted page',
|
||||||
description: 'You have to be logged in to access this page!',
|
description: 'You have to be logged in to access this page!',
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
|
@ -125,6 +125,11 @@ const iniLoadingError = (err: Error) => notification.error({
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tuneNotFound = () => notification.warning({
|
||||||
|
message: 'Tune not found',
|
||||||
|
...baseOptions,
|
||||||
|
});
|
||||||
|
|
||||||
const tuneParsingError = () => notification.error({
|
const tuneParsingError = () => notification.error({
|
||||||
message: 'Tune file is not valid',
|
message: 'Tune file is not valid',
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
|
@ -169,6 +174,7 @@ export {
|
||||||
databaseGenericError,
|
databaseGenericError,
|
||||||
copiedToClipboard,
|
copiedToClipboard,
|
||||||
iniLoadingError,
|
iniLoadingError,
|
||||||
|
tuneNotFound,
|
||||||
tuneParsingError,
|
tuneParsingError,
|
||||||
signatureNotSupportedWarning,
|
signatureNotSupportedWarning,
|
||||||
downloading,
|
downloading,
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import PocketBase, {
|
import PocketBase, { ClientResponseError } from 'pocketbase';
|
||||||
ClientResponseError,
|
|
||||||
Record,
|
|
||||||
} from 'pocketbase';
|
|
||||||
import { fetchEnv } from './utils/env';
|
import { fetchEnv } from './utils/env';
|
||||||
|
|
||||||
const API_URL = fetchEnv('VITE_POCKETBASE_API_URL');
|
const API_URL = fetchEnv('VITE_POCKETBASE_API_URL');
|
||||||
|
@ -28,5 +25,4 @@ export {
|
||||||
formatError,
|
formatError,
|
||||||
removeFilenameSuffix,
|
removeFilenameSuffix,
|
||||||
ClientResponseError,
|
ClientResponseError,
|
||||||
Record,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Record } from '../pocketbase';
|
|
||||||
import {
|
|
||||||
IniFilesRecord,
|
|
||||||
TunesRecord,
|
|
||||||
UsersRecord,
|
|
||||||
} from '../@types/pocketbase-types';
|
|
||||||
|
|
||||||
type Partial<T> = {
|
|
||||||
[A in keyof T]?: T[A];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TunesRecordPartial = Partial<TunesRecord>;
|
|
||||||
|
|
||||||
export interface TunesRecordFull extends TunesRecord, Record { }
|
|
||||||
|
|
||||||
export interface UsersRecordFull extends UsersRecord, Record { }
|
|
||||||
|
|
||||||
export interface IniFilesRecordFull extends IniFilesRecord, Record { }
|
|
|
@ -3,17 +3,17 @@ import {
|
||||||
Logs,
|
Logs,
|
||||||
TuneWithDetails,
|
TuneWithDetails,
|
||||||
} from '@hyper-tuner/types';
|
} from '@hyper-tuner/types';
|
||||||
|
import { TunesResponse } from '../@types/pocketbase-types';
|
||||||
import {
|
import {
|
||||||
CompositeLogEntry,
|
CompositeLogEntry,
|
||||||
ToothLogEntry,
|
ToothLogEntry,
|
||||||
} from '../utils/logs/TriggerLogsParser';
|
} from '../utils/logs/TriggerLogsParser';
|
||||||
import { TunesRecordFull } from './dbData';
|
|
||||||
|
|
||||||
export interface ConfigState extends Config {}
|
export interface ConfigState extends Config {}
|
||||||
|
|
||||||
export interface TuneState extends TuneWithDetails {}
|
export interface TuneState extends TuneWithDetails {}
|
||||||
|
|
||||||
export interface TuneDataState extends TunesRecordFull {}
|
export interface TuneDataState extends TunesResponse {}
|
||||||
|
|
||||||
export interface LogsState {
|
export interface LogsState {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
|
Loading…
Reference in New Issue