Skip to content

Commit

Permalink
[DEV-1315]: highlight and hide webinars questions (#646)
Browse files Browse the repository at this point in the history
* feat(DEV-1315): highlight and hide webinars questions

* feat(DEV-1315): add UpdateItem to host user

* chore: fix typo

* feat(DEV-1315): remove definitions from terraform

* chore: add changeset

* feat(DEV-1358): remove unused code

* feat(DEV-1315): update webinars questions' row

* feat(DEV-1315): update style

Co-authored-by: Marco Ponchia <[email protected]>

* feat(DEV-1315): update style

Co-authored-by: Marco Ponchia <[email protected]>

* feat(DEV-1315): update style

Co-authored-by: Marco Ponchia <[email protected]>

* chore: update changeset

Co-authored-by: marcobottaro <[email protected]>

* feat(DEV-1315): add user names

* chore: remove unused code

* feat(DEV-1315): update tests

* feat(DEV-1315): update code

* chore: remove unused code

* test: update tests

* feat(DEV-1315): update logic

* feat(DEV-1315): add default tcColor

* feat(DEV-1315): update IconButton visibility icon

Co-authored-by: Marco Ponchia <[email protected]>

* feat(DEV-1315): update WebinarQuestion codec to accept undefined values

* chore: remove test webinar

* feat(DEV-1315): unify update question logic

* chore: remove test webinar

* Webinar questions review (#704)

* feat(DEV-1315): update translation

Co-authored-by: marcobottaro <[email protected]>

---------

Co-authored-by: Marco Ponchia <[email protected]>
Co-authored-by: marcobottaro <[email protected]>
Co-authored-by: AF <[email protected]>
Co-authored-by: Marco Comi <[email protected]>
  • Loading branch information
5 people authored Mar 12, 2024
1 parent b7ed47f commit 458c58f
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 67 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-pans-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nextjs-website": minor
---

Add the highlight and hide action on webinars' questions list
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { WebinarQuestion } from '@/lib/webinars/webinarQuestions';
import { Box, IconButton, TableCell, TableRow, useTheme } from '@mui/material';
import { CopyToClipboardButton } from '@pagopa/mui-italia';
import DOMPurify from 'dompurify';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import Visibility from '@mui/icons-material/Visibility';
import { useFormatter, useTranslations } from 'next-intl';
import AutoFixOffIcon from '@mui/icons-material/AutoFixOff';

type WebinarQuestionRowProps = {
question: WebinarQuestion;
userName: string;
onHighlight: (highlighted: boolean) => Promise<void>;
onHide: (hidden: boolean) => Promise<void>;
};

export default function WebinarQuestionRow({
question,
userName,
onHide,
onHighlight,
}: WebinarQuestionRowProps) {
const formatter = useFormatter();
const { palette } = useTheme();
const t = useTranslations('webinar.questionList');

const { hiddenBy, highlightedBy } = question;

const isHidden = !!hiddenBy;
const isHighlighted = !!highlightedBy;

const tcColor =
isHighlighted && !isHidden ? palette.common.white : palette.text.primary;

return (
<TableRow
hover
key={question.id.createdAt.toISOString()}
sx={{
'&:last-child td, &:last-child th': { border: 0 },
'&.MuiTableRow-hover:hover': {
backgroundColor:
isHighlighted && !isHidden
? palette.primary.dark
: palette.action.hover,
},
backgroundColor:
isHighlighted && !isHidden ? palette.primary.light : '',
fontStyle: isHidden ? 'italic' : '',
position: 'relative',
}}
>
<TableCell
sx={{
color: tcColor,
}}
>
{!isHidden
? formatter.dateTime(question.id.createdAt, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
})
: ''}
</TableCell>
<TableCell
width='70%'
sx={{
color: tcColor,
}}
>
{!isHidden
? question.question
: hiddenBy === userName
? `${t('hiddenByMe')}: (${question.question})`
: `${t('hiddenBy')}: ${hiddenBy}`}
</TableCell>
<TableCell>
<Box display={'flex'} justifyContent={'space-between'}>
{(!isHidden || (isHidden && hiddenBy === userName)) && (
<IconButton
onClick={() => onHide(!isHidden)}
sx={{ color: tcColor }}
>
{!isHidden ? <VisibilityOffIcon /> : <Visibility />}
</IconButton>
)}

{!isHidden &&
(userName === highlightedBy || !isHighlighted ? (
<IconButton
onClick={() => onHighlight(!isHighlighted)}
sx={{ color: tcColor }}
>
{!isHidden ? <AutoFixHighIcon /> : <AutoFixOffIcon />}
</IconButton>
) : (
<Box
sx={{
position: 'absolute',
top: 4,
left: 16,
fontSize: 14,
color: palette.common.white,
}}
>
{t('highlightedBy')}: {highlightedBy}
</Box>
))}

{!isHidden && (
<CopyToClipboardButton
sx={{ color: tcColor }}
value={DOMPurify.sanitize(question.question)}
></CopyToClipboardButton>
)}
</Box>
</TableCell>
</TableRow>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const WebinarQuestionsForm = ({

setFormState('submitting');
return await sendWebinarQuestion({
webinarId: webinarSlug,
slug: webinarSlug,
question: question,
});
}, [webinarSlug, question]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import { Webinar } from '@/lib/types/webinar';
import { WebinarQuestion } from '@/lib/webinars/webinarQuestions';
import { useWebinar } from '@/helpers/webinar.helpers';
import { useEffect } from 'react';
import { getWebinarQuestionList } from '@/lib/webinarApi';
import { useFormatter, useTranslations } from 'next-intl';
import { CopyToClipboardButton } from '@pagopa/mui-italia';
import DOMPurify from 'isomorphic-dompurify';
import {
getWebinarQuestionList,
updateWebinarQuestion,
} from '@/lib/webinarApi';
import { useTranslations } from 'next-intl';
import Spinner from '@/components/atoms/Spinner/Spinner';
import useSWR from 'swr';
import PageNotFound from '@/app/not-found';
import { fetchWebinarsQuestionsIntervalMs } from '@/config';
import { useUser } from '@/helpers/user.helper';
import WebinarQuestionRow from '@/components/molecules/WebinarQuestion/WebinarQuestionRow';

type WebinarQuestionsTemplateProps = {
webinar: Webinar;
Expand All @@ -29,7 +32,7 @@ type WebinarQuestionsTemplateProps = {
const WebinarQuestionsTemplate = ({
webinar,
}: WebinarQuestionsTemplateProps) => {
const formatter = useFormatter();
const { user, loading } = useUser();
const { webinarState, setWebinar } = useWebinar();
const t = useTranslations('webinar.questionList');

Expand All @@ -41,12 +44,12 @@ const WebinarQuestionsTemplate = ({
webinar && setWebinar(webinar);
}, [webinar]);

if (error) return <PageNotFound />;
else if (!data) return <Spinner />;
if (error || (!loading && !user)) return <PageNotFound />;
else if (!data || loading || !user) return <Spinner />;
else {
const userName = `${user.attributes['given_name']} ${user.attributes['family_name']}`;
const sortedQuestions = [...data].sort(
(a: WebinarQuestion, b: WebinarQuestion) =>
b.createdAt.getTime() - a.createdAt.getTime()
(a, b) => b.id.createdAt.getTime() - a.id.createdAt.getTime()
);

return (
Expand All @@ -68,28 +71,32 @@ const WebinarQuestionsTemplate = ({
</TableRow>
</TableHead>
<TableBody>
{sortedQuestions.map((row) => (
<TableRow
hover
key={row.createdAt.toJSON()}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell>
{formatter.dateTime(row.createdAt, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
})}
</TableCell>
<TableCell width='70%'>{row.question}</TableCell>
<TableCell>
<CopyToClipboardButton
value={DOMPurify.sanitize(row.question)}
></CopyToClipboardButton>
</TableCell>
</TableRow>
{sortedQuestions.map((webinarQuestion) => (
<WebinarQuestionRow
key={webinarQuestion.id.createdAt.toISOString()}
question={webinarQuestion}
userName={userName}
onHide={async (hide) =>
await updateWebinarQuestion({
id: webinarQuestion.id,
updates: {
hiddenBy: hide
? { operation: 'update', value: userName }
: { operation: 'remove' },
},
})
}
onHighlight={async (highlight) =>
await updateWebinarQuestion({
id: webinarQuestion.id,
updates: {
highlightedBy: highlight
? { operation: 'update', value: userName }
: { operation: 'remove' },
},
})
}
/>
))}
</TableBody>
</Table>
Expand Down
5 changes: 5 additions & 0 deletions apps/nextjs-website/src/lib/webinarApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import * as TE from 'fp-ts/TaskEither';
import { makeBrowserEnv } from '@/BrowserEnv';
import {
InsertWebinarQuestion,
WebinarQuestionUpdate,
insertWebinarQuestion,
listWebinarQuestions,
updateWebinarQuestion as _updateWebinarQuestion,
} from './webinars/webinarQuestions';
import { makeBrowserConfig, publicEnv } from '@/BrowserConfig';

Expand Down Expand Up @@ -37,3 +39,6 @@ export const sendWebinarQuestion = (question: InsertWebinarQuestion) =>

export const getWebinarQuestionList = (webinarId: string) =>
pipe(listWebinarQuestions(webinarId)(browserEnv), makePromiseFromTE)();

export const updateWebinarQuestion = (update: WebinarQuestionUpdate) =>
pipe(_updateWebinarQuestion(update)(browserEnv), makePromiseFromTE)();
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ import {
WebinarEnv,
insertWebinarQuestion,
listWebinarQuestions,
updateWebinarQuestion,
} from '../webinarQuestions';
import { PutItemCommand, QueryCommand } from '@aws-sdk/client-dynamodb';
import {
PutItemCommand,
QueryCommand,
UpdateItemCommand,
} from '@aws-sdk/client-dynamodb';
import { makeDynamodbItemFromWebinarQuestion } from '../dynamodb/codec';

const aWebinarQuestion = {
webinarId: 'aWebinarId',
id: {
slug: 'aWebinarId',
createdAt: new Date(0),
},
question: 'aQuestion',
createdAt: new Date(),
};
const aInsertWebinarQuestion = {
slug: aWebinarQuestion.id.slug,
question: aWebinarQuestion.question,
};
const aDynamoDBItem = makeDynamodbItemFromWebinarQuestion({
...aWebinarQuestion,
Expand All @@ -22,12 +33,12 @@ const makeTestWebinarEnv = () => {
const dynamoDBClientMock = mock<WebinarEnv['dynamoDBClient']>();
const nowDateMock = jest.fn();
// default mock behaviour
dynamoDBClientMock.send.mockImplementation((cmd) => {
if (cmd instanceof PutItemCommand) return Promise.resolve({});
else if (cmd instanceof QueryCommand)
return Promise.resolve({ Items: [aDynamoDBItem] });
// eslint-disable-next-line functional/no-promise-reject
else return Promise.reject(new Error('Unsupported command'));
dynamoDBClientMock.send.mockImplementation(async (cmd) => {
if (cmd instanceof PutItemCommand) return {};
else if (cmd instanceof UpdateItemCommand) return {};
else if (cmd instanceof QueryCommand) return { Items: [aDynamoDBItem] };
// eslint-disable-next-line functional/no-throw-statements
else throw new Error('Unsupported command');
});
nowDateMock.mockImplementation(() => nowDate);
const env = {
Expand All @@ -41,7 +52,7 @@ describe('webinarQuestions', () => {
describe('insertWebinarQuestion', () => {
it('should send dynamodb put command', async () => {
const { env, dynamoDBClientMock, nowDateMock } = makeTestWebinarEnv();
const actual = await insertWebinarQuestion(aWebinarQuestion)(env)();
const actual = await insertWebinarQuestion(aInsertWebinarQuestion)(env)();
const expected = E.right(undefined);

expect(dynamoDBClientMock.send).toBeCalledTimes(1);
Expand All @@ -56,7 +67,7 @@ describe('webinarQuestions', () => {
// eslint-disable-next-line functional/no-promise-reject
dynamoDBClientMock.send.mockImplementation(() => Promise.reject(error));

const actual = await insertWebinarQuestion(aWebinarQuestion)(env)();
const actual = await insertWebinarQuestion(aInsertWebinarQuestion)(env)();
const expected = E.left(error);

expect(dynamoDBClientMock.send).toBeCalledTimes(1);
Expand All @@ -65,23 +76,39 @@ describe('webinarQuestions', () => {
});
});

describe('updateWebinarQuestion', () => {
it('should send dynamodb update command', async () => {
const { env, dynamoDBClientMock } = makeTestWebinarEnv();
const actual = await updateWebinarQuestion({
id: aWebinarQuestion.id,
updates: {
highlightedBy: { operation: 'remove' },
},
})(env)();
const expected = E.right(undefined);

expect(dynamoDBClientMock.send).toBeCalledTimes(1);
expect(actual).toStrictEqual(expected);
});
});

describe('listWebinarQuestion', () => {
it('should return a list of webinar questions', async () => {
const { env } = makeTestWebinarEnv();
const { webinarId } = aWebinarQuestion;
const actual = await listWebinarQuestions(webinarId)(env)();
const { slug } = aWebinarQuestion.id;
const actual = await listWebinarQuestions(slug)(env)();
const expected = E.right([aWebinarQuestion]);

expect(actual).toStrictEqual(expected);
});

it('should return an empty list if questions are undefined', async () => {
const { env, dynamoDBClientMock } = makeTestWebinarEnv();
const { webinarId } = aWebinarQuestion;
const { slug } = aWebinarQuestion.id;
dynamoDBClientMock.send.mockImplementation(() =>
Promise.resolve({ Items: undefined })
);
const actual = await listWebinarQuestions(webinarId)(env)();
const actual = await listWebinarQuestions(slug)(env)();
const expected = E.right([]);

expect(actual).toStrictEqual(expected);
Expand Down
Loading

0 comments on commit 458c58f

Please sign in to comment.