Compare commits

...

3 Commits

Author SHA1 Message Date
Lebaud Antoine 9eb4a1e9e3 🔧(backend) udpate email invitation sender
Replace the placeholder setting by a randomly picked 'do not reply'
email adress. Might be improved later on.
2024-04-02 23:26:38 +02:00
Lebaud Antoine d8b6bcc9eb 📱(frontend) handle extremely long name
Currently, with a very long group name (~100 char), text was overflowing
the flex elements.

I reworked the current layout to prevent this issue in Team info and
Team pannel.

I needed to debug some buggy flex layout issues. I decided to refactor
some styling, and ended up doing a big refactoring.

Working with Box and Text components cause high friction when
manipulating them. They use mix styling approaches ... which is quite
misleading for any developper joining the project.

It's mainly due to the fact that creating such generic wrappers,
relying on (heavy) interpolation functions might be against
styled component spirit.

I love manipulating inline styling, but styled component might not
be the best tool to take this approach.

I tried to align main screen with Johann's design to be pixel-perfect.
However, the actual design is not appropriate for responsive screen.
2024-04-02 23:21:44 +02:00
Lebaud Antoine 418bed945a 🩹(frontend) display email or name in delete modal
User's name could be null, leading to an empty text box when opening the
delete modal.

Prevent it by displaying user's name or email when deleting an user.
2024-04-02 22:53:43 +02:00
14 changed files with 258 additions and 129 deletions
+1 -1
View File
@@ -268,7 +268,7 @@ class Base(Configuration):
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
EMAIL_FROM = values.Value("do-not-reply@desk.beta.numerique.gouv.fr")
AUTH_USER_MODEL = "core.User"
INVITATION_VALIDITY_DURATION = 604800 # 7 days, in seconds
+17 -15
View File
@@ -1,28 +1,30 @@
import { PropsWithChildren } from 'react';
import styled from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { Box, BoxType } from '.';
import { BoxType } from '.';
export const Card = ({
children,
$css,
...props
}: PropsWithChildren<BoxType>) => {
const Wrapper = styled.div`
position: relative;
background-color: white;
border-radius: 0.25rem;
box-shadow: 2px 2px 5px var(--shadow-color) 88;
border: 1px solid var(--border-color);
`;
export const Card = ({ children, ...props }: PropsWithChildren<BoxType>) => {
const { colorsTokens } = useCunninghamTheme();
return (
<Box
$background="white"
$radius="4px"
$css={`
box-shadow: 2px 2px 5px ${colorsTokens()['primary-300']}88;
border: 1px solid ${colorsTokens()['card-border']};
${$css}
`}
<Wrapper
style={{
'--border-color': colorsTokens()['card-border'],
'--shadow-color': colorsTokens()['primary-300'],
}}
{...props}
>
{children}
</Box>
</Wrapper>
);
};
+21 -7
View File
@@ -1,17 +1,31 @@
import styled from 'styled-components';
import { Box } from '@/components';
import { HEADER_HEIGHT, Header } from '@/features/header';
import { Menu } from '@/features/menu';
import { MENU_WIDTH, Menu } from '@/features/menu';
const Container = styled.div`
display: flex;
flex-direction: row;
flex: 1;
`;
const Main = styled.main`
display: flex;
flex-wrap: nowrap;
flex-basis: 100%;
height: calc(100vh - ${HEADER_HEIGHT});
max-width: calc(100% - ${MENU_WIDTH});
`;
export function MainLayout({ children }: { children: React.ReactNode }) {
return (
<Box $height="100vh" $css="overflow:hidden;">
<Box>
<Header />
<Box $css="flex: 1;" $direction="row">
<Container>
<Menu />
<Box as="main" $height={`calc(100vh - ${HEADER_HEIGHT})`} $width="100%">
{children}
</Box>
</Box>
<Main>{children}</Main>
</Container>
</Box>
);
}
@@ -110,9 +110,7 @@ export const SearchMembers = ({
onInputChange={onInputChangeHandle}
inputValue={input}
placeholder={t('Search new members (name or email)')}
noOptionsMessage={() =>
t('Invite new members to {{teamName}}', { teamName: team.name })
}
noOptionsMessage={() => t('Invite new members to your team')}
onChange={(value) => {
setInput('');
setUserQuery('');
@@ -1,6 +1,7 @@
import {
Button,
DataGrid,
Input,
SortModel,
usePagination,
} from '@openfun/cunningham-react';
@@ -17,6 +18,7 @@ import { useTeamAccesses } from '../api/';
import { PAGE_SIZE } from '../conf';
import { MemberAction } from './MemberAction';
import styled from 'styled-components';
interface MemberGridProps {
team: Team;
@@ -50,6 +52,37 @@ function formatSortModel(
return sort === 'desc' ? `-${orderingField}` : orderingField;
}
const Wrapper = styled(Card)`
.c__pagination {
width: fit-content;
margin-bottom: 1rem;
}
.c__pagination__goto {
display: none;
}
table th:first-child,
table td:first-child {
padding-right: 0;
width: 3.5rem;
}
table td:last-child {
text-align: right;
}
`;
const Actions = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 1.25rem;
.filter-container {
width: 50%;
}
`;
export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
const [isModalMemberOpen, setIsModalMemberOpen] = useState(false);
const { t } = useTranslation();
@@ -96,7 +129,14 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
return (
<>
{currentRole !== Role.MEMBER && (
<Box className="m-b mb-s" $align="flex-end">
<Actions>
<div className="filter-container">
<Input
rightIcon={<span className="material-icons">search</span>}
label="Filtrer la liste des membres"
fullWidth
/>
</div>
<Button
aria-label={t('Add members to the team')}
style={{
@@ -108,29 +148,10 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
>
{t('Add')}
</Button>
</Box>
</Actions>
)}
<Card
className="m-b pb-s"
$overflow="auto"
$css={`
margin-top:0;
& .c__pagination__goto {
display: none;
}
& table th:first-child,
& table td:first-child {
padding-right: 0;
width: 3.5rem;
}
& table td:last-child {
text-align: right;
}
`}
aria-label={t('List members card')}
>
<Wrapper aria-label={t('List members card')}>
{error && <TextErrors causes={error.cause} />}
<DataGrid
columns={[
{
@@ -179,7 +200,7 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
onSortModelChange={setSortModel}
sortModel={sortModel}
/>
</Card>
</Wrapper>
{isModalMemberOpen && (
<ModalAddMembers
currentRole={currentRole}
@@ -128,7 +128,7 @@ export const ModalDelete = ({ access, onClose, team }: ModalDeleteProps) => {
$theme="primary"
>
<IconUser width={20} height={20} />
<Text>{access.user.name}</Text>
<Text>{access.user.name || access.user.email}</Text>
</Text>
</Box>
</Modal>
@@ -2,7 +2,6 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import IconGroup from '@/assets/icons/icon-group.svg';
import { Box } from '@/components/';
import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
import MenuItem from './MenuItems';
@@ -11,25 +10,34 @@ import IconContacts from './assets/icon-contacts.svg';
import IconSearch from './assets/icon-search.svg';
import IconFavorite from './assets/icon-stars.svg';
import styled from 'styled-components';
export const MENU_WIDTH = '62px';
const Wrapper = styled.menu`
display: flex;
flex-direction: column;
background-color: var(--bg-color);
width: ${MENU_WIDTH};
gap: 0.8rem;
`;
export const Menu = () => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
return (
<Box
as="menu"
className="m-0 p-0"
$background={colorsTokens()['primary-800']}
$height="100%"
$justify="space-between"
<Wrapper
className="m-0 p-0 pt-l"
style={{
'--bg-color': colorsTokens()['primary-800'],
}}
>
<Box className="pt-l" $direction="column" $gap="0.8rem">
<MenuItem Icon={IconSearch} label={t('Search')} href="/" />
<MenuItem Icon={IconFavorite} label={t('Favorite')} href="/favorite" />
<MenuItem Icon={IconRecent} label={t('Recent')} href="/recent" />
<MenuItem Icon={IconContacts} label={t('Contacts')} href="/contacts" />
<MenuItem Icon={IconGroup} label={t('Groups')} href="/groups" />
</Box>
</Box>
<MenuItem Icon={IconSearch} label={t('Search')} href="/" />
<MenuItem Icon={IconFavorite} label={t('Favorite')} href="/favorite" />
<MenuItem Icon={IconRecent} label={t('Recent')} href="/recent" />
<MenuItem Icon={IconContacts} label={t('Contacts')} href="/contacts" />
<MenuItem Icon={IconGroup} label={t('Groups')} href="/groups" />
</Wrapper>
);
};
@@ -2,6 +2,7 @@ import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import IconGroup from '@/assets/icons/icon-group2.svg';
import { Box, Card, StyledLink, Text } from '@/components';
@@ -11,6 +12,18 @@ import { useCreateTeam } from '../api';
import { InputTeamName } from './InputTeamName';
const Wrapper = styled(Card)`
height: 70%;
padding: 2.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
max-width: 24rem;
min-width: 22rem;
margin: auto;
`;
export const CardCreateTeam = () => {
const { t } = useTranslation();
const router = useRouter();
@@ -28,15 +41,7 @@ export const CardCreateTeam = () => {
const { colorsTokens } = useCunninghamTheme();
return (
<Card
className="p-b"
$height="70%"
$justify="space-between"
$width="100%"
$maxWidth="24rem"
$minWidth="22rem"
aria-label={t('Create new team card')}
>
<Wrapper aria-label={t('Create new team card')}>
<Box $gap="1rem">
<Box $align="center">
<IconGroup
@@ -64,6 +69,6 @@ export const CardCreateTeam = () => {
{t('Create the team')}
</Button>
</Box>
</Card>
</Wrapper>
);
};
@@ -60,11 +60,12 @@ export const Panel = () => {
`}
>
<Box
className="pr-l pl-s pt-s pb-s"
className="pl-s pt-s pb-s"
$direction="row"
$align="center"
$justify="space-between"
$css={`
padding-right: 2.4rem;
border-bottom: 1px solid ${colorsTokens()['primary-300']};
`}
>
@@ -63,7 +63,12 @@ export const TeamItem = ({ team }: TeamProps) => {
`}
>
<StyledLink className="p-s pt-t pb-t" href={`/teams/${team.id}`}>
<Box $align="center" $direction="row" $gap="0.5rem">
<Box
$align="center"
$direction="row"
$gap="0.5rem"
$css="min-width: 0;"
>
{hasMembers ? (
<IconGroup
aria-label={t(`Teams icon`)}
@@ -89,7 +94,10 @@ export const TeamItem = ({ team }: TeamProps) => {
$weight="bold"
$color={!hasMembers ? colorsTokens()['greyscale-600'] : undefined}
$css={`
min-width: 14rem;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
`}
>
{team.name}
@@ -10,6 +10,7 @@ import { Role, Team } from '../types';
import { ModalUpdateTeam } from './ModalUpdateTeam';
import { TeamActions } from './TeamActions';
import styled from 'styled-components';
const format: DateTimeFormatOptions = {
month: '2-digit',
@@ -17,6 +18,71 @@ const format: DateTimeFormatOptions = {
year: 'numeric',
};
const Wrapper = styled(Card)`
min-height: fit-content;
.actions-container {
align-self: flex-end;
position: absolute;
right: 0;
}
margin-bottom: 3rem;
`;
const Header = styled.div`
display: flex;
flex-direction: row;
justify-content: start;
gap: 2rem;
margin: 3rem 1.25rem 2rem 3rem;
div {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
p,
h3 {
margin: 0;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
h3 {
font-weight: bold;
font-size: 1.5rem;
}
p {
margin-top: 0.625rem;
font-size: 0.81rem;
color: #303c4b;
}
}
`;
const SubHeader = styled.div`
gap: 1rem 2rem;
display: flex;
flex-direction: row;
justify-content: start;
border-top: 1px solid var(--border-color);
padding: 1rem 0 1.5rem 8rem;
flex-wrap: wrap;
overflow: hidden;
p {
display: inline;
font-size: 0.81rem;
margin: 0;
}
`;
interface TeamInfoProps {
team: Team;
currentRole: Role;
@@ -38,13 +104,13 @@ export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
return (
<>
<Card className="m-b" style={{ paddingBottom: 0 }}>
<Box $css="align-self: flex-end;" className="m-t" $position="absolute">
<Wrapper>
<div className="actions-container m-t">
<TeamActions currentRole={currentRole} team={team} />
</Box>
<Box className="m-b" $direction="row" $align="center" $gap="1.5rem">
</div>
<Header>
<IconGroup
width={44}
width={48}
color={colorsTokens()['primary-text']}
aria-label={t('icon group')}
style={{
@@ -52,46 +118,35 @@ export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
alignSelf: 'start',
}}
/>
<Box>
<Text as="h3" $weight="bold" $size="1.25rem" className="mt-0">
<div>
<h3>
{t('Members of “{{teamName}}“', {
teamName: team.name,
})}
</Text>
<Text $size="m">
</h3>
<p>
{t('Add people to the “{{teamName}}“ group.', {
teamName: team.name,
})}
</Text>
</Box>
</Box>
<Box
className="p-s"
$gap="3rem"
$direction="row"
$justify="start"
$css={`
border-top: 1px solid ${colorsTokens()['card-border']};
padding-left: 1.5rem;
`}
</p>
</div>
</Header>
<SubHeader
style={{
'--border-color': colorsTokens()['card-border'],
}}
>
<Text $size="s" as="p">
{t('{{count}} member', { count: team.accesses.length })}
</Text>
<Text $size="s" $display="inline" as="p">
<p>{t('{{count}} member', { count: team.accesses.length })}</p>
<p>
{t('Created at')}&nbsp;
<Text $weight="bold" $display="inline">
{created_at}
</Text>
</Text>
<Text $size="s" $display="inline" as="p">
<strong>{created_at}</strong>
</p>
<p>
{t('Last update at')}&nbsp;
<Text $weight="bold" $display="inline">
{updated_at}
</Text>
</Text>
</Box>
</Card>
<strong>{updated_at}</strong>
</p>
</SubHeader>
</Wrapper>
{isModalUpdateOpen && (
<ModalUpdateTeam
onClose={() => setIsModalUpdateOpen(false)}
@@ -1,25 +1,34 @@
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { MainLayout } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Panel } from '@/features/teams';
import styled from 'styled-components';
const Container = styled.div`
display: flex;
flex-direction: column;
background-color: var(--bg-color);
overflow-x: hidden;
overflow-y: overlay;
width: 100%;
height: 100%;
padding: 1.85rem;
`;
export function TeamLayout({ children }: PropsWithChildren) {
const { colorsTokens } = useCunninghamTheme();
return (
<MainLayout>
<Box $height="inherit" $direction="row">
<Panel />
<Box
$background={colorsTokens()['primary-bg']}
$width="100%"
$height="inherit"
>
{children}
</Box>
</Box>
<Panel />
<Container
style={{
'--bg-color': colorsTokens()['primary-bg'],
}}
>
{children}
</Container>
</MainLayout>
);
}
@@ -48,7 +48,7 @@
"Freedom Equality Fraternity Logo": "Logo Liberté Égalité Fraternité",
"Groups": "Groupes",
"Invitation sent to {{email}}": "Invitation envoyée à {{email}}",
"Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}",
"Invite new members to your team": "Invitez de nouveaux membres à rejoindre votre groupe",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
"Language": "Langue",
"Language Icon": "Icône de langue",
@@ -3,10 +3,18 @@ import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, StyledLink } from '@/components';
import { StyledLink } from '@/components';
import { TeamLayout } from '@/features/teams/';
import { NextPageWithLayout } from '@/types/next';
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
`;
const StyledButton = styled(Button)`
width: fit-content;
`;
@@ -15,11 +23,11 @@ const Page: NextPageWithLayout = () => {
const { t } = useTranslation();
return (
<Box $align="center" $justify="center" $height="inherit">
<Container>
<StyledLink href="/teams/create">
<StyledButton>{t('Create a new team')}</StyledButton>
</StyledLink>
</Box>
</Container>
);
};