diff --git a/src/components/Page/index.tsx b/src/components/Page/index.tsx index 1919561..f4778d1 100644 --- a/src/components/Page/index.tsx +++ b/src/components/Page/index.tsx @@ -55,7 +55,7 @@ function Page(props: Props) { onSwipeRight, } = props; - const { width } = useContext(SizeContext); + const { screen } = useContext(SizeContext); const [storedConfig, setStoredConfig] = useLocalStorage('timur-config'); @@ -117,11 +117,11 @@ function Page(props: Props) { !startSidebarShown && styles.startSidebarCollapsed, debouncedStartSidebarCollapsed && styles.debouncedStartSidebarCollapsed, debouncedEndSidebarCollapsed && styles.debouncedEndSidebarCollapsed, - (!endSidebarShown || width <= 900) && styles.endSidebarCollapsed, + (!endSidebarShown || screen === 'mobile') && styles.endSidebarCollapsed, startSidebarShown && !!startAsideContent && styles.startSidebarVisible, endSidebarShown && !!endAsideContent - && width > 900 + && screen === 'desktop' && styles.endSidebarVisible, className, )} @@ -160,7 +160,7 @@ function Page(props: Props) { {children} - {endAsideContent && width > 900 && ( + {endAsideContent && screen === 'desktop' && ( )} - {endAsideContent && width > 900 && ( + {endAsideContent && screen === 'desktop' && ( diff --git a/src/utils/common.ts b/src/utils/common.ts index 5829791..88ea329 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -24,11 +24,15 @@ export function isCallable(value: T | X): value is X { export interface Size { width: number; height: number; + screen: 'desktop' | 'mobile'; } export function getWindowSize(): Size { return { width: window.innerWidth, height: window.innerHeight, + screen: window.innerWidth >= 900 + ? 'desktop' + : 'mobile', }; } diff --git a/src/views/DailyJournal/DayView/WorkItemRow/index.tsx b/src/views/DailyJournal/DayView/WorkItemRow/index.tsx index 9278c80..166c786 100644 --- a/src/views/DailyJournal/DayView/WorkItemRow/index.tsx +++ b/src/views/DailyJournal/DayView/WorkItemRow/index.tsx @@ -104,7 +104,7 @@ function WorkItemRow(props: Props) { } = props; const { enums } = useContext(EnumsContext); - const { width: windowWidth } = useContext(SizeContext); + const { screen } = useContext(SizeContext); const inputRef = useFocusClient(workItem.clientId); const [config] = useLocalStorage('timur-config'); @@ -217,7 +217,7 @@ function WorkItemRow(props: Props) { icons={( config.showInputIcons // NOTE: hide/unhide icon wrt "checkbox for status" flag - && (windowWidth < 900 || !config.checkboxForStatus) + && (screen === 'mobile' || !config.checkboxForStatus) && )} /> @@ -234,7 +234,7 @@ function WorkItemRow(props: Props) { icons={( config.showInputIcons // NOTE: hide/unhide icon wrt "checkbox for status" flag - && (windowWidth >= 900 || !config.checkboxForStatus) + && (screen === 'desktop' || !config.checkboxForStatus) && )} placeholder="Description" @@ -352,7 +352,7 @@ function WorkItemRow(props: Props) { className, )} > - {windowWidth >= 900 ? ( + {screen === 'desktop' ? ( <> {statusInput} {taskInput} diff --git a/src/views/DailyJournal/index.tsx b/src/views/DailyJournal/index.tsx index e4a4e50..24d8188 100644 --- a/src/views/DailyJournal/index.tsx +++ b/src/views/DailyJournal/index.tsx @@ -563,7 +563,7 @@ export function Component() { ); const { midActionsRef } = useContext(NavbarContext); - const { width: windowWidth } = useContext(SizeContext); + const { screen } = useContext(SizeContext); const handleSwipeLeft = useCallback( () => { @@ -631,7 +631,7 @@ export function Component() { - {windowWidth >= 900 && ( + {screen === 'desktop' && ( <> } /> - {windowWidth >= 900 && ( + {screen === 'desktop' && ( )} - {windowWidth >= 900 && ( + {screen === 'desktop' && ( ({ + query: DEADLINES_AND_EVENTS, + }); + + const projects = deadlinesAndEvents.data?.private.allProjects; + const events = deadlinesAndEvents.data?.private.relativeEvents; + + const formattedDate = dateFormatter.format(new Date(date)); + + const upcomingEvents = useMemo(() => { + const deadlines = projects?.flatMap( + (project) => project.deadlines.map((deadline) => ({ + ...deadline, + name: `${project.name}: ${deadline.name}`, + })), + ); + + const iconsMap: Record = { + DEADLINE: , + HOLIDAY: , + RETREAT: , + MISC: , + }; + + return [ + ...(deadlines?.map((deadline) => ({ + key: `DEADLINE-${deadline.id}`, + type: 'DEADLINE' as const, + typeDisplay: 'Deadline', + icon: iconsMap.DEADLINE, + name: deadline.name, + date: deadline.endDate, + remainingDays: deadline.remainingDays, + })) ?? []), + ...(events?.map((otherEvent) => ({ + key: `${otherEvent.type}-${otherEvent.id}`, + type: otherEvent.type, + icon: iconsMap[otherEvent.type], + typeDisplay: otherEvent.typeDisplay, + name: otherEvent.name, + date: otherEvent.startDate, + remainingDays: getDifferenceInDays( + otherEvent.startDate, + date, + ), + })) ?? []), + ].sort((a, b) => compareDate(a.date, b.date)); + }, [events, projects, date]); + + return ( + ( + + + {generalEvent.remainingDays < 0 + && upcomingEvents[index + 1]?.remainingDays >= 0 + && ( + + + + + + )} + + ), + )} + /> + ); +} + +export default DeadlineSection; diff --git a/src/views/DailyStandup/DeadlineSection/styles.module.css b/src/views/DailyStandup/DeadlineSection/styles.module.css new file mode 100644 index 0000000..d1200ed --- /dev/null +++ b/src/views/DailyStandup/DeadlineSection/styles.module.css @@ -0,0 +1,30 @@ +.separator { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: 0 var(--spacing-lg); + + .icon { + animation: run var(--duration-animation-fast) ease-in-out infinite; + font-size: var(--font-size-xl); + } + + .line { + flex-grow: 1; + border-bottom: var(--width-separator-sm) solid var(--color-separator); + } +} + +@keyframes run { + 0% { + transform: rotate(0); + } + + 30% { + transform: rotate(5deg) translateY(2px); + } + 70% { + transform: rotate(-4deg) translateX(-2px); + } +} + diff --git a/src/views/DailyStandup/Slide/index.tsx b/src/views/DailyStandup/Slide/index.tsx index 3e47956..f8d2337 100644 --- a/src/views/DailyStandup/Slide/index.tsx +++ b/src/views/DailyStandup/Slide/index.tsx @@ -5,7 +5,7 @@ import styles from './styles.module.css'; interface SplitVariantProps { variant: 'split'; primaryPreText?: React.ReactNode; - primaryHeading: React.ReactNode; + primaryHeading?: React.ReactNode; primaryDescription?: React.ReactNode; secondaryHeading: React.ReactNode; secondaryContent: React.ReactNode; diff --git a/src/views/DailyStandup/StartSection/index.tsx b/src/views/DailyStandup/StartSection/index.tsx new file mode 100644 index 0000000..d9f872e --- /dev/null +++ b/src/views/DailyStandup/StartSection/index.tsx @@ -0,0 +1,130 @@ +import { + compareNumber, + compareString, +} from '@togglecorp/fujs'; +import { + gql, + useQuery, +} from 'urql'; + +import AvailabilityIndicator from '#components/AvailabilityIndicator'; +import DisplayPicture from '#components/DisplayPicture'; +import { + type JournalLeaveTypeEnum, + type JournalWorkFromHomeTypeEnum, + type UsersAvailabilityQuery, + type UsersAvailabilityQueryVariables, +} from '#generated/types/graphql'; + +import Slide from '../Slide'; + +import styles from './styles.module.css'; + +function getUnavailability( + leave: JournalLeaveTypeEnum | null | undefined, + wfh: JournalWorkFromHomeTypeEnum | null | undefined, +) { + let sum = 0; + if (leave === 'FULL') { + sum += 1; + } else if (leave === 'FIRST_HALF' || leave === 'SECOND_HALF') { + sum += 0.5; + } + if (wfh === 'FULL') { + sum += 0.2; + } else if (wfh === 'FIRST_HALF' || wfh === 'SECOND_HALF') { + sum += 0.1; + } + return sum; +} + +const dateFormatter = new Intl.DateTimeFormat( + [], + { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short', + }, +); + +const USERS_AVAILABILITY = gql` + query UsersAvailability { + private { + users(pagination: {limit: 999}, filters: {departments: [DEVELOPMENT, DESIGN, PROJECT_MANAGER, QUALITY_ASSURANCE]}) { + items { + id + leaveToday + workFromHomeToday + displayPicture + displayName + } + } + } + } +`; + +interface Props { + date: string; +} + +function StartSection(props: Props) { + const { + date, + } = props; + + const formattedDate = dateFormatter.format(new Date(date)); + + const [usersAvailability] = useQuery< + UsersAvailabilityQuery, + UsersAvailabilityQueryVariables + >({ + query: USERS_AVAILABILITY, + }); + + // FIXME: need to check how to sort these information + const sortedUsers = [...(usersAvailability.data?.private.users.items ?? [])].sort( + (foo, bar) => compareNumber( + getUnavailability(foo.leaveToday, foo.workFromHomeToday), + getUnavailability(bar.leaveToday, bar.workFromHomeToday), + -1, + ) || compareString( + foo.displayName, + bar.displayName, + ), + ); + + return ( + ( + + + + {user.displayName ?? 'Anon'} + {' '} + + + + ))} + /> + ); +} + +export default StartSection; diff --git a/src/views/DailyStandup/StartSection/styles.module.css b/src/views/DailyStandup/StartSection/styles.module.css new file mode 100644 index 0000000..682e034 --- /dev/null +++ b/src/views/DailyStandup/StartSection/styles.module.css @@ -0,0 +1,16 @@ +.start-section { + .user { + display: flex; + font-size: var(--font-size-lg); + gap: var(--spacing-xs); + + .display-picture { + font-size: var(--font-size-xl); + } + + .name { + display: inline-flex; + gap: var(--spacing-xs); + } + } +} diff --git a/src/views/DailyStandup/index.tsx b/src/views/DailyStandup/index.tsx index 2d2ecb3..ffe2425 100644 --- a/src/views/DailyStandup/index.tsx +++ b/src/views/DailyStandup/index.tsx @@ -1,5 +1,4 @@ import { - Fragment, useCallback, useContext, useEffect, @@ -7,13 +6,6 @@ import { useRef, useState, } from 'react'; -import { - FcLandscape, - FcLeave, - FcNews, - FcNightLandscape, - FcSportsMode, -} from 'react-icons/fc'; import { IoChevronBack, IoChevronForward, @@ -22,9 +14,7 @@ import { import { useParams } from 'react-router-dom'; import { _cs, - compareDate, encodeDate, - getDifferenceInDays, isDefined, isNotDefined, } from '@togglecorp/fujs'; @@ -39,60 +29,30 @@ import Portal from '#components/Portal'; import DateContext from '#contexts/date'; import NavbarContext from '#contexts/navbar'; import { - AllProjectsAndEventsQuery, - AllProjectsAndEventsQueryVariables, + AllProjectsQuery, + AllProjectsQueryVariables, } from '#generated/types/graphql'; import useKeybind from '#hooks/useKeybind'; import useUrlQueryState from '#hooks/useUrlQueryState'; -import { type GeneralEvent } from '#utils/types'; +import DeadlineSection from './DeadlineSection'; import EndSection from './EndSection'; -import GeneralEventOutput from './GeneralEvent'; import ProjectStandup from './ProjectStandup'; -import Slide from './Slide'; +import StartSection from './StartSection'; import styles from './styles.module.css'; -const dateFormatter = new Intl.DateTimeFormat( - [], - { - year: 'numeric', - month: 'short', - day: 'numeric', - weekday: 'short', - }, -); - -const ALL_PROJECTS_AND_EVENTS_QUERY = gql` - query AllProjectsAndEvents { +const ALL_PROJECTS = gql` + query AllProjects { private { id allProjects { id name - deadlines { - id - name - remainingDays - endDate - totalDays - usedDays - projectId - } - description logoHd { url } } - relativeEvents { - id - name - startDate - typeDisplay - dates - endDate - type - } } } `; @@ -121,10 +81,10 @@ export function Component() { }, [dateFromParams, fullDate]); const [allProjectsResponse] = useQuery< - AllProjectsAndEventsQuery, - AllProjectsAndEventsQueryVariables + AllProjectsQuery, + AllProjectsQueryVariables >({ - query: ALL_PROJECTS_AND_EVENTS_QUERY, + query: ALL_PROJECTS, }); type UrlQueryKey = 'project' | 'page'; @@ -137,17 +97,20 @@ export function Component() { (value) => value, ); - const formattedDate = dateFormatter.format(new Date(selectedDate)); - const projectsMap = useMemo(() => { const allProjectsData = allProjectsResponse?.data?.private.allProjects; if (isNotDefined(allProjectsData)) { return undefined; } + const initialMap: Record> = { start: { prev: undefined, + next: 'deadlines', + }, + deadlines: { + prev: 'start', next: allProjectsData[0].id, }, end: { @@ -160,7 +123,7 @@ export function Component() { (acc, val, index) => { const currentMap = { next: index === (allProjectsData.length - 1) ? 'end' : allProjectsData[index + 1].id, - prev: index === 0 ? 'start' : allProjectsData[index - 1].id, + prev: index === 0 ? 'deadlines' : allProjectsData[index - 1].id, }; acc[val.id] = currentMap; @@ -180,6 +143,14 @@ export function Component() { return; } + if (pageId === 'deadlines') { + setUrlQuery({ + project: undefined, + page: 'deadlines', + }); + return; + } + if (pageId === 'end') { setUrlQuery({ project: undefined, @@ -194,18 +165,12 @@ export function Component() { }); }, [setUrlQuery]); - const mapId = urlQuery.page ?? urlQuery.project; - const prevButtonName = isDefined(mapId) - ? projectsMap?.[mapId]?.prev - : undefined; - const prevButtonDisabled = isNotDefined(mapId) || isNotDefined(projectsMap?.[mapId]?.prev); + const mapId = urlQuery.page ?? urlQuery.project ?? 'start'; + const prevButtonName = projectsMap?.[mapId].prev; + const prevButtonDisabled = isNotDefined(prevButtonName); - const nextButtonName = isDefined(mapId) - ? projectsMap?.[mapId].next - : projectsMap?.start.next; - const nextButtonDisabled = isNotDefined(mapId) - ? false - : isNotDefined(projectsMap?.[mapId].next); + const nextButtonName = projectsMap?.[mapId].next; + const nextButtonDisabled = isNotDefined(nextButtonName); const handleNextButtion = useCallback( () => { @@ -264,48 +229,6 @@ export function Component() { contentRef.current?.requestFullscreen(); }, []); - const events = useMemo(() => { - const allDeadlines = allProjectsResponse.data?.private.allProjects.flatMap( - (project) => project.deadlines.map((deadline) => ({ - ...deadline, - name: `${project.name}: ${deadline.name}`, - })), - ); - - const otherEvents = allProjectsResponse.data?.private.relativeEvents; - - const iconsMap: Record = { - DEADLINE: , - HOLIDAY: , - RETREAT: , - MISC: , - }; - - return [ - ...(allDeadlines?.map((deadline) => ({ - key: `DEADLINE-${deadline.id}`, - type: 'DEADLINE' as const, - typeDisplay: 'Deadline', - icon: iconsMap.DEADLINE, - name: deadline.name, - date: deadline.endDate, - remainingDays: deadline.remainingDays, - })) ?? []), - ...(otherEvents?.map((otherEvent) => ({ - key: `${otherEvent.type}-${otherEvent.id}`, - type: otherEvent.type, - icon: iconsMap[otherEvent.type], - typeDisplay: otherEvent.typeDisplay, - name: otherEvent.name, - date: otherEvent.startDate, - remainingDays: getDifferenceInDays( - otherEvent.startDate, - fullDate, - ), - })) ?? []), - ].sort((a, b) => compareDate(a.date, b.date)); - }, [allProjectsResponse, fullDate]); - return ( - {isNotDefined(mapId) && ( - ( - - - {generalEvent.remainingDays < 0 - && events[i + 1]?.remainingDays >= 0 - && ( - - - - - - )} - - ), - )} + {mapId === 'start' && ( + + )} + {mapId === 'deadlines' && ( + )} - {isNotDefined(urlQuery.page) && isDefined(urlQuery.project) && ( + {mapId !== 'start' && mapId !== 'end' && mapId !== 'deadlines' && ( )} - {urlQuery.page === 'end' && ( + {mapId === 'end' && ( diff --git a/src/views/DailyStandup/styles.module.css b/src/views/DailyStandup/styles.module.css index b7ea5ad..937691b 100644 --- a/src/views/DailyStandup/styles.module.css +++ b/src/views/DailyStandup/styles.module.css @@ -19,40 +19,10 @@ flex-grow: 1; background-color: var(--color-background); overflow: auto; - - .separator { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: 0 var(--spacing-lg); - - .icon { - animation: run var(--duration-animation-fast) ease-in-out infinite; - font-size: var(--font-size-xl); - } - - .line { - flex-grow: 1; - border-bottom: var(--width-separator-sm) solid var(--color-separator); - } - } } } } -@keyframes run { - 0% { - transform: rotate(0); - } - - 30% { - transform: rotate(5deg) translateY(2px); - } - 70% { - transform: rotate(-4deg) translateX(-2px); - } -} - .actions { display: flex; justify-content: flex-end; diff --git a/src/views/Home/index.tsx b/src/views/Home/index.tsx index bc2359c..0f6f5a8 100644 --- a/src/views/Home/index.tsx +++ b/src/views/Home/index.tsx @@ -8,10 +8,7 @@ import { FcVoicePresentation, } from 'react-icons/fc'; import { useNavigate } from 'react-router-dom'; -import { - encodeDate, - isNotDefined, -} from '@togglecorp/fujs'; +import { isNotDefined } from '@togglecorp/fujs'; import Link, { resolvePath } from '#components/Link'; import MonthlyCalendar from '#components/MonthlyCalendar';