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;
}