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