Files
browser-wakatime/src/components/Options.tsx
2024-08-30 00:08:24 +06:00

349 lines
11 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import config, { SuccessOrFailType } from '../config/config';
import apiKeyInvalid from '../utils/apiKey';
import { IS_CHROME } from '../utils/operatingSystem';
import { getSettings, ProjectName, saveSettings, Settings } from '../utils/settings';
import { logUserIn } from '../utils/user';
import CustomProjectNameList from './CustomProjectNameList';
import SitesList from './SitesList';
interface State extends Settings {
alertText: string;
alertType: SuccessOrFailType;
loading: boolean;
}
export default function Options(): JSX.Element {
const [state, setState] = useState<State>({
alertText: config.alert.success.text,
alertType: config.alert.success.type,
allowList: [],
apiKey: '',
apiUrl: config.apiUrl,
customProjectNames: [],
denyList: [],
extensionStatus: 'allGood',
hostname: '',
loading: false,
loggingEnabled: true,
loggingStyle: config.loggingStyle,
loggingType: config.loggingType,
socialMediaSites: config.socialMediaSites,
theme: config.theme,
trackSocialMedia: config.trackSocialMedia,
});
const isApiKeyValid = useMemo(() => apiKeyInvalid(state.apiKey) === '', [state.apiKey]);
const loggingStyleRef = useRef(null);
const restoreSettings = useCallback(async () => {
const settings = await getSettings();
setState((oldState) => ({
...oldState,
...settings,
}));
}, []);
useEffect(() => {
void restoreSettings();
}, [restoreSettings]);
const handleSubmit = async () => {
if (state.loading) return;
setState((oldState) => ({ ...oldState, loading: true }));
if (state.apiUrl.endsWith('/')) {
state.apiUrl = state.apiUrl.slice(0, -1);
}
await saveSettings({
allowList: state.allowList.filter((item) => !!item.trim()),
apiKey: state.apiKey,
apiUrl: state.apiUrl,
customProjectNames: state.customProjectNames.filter(
(item) => !!item.url.trim() && !!item.projectName.trim(),
),
denyList: state.denyList.filter((item) => !!item.trim()),
extensionStatus: state.extensionStatus,
hostname: state.hostname,
loggingEnabled: state.loggingEnabled,
loggingStyle: state.loggingStyle,
loggingType: state.loggingType,
socialMediaSites: state.socialMediaSites.filter((item) => !!item.trim()),
theme: state.theme,
trackSocialMedia: state.trackSocialMedia,
});
setState(state);
await logUserIn(state.apiKey);
if (IS_CHROME) {
window.close();
}
};
const updateDenyListState = useCallback((denyList: string[]) => {
setState((oldState) => ({
...oldState,
denyList,
}));
}, []);
const updateAllowListState = useCallback((allowList: string[]) => {
setState((oldState) => ({
...oldState,
allowList,
}));
}, []);
const updateCustomProjectNamesState = useCallback((customProjectNames: ProjectName[]) => {
setState((oldState) => ({
...oldState,
customProjectNames,
}));
}, []);
const updateLoggingStyle = useCallback((style: string) => {
setState((oldState) => ({
...oldState,
loggingStyle: style === 'allow' ? 'allow' : 'deny',
}));
}, []);
const updateLoggingType = useCallback((type: string) => {
setState((oldState) => ({
...oldState,
loggingType: type === 'url' ? 'url' : 'domain',
}));
}, []);
const updateTheme = useCallback((theme: string) => {
setState((oldState) => ({
...oldState,
theme: theme === 'light' ? 'light' : 'dark',
}));
}, []);
const toggleSocialMedia = useCallback(() => {
setState((oldState) => ({
...oldState,
trackSocialMedia: !oldState.trackSocialMedia,
}));
}, []);
const loggingStyle = useCallback(() => {
// TODO: rewrite SitesList to be structured inputs instead of textarea
if (state.loggingStyle == 'deny') {
return (
<SitesList
handleChange={updateDenyListState}
label="Exclude"
sites={state.denyList}
helpText="Sites that you don't want to show in your reports."
/>
);
}
return (
<SitesList
handleChange={updateAllowListState}
label="Include"
sites={state.allowList}
projectNamePlaceholder="http://google.com&#10;http://myproject.com/MyProject"
helpText="Only track these sites."
/>
);
}, [
state.allowList,
state.denyList,
state.loggingStyle,
updateAllowListState,
updateDenyListState,
]);
return (
<div className="container">
<div className="row">
<div className="col-md-12">
<form className="form-horizontal">
<div className="form-group mb-4">
<label htmlFor="apiKey" className="form-label mb-0">
API Key
</label>
<input
id="apiKey"
autoFocus={true}
type="text"
className={`form-control ${isApiKeyValid ? '' : 'is-invalid'}`}
placeholder="API key"
value={state.apiKey}
onChange={(e) => setState({ ...state, apiKey: e.target.value })}
/>
</div>
<div className="form-group mb-4">
<label htmlFor="loggingStyle" className="form-label">
Logging style
</label>
<select
id="loggingStyle"
ref={loggingStyleRef}
className="form-control"
value={state.loggingStyle}
onChange={(e) => updateLoggingStyle(e.target.value)}
>
<option value="deny">All except excluded sites</option>
<option value="allow">Only allowed sites</option>
</select>
</div>
{loggingStyle()}
<div className="form-group mb-4">
<label htmlFor="loggingType" className="form-label">
Logging type
</label>
<select
id="loggingType"
className="form-control"
value={state.loggingType}
onChange={(e) => updateLoggingType(e.target.value)}
>
<option value="domain">Only the domain</option>
<option value="url">Entire URL</option>
</select>
</div>
<div className="form-group mb-4">
<label htmlFor="selectTheme" className="form-label mb-0">
Theme
</label>
<select
id="selectTheme"
className="form-control"
value={state.theme}
onChange={(e) => updateTheme(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div className="form-group mb-4">
<label htmlFor="selectHost" className="form-label mb-0">
Hostname
</label>
<input
id="selectHost"
type="text"
className="form-control"
value={state.hostname}
onChange={(e) => setState({ ...state, hostname: e.target.value })}
/>
<span className="text-secondary">
Optional name of local machine. By default &apos;Unknown Hostname&apos;.
</span>
</div>
<CustomProjectNameList
sites={state.customProjectNames}
label="Custom Project Names"
handleChange={updateCustomProjectNamesState}
helpText=""
/>
<div className="form-group mb-4">
<label htmlFor="apiUrl" className="form-label mb-0">
API Url
</label>
<input
id="apiUrl"
type="text"
className="form-control"
value={state.apiUrl}
onChange={(e) => setState({ ...state, apiUrl: e.target.value })}
placeholder="https://api.wakatime.com/api/v1"
/>
<span className="help-block">https://api.wakatime.com/api/v1</span>
</div>
<div className="form-group row mb-4">
<div className="col-lg-10 col-lg-offset-2 space-between align-items-center">
<div>
<input
type="checkbox"
className="me-2"
checked={state.trackSocialMedia}
onChange={toggleSocialMedia}
/>
<span onClick={toggleSocialMedia}>Track social media sites</span>
</div>
<button
type="button"
className="btn btn-primary btn-sm"
data-bs-toggle="modal"
data-bs-target="#socialSitesModal"
>
Sites
</button>
<div
className="modal fade"
id="socialSitesModal"
role="dialog"
aria-labelledby="socialSitesModalLabel"
>
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title fs-5" id="socialSitesModalLabel">
Social Media Sites
</h4>
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div className="modal-body">
<SitesList
handleChange={(socialMediaSites) => {
setState((oldState) => ({
...oldState,
socialMediaSites,
}));
}}
label="Social"
sites={state.socialMediaSites}
helpText="Sites that you don't want to show in your reports."
/>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-primary" data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="form-group mb-4">
<div className="d-grid gap-2 col-6 ">
<button
type="button"
className={`btn btn-primary ${state.loading ? 'disabled' : ''}`}
disabled={state.loading}
data-loading-text="Loading..."
onClick={handleSubmit}
>
Save
</button>
</div>
</div>
</form>
</div>
</div>
</div>
);
}