-
Notifications
You must be signed in to change notification settings - Fork 4
feat(react/homepage): #COCO-5441, school widget #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: epic-homepage
Are you sure you want to change the base?
Changes from 6 commits
6138a68
d9f8789
7b7ad1d
36efe29
5d6fcf4
141f105
ad4540e
e61bf71
ec88d15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| :root { | ||
| --school-widget-border-color: #ffe5a3; | ||
| --school-widget-bg-color: #fefaec; | ||
| --school-widget-selected-color: #383838; | ||
| } | ||
|
|
||
| .school-widget { | ||
| border: 1px solid var(--school-widget-border-color); | ||
| border-radius: 2.4rem; | ||
|
|
||
| background-color: var(--school-widget-bg-color); | ||
| background-image: url('./SchoolWidgetBackground.svg'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C'est une image qui va être lié aux préférénces de couleur qui seront accessibles dans le panneau de configuration. Pas en v1 mais faudra y penser.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. bien vu, mais c'est un comportement qu'on va devoir mettre en place à plusieurs endroit. Tu as une idée de comment faire ? on peut passer des proriétés à un svg ? |
||
| background-repeat: no-repeat; | ||
| background-position: bottom right; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
| import SchoolWidget, { SchoolWidgetProps } from './SchoolWidget'; | ||
|
|
||
| const meta: Meta<typeof SchoolWidget> = { | ||
| title: 'Modules/Homepage/SchoolWidget', | ||
| component: SchoolWidget, | ||
| decorators: [ | ||
| (Story) => ( | ||
| <div style={{ height: '35em' }}> | ||
| <div id="portal" /> | ||
| <Story /> | ||
| </div> | ||
| ), | ||
| ], | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| component: | ||
| "Ce storybook documente le composant SchoolWidget, un widget de sélection d'école avec plusieurs variantes possibles.", | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof SchoolWidget>; | ||
|
|
||
| const renderWithProps = (props: SchoolWidgetProps) => () => ( | ||
| <div style={{ maxWidth: 397 }}> | ||
| <SchoolWidget {...props} /> | ||
| </div> | ||
| ); | ||
|
|
||
| const schools = [ | ||
| { | ||
| id: 'school-1', | ||
| name: 'Collège Jean Moulin', | ||
| UAI: '0012345A', | ||
| classes: [], | ||
| exports: [], | ||
| }, | ||
| { | ||
| id: 'school-2', | ||
| name: 'Lycée Jeanne Ferry de Loisette en Royan', | ||
| UAI: '0098765Z', | ||
| classes: [], | ||
| exports: [], | ||
| }, | ||
| ]; | ||
|
|
||
| export const MultipleSchools: Story = { | ||
| render: renderWithProps({ | ||
| schools, | ||
| selectedSchool: { | ||
| id: 'school-2', | ||
| name: 'Lycée Jeanne Ferry de Loisette en Royan', | ||
| UAI: '0098765Z', | ||
| classes: [], | ||
| exports: [], | ||
| }, | ||
| onSelectedSchoolChange: (idx) => | ||
| alert( | ||
| `School id=${schools[idx].id} UAI=${schools[idx].UAI} is selected.`, | ||
| ), | ||
| }), | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: ` | ||
| Affiche une liste de plusieurs écoles avec sélection active. | ||
| <ul> | ||
| <li>2 écoles disponibles (Collège Jean Moulin, Lycée Jeanne Ferry de Loisette en Royan)</li> | ||
| <li>École sélectionnée : Lycée Jeanne Ferry</li> | ||
| <li>Callback au changement de sélection</li> | ||
| </ul>`, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export const SingleSchool: Story = { | ||
| render: renderWithProps({ | ||
| schools: [schools[0]], | ||
| selectedSchool: schools[0], | ||
| }), | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: `Affiche une seule école`, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export const Empty: Story = { | ||
| render: renderWithProps({ selectedSchool: undefined }), | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: `État vide (aucune école)`, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
Comment on lines
+95
to
+104
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C'est vraiment utile ?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. J'aime bien avoir la possibilité de voir les Edge Case dans storybook, ça retire pas mal de questionnements quand on regarde le composant
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. est ce que le cas avec aucune ecole est possible ? |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { School } from '@edifice.io/client'; | ||
| import { Dropdown, Flex, IconButton, useToggle } from '@edifice.io/react'; | ||
| import { IconRafterDown } from '@edifice.io/react/icons'; | ||
jcbe-ode marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import { RefAttributes } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import './SchoolWidget.css'; | ||
|
|
||
| export interface SchoolWidgetProps { | ||
| selectedSchool: School | undefined; | ||
| onSelectedSchoolChange?: (schoolIndex: number) => void; | ||
| schools?: School[]; | ||
| } | ||
|
|
||
| const SchoolWidget = ({ | ||
| schools, | ||
| selectedSchool, | ||
| onSelectedSchoolChange, | ||
| }: SchoolWidgetProps) => { | ||
| const [isExpanded, toggleExpanded] = useToggle(false); | ||
| const { t } = useTranslation(); | ||
|
|
||
| const hasManySchools = schools && schools.length > 1; | ||
|
|
||
| const widgetStyle = { padding: '1.4rem 0.4rem' }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mettre le css en dehors du composant et au même endroit, dans le fichier .scss (cf commentaire precedent). C'est un point important à acter pour le rester du projet
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tout ce style doit être dans un fichier CSS ou SCSS. |
||
| const containerStyle = { padding: '0.8rem' }; | ||
| const selectedSchoolStyle = { | ||
| 'padding': '.4rem 2.9rem', | ||
| 'font-size': '1.6rem', | ||
| 'line-height': '2.2rem', | ||
| 'color': 'var(--school-widget-selected-color)', | ||
jcbe-ode marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| if (!selectedSchool) return null; | ||
|
|
||
| return ( | ||
| <div className="school-widget" style={widgetStyle}> | ||
| <div style={containerStyle}> | ||
| <Flex | ||
| style={selectedSchoolStyle} | ||
| justify="center" | ||
| gap="4" | ||
| align="center" | ||
| > | ||
| <b>{selectedSchool.name}</b> | ||
| {hasManySchools && ( | ||
| <Dropdown placement={'bottom-end'} onToggle={toggleExpanded}> | ||
| {(triggerProps: RefAttributes<HTMLButtonElement>) => ( | ||
| <> | ||
| <IconButton | ||
| {...triggerProps} | ||
| aria-label={t('show')} | ||
| color="tertiary" | ||
jcbe-ode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| variant="ghost" | ||
| icon={ | ||
| <IconRafterDown | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wahou chiant d'avoir dû reimplementer ça. J'ai fait un commentaire à simon https://www.figma.com/design/UbAKNC5vk5XOj62z6CI4Qm?node-id=2146-36161#1690319223 |
||
| className="w-16 min-w-0" | ||
| style={{ | ||
| transition: 'rotate 0.2s ease-out', | ||
| rotate: isExpanded ? '0deg' : '-180deg', | ||
| }} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Si tu peux mettre un TODO pour récupérer ce que j'ai fait sur le composant de Partage
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cad ? Tu peux mettre un lien sur le code en question |
||
| /> | ||
| } | ||
| /> | ||
| <Dropdown.Menu> | ||
| {schools.map((school, index) => ( | ||
| <Dropdown.Item | ||
| key={school.id} | ||
| onClick={() => onSelectedSchoolChange?.(index)} | ||
| > | ||
| <Flex direction="column"> | ||
| <p>{school.name}</p> | ||
| {school.UAI && <p>UAI : {school.UAI}</p>} | ||
| </Flex> | ||
| </Dropdown.Item> | ||
| ))} | ||
| </Dropdown.Menu> | ||
| </> | ||
| )} | ||
| </Dropdown> | ||
| )} | ||
| </Flex> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| SchoolWidget.displayName = 'SchoolWidget'; | ||
|
|
||
| export default SchoolWidget; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { School, WIDGET_NAME, WidgetUserPref } from '@edifice.io/client'; | ||
| import { useEffect, useState } from 'react'; | ||
| import { useEdificeClient } from 'src/providers'; | ||
| import useWidgetPreferences from '../../hooks/useWidgetPreferences'; | ||
|
|
||
| export function useUserSchools() { | ||
| const { userDescription } = useEdificeClient(); | ||
| const { lookup, saveUserPreferences } = useWidgetPreferences(); | ||
| const [userPreferences, setUserPreferences] = useState<WidgetUserPref>(); | ||
| const [selectedSchool, setSelectedSchool] = useState<School>(); | ||
|
|
||
| const schools = userDescription?.schools; | ||
|
|
||
jcbe-ode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| useEffect(() => { | ||
| setSelectedSchool(schools?.[0]); | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| const userPref = lookup?.(WIDGET_NAME.SCHOOL)?.userPref; | ||
| setUserPreferences(userPref); | ||
| }, [lookup]); | ||
|
|
||
| useEffect(() => { | ||
| if (userPreferences?.schoolId && Array.isArray(schools)) { | ||
| const index = schools.findIndex( | ||
| (school) => school.id === userPreferences?.schoolId, | ||
| ); | ||
| setSelectedSchool( | ||
| index < 0 || index >= schools.length ? schools[0] : schools[index], | ||
| ); | ||
| } else { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. est ce vraiment utile de gerer ce cas ? c'est possible fonctionnellement qu'il passe à undefined ? |
||
| setSelectedSchool(undefined); | ||
| } | ||
| }, [userPreferences]); | ||
|
|
||
| return { | ||
| schools: schools ?? [], | ||
| selectedSchool, | ||
| handleSelectedSchoolChange: (school: School) => { | ||
| setSelectedSchool(school); | ||
| if (userPreferences) { | ||
| userPreferences.schoolId = school.id; | ||
| saveUserPreferences(); | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { IWidgetFramework, WidgetFrameworkFactory } from '@edifice.io/client'; | ||
| import { useMutation } from '@tanstack/react-query'; | ||
| import { useEffect, useState } from 'react'; | ||
|
|
||
| export default function useWidgetPreferences() { | ||
| const [svc, setSvc] = useState<IWidgetFramework>(); | ||
|
|
||
| const saveMutation = useMutation({ | ||
| mutationFn: svc?.saveUserPrefs, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| const widgetService = WidgetFrameworkFactory.instance(); | ||
| widgetService.initialize(null, null).then(() => setSvc(widgetService)); | ||
| }, []); | ||
|
|
||
| return { | ||
| list: svc?.list, | ||
| lookup: svc?.lookup, | ||
| saveUserPreferences: saveMutation.mutateAsync, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| import { useEffect, useMemo } from 'react'; | ||
|
|
||
| import { App } from '@edifice.io/client'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { useConf } from '../../hooks/useConf'; | ||
| import { useSession } from '../../hooks/useSession'; | ||
|
|
@@ -9,13 +8,6 @@ import { | |
| EdificeClientProviderProps, | ||
| } from './EdificeClientProvider.context'; | ||
|
|
||
| export interface OdeProviderParams { | ||
| alternativeApp?: boolean; | ||
| app: App; | ||
| cdnDomain?: string | null; | ||
| version?: string | null; | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Merci pour le nettoyage ! |
||
| export function EdificeClientProvider({ | ||
| children, | ||
| params, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.