Skip to content

Commit

Permalink
refactor: improve response display style (#509)
Browse files Browse the repository at this point in the history
fix: highlight and round annotation style
  • Loading branch information
swouf authored Sep 25, 2024
1 parent 0a4dc1f commit 578b64c
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 103 deletions.
10 changes: 10 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const DEFAULT_LANG = 'en';

export const DEFAULT_EVALUATION_TYPE = EvaluationType.Vote;

export const HIGHLIGHT_RESPONSE_TIME_MS = 2000;

export const CATEGORY_COLORS = [
'#ffadad',
'#ffd6a5',
Expand All @@ -59,3 +61,11 @@ export const CATEGORY_COLORS = [
'#bdb2ff',
'#ffc6ff',
];

export const RESPONSES_TOP_COLORS = [
'#DBF9E7',
'#E8C9FA',
'#EFE9B7',
'#F6CCB0',
'#B0E1FA',
];
261 changes: 164 additions & 97 deletions src/modules/common/response/Response.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@ import CardContent from '@mui/material/CardContent';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import grey from '@mui/material/colors/grey';

import { ResponseAppData } from '@/config/appDataTypes';

import Chip from '@mui/material/Chip';
import { useLocalContext } from '@graasp/apps-query-client';
import Box from '@mui/material/Box';
import { RESPONSE_CY } from '@/config/selectors';
import { EvaluationType } from '@/interfaces/evaluation';
import { ResponseEvaluation } from '@/interfaces/response';
import Stack from '@mui/material/Stack';
import styled from '@mui/material/styles/styled';
import { SxProps, useTheme } from '@mui/material/styles';
import { Theme } from '@mui/material/styles/createTheme';
import { RESPONSES_TOP_COLORS } from '@/config/constants';
import { ResponseVisibilityMode } from '@/interfaces/interactionProcess';
import { useSettings } from '@/modules/context/SettingsContext';
import Link from '@mui/material/Link';
import RatingsVisualization from './visualization/RatingsVisualization';
import Vote from './evaluation/Vote';
import Rate from './evaluation/Rate';
Expand All @@ -30,6 +36,11 @@ const ResponsePart: FC<{ children: string }> = ({ children }) => (
</Typography>
);

const TopAnnotationTypography = styled(Typography)(() => ({
fontWeight: 'bold',
textTransform: 'uppercase',
}));

interface ResponseProps {
response: ResponseAppData<ResponseEvaluation>;
onSelect?: (id: string) => void;
Expand Down Expand Up @@ -58,9 +69,11 @@ const Response: FC<ResponseProps> = ({
const { t } = useTranslation('translations', { keyPrefix: 'RESPONSE_CARD' });
const { t: generalT } = useTranslation('translations');
const { accountId } = useLocalContext();
const theme = useTheme();

const { id, data, creator } = response;
const { response: responseContent, round, parentId, assistantId } = data;
const { activity } = useSettings();

const isOwn = creator?.id === accountId && typeof assistantId === 'undefined';
const isAiGenerated = useMemo(
Expand All @@ -71,6 +84,7 @@ const Response: FC<ResponseProps> = ({
const showSelectButton = typeof onSelect !== 'undefined';
const showDeleteButton = typeof onDelete !== 'undefined' && isOwn;
const showActions = showDeleteButton || showSelectButton;
const isLive = activity.mode === ResponseVisibilityMode.OpenLive;

const renderEvaluationComponent = (): JSX.Element | null => {
switch (evaluationType) {
Expand All @@ -85,108 +99,161 @@ const Response: FC<ResponseProps> = ({
}
};

return (
<Card
id={id}
variant="outlined"
sx={{
minWidth: '160pt',
backgroundColor: highlight ? 'hsla(0, 100%, 90%, 0.3)' : 'transparent',
boxShadow: highlight ? '0 0 8pt 4pt hsla(0, 100%, 90%, 0.3)' : 'none',
}}
data-cy={RESPONSE_CY}
>
<CardContent sx={{ minHeight: '32pt' }}>
{typeof responseContent === 'string' ? (
<ResponsePart>{responseContent}</ResponsePart>
) : (
responseContent?.map((r, index) => (
<>
{/* {index !== 0 && <br />} */}
<ResponsePart key={index}>{r}</ResponsePart>
</>
))
)}
<Box
const renderTopResponseAnnotation = (): JSX.Element => {
if (isAiGenerated) {
return (
<TopAnnotationTypography variant="caption" color="primary">
{t('AI_GENERATED')}
</TopAnnotationTypography>
);
}
if (isOwn) {
return (
<TopAnnotationTypography color="GrayText" variant="caption">
{t('OWN')}
</TopAnnotationTypography>
);
}
return <div />;
};

const getTopAnnotationBoxStyle = (): SxProps<Theme> => {
if (isAiGenerated) {
return {
backgroundColor: 'white',
};
}
const rLength =
typeof responseContent === 'string'
? responseContent.length
: responseContent.length;
const colorIndex = rLength % RESPONSES_TOP_COLORS.length;
return {
backgroundColor: RESPONSES_TOP_COLORS[colorIndex],
};
};

const getCardActionStyle = (): SxProps<Theme> | undefined =>
isAiGenerated
? {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
}
: undefined;

if (responseContent) {
return (
<Box
minWidth="160pt"
borderRadius="4px"
sx={{
...getTopAnnotationBoxStyle(),
boxShadow: highlight
? '0 0 8pt 4pt hsla(47.8, 100%, 50%, 0.8)'
: 'none',
transition: 'box-shadow 500ms',
}}
>
<Stack
height="2em"
direction="row"
alignItems="center"
justifyContent="center"
>
{renderTopResponseAnnotation()}
</Stack>
<Card
id={id}
variant="outlined"
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
// backgroundColor: highlight ? 'hsla(0, 100%, 90%, 0.3)' : 'white',
}}
data-cy={RESPONSE_CY}
>
<Typography variant="body2" sx={{ color: grey.A700 }}>
{generalT('ROUND', { round })}
{parentId && (
<>
{' • '}
<a
href={`#${parentId}`}
onClick={() => {
document.getElementById(parentId)?.scrollIntoView();
onParentIdeaClick(parentId);
}}
>
{t('PARENT_IDEA')}
</a>
</>
<CardContent sx={{ minHeight: '32pt', ...getCardActionStyle() }}>
{typeof responseContent === 'string' ? (
<ResponsePart>{responseContent}</ResponsePart>
) : (
responseContent?.map((r, index) => (
<>
{/* {index !== 0 && <br />} */}
<ResponsePart key={index}>{r}</ResponsePart>
</>
))
)}
</Typography>
{isOwn && (
<Chip
sx={{ ml: '1rem' }}
variant="outlined"
size="small"
color="info"
label={t('OWN')}
/>
)}
{isAiGenerated && (
<Chip
sx={{ ml: '1rem' }}
variant="outlined"
size="small"
color="success"
label={t('AI_GENERATED')}
/>
)}
</Box>
</CardContent>
{renderEvaluationComponent()}
{showRatings && <RatingsVisualization />}
{typeof nbrOfVotes !== 'undefined' && <Votes votes={nbrOfVotes} />}
{showActions && (
<>
<Divider />
<CardActions
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
{showSelectButton && (
<Button
disabled={!enableBuildAction}
onClick={() => {
if (typeof onSelect !== 'undefined') onSelect(id);
<Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<Typography
variant="body2"
sx={{
color: isAiGenerated
? theme.palette.grey.A400
: theme.palette.grey.A700,
}}
>
{t('BUILD_ON_THIS')}
</Button>
)}
{showDeleteButton && (
<IconButton
sx={{ marginLeft: 'auto' }}
onClick={() => onDelete(id)}
{!isLive && generalT('ROUND', { round })}
{parentId && (
<>
{' • '}
<Link
href={`#${parentId}`}
onClick={() => {
document.getElementById(parentId)?.scrollIntoView();
onParentIdeaClick(parentId);
}}
>
{t('PARENT_IDEA')}
</Link>
</>
)}
</Typography>
</Box>
</CardContent>
{renderEvaluationComponent()}
{showRatings && <RatingsVisualization />}
{typeof nbrOfVotes !== 'undefined' && <Votes votes={nbrOfVotes} />}
{showActions && (
<>
<Divider />
<CardActions
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<DeleteIcon />
</IconButton>
)}
</CardActions>
</>
)}
</Card>
);
{showSelectButton && (
<Button
disabled={!enableBuildAction}
onClick={() => {
if (typeof onSelect !== 'undefined') onSelect(id);
}}
>
{t('BUILD_ON_THIS')}
</Button>
)}
{showDeleteButton && (
<IconButton
sx={{ marginLeft: 'auto' }}
onClick={() => onDelete(id)}
>
<DeleteIcon />
</IconButton>
)}
</CardActions>
</>
)}
</Card>
</Box>
);
}
return null;
};

export default Response;
2 changes: 1 addition & 1 deletion src/modules/responseCollection/MyResponses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const MyResponses: FC = () => {
<Grid container spacing={2}>
{myResponses ? (
myResponses.map((response) => (
<Grid key={response.id} item xl={6} sm={6} xs={12}>
<Grid key={response.id} item xl={2} sm={4} xs={6}>
<Response
key={response.id}
response={response}
Expand Down
16 changes: 13 additions & 3 deletions src/modules/responseCollection/ResponseChoose.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useMemo, useState } from 'react';
import { FC, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
Expand All @@ -12,6 +12,7 @@ import Response from '@/modules/common/response/Response';
import { useAppDataContext } from '@/modules/context/AppDataContext';

import { Loader } from '@graasp/ui';
import { HIGHLIGHT_RESPONSE_TIME_MS } from '@/config/constants';
import { useSettings } from '../context/SettingsContext';

interface ResponseChooseProps {
Expand All @@ -23,6 +24,7 @@ const ResponseChoose: FC<ResponseChooseProps> = ({ responses, onChoose }) => {
const { t } = useTranslation();

const [highlightId, setHighlightId] = useState<string>();
const highlightTimeout = useRef<NodeJS.Timeout>();

const { isLoading, invalidateAppData, deleteAppData } = useAppDataContext();
const { instructions } = useSettings();
Expand Down Expand Up @@ -69,14 +71,22 @@ const ResponseChoose: FC<ResponseChooseProps> = ({ responses, onChoose }) => {
<Grid container spacing={2}>
{responses
? responses.map((response) => (
<Grid key={response.id} item xl={6} lg={6} xs={12}>
<Grid key={response.id} item xl={2} sm={4} xs={6}>
<Response
key={response.id}
response={response}
onSelect={handleChoose}
onDelete={() => deleteAppData({ id: response.id })}
highlight={highlightId === response.id}
onParentIdeaClick={(id: string) => setHighlightId(id)}
onParentIdeaClick={(id: string) => {
setHighlightId(id);
highlightTimeout.current = setTimeout(() => {
setHighlightId(undefined);
if (highlightTimeout?.current) {
clearTimeout(highlightTimeout.current);
}
}, HIGHLIGHT_RESPONSE_TIME_MS);
}}
/>
</Grid>
))
Expand Down
2 changes: 1 addition & 1 deletion src/modules/responseEvaluation/ResponseEvaluation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const ResponseEvaluation: FC = () => {
<Grid container spacing={2}>
{responses
? responses.map((response) => (
<Grid item key={response.id} md={6} sm={12} xs={12}>
<Grid item key={response.id} xl={2} sm={4} xs={6}>
<Response
key={response.id}
response={response}
Expand Down
Loading

0 comments on commit 578b64c

Please sign in to comment.