diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6d82d89..d111e33 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -231,6 +231,9 @@ "go_to_timesheet": { "message": "Go to Timesheet" }, + "see_full_timesheet": { + "message": "See full Timesheet" + }, "go_to_subscriton_page": { "message": "Go to subscription page" }, @@ -269,5 +272,14 @@ }, "click_the_button_below_to_login_to_your_timecamp_account": { "message": "Click the button below to login to your TimeCamp account" + }, + "focus_phase":{ + "message":"Focus phase" + }, + "short_break":{ + "message":"Short break" + }, + "long_break":{ + "message":"Long break" } } diff --git a/images/logoTC-with-name-dark.svg b/images/logoTC-with-name-dark.svg new file mode 100644 index 0000000..d1febd1 --- /dev/null +++ b/images/logoTC-with-name-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/package.json b/package.json index c95592b..e98a479 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timecamp-chrome-extension", - "version": "2.85.2", + "version": "2.85.74", "description": "TimeCamp Google Chrome Time Tracking Extension", "main": "index.js", "scripts": { diff --git a/scripts-2.0/ApiService.js b/scripts-2.0/ApiService.js index 5957dfc..6037d7c 100644 --- a/scripts-2.0/ApiService.js +++ b/scripts-2.0/ApiService.js @@ -999,4 +999,21 @@ export default class ApiService { method: "POST" }); } + getTheme(userId) { + return this.authorizeAndCall((token, resolve, reject) => { + this.call({ + url: pathService.getThemeUrl(userId), + method: METHOD_GET, + apiToken: token, + }) + .then((response) => { + let responseData = JSON.parse(response.response); + resolve(responseData); + }) + .catch((response) => { + logger.error(response); + reject(response); + }); + }); + } } diff --git a/scripts-2.0/PathService.js b/scripts-2.0/PathService.js index 69d5078..1cee1d1 100644 --- a/scripts-2.0/PathService.js +++ b/scripts-2.0/PathService.js @@ -172,4 +172,7 @@ export default class PathService { getUsersUrl(){ return this.serverUrl + "third_party/api/users?active_only=true"; } + getThemeUrl(userId) { + return this.serverUrl + `third_party/api/user/${userId}/setting?name=theme`; + } } diff --git a/scripts-2.0/StorageManager.js b/scripts-2.0/StorageManager.js index a486700..b106ff7 100644 --- a/scripts-2.0/StorageManager.js +++ b/scripts-2.0/StorageManager.js @@ -4,12 +4,37 @@ import Logger from './Logger'; const logger = new Logger(); const TRELLO_POWER_UP_AD_VISIBLE = 'trello_power_up_ad_visible'; +const TC_THEME = 'tc_theme'; +const POMODORO_IS_ENABLED = 'pomodoro_is_enabled' +const POMODORO_TYPE = 'pomodoro_type' +const POMODORO_DURATION_VALUES = 'pomodoro_duration_values' +const POMODORO_IS_OPTIONS_OPEN = 'pomodoro_is_options_open' export default class StorageManager { static get TRELLO_POWER_UP_AD_VISIBLE() { return TRELLO_POWER_UP_AD_VISIBLE; } + static get TC_THEME() { + return TC_THEME; + } + + static get POMODORO_IS_ENABLED(){ + return POMODORO_IS_ENABLED; + } + + static get POMODORO_TYPE(){ + return POMODORO_TYPE; + } + + static get POMODORO_DURATION_VALUES(){ + return POMODORO_DURATION_VALUES; + } + + static get POMODORO_IS_OPTIONS_OPEN(){ + return POMODORO_IS_OPTIONS_OPEN; + } + buildKey(params) { return params.join('_'); } diff --git a/scripts-2.0/background.js b/scripts-2.0/background.js index 27cb2df..0865ed4 100644 --- a/scripts-2.0/background.js +++ b/scripts-2.0/background.js @@ -369,6 +369,13 @@ const TcButton = { reject(error); }); break; + case "getTheme": + apiService.getTheme(request.userId).then((response) => { + resolve(response); + }).catch((error) => { + reject(error); + }); + break; } } catch (e) { logger.error(e); diff --git a/scripts-2.0/components/Billable/styles.scss b/scripts-2.0/components/Billable/styles.scss index 35258e8..1f4a91c 100644 --- a/scripts-2.0/components/Billable/styles.scss +++ b/scripts-2.0/components/Billable/styles.scss @@ -14,6 +14,7 @@ cursor: pointer; padding-left: 26px; user-select: none; + color: var(--theme-color); } &__checkbox { @@ -45,7 +46,7 @@ height: 16px; border-radius: 100%; background: #fff; - display: block;; + display: block; } input:checked ~ .checkmark { diff --git a/scripts-2.0/components/Checkbox/Checkbox.tsx b/scripts-2.0/components/Checkbox/Checkbox.tsx index a880cbb..0a2a07e 100644 --- a/scripts-2.0/components/Checkbox/Checkbox.tsx +++ b/scripts-2.0/components/Checkbox/Checkbox.tsx @@ -3,11 +3,12 @@ import './styles.scss'; export interface PropTypes { isChecked: boolean; + onClick?: ()=>void } const Checkbox: React.FC = (props) => { return ( -
+
diff --git a/scripts-2.0/components/ContextMenu/index.tsx b/scripts-2.0/components/ContextMenu/index.tsx index 76ee817..502e02e 100644 --- a/scripts-2.0/components/ContextMenu/index.tsx +++ b/scripts-2.0/components/ContextMenu/index.tsx @@ -24,7 +24,9 @@ import TimeSelectors from "../TimeSelectors"; import DateTime from "../../helpers/DateTime"; import TimeFormatter from "../../TimeFormatter"; import WrongDatesError from "./Error/WrongDatesError"; - +import { TCTheme } from "../../types/theme"; +import { getTheme, retrieveThemeFromStorage } from "../../helpers/theme"; + const pathService = new PathService(); const logger = new Logger(); const dateTime = new DateTime(); @@ -38,7 +40,7 @@ export interface ContextMenuInterface { note: string, billable: boolean, startTimerCallback: Function, - addTimeEntryCallback: Function, + addTimeEntryCallback?: Function, onCloseCallback: Function, billableInputVisibility: boolean|null, externalTaskId: string, @@ -47,6 +49,7 @@ export interface ContextMenuInterface { taskNotFoundInBackendIntegrationInfo: string, embedOnPopup?: boolean|null, trelloPowerUpAdVisible?: boolean|null, + isPomodoroEnabled?: boolean } const ContextMenu: React.FC = (props) => { @@ -65,7 +68,9 @@ const ContextMenu: React.FC = (props) => { const startTimerCallback = props.startTimerCallback; const addTimeEntryCallback = props.addTimeEntryCallback; const onCloseCallback = props.onCloseCallback; + const isPomodoroEnabled = props.isPomodoroEnabled || false const [userId, setUserId] = useState(0); + const [theme, setTheme] = useState("default"); const [isAdmin, setIsAdmin] = useState(false); const [clearTriggerForTimePicker, setClearTriggerForTimePicker] = useState(false); const [externalTaskId, setExternalTaskId] = useState(props.externalTaskId); @@ -91,6 +96,19 @@ const ContextMenu: React.FC = (props) => { const THIRTY_DAYS_IN_MILISEC = 2592000000; + const isTimeEntryInValid = + props.service === "chrome_plugin" && + props.addTimeEntryCallback && + !stopTime; + + const shouldShowAddEntryButton = + props.service !== "chrome_plugin" + ? false + : props.addTimeEntryCallback + ? true + : false; + + useEffect(() => { setNote(props.note); @@ -111,6 +129,10 @@ const ContextMenu: React.FC = (props) => { }; }, [props]); + useEffect(()=>{ + retrieveThemeFromStorage(setTheme) + },[]) + useMemo(() => { if (service === TRELLO) { @@ -139,6 +161,7 @@ const ContextMenu: React.FC = (props) => { setUserId(parseInt(data.user_id)); setIsAdmin(data.permissions.role_administrator); getBackendIntegrationAdData(data.user_id); + getTheme(data.user_id, setTheme); }); if (billableInputVisibility === null) { @@ -204,6 +227,7 @@ const ContextMenu: React.FC = (props) => { }); }, []); + const getBackendIntegrationAdData = (userId: number) => { browser.runtime.sendMessage({ type: 'getUserSetting', @@ -298,7 +322,7 @@ const ContextMenu: React.FC = (props) => { }; const addTimeEntry = (startTime, stopTime) => { - addTimeEntryCallback( + addTimeEntryCallback?.( taskId, note, service, @@ -336,6 +360,7 @@ const ContextMenu: React.FC = (props) => { setClearTriggerForTimePicker(!clearTriggerForTimePicker); setOpen(false); setTaskId(0); + onCloseCallback() }; const renderTagPicker = () => { @@ -382,9 +407,17 @@ const ContextMenu: React.FC = (props) => { .replace('*linkClose*', ''))); }; + const onStopTimeValueChange = (value) => { + if (value !== null) { + setWrongDatesErrorMessage(''); + } + setStopTime(value); + } + //data-elevation is fix for trello card modal (it close when click on context menu) return (
= (props) => { -
+
= (props) => { stopTime={stopTime} clearFormTrigger={clearTriggerForTimePicker} durationFormat={durationFormat} + isPomodoroEnabled={isPomodoroEnabled} onStartTimeValueChange={(value) => { setStartTime(value); const now = dateTime.getNow(); setWrongDatesErrorMessage((now < value && stopTime === null) ? translate('Start time cannot be greater than now') : ''); }} - onStopTimeValueChange={(value) => { - if (value !== null) { - setWrongDatesErrorMessage(''); - } - setStopTime(value); - }} + onStopTimeValueChange={ props.addTimeEntryCallback? onStopTimeValueChange : undefined} /> { billableInputVisibility && - {setBillable(newBillable)}} - /> +
+ {setBillable(newBillable)}} + /> +
}
); diff --git a/scripts-2.0/components/ContextMenu/styles.scss b/scripts-2.0/components/ContextMenu/styles.scss index 03a1434..274974e 100644 --- a/scripts-2.0/components/ContextMenu/styles.scss +++ b/scripts-2.0/components/ContextMenu/styles.scss @@ -2,20 +2,23 @@ .timecamp { .context-menu { + @include theme-variables; + font-family: $defaultFontFamily; font-size: 14px; font-weight: normal; color: $primaryTextColor; width: 440px; - height: 563px; box-sizing: border-box; - border: #aea6ab 1px solid; + border: #aea6ab 1px var(--theme-border-color); display: block; position: fixed; - background-color: $primaryBackgroundColor; + background-color: var(--theme-background-color); line-height: initial; z-index: 99999; overflow: hidden; + + &--hidden { display: none; @@ -23,9 +26,12 @@ .context-menu-overflow-wrapper { overflow: auto; - height: calc(100% - 72px); padding: 20px; box-sizing: border-box; + + .billable-wrapper { + margin-top:20px; + } } &-extended-height { @@ -36,10 +42,6 @@ margin-top: 15px; } - .billable { - margin-top: 15px; - } - &__info-field { padding: 10px 14px 10px 16px; border-radius: 8px; diff --git a/scripts-2.0/components/ContextMenu/types.ts b/scripts-2.0/components/ContextMenu/types.ts new file mode 100644 index 0000000..f48d91c --- /dev/null +++ b/scripts-2.0/components/ContextMenu/types.ts @@ -0,0 +1,4 @@ +export enum ContextMenuType { + TIMER = "TIMER", + ENTRY = "ENTRY", +} diff --git a/scripts-2.0/components/Dropdown/styles.scss b/scripts-2.0/components/Dropdown/styles.scss index 0494bb0..cc5d523 100644 --- a/scripts-2.0/components/Dropdown/styles.scss +++ b/scripts-2.0/components/Dropdown/styles.scss @@ -10,11 +10,12 @@ $disabledFontColor: #8a8a8a; .Dropdown__button { font-family: Poppins; border: 1px solid; - border-color: $borderColor; + border-color: var(--theme-border-color-secondary); border-radius: $defaultBorderRadius; padding: 10px 15px; display: flex; cursor: pointer; + background-color: var(--theme-background-color); &:hover { border-color: $hoverBorderColor; @@ -26,13 +27,12 @@ $disabledFontColor: #8a8a8a; } &--disabled { - background-color: $disabledBackgroundColor; - border-color: $disabledBackgroundColor; + background-color: var(--theme-background-color-secondary); color: $disabledFontColor; pointer-events: none; &:hover { - border-color: $disabledBackgroundColor; + border-color: var(--theme-border-disabled-color); color: $disabledFontColor; } } @@ -44,6 +44,7 @@ $disabledFontColor: #8a8a8a; -o-text-overflow: ellipsis; text-overflow: ellipsis; white-space: nowrap; + color: var(--theme-color); } .Dropdown__icon { @@ -62,7 +63,7 @@ $disabledFontColor: #8a8a8a; position: fixed; top: 0; left: 0; - box-shadow: 0 6px 32px 0 #dcdcdc; + box-shadow: var(--theme-box-shadow); border-radius: $defaultBorderRadius; overflow-y: hidden; z-index: 999999; diff --git a/scripts-2.0/components/DurationSelector/DurationSelector.tsx b/scripts-2.0/components/DurationSelector/DurationSelector.tsx new file mode 100644 index 0000000..d236f11 --- /dev/null +++ b/scripts-2.0/components/DurationSelector/DurationSelector.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; + +import "./styles.scss"; + +const DurationSelector = (props) => { + const inputRef = React.useRef(null); + + return ( +
+ +
+ ); +}; + +export default DurationSelector; diff --git a/scripts-2.0/components/DurationSelector/styles.scss b/scripts-2.0/components/DurationSelector/styles.scss new file mode 100644 index 0000000..88e667f --- /dev/null +++ b/scripts-2.0/components/DurationSelector/styles.scss @@ -0,0 +1,42 @@ +.duration-selector { + small { + margin-left: 3px; + + color: var(--theme-label-color); + } + + &__input-wrapper { + position: relative; + + &::after { + content: "min"; + right: 5px; + top: 50%; + color: #8a8a8a; + position: absolute; + + transform: translateY(-50%); + } + &__input { + display: flex; + justify-content: center; + height: 37px; + + width: 77px; + border-radius: 8px; + border: 1px solid var(--theme-border-color-secondary); + + outline: none; + text-transform: capitalize; + background-color: var(--theme-background-color); + color: var(--theme-color); + margin-top: 3px; + padding-left: 7px; + } + } + + p { + text-transform: capitalize; + color: var(--theme-label-color); + } +} diff --git a/scripts-2.0/components/Footer/index.tsx b/scripts-2.0/components/Footer/index.tsx index 6100973..9ffe720 100644 --- a/scripts-2.0/components/Footer/index.tsx +++ b/scripts-2.0/components/Footer/index.tsx @@ -2,48 +2,62 @@ import * as React from "react"; import translate from "../../Translator"; import Button from "../common/Button"; -import './styles.scss'; -import {IconName} from "../../icons/types"; +import "./styles.scss"; +import { IconName } from "../../icons/types"; import Icon from "../Icon"; export interface FooterInterface { - onClickSave: any, - onClickCancel: any, - idDisabled?: boolean - showAddEntryButton?: boolean + onClickSave: any; + onClickCancel: any; + idDisabled?: boolean; + showAddEntryButton?: boolean; } const Footer: React.FC = (props) => { - const renderStartTimerButton = () => { - return -   - {translate('start_timer')} - - } - - const renderAddEntryButton = () => { - return - {translate('add_time_entry')} - - } - + const renderStartTimerButton = () => { return ( -
-
- - - -
+ + + {" "} + {translate("start_timer")} +    + +
+ +  
+
+ ); + }; + + const renderAddEntryButton = () => { + return ( + + {translate("add_time_entry")} + ); + }; + + return ( +
+
+ + + +
+
+ ); }; export default Footer; diff --git a/scripts-2.0/components/Footer/styles.scss b/scripts-2.0/components/Footer/styles.scss index bde1f92..6817d10 100644 --- a/scripts-2.0/components/Footer/styles.scss +++ b/scripts-2.0/components/Footer/styles.scss @@ -1,18 +1,17 @@ -@import '../../style/common'; +@import "../../style/common"; .timecamp { .footer { - position: absolute; - bottom: 0; - margin: 0; - padding: 20px 0 20px 0; + bottom: 0; width: 100%; - border-top: $primaryBorderColor 1px solid; + border-top: var(--theme-border-display-color) 1px solid; background-color: inherit; &__buttons { - float: right; + width: 100%; margin: 0 21px 0 0; + padding: 1rem; + display: flex; } &__button-save { @@ -21,17 +20,30 @@ border: none; font-family: $defaultFontFamily; font-size: $defaultFontSize; - width: 133px; + width: unset; + margin-left: auto; + margin-right: 33px; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + + & span { + text-transform: uppercase; + } } &__button-cancel { border: solid 1px $primaryBorderColor; font-family: $defaultFontFamily; font-size: $defaultFontSize; + background-color: var(--theme-background-color); + color: var(--theme-color); + margin-right: 10px; } &__button-disabled { - background-color: $buttonDisabledColor; + background-color: var(--theme-background-color-secondary); color: $disabledFontColor; cursor: not-allowed; } diff --git a/scripts-2.0/components/Header/index.tsx b/scripts-2.0/components/Header/index.tsx index 96d79ce..675a995 100644 --- a/scripts-2.0/components/Header/index.tsx +++ b/scripts-2.0/components/Header/index.tsx @@ -1,19 +1,23 @@ import * as React from "react"; -// @ts-ignore -import tcLogo from "../../../images/logoTC-with-name.svg"; -import './styles.scss'; +import "./styles.scss"; +import { getLogoByTheme } from "../../helpers/theme"; export interface HeaderInterface { + theme?: string; } -const Header: React.FC = () => { - // @ts-ignore - return ( -
- TimeCamp logo -
- ); -} +const Header: React.FC = ({ theme = "default" }) => { + + return ( +
+ TimeCamp logo +
+ ); +}; export default Header; diff --git a/scripts-2.0/components/Header/styles.scss b/scripts-2.0/components/Header/styles.scss index 4706403..102711f 100644 --- a/scripts-2.0/components/Header/styles.scss +++ b/scripts-2.0/components/Header/styles.scss @@ -3,6 +3,7 @@ .timecamp { .header { margin: 0 0 30px 0; + display: flex; &__logo-svg { top: 5px; diff --git a/scripts-2.0/components/InfoBox/InfoBox.tsx b/scripts-2.0/components/InfoBox/InfoBox.tsx new file mode 100644 index 0000000..977abe7 --- /dev/null +++ b/scripts-2.0/components/InfoBox/InfoBox.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import "./styles.scss"; +import { InfoBoxStatus } from "./types"; +import InfoboxesContent from "./infoboxesContent"; + +interface InfoBoxProps { + children?: React.ReactNode; + onClose: () => void; + + status: InfoBoxStatus | undefined; +} + +const contentByStatus = { + [InfoBoxStatus.SUCCESS]: , + [InfoBoxStatus.ERROR]: , +}; + +const InfoBox = ({ onClose, status, children = null }: InfoBoxProps) => { + if (!status) return null; + + const selectedContent = contentByStatus[status] || null; + + return ( +
+ {selectedContent} + {children} + + + X + +
+ ); +}; + +export default InfoBox; diff --git a/scripts-2.0/components/InfoBox/index.tsx b/scripts-2.0/components/InfoBox/index.tsx new file mode 100644 index 0000000..303f07e --- /dev/null +++ b/scripts-2.0/components/InfoBox/index.tsx @@ -0,0 +1 @@ +export { default } from "./InfoBox"; diff --git a/scripts-2.0/components/InfoBox/infoboxesContent/InfoBoxErrorContent.tsx b/scripts-2.0/components/InfoBox/infoboxesContent/InfoBoxErrorContent.tsx new file mode 100644 index 0000000..fcc1e41 --- /dev/null +++ b/scripts-2.0/components/InfoBox/infoboxesContent/InfoBoxErrorContent.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; + +const InfoBoxError = () => { + return ( + + There was problem when creating time entry. + + ); +}; + +export default InfoBoxError; diff --git a/scripts-2.0/components/InfoBox/infoboxesContent/InfoBoxSuccessContent.tsx b/scripts-2.0/components/InfoBox/infoboxesContent/InfoBoxSuccessContent.tsx new file mode 100644 index 0000000..2911544 --- /dev/null +++ b/scripts-2.0/components/InfoBox/infoboxesContent/InfoBoxSuccessContent.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import Icon from "../../Icon"; +import { IconName } from "../../../icons/types"; +import PathService from "../../../PathService" + +const serverUrl = new PathService().getBaseUrl(); + +const InfoBoxSuccess = () => { + return ( + + +   Time entry added.  + + Edit in Timesheet. + + + ); +}; + +export default InfoBoxSuccess; diff --git a/scripts-2.0/components/InfoBox/infoboxesContent/index.tsx b/scripts-2.0/components/InfoBox/infoboxesContent/index.tsx new file mode 100644 index 0000000..014fe14 --- /dev/null +++ b/scripts-2.0/components/InfoBox/infoboxesContent/index.tsx @@ -0,0 +1,7 @@ +import InfoBoxError from "./InfoBoxErrorContent"; +import InfoBoxSuccess from "./InfoBoxSuccessContent"; + +export default { + success: InfoBoxSuccess, + error: InfoBoxError, +}; diff --git a/scripts-2.0/components/InfoBox/styles.scss b/scripts-2.0/components/InfoBox/styles.scss new file mode 100644 index 0000000..f3c549e --- /dev/null +++ b/scripts-2.0/components/InfoBox/styles.scss @@ -0,0 +1,37 @@ +.infobox { + display: flex; + gap: 0.5rem; + color: #17b54e; + background-color: #f1f9f3; + border-radius: 8px; + padding: 10px; + align-items: center; + margin-top: 7px; + + & a { + color: #17b54e; + font-weight: 900; + } + + & span { + color: #17b54e; + text-decoration-color: #17b54e; + } + + &__cross { + font-size: 16px; + margin-left: auto; + cursor: pointer; + } + + &[data-status="ERROR"] { + color: #8c0517; + background-color: #e375bb; + & a { + color: #8c0517; + } + & span { + color: #8c0517; + } + } +} diff --git a/scripts-2.0/components/InfoBox/types.ts b/scripts-2.0/components/InfoBox/types.ts new file mode 100644 index 0000000..6777aef --- /dev/null +++ b/scripts-2.0/components/InfoBox/types.ts @@ -0,0 +1,4 @@ +export enum InfoBoxStatus { + SUCCESS = "SUCCESS", + ERROR = "ERROR", +} diff --git a/scripts-2.0/components/Note/styles.scss b/scripts-2.0/components/Note/styles.scss index 7115320..f1f9289 100644 --- a/scripts-2.0/components/Note/styles.scss +++ b/scripts-2.0/components/Note/styles.scss @@ -9,7 +9,9 @@ &__label { margin-top: 0; display: inline-block; + color: var(--theme-label-color); } + &__textarea { width: calc(100% - 30px); height: 81px; @@ -17,9 +19,9 @@ padding: 10px 15px 10px 15px; object-fit: contain; border-radius: 8px; - border: solid 1px $primaryBorderColor; - background-color: $primaryBackgroundColor; - color: $primaryTextColor; + border: solid 1px var(--theme-border-color-secondary); + background-color: var(--theme-background-color); + color: var(--theme-color); outline: none; resize: none; box-sizing: unset; diff --git a/scripts-2.0/components/PopupMenu/Footer/index.tsx b/scripts-2.0/components/PopupMenu/Footer/index.tsx index d78c7c1..43ca6c6 100644 --- a/scripts-2.0/components/PopupMenu/Footer/index.tsx +++ b/scripts-2.0/components/PopupMenu/Footer/index.tsx @@ -1,102 +1,56 @@ import * as React from "react"; -import {useEffect, useRef, useState} from "react"; -import './styles.scss'; -import Button from "../../common/Button"; +import { IconName } from "../../../icons/types"; import Icon from "../../Icon"; -import {IconName} from "../../../icons/types"; -import {User} from "../index"; -import translate from "../../../Translator"; +import Button from "../../common/Button"; +import { User } from "../index"; +import "./styles.scss"; + +import { ContextMenuType } from "../../ContextMenu/types"; + +import { PomodoroContext } from "../Pomodoro/PomodoroContext"; +import PomodoroTypeSelector from "../Pomodoro/PomodoroTypeSelector"; export interface FooterInterface { - isUserWindowOpen: boolean, - logoutCallback: Function, - onPlusButtonClickCallback: Function, - user: User + logoutCallback: Function; + onPlusButtonClickCallback: Function; + user: User; + onSetContextMenuType?: React.Dispatch< + React.SetStateAction + >; } const Footer: React.FC = (props) => { - const node = useRef(null); - const [isUserWindowOpen, setIsUserWindowOpen] = useState(props.isUserWindowOpen); - const [user, setUser] = useState(props.user); - const logoutCallback = props.logoutCallback; - const onPlusButtonClickCallback = props.onPlusButtonClickCallback; - const serverUrl = process.env.SERVER_PROTOCOL + '://' + process.env.SERVER_DOMAIN + '/'; - - useEffect(() => { - setUser(props.user); - }, [props.user]); - - useEffect(() => { - setIsUserWindowOpen(props.isUserWindowOpen); - document.addEventListener("click", onClickOutside); - - return () => { - document.removeEventListener("click", onClickOutside); - }; - }, [props]); - - const onClickOutside = e => { - if (e.target.classList.contains('user-img')) { - return; - } - - const shouldStayOpen = node.current !== null && node.current.contains(e.target); - if (!shouldStayOpen) { - setIsUserWindowOpen(false); - } - }; - - return ( -
- - -
{setIsUserWindowOpen(!isUserWindowOpen)}}> - -
- - + const { onSetContextMenuType } = props; + const { + isPomodoroEnabled: [isPomodoroEnabled] + } = React.useContext(PomodoroContext) + + return ( +
+ {isPomodoroEnabled ? ( +
+ +
+ ) : ( + + )} + + +
+ ); }; export default Footer; diff --git a/scripts-2.0/components/PopupMenu/Footer/styles.scss b/scripts-2.0/components/PopupMenu/Footer/styles.scss index 9969ed1..c676a5b 100644 --- a/scripts-2.0/components/PopupMenu/Footer/styles.scss +++ b/scripts-2.0/components/PopupMenu/Footer/styles.scss @@ -1,4 +1,4 @@ -@import '../../../style/common'; +@import "../../../style/common"; .tc-popup-footer { font-family: $defaultFontFamily; @@ -7,30 +7,48 @@ display: flex; justify-content: space-between; - border-top: solid 1px $popupBorderGray; padding-top: 20px; margin-top: 28px; + color: var(--theme-color); + margin-top: 10px; + &__button-focus { + margin-left:auto; + position:relative; + + display: flex; + align-items: center; + margin-right:15px; + - .user-img { + } + + &__button-cancel { + border: solid 1px $primaryBorderColor; + font-family: $defaultFontFamily; + font-size: $defaultFontSize; + width: 39px; height: 39px; - border-radius: 50%; - - &:hover { - border: solid 1px $popupGreen; - padding: 5px; - margin: -6px; - } + background-color: transparent; + color: #4f4f4f; + color: var(--theme-icon-color); + border-radius: 8px; + cursor: pointer; + margin-left: auto; + margin-right: 15px; } - &__button-add { - width: 39px; + width: 133px; height: 39px; border-radius: 8px; background-color: $popupGreen; border: none; color: white; cursor: pointer; - + display: flex; + justify-content: flex-start; + align-items: center; + padding: 0 0 0 0.5rem; + &:hover { opacity: 0.8; } @@ -38,6 +56,17 @@ svg { font-size: 17px; } + + &__icon { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + } + + & span { + text-transform: uppercase; + } } &__user-img { @@ -68,11 +97,12 @@ } &__user-slide-window { - box-shadow: 0 6px 32px 0 $popupShadowGray; + box-shadow: var(--theme-box-shadow); padding: 10px; height: 114px; width: 265px; - background: $popupBackground; + background-color: var(--theme-background-color); + color: var(--theme-color); position: absolute; top: 25%; right: -1px; @@ -84,7 +114,7 @@ padding: 10px; &:hover { - background-color: $popupGrayThird; + background-color: var(--theme-background-hover); border-radius: 8px; } @@ -116,19 +146,19 @@ padding-top: 10px; &__button-sign-out { - border: none; + border: none; cursor: pointer; font-size: 14px; text-align: left; width: 100%; height: 35px; - background-color: white; - color: $popupGraySecond; + background-color: var(--theme-background-color); + color: var(--theme-color); font-family: $defaultFontFamily; padding: 9px; &:hover { - background-color: $popupGrayThird; + background-color: var(--theme-background-hover); border-radius: 8px; } diff --git a/scripts-2.0/components/PopupMenu/Header/Header.tsx b/scripts-2.0/components/PopupMenu/Header/Header.tsx new file mode 100644 index 0000000..bd9a723 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Header/Header.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +// @ts-ignore +import tcLogo from "../../../../images/logoTC-with-name.svg"; + +// @ts-ignore +import tcLogoDark from "../../../images/logoTC-with-name-dark.svg"; + +// @ts-ignore +import AvatarIconSVG from "../../../icons/user-circle-fas.svg"; +import { User } from ".."; +import "./styles.scss"; + +import UserPopup from "./UserPopup"; +import { getLogoByTheme } from "../../../helpers/theme"; + +export interface HeaderInterface { + user: User; + setIsUserWindowOpen: React.Dispatch>; + isUserWindowOpen: boolean; + logoutCallback: () => void; + theme?: string; +} + +const Header: React.FC = ({ + user, + isUserWindowOpen, + setIsUserWindowOpen, + logoutCallback, + theme='default' +}) => { + const handleClickUserAvatar = (e) => { + if (isUserWindowOpen) return; + + setIsUserWindowOpen(true); + e.stopPropagation(); + }; + + return ( +
+ TimeCamp logo +
+
+ +
+
+ + +
+ ); +}; + +export default Header; diff --git a/scripts-2.0/components/PopupMenu/Header/SVGShutDownIcon.tsx b/scripts-2.0/components/PopupMenu/Header/SVGShutDownIcon.tsx new file mode 100644 index 0000000..40b6bc6 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Header/SVGShutDownIcon.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; + +const SVGShutdownIcon = () => { + return ( + + + + ); +}; + +export default SVGShutdownIcon; diff --git a/scripts-2.0/components/PopupMenu/Header/UserPopup.tsx b/scripts-2.0/components/PopupMenu/Header/UserPopup.tsx new file mode 100644 index 0000000..2992f97 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Header/UserPopup.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import translate from "../../../Translator.js"; +import Button from "../../common/Button/"; +import "./styles.scss"; +import PathService from "../../../PathService.js"; + + +import SVGShutdownIcon from "./SVGShutDownIcon"; + +const serverUrl = new PathService().getBaseUrl(); + +const UserPopup = ({ + setIsUserWindowOpen, + isUserWindowOpen, + user, + logoutCallback, +}) => { + const node = React.useRef(null); + const wrapperClassName = `tc-popup-header__user-slide-window ${isUserWindowOpen ? "tc-popup-header__user-slide-window--visible" : ""}`; + + React.useEffect(() => { + document.addEventListener("click", onClickOutside); + + return () => { + document.removeEventListener("click", onClickOutside); + }; + }, [setIsUserWindowOpen]); + + const onClickOutside = (e) => { + const shouldStayOpen = + node.current !== null && node.current.contains(e.target); + if (!shouldStayOpen) { + setIsUserWindowOpen(false); + } + }; + + return ( + + ); +}; + +export default UserPopup; diff --git a/scripts-2.0/components/PopupMenu/Header/index.tsx b/scripts-2.0/components/PopupMenu/Header/index.tsx new file mode 100644 index 0000000..2764567 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Header/index.tsx @@ -0,0 +1 @@ +export { default } from "./Header"; diff --git a/scripts-2.0/components/PopupMenu/Header/styles.scss b/scripts-2.0/components/PopupMenu/Header/styles.scss new file mode 100644 index 0000000..8178b66 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Header/styles.scss @@ -0,0 +1,158 @@ +@import "../../../style/common"; + +@mixin before-divider-mixin { + position: relative; + &:before { + content: ""; + top: 0; + left: 50%; + height: 1px; + width: 90%; + z-index: 10; + transform: translateX(-50%); + background: var(--theme-popup-section-separator-color); + position: absolute; + } +} + +.tc-popup-header { + display: flex; + color: var(--theme-color); + &__logo-svg { + top: 5px; + } + + &__user-logo { + margin-left: auto; + + &__user-img { + cursor: pointer; + + &__avatar { + height: 38px; + border-radius: 50%; + border: 1px solid transparent; + box-shadow: 0 0 0 2px transparent; + &[data-status="active"] { + box-shadow: 0 0 0 2px #4bb063; + } + } + } + } + &__user-slide-window { + box-shadow: var(--theme-box-shadow); + padding: 10px; + width: 265px; + background: var(--theme-background-color); + color: var(--theme-color); + position: absolute; + top: 25%; + right: -1px; + display: none; + + &--visible { + display: block; + } + + .section-top { + padding: 10px; + + &:hover { + background-color: var(--theme-background-hover); + border-radius: 8px; + } + + a { + display: flex; + text-decoration: none; + + &:visited { + color: inherit; + } + } + + &__image { + display: flex; + justify-content: center; + align-items: center; + + .user-img { + height: 39px; + border-radius: 50%; + + &:hover { + border: solid 1px $popupGreen; + padding: 5px; + margin: -6px; + } + } + } + + &__user { + padding-left: 10px; + + &__display-name { + font-size: 14px; + font-weight: 400; + } + + &__email { + white-space: nowrap; + font-size: 10px; + } + } + } + + .section-middle-timecamp { + display: flex; + color: inherit; + text-decoration: none; + align-items: center; + padding: 15px; + padding-left: 9px; + + @include before-divider-mixin; + + & img { + margin-right: 10px; + margin-left: 5px; + } + + &:hover { + background-color: var(--theme-background-hover); + border-radius: 8px; + } + } + + .section-bottom { + padding: 15px; + border: none; + cursor: pointer; + font-size: 14px; + text-align: left; + width: 100%; + background-color: var(--theme-background-color); + color: var(--theme-color); + font-family: $defaultFontFamily; + display: flex; + align-items: center; + z-index:1; + + &:hover { + background-color: var(--theme-background-hover); + border-radius: 8px; + + & button { + background-color: $popupGrayThird; + } + } + svg { + fill: var(--theme-icon-color); + font-size: 18px; + margin-right: 15px; + } + + @include before-divider-mixin; + } + } +} diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/Pomodoro.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/Pomodoro.tsx new file mode 100644 index 0000000..bf822c4 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/Pomodoro.tsx @@ -0,0 +1,155 @@ +import * as React from "react"; +import translate from "../../../Translator"; + +import DurationSelector from "../../DurationSelector/DurationSelector"; +import SVGGearIcon from "./SVGGearIcon"; +import "./styles.scss"; + +const browser = require("webextension-polyfill"); + +import StorageManager from "../../../StorageManager"; +import PomodoroCheckbox from "./PomodoroCheckbox"; +import { PomodoroContext } from "./PomodoroContext"; + + +const Pomodoro = () => { + const debouncedRef = React.useRef(undefined); + const debouncedValueRef = React.useRef(""); + + const { + isPomodoroEnabled: [ isPomodoroEnabled, setIsPomodoroEnabled], + isPomodoroOptionsOpen: [ isPomodoroOptionsOpen, setIsPomodoroOptionsOpen], + pomodoroDurations: [ durationValues, setDurationValues] + + } = React.useContext(PomodoroContext) + + const onSetIsPomodoroEnabled = () => { + const currentValue = !isPomodoroEnabled; + + browser.runtime.sendMessage({ + type: "saveSettingToStorage", + name: StorageManager.POMODORO_IS_ENABLED, + value: currentValue, + }); + + setIsPomodoroEnabled(currentValue); + }; + + + const toggleSetIsPomodoroOptionsOpen = () => { + const newValue = !isPomodoroOptionsOpen; + setIsPomodoroOptionsOpen(newValue); + + browser.runtime.sendMessage({ + type: "saveSettingToStorage", + name: StorageManager.POMODORO_IS_OPTIONS_OPEN, + value: newValue, + }); + }; + + const debounce = (value) => { + debouncedValueRef.current = value; + if (debouncedRef.current !== undefined) { + clearTimeout(debouncedRef.current); + } + + debouncedRef.current = setTimeout(() => { + browser.runtime.sendMessage({ + type: "saveSettingToStorage", + name: StorageManager.POMODORO_DURATION_VALUES, + value: debouncedValueRef.current, + }); + }, 300); + }; + + return ( +
+
+ + +
+ +
+
+ {isPomodoroOptionsOpen && ( + +
+
+ { + setDurationValues(values=>({ + ...values, + pomodoroFocusPhase: e + })) + + debounce( + JSON.stringify({ + ...durationValues, + pomodoroFocusPhase: e, + + }), + ); + }} + /> +
+
+ { + setDurationValues(values=>({ + ...values, + pomodoroShortBreak:e + })) + + debounce( + JSON.stringify({ + ...durationValues, + pomodoroShortBreak: e, + }), + ); + }} + /> +
+
+ { + + setDurationValues(values=>({ + ...values, + pomodoroLongBreak: e + })) + + debounce( + JSON.stringify({ + ...durationValues, + pomodoroLongBreak: e, + + }), + ); + }} + /> +
+
+
+ )} +
+ ); +}; + +export default Pomodoro; diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/PomodoroCheckbox.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/PomodoroCheckbox.tsx new file mode 100644 index 0000000..565e186 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/PomodoroCheckbox.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import "./styles.scss"; +export interface PomodoroCheckboxProps { + isChecked: boolean; + onClick?: () => void; +} + +const PomodoroCheckbox: React.FC = (props) => { + return ( + + ); +}; + +export default PomodoroCheckbox; diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/index.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/index.tsx new file mode 100644 index 0000000..aa5cd0b --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/index.tsx @@ -0,0 +1 @@ +export { default } from './PomodoroCheckbox' \ No newline at end of file diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/styles.scss b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/styles.scss new file mode 100644 index 0000000..2d1c8f9 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroCheckbox/styles.scss @@ -0,0 +1,17 @@ +.pomodoro-checkbox{ + &__svg{ + border: 1px solid lightgray; + padding: 3px; + cursor: pointer; + background-color: white + } + + &[data-ischecked='true'] svg{ + background-color:#4BB064; + } + + input{ + display: none; + } +} + diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroContext.ts b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroContext.ts new file mode 100644 index 0000000..17949ed --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroContext.ts @@ -0,0 +1,28 @@ +import * as React from "react"; +import { PomodoroDurationValues } from ".."; +import { PomodoroTypes } from "../../../helpers/pomodoro"; + +export const initialPomodoroDurationsState = { + pomodoroFocusPhase: "25:00", + pomodoroShortBreak: "05:00", + pomodoroLongBreak: "15:00", +}; + +export type UseStateContextType = [ + T, + React.Dispatch>, +]; + +export type PomodoroContextType = { + type: UseStateContextType; + pomodoroDurations: UseStateContextType; + isPomodoroOptionsOpen: UseStateContextType; + isPomodoroEnabled: UseStateContextType; +}; + +export const PomodoroContext = React.createContext({ + type: [PomodoroTypes.FOCUS_PHASE, () => {}], + pomodoroDurations: [initialPomodoroDurationsState, () => {}], + isPomodoroOptionsOpen: [false, () => {}], + isPomodoroEnabled: [false, () => {}], +}); diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelector.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelector.tsx new file mode 100644 index 0000000..a5501e8 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelector.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import Dropdown from '../../../Dropdown'; +import './styles.scss'; + + +const browser = require("webextension-polyfill"); + +import StorageManager from '../../../../StorageManager'; +import FocusPhaseDropdownContent from './PomodoroTypeSelectorContent'; +import FocusPhaseDropdownLabel from './PomodoroTypeSelectorLabel'; +import { PomodoroContext } from '../PomodoroContext'; + +const FocusPhase =( )=>{ + const { + type: [selectedFocusPhase, setSelectedFocusPhase] + } = React.useContext(PomodoroContext) + const [ isDropdownOpen, setIsDropdownOpen] = React.useState(false) + const ulRef = React.useRef(null) + + React.useEffect(()=>{ + const handler = (e)=>{ + if(ulRef.current?.contains(e.target)){ + return + } + setIsDropdownOpen(false); + } + + document.addEventListener('click', handler, true) + + return ()=>{ + document.removeEventListener('click', handler, true) + } + + },[]) + + return { + setIsDropdownOpen(!isDropdownOpen) + }} + children={ { + browser.runtime.sendMessage({ + type: 'saveSettingToStorage', + name: StorageManager.POMODORO_TYPE, + value: e}) + + setSelectedFocusPhase(e) + setIsDropdownOpen(false) + }}/>} + text={} + onBackdropClick={()=>{ + setIsDropdownOpen(false) + }} + /> +} + +export default FocusPhase \ No newline at end of file diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelectorContent.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelectorContent.tsx new file mode 100644 index 0000000..ca3c34c --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelectorContent.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import { PomodoroTypes } from "../../../../helpers/pomodoro"; + +type FocusPhaseDropdownContentType = { + onClick: (pomodoroType: PomodoroTypes) => void; +}; + +const FocusPhaseDropdownContent = React.forwardRef< + HTMLUListElement, + FocusPhaseDropdownContentType +>((props, ref) => { + const { onClick } = props; + + return ( +
    + {Object.values(PomodoroTypes).map((focusPhase) => ( +
  • { + onClick(focusPhase); + }} + > + {focusPhase} +
  • + ))} +
+ ); +}); + +export default FocusPhaseDropdownContent; diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelectorLabel.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelectorLabel.tsx new file mode 100644 index 0000000..96650b3 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/PomodoroTypeSelectorLabel.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { PomodoroTypes } from '../../../../helpers/pomodoro'; + +interface FocusPhaseDropdownLabelProps { + value: PomodoroTypes; +} + +const FocusPhaseDropdownLabel =({value}: FocusPhaseDropdownLabelProps)=>{ + return {value} +} + +export default FocusPhaseDropdownLabel \ No newline at end of file diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/index.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/index.tsx new file mode 100644 index 0000000..ff8d0d5 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/index.tsx @@ -0,0 +1 @@ +export {default} from './PomodoroTypeSelector' \ No newline at end of file diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/styles.scss b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/styles.scss new file mode 100644 index 0000000..8619d7a --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/PomodoroTypeSelector/styles.scss @@ -0,0 +1,60 @@ +.focus-phase { + cursor: relative; + + & > div { + display: flex; + &::after { + width: 100%; + height: 1px; + content: " "; + background: var(--theme-popup-section-separator-color); + position: absolute; + bottom: 10px; + left: 0; + } + } + + &__dropdown-open{ + & > div::after{ + background-color: #4BB063; + } + .Dropdown__icon-box{ + color: #4BB063; + } + } +} + +.focus-phase-dropdown-content { + position: absolute; + bottom: 0; + left: 0; + background: var(--theme-background-color); + box-shadow: var(--theme-box-shadow); + padding: 10px; + border-radius: 8px; + color: var(--theme-color); + z-index: 1; + transform: translateY(-25%); + + & > li { + color: var(--theme-color); + min-width: 106px; + cursor: pointer; + list-style: none; + padding: 5px; + border-radius: 8px; + &:hover { + color:#4BB063; + background: var(--theme-background-hover-modern); + } + & span{ + margin-left:5px; + } + } +} + +.focus-phase-dropdown-label { + margin-right: 5px; + cursor: pointer; + position: relative; +} diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/SVGGearIcon.tsx b/scripts-2.0/components/PopupMenu/Pomodoro/SVGGearIcon.tsx new file mode 100644 index 0000000..cddcd80 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/SVGGearIcon.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + + + +const SVGGearIcon =({ + onClick, + isActive +})=>{ + + const color = isActive? "green" : "currentColor" + + return + + + + + + + + + + + +} + +export default SVGGearIcon \ No newline at end of file diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/gear.svg b/scripts-2.0/components/PopupMenu/Pomodoro/gear.svg new file mode 100644 index 0000000..00be358 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/gear.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/index.ts b/scripts-2.0/components/PopupMenu/Pomodoro/index.ts new file mode 100644 index 0000000..7c271d9 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/index.ts @@ -0,0 +1 @@ +export {default} from './Pomodoro' \ No newline at end of file diff --git a/scripts-2.0/components/PopupMenu/Pomodoro/styles.scss b/scripts-2.0/components/PopupMenu/Pomodoro/styles.scss new file mode 100644 index 0000000..aca4803 --- /dev/null +++ b/scripts-2.0/components/PopupMenu/Pomodoro/styles.scss @@ -0,0 +1,46 @@ +.pomodoro { + color: var(--theme-color); + &__checkbox { + display: flex; + align-items: center; + margin: 20px 0 0px 0px; + + input { + cursor: pointer; + } + + &__svg-gear-icon { + cursor: pointer; + transform: translateY(-5%); + } + } + + &__checkbox-paragraph { + display: flex; + cursor: pointer; + + &__label{ + margin-left: 7px; + margin-right: 7px; + user-select: none; + } + } + + &__selectors { + display: flex; + + &__duration-selector { + margin-right: 15px; + } + } + + &__checkbox-breadcrumbs { + margin-left: 20px; + color: var(--theme-label-color); + } + + svg { + fill: red; + font-size: 16px; + } +} diff --git a/scripts-2.0/components/PopupMenu/WorkingTimerSection/index.tsx b/scripts-2.0/components/PopupMenu/WorkingTimerSection/index.tsx index 34e9a2d..fcaa2b7 100644 --- a/scripts-2.0/components/PopupMenu/WorkingTimerSection/index.tsx +++ b/scripts-2.0/components/PopupMenu/WorkingTimerSection/index.tsx @@ -1,14 +1,21 @@ import * as React from "react"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import './styles.scss'; // @ts-ignore -import tcStopButton from "../../../icons/stop-button-small.svg"; -import {Entry} from "../index"; +import PathService from "../../../PathService"; +import TimeFormatter, { DURATION_FORMATS } from "../../../TimeFormatter"; import translate from "../../../Translator"; -import TimeFormatter, {DURATION_FORMATS} from "../../../TimeFormatter"; - +import { PomodoroTypes, subtractTime } from "../../../helpers/pomodoro"; +// @ts-ignore +import tcStopButton from "../../../icons/stop-button-small.svg"; +import { IconName } from '../../../icons/types'; +import Icon from '../../Icon'; +import { Entry } from "../index"; +import { PomodoroContext } from "../Pomodoro/PomodoroContext"; const timeFormatter = new TimeFormatter(); +const serverUrl = new PathService().getBaseUrl(); + export interface WorkingTimerSectionInterface { isTimerWorking: boolean, entry: Entry, @@ -16,21 +23,59 @@ export interface WorkingTimerSectionInterface { } const WorkingTimerSection: React.FC = (props) => { + const { + isPomodoroEnabled:[isPomodoroEnabled], + pomodoroDurations: [pomodoroDurationValues], + type: [pomodoroStartedType, setCurrentSelectedType] + } = React.useContext(PomodoroContext) const [isTimerWorking, setIsTimerWorking] = useState(false); const [entry, setEntry] = useState(props.entry); const [currentTimer, setCurrentTimer] = useState(timeFormatter.formatToDuration(0)); + const stopTimerCallback = props.stopTimerCallback; const durationFormat = DURATION_FORMATS.HHMMSS; + + const pomodoroValuesByType ={ + [PomodoroTypes.FOCUS_PHASE]: pomodoroDurationValues.pomodoroFocusPhase, + [PomodoroTypes.SHORT_BREAK]: pomodoroDurationValues.pomodoroShortBreak, + [PomodoroTypes.LONG_BREAK]: pomodoroDurationValues.pomodoroLongBreak + } + + useEffect(()=>{ + if(!isTimerWorking) { + setCurrentSelectedType(pomodoroStartedType) + } + },[ + pomodoroStartedType + ]) + + useEffect(()=>{ + if(!props.isTimerWorking){ + setCurrentSelectedType(pomodoroStartedType) + } + },[ + props.isTimerWorking + ]) + + useEffect(() => { setIsTimerWorking(props.isTimerWorking); setEntry(props.entry); - const interval = setInterval(() => { + const interval = setInterval(() => { if (isTimerWorking) { - setCurrentTimer( - timeFormatter.format(props.entry.startedAt, durationFormat) - ); + const diff = timeFormatter.format(props.entry.startedAt, durationFormat) + const pomodoroValue = pomodoroValuesByType[pomodoroStartedType] + + const newTimer = !isPomodoroEnabled? diff : subtractTime( diff, pomodoroValue) + + if(newTimer==='00:00:00' || newTimer.includes('-')){ + stopTimer() + return + } + + setCurrentTimer(newTimer); } }, 1000); return () => clearInterval(interval); @@ -38,11 +83,27 @@ const WorkingTimerSection: React.FC = (props) => { const stopTimer = () => { stopTimerCallback(); + setCurrentSelectedType(pomodoroStartedType) } return (
-

{translate('currently_running_timer')}:

+

+ {translate('currently_running_timer')}: + +

+ +

@@ -56,7 +117,10 @@ const WorkingTimerSection: React.FC = (props) => {
{entry.taskName}
{entry.breadcrumb &&
{entry.breadcrumb}
} -
{entry.note}
+ {entry.note &&
+     + {entry.note} +
}
diff --git a/scripts-2.0/components/PopupMenu/WorkingTimerSection/styles.scss b/scripts-2.0/components/PopupMenu/WorkingTimerSection/styles.scss index d435cbc..eb35b77 100644 --- a/scripts-2.0/components/PopupMenu/WorkingTimerSection/styles.scss +++ b/scripts-2.0/components/PopupMenu/WorkingTimerSection/styles.scss @@ -1,18 +1,39 @@ -@import '../../../style/common'; +@import "../../../style/common"; .working-timer { font-family: $defaultFontFamily; font-size: 14px; font-weight: normal; padding-top: 15px; + color: var(--theme-color); &__title { margin: 0 0 10px 7px; font-weight: bold; + color: var(--theme-label-color); + display: flex; + &__timesheet{ + margin-left: auto; + margin-right: 7px; + } + } + + &__button-anchor { + display: flex; + justify-content: center; + align-items: flex-end; + + &__href { + color: #2380e3; + font-size: 12px; + } + & svg { + margin-right: 0.3rem; + } } &__entry { - border: solid 1px $popupBorderGray; + border: dashed 2px var(--theme-border-color-secondary); border-radius: 8px; width: 360px; height: 52px; @@ -21,6 +42,10 @@ align-items: center; padding: 15px; + &--active { + border-style: solid; + } + .entry-empty-info { color: $popupDisabledFontColor; } @@ -80,7 +105,13 @@ .right { display: flex; - flex-direction: column; + flex-direction: row; + align-items: center; + white-space: nowrap; + + & .entry__timer{ + margin-right:10px; + } } } diff --git a/scripts-2.0/components/PopupMenu/index.tsx b/scripts-2.0/components/PopupMenu/index.tsx index deb56d8..276bfa5 100644 --- a/scripts-2.0/components/PopupMenu/index.tsx +++ b/scripts-2.0/components/PopupMenu/index.tsx @@ -1,259 +1,390 @@ -const browser = require('webextension-polyfill'); +const browser = require("webextension-polyfill"); import * as React from "react"; -import Header from "../Header"; -import {useEffect, useState, useMemo} from "react"; -import WorkingTimerSection from "./WorkingTimerSection"; -import Footer from "./Footer"; -import './styles.scss'; -import LoginWindow from "../LoginWindow"; -import ContextMenu from "../ContextMenu"; -import Gravatar from "../../helpers/Gravatar"; +import { useEffect, useMemo, useState } from "react"; import DateTime from "../../helpers/DateTime"; +import Gravatar from "../../helpers/Gravatar"; +import { getTheme, retrieveThemeFromStorage } from "../../helpers/theme"; +import ContextMenu from "../ContextMenu"; +import LoginWindow from "../LoginWindow"; +import Footer from "./Footer"; +import Header from "./Header"; +import "./styles.scss"; +import WorkingTimerSection from "./WorkingTimerSection"; + +import { ContextMenuType } from "../ContextMenu/types"; +import InfoBox from "../InfoBox"; +import { InfoBoxStatus } from "../InfoBox/types"; +import Pomodoro from "./Pomodoro"; + +import StorageManager from "../../StorageManager"; +import { PomodoroTypes, isObjValid, isPomodoroTypeValid } from "../../helpers/pomodoro"; +import { PomodoroContext, PomodoroContextType, initialPomodoroDurationsState } from "./Pomodoro/PomodoroContext"; const gravatar = new Gravatar(); const dateTime = new DateTime(); -export interface PopupMenuInterface { -} +export interface PopupMenuInterface {} export interface User { - displayName: string; - email: string; - gravatarUrl: string; + displayName: string; + email: string; + gravatarUrl: string; +} + +export interface PomodoroDurationValues { + pomodoroFocusPhase: string; + pomodoroShortBreak: string; + pomodoroLongBreak: string; } export interface Entry { - taskName: string; - note: string; - startedAt: string|null; - breadcrumb: string|null; - color: string|null; + taskName: string; + note: string; + startedAt: string | null; + breadcrumb: string | null; + color: string | null; } const PopupMenu: React.FC = (props) => { - const emptyUser = { - displayName: '', - email: '', - gravatarUrl: gravatar.getDefaultImageUrl(), - } + const emptyUser = { + displayName: "", + email: "", + gravatarUrl: gravatar.getDefaultImageUrl(), + }; + + const emptyEntry = { + taskName: "(no task)", + note: "", + startedAt: null, + breadcrumb: null, + color: "#aaa", + }; + + const [theme, setTheme] = useState<"default" | "darkmode">("default"); + const [isPomodoroEnabled, setIsPomodoroEnabled] = useState(false); + const [pomodoroStartedType, setPomodoroStartedType] = + useState(PomodoroTypes.FOCUS_PHASE); + const [isPomodoroOptionsOpen, setIsPomodoroOptionsOpen] = useState(false); + + + const [ pomodoroDurations, setPomodoroDurations ] = React.useState(initialPomodoroDurationsState) + + + + useEffect(() => { + retrieveThemeFromStorage(setTheme); + browser.runtime + .sendMessage({ + type: "getSettingFromStorage", + name: StorageManager.POMODORO_IS_OPTIONS_OPEN, + }) + .then((value) => { + setIsPomodoroOptionsOpen(value); + }); + browser.runtime + .sendMessage({ + type: "getSettingFromStorage", + name: StorageManager.POMODORO_IS_ENABLED, + }) + .then(setIsPomodoroEnabled); + browser.runtime + .sendMessage({ + type: "getSettingFromStorage", + name: StorageManager.POMODORO_TYPE, + }) + .then((value) => { + if (!isPomodoroTypeValid(value)) return; + setPomodoroStartedType(value); + }); + browser.runtime + .sendMessage({ + type: "getSettingFromStorage", + name: StorageManager.POMODORO_DURATION_VALUES, + }) + .then((values) => { + const jParsed = JSON.parse(values) || {}; + + if (!isObjValid(jParsed)) return; + + setPomodoroDurations(jParsed) + }); + browser.runtime + .sendMessage({ + type: "getUserData", + }) + .then((data) => { + getTheme(data.user_id, setTheme); + }); + }, []); - const emptyEntry = { - taskName: '(no task)', - note: '', - startedAt: null, - breadcrumb: null, - color: '#aaa', + const [isUserWindowOpen, setIsUserWindowOpen] = useState(false); + const [contextMenuType, setContextMenuType] = useState< + ContextMenuType | undefined + >(undefined); + const [isUserLogged, setIsUserLogged] = useState(false); + const [user, setUser] = useState(emptyUser); + const [entry, setEntry] = useState(emptyEntry); + const [isTimerWorking, setIsTimerWorking] = useState(false); + const [timeEntryAddedInfoBoxStatus, settimeEntryAddedInfoBoxStatus] = + useState(undefined); + + const setCurrentEntry = (currentEntry) => { + if (currentEntry === null) { + setIsTimerWorking(false); + return; } - const [isUserWindowOpen, setIsUserWindowOpen] = useState(false); - const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); - const [isUserLogged, setIsUserLogged] = useState(false); - const [user, setUser] = useState(emptyUser); - const [entry, setEntry] = useState(emptyEntry); - const [isTimerWorking, setIsTimerWorking] = useState(false); - - const setCurrentEntry = (currentEntry) => { - if (currentEntry === null) { - setIsTimerWorking(false); - return; - } - - let entry = { - taskName: currentEntry.description !== undefined ? currentEntry.description : emptyEntry.taskName, - note: currentEntry.note, - startedAt: currentEntry.start, - color: currentEntry.color, - breadcrumb: currentEntry.breadcrumb, - } as Entry; - - setIsTimerWorking(true); - setEntry(entry); - }; - - const updateUser = () => { - browser.runtime.sendMessage({ - type: "getUserData" - }).then((user) => { - createAndSetUserObject(user); - }); - }; - - const createAndSetUserObject = (user) => { - let userObj = { - displayName: user.display_name, - email: user.email, - gravatarUrl: gravatar.getGravatarUrlForEmail(user.email) - } as User; - - setUser(userObj); - }; - - const logOut = () => { - browser.runtime.sendMessage({ - type: 'logOut' - }).then(() => { - setIsUserWindowOpen(false); - setIsUserLogged(false); - setUser(emptyUser); - }).catch(() => { - }); - }; + let entry = { + taskName: + currentEntry.description !== undefined + ? currentEntry.description + : emptyEntry.taskName, + note: currentEntry.note, + startedAt: currentEntry.start, + color: currentEntry.color, + breadcrumb: currentEntry.breadcrumb, + } as Entry; + + setIsTimerWorking(true); + setEntry(entry); + }; + + const updateUser = () => { + browser.runtime + .sendMessage({ + type: "getUserData", + }) + .then((user) => { + createAndSetUserObject(user); + }); + }; + + const createAndSetUserObject = (user) => { + let userObj = { + displayName: user.display_name, + email: user.email, + gravatarUrl: gravatar.getGravatarUrlForEmail(user.email), + } as User; - const stopTimer = () => { - browser.runtime.sendMessage({ - action: 'track', - type: "timerStop" + setUser(userObj); + }; + + const logOut = () => { + browser.runtime + .sendMessage({ + type: "logOut", + }) + .then(() => { + setIsUserWindowOpen(false); + setContextMenuType(undefined); + setIsUserLogged(false); + setUser(emptyUser); + }) + .catch(() => {}); + }; + + const stopTimer = () => { + browser.runtime.sendMessage({ + action: "track", + type: "timerStop", + }); + browser.runtime + .sendMessage({ + type: "stop", + }) + .then(() => { + setIsTimerWorking(false); + setEntry(emptyEntry); + }) + .catch(() => {}); + }; + + const startTimer = ( + taskId, + note, + service, + externalTaskId, + buttonHash, + startTime, + ) => { + return new Promise((resolve, reject) => { + browser.runtime.sendMessage({ + action: "track", + type: "timerStart", + }); + browser.runtime + .sendMessage({ + type: "startTimer", + startTime: startTime, + externalTaskId: externalTaskId, + taskId: taskId, + description: note, + service: service, + buttonHash: buttonHash, }) - browser.runtime.sendMessage({ - type: 'stop' - }).then(() => { - setIsTimerWorking(false); - setEntry(emptyEntry); - }).catch(() => { - }); - }; - - const startTimer = ( - taskId, - note, - service, - externalTaskId, - buttonHash, - startTime, - ) => { - return new Promise((resolve, reject) => { - browser.runtime.sendMessage({ - action: 'track', - type: "timerStart" - }) - browser.runtime.sendMessage({ - type: 'startTimer', - startTime: startTime, - externalTaskId: externalTaskId, - taskId: taskId, - description: note, - service: service, - buttonHash: buttonHash - }).then((response) => { - setIsTimerWorking(true); - setIsContextMenuOpen(false); - - let entry = { - taskName: response.name === null ? emptyEntry.taskName : response.name, - note: note, - startedAt: startTime, - color: emptyEntry.color - } as Entry; - - setEntry(entry); - resolve(response); - }).catch((e) => { - reject(e); - }); + .then((response) => { + setIsTimerWorking(true); + setContextMenuType(undefined); + + let entry = { + taskName: + response.name === null ? emptyEntry.taskName : response.name, + note: note, + startedAt: startTime, + color: emptyEntry.color, + } as Entry; + + setEntry(entry); + resolve(response); + }) + .catch((e) => { + reject(e); }); - }; - - const addTimeEntry = ( - taskId, - note, - service, - externalTaskId, - buttonHash, - billable, - startTime, - stopTime, - ) => { - return new Promise((resolve, reject) => { - browser.runtime.sendMessage({ - type: 'addTimeEntry', - date: dateTime.formatToYmd(startTime), - startTime: dateTime.formatToHis(startTime), - endTime: dateTime.formatToHis(stopTime), - billable: billable, - externalTaskId: externalTaskId, - taskId: taskId, - description: note, - service: service, - buttonHash: buttonHash - }).then((response) => { - resolve(response); - }).catch((response) => { - reject(response); - }); + }); + }; + + const addTimeEntry = ( + taskId, + note, + service, + externalTaskId, + buttonHash, + billable, + startTime, + stopTime, + ) => { + return new Promise((resolve, reject) => { + browser.runtime + .sendMessage({ + type: "addTimeEntry", + date: dateTime.formatToYmd(startTime), + startTime: dateTime.formatToHis(startTime), + endTime: dateTime.formatToHis(stopTime), + billable: billable, + externalTaskId: externalTaskId, + taskId: taskId, + description: note, + service: service, + buttonHash: buttonHash, + }) + .then((response) => { + settimeEntryAddedInfoBoxStatus(InfoBoxStatus.SUCCESS); + resolve(response); + }) + .catch((response) => { + settimeEntryAddedInfoBoxStatus(InfoBoxStatus.ERROR); + reject(response); }); - }; + }); + }; - useEffect(() => { - }, [props]); + useEffect(() => {}, [props]); - useMemo(() => { - browser.runtime.sendMessage({ - type: "isUserLogged" - }).then((isUserLogged) => { - setIsUserLogged(!!isUserLogged) - }); + useMemo(() => { + browser.runtime + .sendMessage({ + type: "isUserLogged", + }) + .then((isUserLogged) => { + setIsUserLogged(!!isUserLogged); + }); - updateUser(); + updateUser(); - browser.runtime.sendMessage({ - type: "currentEntry" - }).then((data) => { - setCurrentEntry(data.currentEntry); - }); + browser.runtime + .sendMessage({ + type: "currentEntry", + }) + .then((data) => { + setCurrentEntry(data.currentEntry); + }); - browser.runtime.onMessage.addListener((request) => { - switch (request.type) { - case 'currentEntryUpdated': - setCurrentEntry(request.currentEntry); - break; - default: - break; - } - }); - }, []); - - return ( -
-
-
- -