diff --git a/client/src/components/forms/AssigneesForm/assignees-form.scss b/client/src/components/forms/AssigneesForm/assignees-form.scss index 524f36da7b6a2d8a03eea3e4f0e9a4c12599852b..66de42f7c95e28e47e5da1524242aada28035847 100644 --- a/client/src/components/forms/AssigneesForm/assignees-form.scss +++ b/client/src/components/forms/AssigneesForm/assignees-form.scss @@ -6,7 +6,7 @@ width: 100%; padding: 20px; border-radius: 15px; - background: s.$gray; + background: s.$background-white; position: relative; margin: 10px 0; diff --git a/client/src/components/forms/AssigneesForm/index.tsx b/client/src/components/forms/AssigneesForm/index.tsx index f68402368db0c2e19f30709e04c9bb1640270eed..195bdb3190e4e19c2cceab433faf494a39ddf280 100644 --- a/client/src/components/forms/AssigneesForm/index.tsx +++ b/client/src/components/forms/AssigneesForm/index.tsx @@ -6,39 +6,45 @@ import { TaskAssignment } from "adapters/task"; import Popup from 'components/ui/Popup'; import Button from 'components/ui/Button'; import { PossibleMember } from "components/forms/TaskForm"; +import TimeInput from "components/ui/TimeInput"; import './assignees-form.scss'; interface Props { assignees: TaskAssignment[]; - setAssignees: Function; members: PossibleMember[] + onNew: (req: TaskAssignment) => any, + onDelete: (req: string) => any } -export default function AssigneesForm({ assignees, setAssignees, members }: Props) { +export default function AssigneesForm({ assignees, members, onNew, onDelete }: Props) { const [possibleMembers, setPossibleMembers] = useState<PossibleMember[]>([]); const [addNew, setAddNew] = useState(false); const [selectedMember, setSelectedMember] = useState(''); - const [selectedTime, setSelectedTime] = useState(''); + const [selectedTime, setSelectedTime] = useState(Number.NaN); useEffect(() => { setPossibleMembers(members.filter(member => !assignees.find(r => r.user === member.id))); - }, [members, assignees, setAssignees]) + }, [members, assignees]) const addAssignee = useCallback((e) => { e.preventDefault(); - if (selectedTime && selectedMember) { - setAssignees((state: any) => [...state, { user: selectedMember, time: selectedTime }]); + if (!Number.isNaN(selectedTime) && selectedMember) { + onNew({ + user: selectedMember, + time: selectedTime * 60, + finished: false, + }); setAddNew(false); setSelectedMember(''); - setSelectedTime(''); + setSelectedTime(Number.NaN); } - }, [setAssignees, selectedMember, selectedTime]) + }, [selectedMember, selectedTime, onNew]) const removeAssignee = useCallback((member: string) => { - setAssignees((state: any) => state.filter((r: any) => r.user !== member)); - }, [setAssignees]) + onDelete(member); + }, [onDelete]) return ( <> @@ -78,10 +84,8 @@ export default function AssigneesForm({ assignees, setAssignees, members }: Prop )) } </select> - <div className="time-field"> - <input type="number" min={1} onChange={(e) => setSelectedTime(e.target.value)} /> - </div> - <Button type="submit" onClick={addAssignee} className="Expanded"> + <TimeInput onChange={value => setSelectedTime(value)} /> + <Button type="submit" onClick={addAssignee} className="expanded"> Add the assignee </Button> </Popup> diff --git a/client/src/components/forms/RequirementsForm/index.tsx b/client/src/components/forms/RequirementsForm/index.tsx index 42739551df5c9e0a64c287835eae8e9e81b794ff..a1f346733e39267d8597e60b36a5068959c2337d 100644 --- a/client/src/components/forms/RequirementsForm/index.tsx +++ b/client/src/components/forms/RequirementsForm/index.tsx @@ -6,20 +6,22 @@ import { TaskRequirement } from 'adapters/task'; import { PossibleRole } from 'components/forms/TaskForm'; import Popup from 'components/ui/Popup'; import Button from 'components/ui/Button'; +import TimeInput from 'components/ui/TimeInput' import './requirements-form.scss'; interface Props { roles: PossibleRole[], requirements: TaskRequirement[], - setRequirements: Function + onNew: (req: TaskRequirement) => any, + onDelete: (req: string) => any } -export default function RequirementsForm({ roles, requirements, setRequirements }: Props) { +export default function RequirementsForm({ roles, requirements, onNew, onDelete }: Props) { const [possibleRoles, setPossibleRoles] = useState<PossibleRole[]>([]); const [addNew, setAddNew] = useState(false); const [selectedRole, setSelectedRole] = useState(''); - const [selectedTime, setSelectedTime] = useState(''); + const [selectedTime, setSelectedTime] = useState(Number.NaN); useEffect(() => { setPossibleRoles(roles.filter(role => !requirements.find(r => r.role === role.id))); @@ -27,17 +29,20 @@ export default function RequirementsForm({ roles, requirements, setRequirements const addRequirement = useCallback((e) => { e.preventDefault(); - if (selectedTime && selectedRole) { - setRequirements((state: any) => [...state, { role: selectedRole, time: selectedTime }]); + if (!Number.isNaN(selectedTime) && selectedRole) { + onNew({ + role: selectedRole, + time: selectedTime * 60, + }); setAddNew(false); setSelectedRole(''); - setSelectedTime(''); + setSelectedTime(Number.NaN); } - }, [selectedRole, selectedTime, setRequirements]) + }, [selectedRole, selectedTime, onNew]) const removeRequirement = useCallback((role: string) => { - setRequirements((state: any) => state.filter((r: any) => r.role !== role)); - }, [setRequirements]) + onDelete(role); + }, [onDelete]) return ( <> @@ -75,14 +80,11 @@ export default function RequirementsForm({ roles, requirements, setRequirements )) } </select> - <div className="time-field"> - <input type="number" min={1} onChange={(e) => setSelectedTime(e.target.value)} /> - </div> + <TimeInput onChange={value => setSelectedTime(value)} /> <Button type="submit" onClick={addRequirement} className="expanded"> Create new requirement </Button> </Popup> - ) } </> diff --git a/client/src/components/forms/RequirementsForm/requirements-form.scss b/client/src/components/forms/RequirementsForm/requirements-form.scss index c17b916e101ac43061956c8224b23c82d0e634fc..e7dcfcba6f703c212d573dd1f04ba63b13390ceb 100644 --- a/client/src/components/forms/RequirementsForm/requirements-form.scss +++ b/client/src/components/forms/RequirementsForm/requirements-form.scss @@ -6,7 +6,7 @@ width: 100%; padding: 20px; border-radius: 15px; - background: s.$gray; + background: s.$background-white; position: relative; margin: 10px 0; diff --git a/client/src/components/forms/RoleForm/role-form.scss b/client/src/components/forms/RoleForm/role-form.scss index 467151f8f71abc33c22927bc995f2ddeab78fbb8..c13a60132d0a138184882ab68db528006eff18e2 100644 --- a/client/src/components/forms/RoleForm/role-form.scss +++ b/client/src/components/forms/RoleForm/role-form.scss @@ -6,7 +6,7 @@ flex-direction: column; .role-item { - background: s.$light; + background: s.$background-light; border-radius: 25px; font-size: 18px; width: 100%; @@ -81,7 +81,7 @@ transform: translate(-50%, -50%); width: 10px; height: 10px; - background: s.$white; + background: s.$background-white; border-radius: 50%; opacity: 0; transition: 300ms ease; diff --git a/client/src/components/forms/TaskForm/index.tsx b/client/src/components/forms/TaskForm/index.tsx index f962428b7464ca5d39e17fce8b6bd433569c334c..890d4ad563f47e875c6b3dc052f75c4ebb9f431f 100644 --- a/client/src/components/forms/TaskForm/index.tsx +++ b/client/src/components/forms/TaskForm/index.tsx @@ -243,7 +243,12 @@ export default function TaskForm({ task, onSubmit, project }: Props) { <div className="col"> { allRoles ? allRoles.length > 0 && ( - <RequirementsForm setRequirements={setRequirements} roles={allRoles} requirements={requirements} /> + <RequirementsForm + roles={allRoles} + requirements={requirements} + onNew={req => setRequirements([ ...requirements, req ])} + onDelete={role => setRequirements(requirements.filter(req => req.role !== role))} + /> ) : <LoadingScreen /> } @@ -251,7 +256,12 @@ export default function TaskForm({ task, onSubmit, project }: Props) { <div className="col"> { allMembers ? allMembers.length > 0 && ( - <AssigneesForm members={allMembers} setAssignees={setAssignees} assignees={assignees} /> + <AssigneesForm + members={allMembers} + assignees={assignees} + onNew={mem => setAssignees([ ...assignees, mem ])} + onDelete={user => setAssignees(assignees.filter(ass => ass.user !== user))} + /> ) : <LoadingScreen /> } diff --git a/client/src/components/forms/TaskForm/task-form.scss b/client/src/components/forms/TaskForm/task-form.scss index e464d10dc3521e41e6d603f61a1cbc1956a1e437..93c6ce11846ed946cf16ec4671524ef7cda014b4 100644 --- a/client/src/components/forms/TaskForm/task-form.scss +++ b/client/src/components/forms/TaskForm/task-form.scss @@ -1,10 +1,13 @@ +@use 'styles/settings' as s; + .task-form { .emoji-mart { width: 100% !important; height: 340px; border-radius: 15px; overflow: hidden; + background: s.$background-white; .emoji-mart-scroll { height: 247.5px; @@ -17,6 +20,15 @@ .emoji-mart-emoji span { cursor: pointer; } + + .emoji-mart-category-label span { + background: rgba(s.$background-white-rgb, 0.95); + color: s.$text; + } + + .emoji-mart-category .emoji-mart-emoji:hover::before { + background: s.$background-input; + } } .current-icon { diff --git a/client/src/components/forms/UserForm/user-form.scss b/client/src/components/forms/UserForm/user-form.scss index f669c7bb7dd4d95e5527ae9f6d9fe3dade36515a..aed014d01c77f50cd206d0a5dc572c7f5e570dc3 100644 --- a/client/src/components/forms/UserForm/user-form.scss +++ b/client/src/components/forms/UserForm/user-form.scss @@ -27,13 +27,9 @@ width: 100%; height: 140px; margin-bottom: 20px; - background: rgba(0, 0, 0, 0.025); + background: s.$background-input; font-size: 18px; - &:hover { - background: rgba(0, 0, 0, 0.04); - } - @include mx.breakpoint(large) { margin-top: 30px; } diff --git a/client/src/components/forms/form.scss b/client/src/components/forms/form.scss index f4de77af20dbee5197224b711941536430178b73..d00f83cd074960d7533e02d0076bd38842a94c82 100644 --- a/client/src/components/forms/form.scss +++ b/client/src/components/forms/form.scss @@ -49,19 +49,9 @@ form { transform: translateY(-50%); } - .time-field { - margin: 20px 0; - display: flex; - align-items: baseline; - - &:after { - content: 'min'; - margin-left: 10px; - } - } - select, - input { + input, + textarea { width: 100%; font-size: 16px; border: none; @@ -71,10 +61,10 @@ form { position: relative; display: block; border-radius: 15px; - color: s.$body-color; + color: s.$text; font-weight: s.$weight-regular; font-family: s.$body-font; - background: rgba(0, 0, 0, 0.025); + background: s.$background-input; } h2 { diff --git a/client/src/components/graphs/BarChart/bar-chart.scss b/client/src/components/graphs/BarChart/bar-chart.scss index 5fdba90f68a29ab33543677e882d3c39741481fc..864912dfdcb6e270c3f27ebfadf7626dc252c365 100644 --- a/client/src/components/graphs/BarChart/bar-chart.scss +++ b/client/src/components/graphs/BarChart/bar-chart.scss @@ -5,7 +5,7 @@ height: 180px; width: 100%; padding: 50px; - background: s.$white; + background: s.$background-white; border-radius: 10px; .error-screen { @@ -30,7 +30,7 @@ bottom: 0; left: 0; height: 24px; - background: s.$light; + background: s.$background-light; width: 100%; z-index: 0; border-radius: 5px; @@ -47,7 +47,7 @@ transform: translate(-50%, -75%); padding: 10px; background: s.$white; - color: s.$body-color; + color: s.$black; border-radius: 5px; display: block; visibility: hidden; diff --git a/client/src/components/graphs/CircularProgress/circular-progress.scss b/client/src/components/graphs/CircularProgress/circular-progress.scss index 0e91af668921f68fd049c9c0047826998ebe5f47..7bcc31ee3c76aa9c9f156e58f960cd2922c47f12 100644 --- a/client/src/components/graphs/CircularProgress/circular-progress.scss +++ b/client/src/components/graphs/CircularProgress/circular-progress.scss @@ -44,7 +44,7 @@ circle { fill: none; - stroke: #F3F3F3; + stroke: s.$background; stroke-width: 8; } } diff --git a/client/src/components/graphs/LinearProgress/linear-progress.scss b/client/src/components/graphs/LinearProgress/linear-progress.scss index 5106d1d4bb89dac91c052edb15b85500750d6904..dbe1ad2a8705fecc260e480392eabc43518aa749 100644 --- a/client/src/components/graphs/LinearProgress/linear-progress.scss +++ b/client/src/components/graphs/LinearProgress/linear-progress.scss @@ -3,7 +3,7 @@ .linear-progress { width: 120px; - background: s.$light; + background: s.$background; position: relative; height: 10px; border-radius: 5px; diff --git a/client/src/components/helpers/Filter/filter.scss b/client/src/components/helpers/Filter/filter.scss index f14e23afeecbde1168bcffe8e3acf2f9327e8817..c111333afeb2cbc710645c3b151e082de841bc45 100644 --- a/client/src/components/helpers/Filter/filter.scss +++ b/client/src/components/helpers/Filter/filter.scss @@ -23,6 +23,8 @@ outline: none; border-radius: 15px; box-shadow: 0 0 15px rgba(s.$black, 0.05); + background: s.$background-white; + color: s.$text; } label { position: absolute; diff --git a/client/src/components/layout/CommentList/comment-list.scss b/client/src/components/layout/CommentList/comment-list.scss index c71021d445640f151e3dbb6fab2f3e9853640dcb..9f464bc1d3fbb1a9fef82b5f75d5389884332812 100644 --- a/client/src/components/layout/CommentList/comment-list.scss +++ b/client/src/components/layout/CommentList/comment-list.scss @@ -1,4 +1,5 @@ -@use 'styles/settings.scss'as s; + +@use 'styles/settings.scss' as s; .comment-list { .comment-container { @@ -16,6 +17,7 @@ position: relative; label { + z-index: 10; position: absolute; left: 30px; top: 0; @@ -40,4 +42,4 @@ transform: translateY(50%); } } -} \ No newline at end of file +} diff --git a/client/src/components/layout/Page/page.scss b/client/src/components/layout/Page/page.scss index 309716e62f4682499e5fb5163558aac8aa2ba8d0..156969843d416c0e17e7e0cc3f665bf91404d9df 100644 --- a/client/src/components/layout/Page/page.scss +++ b/client/src/components/layout/Page/page.scss @@ -1,4 +1,5 @@ +@use 'styles/settings' as s; @use 'styles/mixins' as mx; body { @@ -12,7 +13,7 @@ body { max-width: 1540px; min-height: 100vh; margin: 0 auto; - background: rgba(255, 255, 255, 0.5); + background: rgba(s.$background-white-rgb, 0.5); padding-bottom: 80px; @include mx.breakpoint(xlarge) { diff --git a/client/src/components/layout/ProjectGrid/project-grid.scss b/client/src/components/layout/ProjectGrid/project-grid.scss index d18551f7d53487baeb9b556ff9aed4b89a23412a..b361894eed307def314423911a4e807c1e182450 100644 --- a/client/src/components/layout/ProjectGrid/project-grid.scss +++ b/client/src/components/layout/ProjectGrid/project-grid.scss @@ -21,7 +21,7 @@ cursor: pointer; a { - color: s.$body-color; + color: s.$text; } } diff --git a/client/src/components/layout/ProjectsSlider/projects-slider.scss b/client/src/components/layout/ProjectsSlider/projects-slider.scss index c436c070e3a220406f0273825d5c76023d3adc91..2be092d050d22b2e63c5535c6b8d1dab71bbedd7 100644 --- a/client/src/components/layout/ProjectsSlider/projects-slider.scss +++ b/client/src/components/layout/ProjectsSlider/projects-slider.scss @@ -16,7 +16,7 @@ cursor: pointer; position: absolute; opacity: 0.75; - background-color: s.$light; + background-color: s.$background-light; width: 50px; height: 100px; border-radius: 25px; @@ -55,5 +55,18 @@ width: calc(33.3% - 24px); } } + + @include mx.breakpoint(large) { + padding: 30px 90px; + margin: -30px -90px; + + .prev-button { + left: 70px; + } + + .next-button { + right: 70px; + } + } } diff --git a/client/src/components/layout/TaskList/task-list.scss b/client/src/components/layout/TaskList/task-list.scss index 5dc75e2f144008cb260dc8f6fbd23405845cc523..f47e77d06b8dbb4385c01d766694df6248ca98c6 100644 --- a/client/src/components/layout/TaskList/task-list.scss +++ b/client/src/components/layout/TaskList/task-list.scss @@ -12,9 +12,9 @@ display: flex; justify-content: center; align-items: center; - background: s.$white; + background: s.$background-white; font-weight: s.$weight-semi-bold; - color: s.$body-color; + color: s.$text; font-size: 36px; border-radius: 15px; cursor: pointer; diff --git a/client/src/components/layout/UserList/user-list.scss b/client/src/components/layout/UserList/user-list.scss index d3218d717f0cf6b8f5755251660c2200c1b92323..b0e7413a4913020b1433cbfcd2bcd49afa1f52b5 100644 --- a/client/src/components/layout/UserList/user-list.scss +++ b/client/src/components/layout/UserList/user-list.scss @@ -8,7 +8,7 @@ display: flex; justify-content: center; align-items: center; - background: s.$white; + background: s.$background-white; font-weight: s.$weight-semi-bold; font-size: 36px; border-radius: 15px; diff --git a/client/src/components/navigation/Dropdown/dropdown.scss b/client/src/components/navigation/Dropdown/dropdown.scss index acbf801a843cb1b801191fb069c204aab5d6f4ce..cee8489ab724f61f6b7c49b490ec0cbf2a2c372e 100644 --- a/client/src/components/navigation/Dropdown/dropdown.scss +++ b/client/src/components/navigation/Dropdown/dropdown.scss @@ -37,7 +37,7 @@ bottom: 0; left: 0; transform: translateY(75%); - background: s.$white; + background: s.$background-white; z-index: 2000; border-radius: 5px; white-space: nowrap; @@ -53,7 +53,7 @@ .dropdown-item { padding: 10px 20px; display: block; - color: s.$body-color; + color: s.$text; border-radius: 5px; min-width: 100px; diff --git a/client/src/components/navigation/Header/header.scss b/client/src/components/navigation/Header/header.scss index 816813fc841ec3549222f30324ee0555d6a4b224..28cc816d10ac215eca8ee1bb8dde970b64ec0269 100644 --- a/client/src/components/navigation/Header/header.scss +++ b/client/src/components/navigation/Header/header.scss @@ -20,7 +20,7 @@ right: 0; height: 4px; width: 75%; - background: s.$body-color; + background: s.$text; border-radius: 5px; } @@ -40,7 +40,7 @@ height: 4px; top: 0; right: 0; - background: s.$body-color; + background: s.$text; border-radius: 5px; } diff --git a/client/src/components/navigation/Navigation/navigation.scss b/client/src/components/navigation/Navigation/navigation.scss index c17bd21a3533de0ac971b2d91dc3eb7a734a44a1..ca95ae1cc0efac59b58391ca54b7ef03b073cb1f 100644 --- a/client/src/components/navigation/Navigation/navigation.scss +++ b/client/src/components/navigation/Navigation/navigation.scss @@ -12,7 +12,7 @@ align-items: center; justify-content: center; z-index: 200; - background: s.$white; + background: s.$background-white; box-shadow: 0 0 25px rgba(s.$black, 0.1); @include mx.breakpoint(medium) { @@ -20,7 +20,7 @@ } .nav-link { - color: s.$body-color; + color: s.$text; position: relative; padding: 10px 30px; display: flex; diff --git a/client/src/components/navigation/Sidebar/index.tsx b/client/src/components/navigation/Sidebar/index.tsx index b5a2643c90dc5662d5a8d1cc234d13c67055c95b..68aaea56796aab774d3eabb396d1b844ed4bd9cc 100644 --- a/client/src/components/navigation/Sidebar/index.tsx +++ b/client/src/components/navigation/Sidebar/index.tsx @@ -2,9 +2,10 @@ import { NavLink, useHistory } from 'react-router-dom'; import { useEffect, useState } from 'react'; +import { subtractTime } from 'timely'; +import { getTheme, toggleTheme } from 'index'; import { clearToken, isLoggedIn } from 'adapters/auth'; import { getCurrentUser, getUserActivity, User } from 'adapters/user'; -import { subtractTime } from 'timely'; import Navigation from 'components/navigation/Navigation'; import Avatar from 'components/ui/Avatar'; @@ -21,6 +22,7 @@ interface Props { export default function Sidebar({ mobileShown, setMobileShown }: Props) { const [user, setUser] = useState<User>(); const [activity, setActivity] = useState<ChartItem[]>(); + const [theme, setTheme] = useState(getTheme()); useEffect(() => { if (isLoggedIn()) { @@ -38,6 +40,11 @@ export default function Sidebar({ mobileShown, setMobileShown }: Props) { history.push('/login'); } + const changeTheme = () => { + toggleTheme(); + setTheme(getTheme()); + } + return ( <aside className={'site-aside' + (mobileShown ? ' shown' : '')}> <div className="top"> @@ -60,6 +67,14 @@ export default function Sidebar({ mobileShown, setMobileShown }: Props) { </span> Settings </NavLink> + </nav> + <nav className="secondary-nav"> + <button className="nav-link" onClick={changeTheme}> + <span className="icon material-icons-outlined"> + {theme + '_mode'} + </span> + Change theme + </button> <button className="nav-link" onClick={logout}> <span className="icon material-icons-outlined"> logout diff --git a/client/src/components/navigation/Sidebar/sidebar.scss b/client/src/components/navigation/Sidebar/sidebar.scss index b2916ed8abbff4375321521f4b8cb31c5065e856..c454a044555d7cebf63a872d6d9bb545038ed9f3 100644 --- a/client/src/components/navigation/Sidebar/sidebar.scss +++ b/client/src/components/navigation/Sidebar/sidebar.scss @@ -10,7 +10,7 @@ bottom: 0; max-width: 340px; width: 100%; - background: s.$dark; + background: s.$background-dark; z-index: 2000; border-top-right-radius: 25px; border-bottom-right-radius: 25px; @@ -99,7 +99,7 @@ left: 0; height: 2px; width: 40px; - background: rgba(s.$white, 0.1); + background: rgba(s.$background-white-rgb, 0.05); } @@ -121,6 +121,7 @@ .secondary-nav { display: block; + margin-bottom: 24px; .nav-link { font-size: fn.toRem(18); @@ -143,7 +144,8 @@ .bar-chart { &:before, &:after { - opacity: 0.15; + opacity: 0.1; + background: s.$white; } } diff --git a/client/src/components/navigation/Tabs/tabs.scss b/client/src/components/navigation/Tabs/tabs.scss index 7564645baa8d174ec7c1b5edaa17c310ad4d3c1c..3d32c4ac3116fdd69fde92da5a05e6af6011f2e5 100644 --- a/client/src/components/navigation/Tabs/tabs.scss +++ b/client/src/components/navigation/Tabs/tabs.scss @@ -4,7 +4,7 @@ .tabs-container { display: flex; justify-content: space-between; - background: rgba(s.$white, 0.25); + background: rgba(s.$background-white-rgb, 0.25); border-radius: 15px; margin-top: 30px; position: relative; @@ -15,7 +15,7 @@ align-items: center; width: calc(50% - 20px); height: 50px; - color: s.$body-color; + color: s.$text; font-weight: s.$weight-semi-bold; font-size: 14px; position: relative; @@ -63,7 +63,7 @@ .background { position: absolute; height: 100%; - background: s.$white; + background: s.$background-white; z-index: 0; top: 0; left: 0; diff --git a/client/src/components/ui/Avatar/avatar.scss b/client/src/components/ui/Avatar/avatar.scss index 99b3e6a8f9007b24175d8c765d84ca2a98f43b15..80e9b0de118a1db01b618667c3a1dbec6c0c31fa 100644 --- a/client/src/components/ui/Avatar/avatar.scss +++ b/client/src/components/ui/Avatar/avatar.scss @@ -5,7 +5,12 @@ border-radius: 50%; overflow: hidden; - img, .standard-image { + img { + width: 100%; + height: 100%; + } + + .standard-image { width: 100%; height: 100%; background: s.$secondary; diff --git a/client/src/components/ui/Comment/comment.scss b/client/src/components/ui/Comment/comment.scss index a0ee9eb0cb40a25b8dcd1532f815c54c21150b1d..74f3123669312847cc5ec84e96d02b18916fa222 100644 --- a/client/src/components/ui/Comment/comment.scss +++ b/client/src/components/ui/Comment/comment.scss @@ -3,7 +3,7 @@ .comment-container { position: relative; - background: rgba(s.$white, 0.95); + background: rgba(s.$background-white-rgb, 0.95); box-shadow: 0 0 25px rgba(s.$black, 0.1); border-radius: 25px; margin-top: 10px; @@ -67,7 +67,7 @@ textarea { transition: none; - background: s.$light; + background: s.$background-input; border: none; border-radius: 25px; width: 100%; @@ -79,6 +79,7 @@ resize: vertical; outline: none; min-height: 150px; + color: s.$text; } .buttons { diff --git a/client/src/components/ui/Completion/completion.scss b/client/src/components/ui/Completion/completion.scss index 105ab9baa6d027cc2a72bfe4a1fcd35c62ea08a1..8004e09b731c97f1d8498463012e45f45b99e754 100644 --- a/client/src/components/ui/Completion/completion.scss +++ b/client/src/components/ui/Completion/completion.scss @@ -3,7 +3,7 @@ .completion { border-radius: 10px; - background: s.$white; + background: s.$background-white; position: relative; width: 100%; padding-bottom: 100%; diff --git a/client/src/components/ui/DetailBox/detail-box.scss b/client/src/components/ui/DetailBox/detail-box.scss index 7e61c5b7aab16bc39dad5378baddaf8d2c49062d..33259d18da9a5ff21170e6a65d8ca227ed86c6c7 100644 --- a/client/src/components/ui/DetailBox/detail-box.scss +++ b/client/src/components/ui/DetailBox/detail-box.scss @@ -5,7 +5,7 @@ .detail-box { width: 100%; padding-bottom: 100%; - background: s.$white; + background: s.$background-white; position: relative; border-radius: 10px; @@ -23,7 +23,7 @@ .icon-container { width: 50px; height: 50px; - background: s.$light; + background: s.$background-light; display: flex; justify-content: center; border-radius: 10px; diff --git a/client/src/components/ui/LongText/markdown.scss b/client/src/components/ui/LongText/markdown.scss index 518888ea41d0f515cd949007f883c5f1810d216c..fca352089b625c377875db4c3b528c64445cbf2d 100644 --- a/client/src/components/ui/LongText/markdown.scss +++ b/client/src/components/ui/LongText/markdown.scss @@ -1,4 +1,6 @@ +@use 'styles/settings.scss' as s; + .markdown { font-size: 1rem; line-height: 150%; @@ -9,7 +11,7 @@ .md-header-1, .md-header-2 { padding-bottom: 0.1rem; - border-bottom: 1px solid #00000015; + border-bottom: 1px solid rgba(s.$black, 0.1); } .md-header-1 { @@ -59,8 +61,8 @@ display: flex; flex-flow: row nowrap; overflow-x: auto; - color: #abb2bf; - background: #282c34; + color: #bbc2cf; + background: s.$background-dark; } .md-code .md-code-content { @@ -72,7 +74,7 @@ display: flex; flex-flow: column; padding: 1rem 0.5rem; - border-right: 1px solid #ffffff20; + border-right: 1px solid rgba(s.$white, 0.15); user-select: none; text-align: right; } @@ -83,7 +85,7 @@ } .md-inline-code { - background: #00000010; + background: rgba(s.$black, 0.05); padding: 0.1rem 0.25rem; border-radius: 4px; font-family: 'IBM Plex Mono', monospace; @@ -92,12 +94,12 @@ } .md-quote { - border-left: 0.25rem solid #00000018; + border-left: 0.25rem solid rgba(s.$black, 0.1); padding-left: 1.25rem; } .md-hrule { - border: 0.125rem solid #00000018; + border: 0.125rem solid rgba(s.$black, 0.1); height: 0; } @@ -156,7 +158,7 @@ li > span > .md-paragraph { .md-table, .md-table-row, .md-table-data, .md-table-header { border-collapse: collapse; - border: 1px solid #00000018; + border: 1px solid rgba(s.$black, 0.1); } .md-table-data, .md-table-header { @@ -165,11 +167,11 @@ li > span > .md-paragraph { .md-table-header { font-weight: 500; - background: #0000000a; + background: s.$background-input; } .md-table-row:nth-child(odd) { - background: #00000005; + background: rgba(s.$background-input-rgb, 0.5); } .md-info-wrap { diff --git a/client/src/components/ui/Popup/popup.scss b/client/src/components/ui/Popup/popup.scss index 61a915526305ef90623eea544103ca6351d03918..bf6e8379d7d665376d42cb4e4b3e37e0be1ca610 100644 --- a/client/src/components/ui/Popup/popup.scss +++ b/client/src/components/ui/Popup/popup.scss @@ -17,17 +17,15 @@ animation: moveup 300ms ease; max-height: 100vh; overflow: auto; - padding: 30px; + padding: 50px; max-width: 960px; - background: s.$white; + background: s.$background-white; border-radius: 25px; position: relative; z-index: 2; @include mx.breakpoint(medium) { margin: -20px 0; - width: 50%; - padding: 15px; padding: 75px 100px; } } diff --git a/client/src/components/ui/Project/index.tsx b/client/src/components/ui/Project/index.tsx index ef6a7a0a47df32f56f484ee9bedcfedf1cf239ff..58e18da0cd8d14516178a666f9aee2764c9947c1 100644 --- a/client/src/components/ui/Project/index.tsx +++ b/client/src/components/ui/Project/index.tsx @@ -64,15 +64,15 @@ export default function Project({ project, large, demo }: ProjectProps) { if (demo) { return ( - <Link to={'/projects/' + project.id} className={'project ' + (large ? 'large' : '')}> + <div className={'project ' + (large ? 'large' : '')}> { content } - </Link> + </div> ); } else { return ( - <div className={'project ' + (large ? 'large' : '')}> + <Link to={'/projects/' + project.id} className={'project ' + (large ? 'large' : '')}> { content } - </div> + </Link> ); } } diff --git a/client/src/components/ui/Project/project.scss b/client/src/components/ui/Project/project.scss index febb4a381b33fd361bf001f979275ce86c0556d0..f04570d1d439a21e0b8d35872944eb0f7a5a9730 100644 --- a/client/src/components/ui/Project/project.scss +++ b/client/src/components/ui/Project/project.scss @@ -4,16 +4,16 @@ .project { border-radius: 10px; - background: s.$white; - box-shadow: 0px 5px 25px rgba(0, 0, 0, 0.05); + background: s.$background-white; + box-shadow: 0px 5px 25px rgba(s.$black, 0.05); position: relative; width: 160px; - color: s.$body-color; + color: s.$text; height: 160px; &:hover, &:focus { - color: s.$body-color; + color: s.$text; box-shadow: 0 0 35px rgba(s.$black, 0.1); transform: translateY(-5px); } diff --git a/client/src/components/ui/ProjectSlide/project-slide.scss b/client/src/components/ui/ProjectSlide/project-slide.scss index 75e8860127588312aef54ca2b14c1ecd4e610c7b..24b84f0475094c11302b5efb38e082b6d388437e 100644 --- a/client/src/components/ui/ProjectSlide/project-slide.scss +++ b/client/src/components/ui/ProjectSlide/project-slide.scss @@ -3,18 +3,18 @@ .project-slide { padding: 30px; - background: s.$white; + background: s.$background-white; border-radius: 25px; box-shadow: 0 0 25px rgba(s.$black, 0.05); min-height: 180px; - color: s.$body-color; + color: s.$text; display: flex; flex-direction: column; justify-content: space-between; &:hover , &:focus { - color: s.$body-color; + color: s.$text; box-shadow: 0 0 35px rgba(s.$black, 0.1); transform: translateY(-5px); } diff --git a/client/src/components/ui/Task/task.scss b/client/src/components/ui/Task/task.scss index a91c11bb2661da9026286b66800fef535b1b1e3f..e39600f8965d0a59a36a0ac672ece710dd0661c2 100644 --- a/client/src/components/ui/Task/task.scss +++ b/client/src/components/ui/Task/task.scss @@ -1,20 +1,21 @@ -@use 'styles/settings.scss'as s; -@use 'styles/mixins.scss'as mx; + +@use 'styles/settings.scss' as s; +@use 'styles/mixins.scss' as mx; .task { padding: 30px; border-radius: 10px; - background: s.$white; + background: s.$background-white; width: 100%; box-shadow: 0 0px 20px rgba(s.$black, 0.05); display: block; - color: s.$body-color; + color: s.$text; font-weight: s.$weight-regular; position: relative; &:hover, &:focus { - color: s.$body-color; + color: s.$text; box-shadow: 0 5px 30px rgba(s.$black, 0.15); transform: translateY(-5px); @@ -46,7 +47,7 @@ margin-right: 15px; width: 50px; height: 50px; - background: s.$light-gray; + background: s.$background; display: flex; justify-content: center; align-items: center; @@ -79,4 +80,4 @@ right: -15px; bottom: -20px; } -} \ No newline at end of file +} diff --git a/client/src/components/ui/TextInput/text-input.scss b/client/src/components/ui/TextInput/text-input.scss index dca7e340ac6623456fa4593558cb914395f17ed1..f9a2a87cf7e741bb9fe536fd47d4ae211c35bfd6 100644 --- a/client/src/components/ui/TextInput/text-input.scss +++ b/client/src/components/ui/TextInput/text-input.scss @@ -49,10 +49,10 @@ position: relative; display: block; border-radius: 15px; - color: s.$body-color; + color: s.$text; font-weight: s.$weight-regular; font-family: s.$body-font; - background: rgba(0, 0, 0, 0.025); + background: s.$background-input; } } diff --git a/client/src/components/ui/TimeInput/index.tsx b/client/src/components/ui/TimeInput/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f9acbe76bcdaa2743e21795919b66817d6065036 --- /dev/null +++ b/client/src/components/ui/TimeInput/index.tsx @@ -0,0 +1,35 @@ + +import { useCallback, useState } from 'react'; + +import { durationFor, formatDuration } from 'timely'; + +import './time-input.scss'; + +interface Props { + onChange: (state: number) => void; +} + +export default function TimeInput({ onChange: userOnChange }: Props) { + const [formatted, setFormatted] = useState(''); + + const onChange = useCallback(event => { + const value = parseFloat(event.target.value); + userOnChange(value); + if (!Number.isNaN(value)) { + setFormatted( + formatDuration(durationFor(value, 'hour'), 'second', 2, true) + ); + } else { + setFormatted(''); + } + }, [userOnChange]); + + return ( + <div className="time-field"> + <label htmlFor="time">Time in hours</label> + <input type="number" name="time" min={0} onChange={onChange} /> + <span className="formatted">{formatted}</span> + </div> + ); +} + diff --git a/client/src/components/ui/TimeInput/time-input.scss b/client/src/components/ui/TimeInput/time-input.scss new file mode 100644 index 0000000000000000000000000000000000000000..83cb451067587bdf259e55d87d8a1686692ec38a --- /dev/null +++ b/client/src/components/ui/TimeInput/time-input.scss @@ -0,0 +1,45 @@ + +@use 'styles/settings.scss' as s; + +.time-field { + position: relative; + margin: 20px 0; + display: flex; + align-items: baseline; + + .formatted { + flex: 0 0 auto; + content: 'min'; + margin-left: 10px; + min-width: 64px; + white-space: nowrap; + text-align: center; + } + + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + /* Firefox */ + input[type=number] { + -moz-appearance: textfield; + } + + + label { + &:after { + content: ' *'; + color: s.$primary; + } + + font-size: 16px; + position: absolute; + left: 20px; + font-weight: s.$weight-bold; + z-index: 20; + transform: translateY(-50%); + } +} + diff --git a/client/src/components/ui/Tooltip/tooltip.scss b/client/src/components/ui/Tooltip/tooltip.scss index c18ebe7059d2464e744f2ae92cccd01fe6ace35f..ab399b0ae06d439d24c1e20b659f52ec9f374f2c 100644 --- a/client/src/components/ui/Tooltip/tooltip.scss +++ b/client/src/components/ui/Tooltip/tooltip.scss @@ -22,7 +22,7 @@ top: -10px; left: 50%; padding: 20px; - background: s.$white; + background: s.$background-white; max-width: 300px; border-radius: 5px; box-shadow: 0 0 15px rgba(s.$black, 0.2); @@ -37,7 +37,7 @@ left: 50%; transform: translate(-50%, 100%); border-width: 8px 8px 0 8px; - border-color: s.$white transparent transparent transparent; + border-color: s.$background-white transparent transparent transparent; border-style: solid; } } diff --git a/client/src/components/ui/User/user.scss b/client/src/components/ui/User/user.scss index 1b29e44b623f9e6fefc10f2bc5f676d48725fe82..2c687dd4d2124066c350bd0a419fbd78ddb71c4c 100644 --- a/client/src/components/ui/User/user.scss +++ b/client/src/components/ui/User/user.scss @@ -6,7 +6,7 @@ align-items: center; width: 85%; padding: 10px; - background: s.$white; + background: s.$background-white; border-radius: 15px; box-shadow: 0 0 10px rgba(s.$black, 0.1); position: relative; diff --git a/client/src/index.scss b/client/src/index.scss index 520300a5bf15b55446a8d518305e924174e5eca6..9ac10fce2d565e9b63dcb7b2f5b2d431a1e12605 100644 --- a/client/src/index.scss +++ b/client/src/index.scss @@ -4,8 +4,65 @@ @use 'styles/functions' as fn; :root { - --primary: #AC42FF; - --primary-dark: #930AFF; + &::before { + display: none; + content: 'light'; + } + + @each $key, $value in s.$light-colors { + --#{$key}: #{$value}; + } + + @each $key, $value in s.$light-colors { + --#{$key}-rgb: #{red($value), green($value), blue($value)}; + } +} + +@media (prefers-color-scheme: dark) { + :root { + &::before { + display: none; + content: 'dark'; + } + + @each $key, $value in s.$dark-colors { + --#{$key}: #{$value}; + } + + @each $key, $value in s.$dark-colors { + --#{$key}-rgb: #{red($value), green($value), blue($value)}; + } + } +} + +:root.light-theme { + &::before { + display: none; + content: 'light'; + } + + @each $key, $value in s.$light-colors { + --#{$key}: #{$value}; + } + + @each $key, $value in s.$light-colors { + --#{$key}-rgb: #{red($value), green($value), blue($value)}; + } +} + +:root.dark-theme { + &::before { + display: none; + content: 'dark'; + } + + @each $key, $value in s.$dark-colors { + --#{$key}: #{$value}; + } + + @each $key, $value in s.$dark-colors { + --#{$key}-rgb: #{red($value), green($value), blue($value)}; + } } * { @@ -21,9 +78,9 @@ html { } body { - color: s.$body-color; + color: s.$text; position: relative; - background: #EEF5F5; + background: s.$background; } button { @@ -173,7 +230,7 @@ $color in s.$themeLightMap { font-size: 30px; height: 84px; cursor: pointer; - color: s.$body-color; + color: s.$text; user-select: none; } diff --git a/client/src/index.tsx b/client/src/index.tsx index f277e1bbc445f9d26941038fa403cc14cbdf2389..39ebdae67dc8746bb95b1fd3f2a0d02a6bac9768 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -4,10 +4,37 @@ import ReactDOM from 'react-dom'; import App from 'App'; import * as serviceWorkerRegistration from './serviceWorkerRegistration'; -import reportWebVitals from './reportWebVitals'; import 'index.scss'; +serviceWorkerRegistration.register(); + +const root = document.getElementsByTagName('html')[0]; + +export function getTheme() { + return root.classList.contains('dark-theme') ? 'dark' : 'light'; +} + +export function toggleTheme() { + const current = getComputedStyle(root, '::before').getPropertyValue('content'); + if (current === '"dark"') { + root.classList.remove('dark-theme'); + root.classList.add('light-theme'); + localStorage.setItem('selected-theme', 'light'); + } else { + root.classList.remove('light-theme'); + root.classList.add('dark-theme'); + localStorage.setItem('selected-theme', 'dark'); + } +} + +const lastTheme = localStorage.getItem('selected-theme'); +if (lastTheme === 'dark') { + root.classList.add('dark-theme'); +} else if (lastTheme === 'light') { + root.classList.add('light-theme'); +} + function render() { ReactDOM.render( <React.StrictMode> @@ -27,6 +54,3 @@ export function reload() { render(); -serviceWorkerRegistration.register(); -reportWebVitals(); - diff --git a/client/src/pages/Home/home.scss b/client/src/pages/Home/home.scss index df45e3ca18be6548bf63f412e49f6274071fc64d..478a096bf45ffe5cb45d372a7875dd113f807974 100644 --- a/client/src/pages/Home/home.scss +++ b/client/src/pages/Home/home.scss @@ -54,7 +54,7 @@ display: flex; a { - color: s.$body-color; + color: s.$text; font-weight: s.$weight-bold; margin-left: 40px; display: none; @@ -156,7 +156,7 @@ width: 100%; border: 7px solid #303030; border-radius: 25px; - box-shadow: 0 0 40px rgba(0, 0, 0, .15); + box-shadow: 0 0 40px rgba(s.$black, 0.15); @include mx.breakpoint(medium) { border-width: 10px; @@ -206,7 +206,7 @@ grid-gap: 24px; justify-content: center; align-items: center; - background: rgba(255, 255, 255, .5); + background: rgba(s.$background-white-rgb, 0.5); border-radius: 25px; padding: 24px; @@ -272,11 +272,11 @@ @keyframes move-up { 5%, 35% { transform: translateY(0); - box-shadow: 0px 5px 25px rgba(0, 0, 0, 0.05); + box-shadow: 0px 5px 25px rgba(s.$black, 0.05); } 10%, 30% { transform: translateY(-10px); - box-shadow: 0px 5px 30px rgba(0, 0, 0, 0.15); + box-shadow: 0px 5px 30px rgba(s.$black, 0.15); } } diff --git a/client/src/pages/Tasks/tasks.scss b/client/src/pages/Tasks/tasks.scss index 59515a981b951bad7a0837bafa344d84b10bf917..4da644bbc87ee5a450620ac07c6bbdc66b3e43fb 100644 --- a/client/src/pages/Tasks/tasks.scss +++ b/client/src/pages/Tasks/tasks.scss @@ -29,7 +29,7 @@ margin-top: 20px; border-radius: 5px; padding: 20px; - background: s.$white; + background: s.$background-white; } } diff --git a/client/src/styles/settings.scss b/client/src/styles/settings.scss index 6804c37a9b6d7271fae81e5a62957a614880435d..20d59225508503e1c0c606455996ecffa7d27e82 100644 --- a/client/src/styles/settings.scss +++ b/client/src/styles/settings.scss @@ -1,27 +1,57 @@ -// Colors +$light-colors: ( + 'primary': #AC42FF, + 'primary-dark': #930AFF, + 'secondary': #7DEFFF, + 'red': #E51C4A, + 'background-dark': #11061A, + 'background-light': #f9f9f9, + 'background-white': #ffffff, + 'background-input': #f4f4f4, + 'background': #EEF5F5, + 'text': #3A5255, +); + +$dark-colors: ( + 'primary': #AC42FF, + 'primary-dark': #930AFF, + 'secondary': #7DEFFF, + 'red': #E51C4A, + 'background-dark': #000000, + 'background-light': #090909, + 'background-white': #151515, + 'background-input': #181818, + 'background': #0a0a0a, + 'text': #eAe2e5, +); + $primary: var(--primary); $primary-dark: var(--primary-dark); -$secondary: #7DEFFF; -$red: #E51C4A; -$light: #F9F9F9; -$dark: #11061A; +$secondary: var(--secondary); +$red: var(--red); +$background: var(--background); +$background-dark: var(--background-dark); +$background-light: var(--background-light); +$background-white: var(--background-white); +$background-input: var(--background-input); +$text: var(--text); -$light-gray: #F8F8F8; -$gray: #F9F9F9; +$primary-rgb: var(--primary-rgb); +$primary-dark-rgb: var(--primary-dark-rgb); +$secondary-rgb: var(--secondary-rgb); +$red-rgb: var(--red-rgb); +$background-rgb: var(--background-rgb); +$background-dark-rgb: var(--background-dark-rgb); +$background-light-rgb: var(--background-light-rgb); +$background-white-rgb: var(--background-white-rgb); +$background-input-rgb: var(--background-input-rgb); +$text-rgb: var(--text-rgb); $white: #fff; $black: #000; $error-color: $secondary; -$colors: ( - 'primary': $primary, - 'secondary': $secondary, - 'light': $light, - 'dark': $dark, -); - $themeDarkMap: ( 'red': #e61e4d, 'orange': #c94b02, @@ -46,9 +76,6 @@ $linear-gradient: linear-gradient(to bottom, $primary, $primary-dark); $linear-gradient-horizontal: linear-gradient(to right, $primary, $primary-dark); $radial-gradient: radial-gradient(100% 115% at 0% 0%, $primary 0%, $primary-dark 100%); -$body-color: #3A5255; -$body-font: 'Poppins', sans-serif; - // Typography $weight-light: 300; $weight-regular: 400; @@ -57,6 +84,8 @@ $weight-semi-bold: 600; $weight-bold: 700; $weight-xbold: 800; +$body-font: 'Poppins', sans-serif; + // Breakpoints $breakpoints: ( 'small': 480px, diff --git a/client/src/timely.test.ts b/client/src/timely.test.ts index 80b84f82a986f837f78cbec83d6248208d3d11b3..79c7269ee15b267f374a6920cfc07461381196c4 100644 --- a/client/src/timely.test.ts +++ b/client/src/timely.test.ts @@ -7,6 +7,7 @@ import { formatSimpleDuration, addTime, subtractTime, + durationFor, } from 'timely'; test('simple duration format works as expected', () => { @@ -384,3 +385,15 @@ test('in four years formats as expected', () => { .toEqual('in 4 years'); }); +test('duration formatting can contain multiple units', () => { + expect(formatDuration(1.5 * 60 * 60 * 1000, 'minute', 2)).toEqual('one hour 30 minutes'); +}); + +test('duration formatting can be short', () => { + expect(formatDuration(1.5 * 60 * 60 * 1000, 'minute', 2, true)).toEqual('1h 30m'); +}); + +test('get duration from amount and unit', () => { + expect(durationFor(10, 'hour')).toEqual(10 * 60 * 60 * 1000); +}); + diff --git a/client/src/timely.ts b/client/src/timely.ts index a9ddd8713d7be596bd51a224e75b0da365441122..6620021a2fc5df6cb59d3bf1f83ca4bf36af9bdf 100644 --- a/client/src/timely.ts +++ b/client/src/timely.ts @@ -13,22 +13,32 @@ const UNITS = { type Unit = (keyof typeof UNITS); -function formatAmount(amount: number, base: string): string { - amount = Math.floor(amount); - if (amount === 0) { - return 'zero ' + base + 's'; - } else if (amount === 1) { - return 'one ' + base; +function formatAmount(amount: number, base: string, short: boolean): string { + if (short) { + return Math.floor(amount) + base[0]; } else { - return amount.toString() + ' ' + base + 's'; + amount = Math.floor(amount); + if (amount === 0) { + return 'zero ' + base + 's'; + } else if (amount === 1) { + return 'one ' + base; + } else { + return amount.toString() + ' ' + base + 's'; + } } } -export function formatDuration(millis: number, precision: Unit = 'minute'): string { +export function formatDuration(millis: number, precision: Unit = 'minute', count = 1, short = false): string { if (millis >= UNITS[precision]) { for (const key of (Object.keys(UNITS) as Unit[])) { if (millis >= UNITS[key]) { - return formatAmount(millis / UNITS[key], key); + const rest = millis % UNITS[key]; + const significant = formatAmount(millis / UNITS[key], key, short); + if (count <= 1 || rest < UNITS[precision]) { + return significant; + } else { + return significant + ' ' + formatDuration(rest, precision, count - 1, short); + } } } } @@ -195,6 +205,10 @@ export function subtractTime(date: Date, time: number, unit: Unit): Date { return addTime(date, -time, unit); } +export function durationFor(time: number, unit: Unit): number { + return time * UNITS[unit]; +} + export function durationBetween(from: Date, to: Date): number { return to.getTime() - from.getTime(); }