diff --git a/backend b/backend index 6a90d8c..3f638e0 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 6a90d8c605a3e02ea16968b03e8732c2f95feb9c +Subproject commit 3f638e051c66d561b36758e28553a0f188f461bb diff --git a/src/App/index.tsx b/src/App/index.tsx index a2f378d..53db948 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -57,6 +57,7 @@ const ME_QUERY = gql` id lastName isStaff + loginExpire } } } diff --git a/src/components/Clock/index.tsx b/src/components/Clock/index.tsx deleted file mode 100644 index 3646d4c..0000000 --- a/src/components/Clock/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - useEffect, - useState, -} from 'react'; - -const dateTimeFormatter = new Intl.DateTimeFormat( - [], - { - year: 'numeric', - month: 'short', - day: 'numeric', - weekday: 'short', - hour: 'numeric', - minute: 'numeric', - // second: 'numeric', - hour12: true, - }, -); - -function formatTime(date: Date) { - return dateTimeFormatter.format(date); -} - -function Clock() { - const [dateStr, setDateStr] = useState(() => { - const date = new Date(); - return formatTime(date); - }); - useEffect( - () => { - const timeout = window.setInterval( - () => { - const date = new Date(); - const dateAsString = formatTime(date); - setDateStr(dateAsString); - }, - 500, - ); - return () => { - window.clearInterval(timeout); - }; - }, - [], - ); - return ( -
- {dateStr} -
- ); -} - -export default Clock; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 1bb81e2..bfd4715 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -102,7 +102,7 @@ function Navbar(props: Props) { {isNotDefined(userAuth) && ( Login diff --git a/src/components/StandupConductors/index.tsx b/src/components/StandupConductors/index.tsx new file mode 100644 index 0000000..56d5172 --- /dev/null +++ b/src/components/StandupConductors/index.tsx @@ -0,0 +1,100 @@ +import { + _cs, + encodeDate, +} from '@togglecorp/fujs'; +import { + gql, + useQuery, +} from 'urql'; + +import DisplayPicture from '#components/DisplayPicture'; +import TextOutput from '#components/TextOutput'; +import { + type StandupConductorsQuery, + type StandupConductorsQueryVariables, +} from '#generated/types/graphql'; + +import styles from './styles.module.css'; + +const STANDUP_CONDUCTORS = gql` + query StandupConductors($date: Date!){ + private { + dailyStandup(date: $date) { + conductor { + id + displayName + displayPicture + } + fallbackConductor { + id + displayName + displayPicture + } + } + } + } +`; + +const todayDate = encodeDate(new Date()); + +function StandupConductors() { + const [conductorsResponse] = useQuery({ + query: STANDUP_CONDUCTORS, + variables: { date: todayDate }, + requestPolicy: 'cache-and-network', + }); + + const standupConductors = conductorsResponse.data?.private.dailyStandup; + + return ( +
+ + + + {standupConductors?.conductor?.displayName + ?? 'Anonymous'} + + + )} + block + hideLabelColon + /> + + + + {standupConductors?.fallbackConductor?.displayName + ?? 'Anonymous'} + + + )} + block + hideLabelColon + /> +
+ ); +} + +export default StandupConductors; diff --git a/src/components/StandupConductors/styles.module.css b/src/components/StandupConductors/styles.module.css new file mode 100644 index 0000000..3b44308 --- /dev/null +++ b/src/components/StandupConductors/styles.module.css @@ -0,0 +1,24 @@ +.conductors { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + + .conductor-item { + display: flex; + justify-content: center; + gap: var(--spacing-xs); + + &.hidden { + visibility: hidden; + } + + .conductor-value { + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-normal); + gap: var(--spacing-xs); + } + } +} diff --git a/src/components/TextOutput/index.tsx b/src/components/TextOutput/index.tsx new file mode 100644 index 0000000..fbf45b0 --- /dev/null +++ b/src/components/TextOutput/index.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { _cs } from '@togglecorp/fujs'; + +import styles from './styles.module.css'; + +interface Props { + className?: string; + label?: React.ReactNode; + labelContainerClassName?: string; + description?: React.ReactNode; + descriptionContainerClassName?: string; + valueContainerClassName?: string; + hideLabelColon?: boolean; + block?: boolean; + value?: React.ReactNode; +} + +function TextOutput(props: Props) { + const { + className, + label, + labelContainerClassName, + valueContainerClassName, + description, + descriptionContainerClassName, + hideLabelColon, + block, + value, + } = props; + + return ( +
+ {label && ( +
+ {label} +
+ )} +
+ {value} +
+ {description && ( +
+ {description} +
+ )} +
+ ); +} + +export default TextOutput; diff --git a/src/components/TextOutput/styles.module.css b/src/components/TextOutput/styles.module.css new file mode 100644 index 0000000..58e4e17 --- /dev/null +++ b/src/components/TextOutput/styles.module.css @@ -0,0 +1,37 @@ +.text-output { + --spacing: var(--spacing-md); + display: flex; + align-items: baseline; + font-size: var(--font-size-md); + gap: var(--spacing-sm); + + &.blok { + align-items: initial; + flex-direction: column; + } + + >.label { + color: var(--color-text); + } + + >.value { + font-weight: var(--font-weight-bold); + + /* Useful for nested text outputs */ + .text-output { + font-weight: var(--font-weight-semibold); + } + } + + >.description { + color: var(--color-text-light); + } + + &.with-label-colon { + >.label { + &:after { + content: ':'; + } + } + } +} diff --git a/src/contexts/user.tsx b/src/contexts/user.tsx index e3ef297..f85d2b8 100644 --- a/src/contexts/user.tsx +++ b/src/contexts/user.tsx @@ -4,7 +4,7 @@ import { UserMeType } from '#generated/types/graphql'; export type UserAuth = Pick< UserMeType, - 'displayName' | 'displayPicture' | 'email' | 'firstName' | 'id' | 'lastName' | 'isStaff' + 'displayName' | 'displayPicture' | 'email' | 'firstName' | 'id' | 'lastName' | 'isStaff' | 'loginExpire' >; export interface UserContextProps { diff --git a/src/hooks/useCurrentDate.ts b/src/hooks/useCurrentDate.ts new file mode 100644 index 0000000..a21b08f --- /dev/null +++ b/src/hooks/useCurrentDate.ts @@ -0,0 +1,30 @@ +import { + useEffect, + useState, +} from 'react'; + +function useCurrentDate() { + const [dateStr, setDateStr] = useState(() => { + const date = new Date(); + return date; + }); + + useEffect( + () => { + const timeout = window.setInterval( + () => { + const date = new Date(); + setDateStr(date); + }, + 5000, + ); + return () => { + window.clearInterval(timeout); + }; + }, + [], + ); + return dateStr; +} + +export default useCurrentDate; diff --git a/src/utils/common.ts b/src/utils/common.ts index cc02a0b..10fab46 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -395,3 +395,21 @@ export function putUndefined(value: T) { return copy as PutUndefined; } + +const dateTimeFormatter = new Intl.DateTimeFormat( + [], + { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short', + hour: 'numeric', + minute: 'numeric', + // second: 'numeric', + hour12: true, + }, +); + +export function formatDateTime(date: Date) { + return dateTimeFormatter.format(date); +} diff --git a/src/views/DailyStandup/DeadlineSection/index.tsx b/src/views/DailyStandup/DeadlineSection/index.tsx index 9c615be..a7b297b 100644 --- a/src/views/DailyStandup/DeadlineSection/index.tsx +++ b/src/views/DailyStandup/DeadlineSection/index.tsx @@ -15,11 +15,13 @@ import { useQuery, } from 'urql'; -import Clock from '#components/Clock'; +import StandupConductors from '#components/StandupConductors'; import { type DeadlinesAndEventsQuery, type DeadlinesAndEventsQueryVariables, } from '#generated/types/graphql'; +import useCurrentDate from '#hooks/useCurrentDate'; +import { formatDateTime } from '#utils/common'; import { type GeneralEvent } from '#utils/types'; import Slide from '../Slide'; @@ -97,13 +99,19 @@ function DeadlineSection() { })) ?? []), ].sort((a, b) => compareNumber(a.remainingDays, b.remainingDays)); }, [events, projects]); + const todayDate = useCurrentDate(); return ( } + primaryDescription={( +
+
{formatDateTime(todayDate)}
+ +
+ )} secondaryHeading="Upcoming Events" secondaryContent={upcomingEvents.map( (generalEvent, index) => ( diff --git a/src/views/DailyStandup/DeadlineSection/styles.module.css b/src/views/DailyStandup/DeadlineSection/styles.module.css index d1200ed..c7c5dcd 100644 --- a/src/views/DailyStandup/DeadlineSection/styles.module.css +++ b/src/views/DailyStandup/DeadlineSection/styles.module.css @@ -1,3 +1,10 @@ +.primary-section{ + display: flex; + flex-direction: column; + text-align: center; + gap: var(--spacing-2xl); +} + .separator { display: flex; align-items: center; diff --git a/src/views/DailyStandup/Slide/styles.module.css b/src/views/DailyStandup/Slide/styles.module.css index d468301..269ed4c 100644 --- a/src/views/DailyStandup/Slide/styles.module.css +++ b/src/views/DailyStandup/Slide/styles.module.css @@ -11,7 +11,7 @@ gap: var(--spacing-lg); .heading { - word-break: break-word; + overflow-wrap: break-word; color: var(--color-primary); font-size: var(--font-size-3xl); font-weight: var(--font-weight-light); @@ -38,19 +38,19 @@ text-align: center; .primary-pre-text { - word-break: break-word; + overflow-wrap: break-word; font-size: var(--font-size-xl); } .primary-heading { - word-break: break-word; + overflow-wrap: break-word; color: var(--color-primary); font-size: var(--font-size-3xl); font-weight: var(--font-weight-light); } .primary-description { - word-break: break-word; + overflow-wrap: break-word; color: var(--color-text-light); font-size: var(--font-size-xl); } diff --git a/src/views/DailyStandup/StartSection/index.tsx b/src/views/DailyStandup/StartSection/index.tsx index 4fcc837..f793aa4 100644 --- a/src/views/DailyStandup/StartSection/index.tsx +++ b/src/views/DailyStandup/StartSection/index.tsx @@ -8,14 +8,16 @@ import { } from 'urql'; import AvailabilityIndicator from '#components/AvailabilityIndicator'; -import Clock from '#components/Clock'; import DisplayPicture from '#components/DisplayPicture'; +import StandupConductors from '#components/StandupConductors'; import { type JournalLeaveTypeEnum, type JournalWorkFromHomeTypeEnum, type UsersAvailabilityQuery, type UsersAvailabilityQueryVariables, } from '#generated/types/graphql'; +import useCurrentDate from '#hooks/useCurrentDate'; +import { formatDateTime } from '#utils/common'; import Slide from '../Slide'; @@ -77,6 +79,7 @@ function StartSection() { bar.displayName, ), ); + const todayDate = useCurrentDate(); return ( } - secondaryHeading="Availability" + primaryDescription={( +
+
{formatDateTime(todayDate)}
+ +
+ )} + secondaryHeading="Unavailability" secondaryContent={sortedUsers?.map((user) => (
( + compareDate(userAuth?.loginExpire, todayDate) / (60 * 60 * 1000 * 24) + ), [ + todayDate, + userAuth?.loginExpire, + ]); return (
@@ -25,6 +47,11 @@ export function Component() { )} /> )} + {userAuth && daysBeforeLogout < REMAINING_DAYS_THRESHOLD && ( +
+ {`You'll be automatically logged out in ${Math.floor(daysBeforeLogout)} days. Please re-login to avoid unexpected logout.`} +
+ )}
diff --git a/src/views/RootLayout/styles.module.css b/src/views/RootLayout/styles.module.css index b8cea23..b19f316 100644 --- a/src/views/RootLayout/styles.module.css +++ b/src/views/RootLayout/styles.module.css @@ -20,6 +20,13 @@ } } + .nagbar { + flex-shrink: 0; + background: var(--color-primary); + padding: var(--spacing-xs); + text-align: center; + color: var(--color-tertiary); + } .navbar { flex-shrink: 0; }