├── .dockerignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── 404.html ├── 50x.html ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANUAL_E2E_TESTS_CHECKLIST.md ├── README.md ├── __mocks__ ├── cslMock.js ├── fileMock.js ├── styleMock.js └── xmlMock.js ├── android-chrome-96x96.png ├── apple-touch-icon.png ├── browserconfig.xml ├── config.json ├── config ├── custom-environment-variables.json ├── default.json ├── production.json └── sample.json ├── docker-cmd.sh ├── docker-nginx.conf ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonio-rs.png ├── index.html ├── index.prod.html.template ├── manifest.json ├── mstile-150x150.png ├── package-lock.json ├── package.json ├── safari-pinned-tab.svg ├── src ├── Application.js ├── Application.scss ├── components │ ├── AssetPreview │ │ ├── AssetPreview.js │ │ ├── AssetPreview.scss │ │ └── index.js │ ├── AuthorsManager │ │ ├── AuthorsManager.js │ │ └── index.js │ ├── BibHelpModal │ │ ├── BibHelpModal.js │ │ └── index.js │ ├── BibRefsEditor │ │ ├── BibRefsEditor.js │ │ └── index.js │ ├── BibliographicPreview │ │ ├── BibliographicPreview.js │ │ ├── BibliographicPreview.scss │ │ └── index.js │ ├── CitationStyleSelector │ │ ├── CitationStyleSelector.js │ │ ├── CitationStyleSelector.scss │ │ └── index.js │ ├── ConfirmToDeleteModal │ │ ├── ConfirmToDeleteModal.js │ │ └── index.js │ ├── DataUrlProvider │ │ ├── DataUrlProvider.js │ │ └── index.js │ ├── DemoLeaveConfirmModal │ │ ├── DemoLeaveConfirmModal.js │ │ └── index.js │ ├── EmbedHelpModal │ │ ├── EmbedHelpModal.js │ │ └── index.js │ ├── ExplainedLabel │ │ ├── ExplainedLabel.js │ │ └── index.js │ ├── ExportModal │ │ ├── ExportModal.js │ │ └── index.js │ ├── GlossaryModal │ │ ├── GlossaryModal.js │ │ └── index.js │ ├── IconBtn │ │ ├── IconBtn.js │ │ └── index.js │ ├── IdentificationModal │ │ ├── IdentificationModal.js │ │ └── index.js │ ├── InternalLinkModal │ │ ├── InternalLinkModal.js │ │ └── index.js │ ├── LanguageToggler │ │ ├── LanguageToggler.js │ │ └── index.js │ ├── LinkModal │ │ ├── LinkModal.js │ │ └── index.js │ ├── LoadingScreen │ │ ├── LoadingScreen.js │ │ └── index.js │ ├── MetadataForm │ │ ├── MetadataForm.js │ │ └── index.js │ ├── MovePad │ │ ├── MovePad.js │ │ ├── MovePad.scss │ │ └── index.js │ ├── NewSectionForm │ │ ├── NewSectionForm.js │ │ └── index.js │ ├── PageNotFound │ │ ├── PageNotFound.js │ │ └── index.js │ ├── PaginatedList │ │ ├── PaginatedList.js │ │ ├── PaginatedList.scss │ │ └── index.js │ ├── PasswordInput │ │ ├── PasswordInput.js │ │ └── index.js │ ├── PastingModal │ │ ├── PastingModal.js │ │ └── index.js │ ├── ResourceForm │ │ ├── ResourceForm.js │ │ └── index.js │ ├── SectionEditor │ │ ├── AssetButton.js │ │ ├── Bibliography.js │ │ ├── BlockContextualizationContainer.js │ │ ├── GlossaryMention.js │ │ ├── InlineCitation.js │ │ ├── LinkContextualization.js │ │ ├── NoteButton.js │ │ ├── NotePointer.js │ │ ├── ResourceSearchWidget.js │ │ ├── SectionEditor.js │ │ ├── SectionEditor.scss │ │ ├── SectionEditorWrapper.js │ │ ├── assets │ │ │ ├── apa.csl │ │ │ └── english-locale.xml │ │ ├── buttons │ │ │ ├── AssetButton.js │ │ │ ├── BlockButton.js │ │ │ ├── BlockQuoteButton.js │ │ │ ├── BoldButton.js │ │ │ ├── ButtonStyles.scss │ │ │ ├── CodeBlockButton.js │ │ │ ├── GlossaryButton.js │ │ │ ├── HeaderOneButton.js │ │ │ ├── HeaderTwoButton.js │ │ │ ├── InlineButton.js │ │ │ ├── InternalLinkButton.js │ │ │ ├── ItalicButton.js │ │ │ ├── LinkButton.js │ │ │ ├── NoteButton.js │ │ │ ├── OrderedListItemButton.js │ │ │ ├── RemoveFormattingButton.js │ │ │ ├── Separator.js │ │ │ ├── UnorderedListItemButton.js │ │ │ └── defaultButtons.js │ │ └── index.js │ └── UploadModal │ │ ├── UploadModal.js │ │ └── index.js ├── config.js ├── features │ ├── AuthManager │ │ ├── components │ │ │ ├── AuthManagerContainer.js │ │ │ ├── AuthManagerLayout.js │ │ │ └── index.js │ │ └── duck.js │ ├── ConnectionsManager │ │ ├── duck.js │ │ └── duck.spec.js │ ├── DesignView │ │ ├── components │ │ │ ├── AsideDesignColumn.js │ │ │ ├── AsideDesignContents.js │ │ │ ├── DesignViewContainer.js │ │ │ ├── DesignViewLayout.js │ │ │ ├── MainDesignColumn.js │ │ │ ├── StyleEditor.js │ │ │ └── index.js │ │ ├── duck.js │ │ └── utils │ │ │ └── buildCssHelp.js │ ├── EditionUiWrapper │ │ ├── components │ │ │ ├── EditionUiWrapperContainer.js │ │ │ ├── EditionUiWrapperLayout.js │ │ │ └── index.js │ │ └── duck.js │ ├── ErrorMessageManager │ │ ├── components │ │ │ ├── ErrorMessageContainer.js │ │ │ └── index.js │ │ └── duck.js │ ├── HomeView │ │ ├── assets │ │ │ ├── logo-forccast.svg │ │ │ ├── logo-medialab.svg │ │ │ └── user-guide-fr.pdf │ │ ├── components │ │ │ ├── ChangePasswordModal.js │ │ │ ├── DeleteStoryModal.js │ │ │ ├── EnterPasswordModal.js │ │ │ ├── Footer.js │ │ │ ├── HomeViewContainer.js │ │ │ ├── HomeViewLayout.js │ │ │ ├── NewStoryForm.js │ │ │ ├── OtherUsersWidget.js │ │ │ ├── ProfileWidget.js │ │ │ ├── StoryCard.js │ │ │ ├── StoryCard.scss │ │ │ ├── StoryCardWrapper.js │ │ │ └── index.js │ │ ├── duck.js │ │ └── duck.spec.js │ ├── LibraryView │ │ ├── components │ │ │ ├── ConfirmBatchDeleteModal.js │ │ │ ├── LibraryFiltersBar.js │ │ │ ├── LibraryViewContainer.js │ │ │ ├── LibraryViewLayout.js │ │ │ ├── ResourceCard.js │ │ │ ├── ResourceCard.scss │ │ │ └── index.js │ │ └── duck.js │ ├── ReadStoryView │ │ └── components │ │ │ ├── ReadStoryViewContainer.js │ │ │ └── index.js │ ├── SectionView │ │ ├── components │ │ │ ├── AsideSectionColumn.js │ │ │ ├── AsideSectionContents.js │ │ │ ├── MainSectionAside.js │ │ │ ├── MainSectionColumn.js │ │ │ ├── MoveButton.js │ │ │ ├── ResourceMiniCard.js │ │ │ ├── ResourceMiniCard.scss │ │ │ ├── ResourcesList.js │ │ │ ├── SectionHeader.js │ │ │ ├── SectionMiniCard.js │ │ │ ├── SectionViewContainer.js │ │ │ ├── SectionViewLayout.js │ │ │ ├── ShortcutsModal.js │ │ │ ├── SortableMiniSectionsList.js │ │ │ └── index.js │ │ └── duck.js │ ├── SectionsManager │ │ └── duck.js │ ├── StoryManager │ │ ├── duck.js │ │ └── duck.spec.js │ ├── SummaryView │ │ ├── components │ │ │ ├── AuthorItem.js │ │ │ ├── SectionCard.js │ │ │ ├── SortableSectionsList.js │ │ │ ├── SummaryViewContainer.js │ │ │ ├── SummaryViewLayout.js │ │ │ └── index.js │ │ └── duck.js │ └── UserInfoManager │ │ └── duck.js ├── helpers │ ├── assetsUtils.js │ ├── assetsUtils.spec.js │ ├── citationUtils.js │ ├── clipboardUtils │ │ ├── __mocks__ │ │ │ ├── README.md │ │ │ ├── copyTests.json │ │ │ ├── pasteInsideTests.json │ │ │ ├── pasteOutsideTests.json │ │ │ └── services.js │ │ ├── handleCopy.js │ │ ├── handleCopy.spec.js │ │ ├── handlePaste.js │ │ ├── index.js │ │ ├── makeReactCitations.js │ │ ├── parsePastedImage.js │ │ ├── parsePastedLink.js │ │ ├── pasteFromInside.js │ │ ├── pasteFromInside.spec.js │ │ ├── pasteFromOutside.js │ │ └── pasteFromOutside.spec.js │ ├── draftUtils.js │ ├── editorToStoryUtils.js │ ├── editorUtils.js │ ├── fileDownloader.js │ ├── fileLoader.js │ ├── fileLoader.spec.js │ ├── localStorageUtils.js │ ├── lockUtils.js │ ├── misc.js │ ├── postcss.js │ ├── projectBundler.js │ ├── reduxUtils.js │ ├── resourcesUtils.js │ ├── schemaUtils.js │ ├── translateUtils.js │ └── userInfo.js ├── main.js ├── parameters.scss ├── redux │ ├── configureStore.js │ ├── payloadValidatorMiddleware.js │ ├── promiseMiddleware.js │ ├── rootReducer.js │ └── socketIoMiddleware.js ├── sharedAssets │ ├── avatars │ │ ├── amazed-man.svg │ │ ├── angry-man.svg │ │ ├── angry-woman.svg │ │ ├── baby-crying.svg │ │ ├── baby-love.svg │ │ ├── boy-broad-smile.svg │ │ ├── boy-happy-smile.svg │ │ ├── boy-smiling.svg │ │ ├── boy-suffering.svg │ │ ├── delighted-granny.svg │ │ ├── disappointed-boy.svg │ │ ├── embarrased-boy.svg │ │ ├── embarrased-girl.svg │ │ ├── embarrased-granny.svg │ │ ├── emotive-granny.svg │ │ ├── fat-boy-angry.svg │ │ ├── fat-boy-shocked.svg │ │ ├── fat-boy-smiling.svg │ │ ├── fat-boy-sorry.svg │ │ ├── fat-boy.svg │ │ ├── frightened-hipster.svg │ │ ├── girl-air-kissing.svg │ │ ├── girl-crying.svg │ │ ├── girl-embarrased.svg │ │ ├── girl-laughing-to-tears.svg │ │ ├── girl-showing-tongue.svg │ │ ├── girl-smiling.svg │ │ ├── girl-with-poker-face.svg │ │ ├── happy-baby.svg │ │ ├── happy-woman.svg │ │ ├── hipster-smiling.svg │ │ ├── hypnotized-hipster.svg │ │ ├── index.json │ │ ├── inexpressive-girl.svg │ │ ├── kissing-girl.svg │ │ ├── man-with-moustache-smiling.svg │ │ ├── naughty-girl.svg │ │ ├── perplexed-man.svg │ │ ├── philosophizing-boy.svg │ │ ├── sad-baby.svg │ │ ├── sad-girl.svg │ │ ├── sad-hipster.svg │ │ ├── sad-woman.svg │ │ ├── satisfied-woman.svg │ │ ├── shocked-girl.svg │ │ ├── sleeping-granny.svg │ │ ├── sleepy-boy.svg │ │ ├── smiling-baby.svg │ │ ├── smiling-girl.svg │ │ ├── surprised-girl.svg │ │ ├── suspicious-man.svg │ │ ├── teasing-boy.svg │ │ ├── upset-girl.svg │ │ ├── upset-granny.svg │ │ ├── winking-boy.svg │ │ └── wondered-hipster.svg │ ├── bibAssets │ │ ├── apa.csl │ │ ├── chicago-author-date-fr.csl │ │ ├── chicago-author-date.csl │ │ ├── english-locale.xml │ │ ├── iso690-author-date-en.csl │ │ └── iso690-author-date-fr.csl │ ├── cover_forccast.jpg │ ├── internal-link.svg │ ├── logo-quinoa.png │ └── userNames.json └── translations │ ├── index.js │ └── locales │ ├── en.json │ └── fr.json ├── translationScripts ├── addTranslationLanguage.js ├── backfillTranslations.js ├── discoverTranslations.js ├── exportTranslationsToPo.js ├── importTranslationsFromPo.js └── updateTranslationsToPo.js ├── translations ├── en.po └── fr.po ├── webpack.config.dev.js ├── webpack.config.docker.js ├── webpack.config.prod.js └── webpack.config.shared.js /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | Dockerfile 3 | node_modules 4 | .git 5 | .gitignore 6 | LICENSE 7 | VERSION 8 | *.md 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 16 | **Observed behavior** 17 | A clear and concise description of what happens when reproducing the steps above. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. iOS] 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | config/local.json 5 | build/ 6 | data/ 7 | index.prod.html 8 | coverage 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "9" 5 | - "10" 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.13-alpine 2 | 3 | ENV NODE_ENV production 4 | 5 | ENV QUINOA_HOST quinoa 6 | ENV QUINOA_PORT 3001 7 | ENV MAX_SECTION_LEVEL 2 8 | ENV MAX_RESOURCE_SIZE 4000000 9 | ENV MAX_FOLDER_SIZE 50000000 10 | ENV MAX_STORY_SIZE 60000000 11 | ENV MAX_BATCH_NUMBER 50 12 | ENV REQUIRE_PUBLICATION_CONSENT false 13 | ENV URL_PREFIX http://localhost:3000 14 | ENV API_URL ${URL_PREFIX}/quinoa 15 | ENV DEMO_MODE false 16 | 17 | ADD . /fonio 18 | WORKDIR /fonio 19 | 20 | RUN apk add --no-cache --virtual .build-deps git nodejs=8.9.3-r1 build-base python \ 21 | && npm install --quiet --production false --no-audit \ 22 | && npm run build:docker \ 23 | && mv ./build/bundle.js ./build/bundle.js.template \ 24 | && apk del .build-deps \ 25 | && rm -rf ./node_modules /root/.npm /root/.node-gyp /root/.config /usr/lib/node_modules 26 | 27 | RUN rm /etc/nginx/conf.d/default.conf 28 | COPY docker-nginx.conf /etc/nginx/conf.d/docker.template 29 | 30 | CMD /bin/sh docker-cmd.sh 31 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/android-chrome-96x96.png -------------------------------------------------------------------------------- /apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/apple-touch-icon.png -------------------------------------------------------------------------------- /browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxSectionLevel": 6, 3 | "timers": { 4 | "nano": 10, 5 | "short": 100, 6 | "medium": 300, 7 | "long": 1000, 8 | "veryLong": 2000, 9 | "ultraLong": 5000 10 | }, 11 | "savingDelayMs": 2000 12 | } -------------------------------------------------------------------------------- /config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "API_URL", 3 | "sessionName": "SESSION_NAME", 4 | "urlPrefix": "URL_PREFIX", 5 | "maxSectionLevel": "MAX_SECTION_LEVEL", 6 | "maxResourceSize": "MAX_RESOURCE_SIZE", 7 | "maxFolderSize": "MAX_FOLDER_SIZE", 8 | "maxStorySize": "MAX_STORY_SIZE", 9 | "maxBatchNumber": "MAX_BATCH_NUMBER", 10 | "requirePublicationConsent": { 11 | "__name": "REQUIRE_PUBLICATION_CONSENT", 12 | "__format": "json" 13 | }, 14 | "demoMode": { 15 | "__name": "DEMO_MODE", 16 | "__format": "json" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "http://localhost:3001", 3 | "maxSectionLevel": 2, 4 | "maxResourceSize": 4000000, 5 | "maxFolderSize": 50000000, 6 | "maxStorySize": 60000000, 7 | "maxBatchNumber": 50, 8 | "sessionName": "Introduction", 9 | "urlPrefix": "", 10 | "requirePublicationConsent": false, 11 | "demoMode": false 12 | } 13 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "http://localhost:3001", 3 | "maxSectionLevel": 2, 4 | "maxResourceSize": 4000000, 5 | "maxFolderSize": 50000000, 6 | "maxStorySize": 60000000, 7 | "maxBatchNumber": 50, 8 | "sessionName": "My super classroom name", 9 | "urlPrefix": "", 10 | "requirePublicationConsent": true, 11 | "demoMode": false 12 | } 13 | -------------------------------------------------------------------------------- /config/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "http://localhost:3001", 3 | "maxSectionLevel": 2, 4 | "maxResourceSize": 4000000, 5 | "maxFolderSize": 50000000, 6 | "maxStorySize": 60000000, 7 | "maxBatchNumber": 50, 8 | "sessionName": "My super classroom name", 9 | "urlPrefix": "", 10 | "requirePublicationConsent": true, 11 | "demoMode": false 12 | } 13 | -------------------------------------------------------------------------------- /docker-cmd.sh: -------------------------------------------------------------------------------- 1 | # Templating the bundle file 2 | sed "s;@@URL_PREFIX@@;${URL_PREFIX};" /fonio/build/bundle.js.template > /fonio/build/bundle.js 3 | 4 | # Templating the HTML index 5 | envsubst < /fonio/index.prod.html.template > /fonio/index.html 6 | 7 | # Templating the NGINX conf 8 | export NS=$(awk '/^nameserver/{print $2}' /etc/resolv.conf) 9 | envsubst '\$NS \$QUINOA_HOST \$QUINOA_PORT \$MAX_STORY_SIZE' < /etc/nginx/conf.d/docker.template > /etc/nginx/conf.d/default.conf 10 | 11 | # No daemon 12 | nginx -g 'daemon off;' 13 | -------------------------------------------------------------------------------- /docker-nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3000; 3 | server_name localhost; 4 | 5 | # TODO: discriminate between MAX_RESOURCE_SIZE & MAX_STORY_SIZE 6 | client_max_body_size ${MAX_STORY_SIZE}; 7 | 8 | server_tokens off; 9 | add_header X-Frame-Options SAMEORIGIN; 10 | add_header X-Content-Type-Options nosniff; 11 | add_header X-XSS-Protection "1; mode=block"; 12 | add_header Strict-Transport-Security "max-age=31536000;"; 13 | add_header X-Robots-Tag "noindex, nofollow"; 14 | 15 | resolver ${NS} ipv6=off; 16 | set $api "http://${QUINOA_HOST}:${QUINOA_PORT}"; 17 | 18 | ### API 19 | location /quinoa/ { 20 | rewrite ^/quinoa(/.*)$ $1 break; 21 | proxy_pass $api; 22 | proxy_http_version 1.1; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Connection "Upgrade"; 25 | } 26 | 27 | ### Static HTML5/JS 28 | location / { 29 | root /fonio/; 30 | index index.html index.htm; 31 | try_files $uri $uri/ /index.html?$query_string; 32 | } 33 | 34 | location ^~ /quinoa/static { 35 | alias /fonio/stories-data/stories; 36 | } 37 | 38 | error_page 500 502 503 504 /50x.html; 39 | location = /50x.html { 40 | root /usr/share/nginx/html; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/favicon-16x16.png -------------------------------------------------------------------------------- /favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/favicon-32x32.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/favicon.ico -------------------------------------------------------------------------------- /fonio-rs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/fonio-rs.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fonio", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-96x96.png", 6 | "sizes": "96x96", 7 | "type": "image/png" 8 | } 9 | ], 10 | "theme_color": "#ffffff", 11 | "background_color": "#ffffff", 12 | "display": "standalone" 13 | } -------------------------------------------------------------------------------- /mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/mstile-150x150.png -------------------------------------------------------------------------------- /safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/AssetPreview/index.js: -------------------------------------------------------------------------------- 1 | import AssetPreview from './AssetPreview'; 2 | 3 | export default AssetPreview; 4 | -------------------------------------------------------------------------------- /src/components/AuthorsManager/index.js: -------------------------------------------------------------------------------- 1 | import Component from './AuthorsManager'; 2 | 3 | export default Component; 4 | -------------------------------------------------------------------------------- /src/components/BibHelpModal/BibHelpModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { translateNameSpacer } from '../../helpers/translateUtils'; 4 | 5 | import { 6 | ModalCard, 7 | Button, 8 | Content, 9 | } from 'quinoa-design-library/components/'; 10 | const BibHelpModal = ( { 11 | isOpen, 12 | onRequestClose, 13 | onChooseSample 14 | }, context ) => { 15 | const translate = translateNameSpacer( context.t, 'Components.BibHelpModal' ); 16 | 17 | const example = `@article{10.2307/1511524, 18 | ISSN = {07479360, 15314790}, 19 | author = {Richard Buchanan}, 20 | journal = {Design Issues}, 21 | number = {1}, 22 | pages = {4--22}, 23 | publisher = {The MIT Press}, 24 | title = {Declaration by Design: Rhetoric, Argument, and Demonstration in Design Practice}, 25 | volume = {2}, 26 | year = {1985} 27 | }`; 28 | const onTest = () => { 29 | onChooseSample( example ); 30 | }; 31 | return ( 32 | 42 |

43 | {translate( 'In higher education and scientific research, a good practice for managing references and bibliographies is to use specialized software for storing and documenting references in a form which is independent of a specific citation style.' )} 44 |

45 |

46 | {translate( 'For a fast bibliography making tool, you can use zbib which helps building small bibliographies online:' )} 47 | 52 | zbib 53 | 54 |

55 |

56 | {translate( 'For a more substantial bibliography making tool, you may want to install zotero software:' )} 57 | 62 | Zotero 63 | 64 |

65 |

66 | {translate( 'Fonio accepts citation files in the form of bibtex files, which can be exported from all bibliography-related tools and websites.' )} 67 |

68 |

69 | {translate( 'Bibtex is a standard format for exchanging academic references accross tools. You should not have to manipulate them directly, but Bibtex data looks like this: ' )} 70 |

71 |
72 |
{example}
73 |
74 | 75 | 76 | } 77 | /> 78 | ); 79 | }; 80 | 81 | BibHelpModal.contextTypes = { 82 | t: PropTypes.func, 83 | }; 84 | 85 | export default BibHelpModal; 86 | -------------------------------------------------------------------------------- /src/components/BibHelpModal/index.js: -------------------------------------------------------------------------------- 1 | import BibHelpModal from './BibHelpModal'; 2 | 3 | export default BibHelpModal; 4 | -------------------------------------------------------------------------------- /src/components/BibRefsEditor/BibRefsEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a component allowing to edit raw bibtex data. 3 | * It handles errors and propagates an update only if input data is valid. 4 | * @module fonio/components/AuthorsManager 5 | */ 6 | /* eslint react/no-set-state : 0 */ 7 | /** 8 | * Imports Libraries 9 | */ 10 | import React, { Component } from 'react'; 11 | import PropTypes from 'prop-types'; 12 | import { 13 | CodeEditor 14 | } from 'quinoa-design-library'; 15 | import Cite from 'citation-js'; 16 | 17 | class BibRefsEditor extends Component { 18 | constructor( props ) { 19 | super( props ); 20 | this.state = { 21 | refsInput: '', 22 | }; 23 | } 24 | componentDidMount = () => { 25 | this.updateBibInput( this.props.data ); 26 | } 27 | componentWillReceiveProps = ( nextProps ) => { 28 | if ( this.props.data !== nextProps.data ) { 29 | this.updateBibInput( nextProps.data ); 30 | } 31 | } 32 | 33 | updateBibInput = ( data ) => { 34 | const resAsBibTeXParser = new Cite( data ); 35 | const resAsBibTeX = resAsBibTeXParser.get( { type: 'string', style: 'bibtex' } ); 36 | if ( resAsBibTeX !== this.state.refsInput ) { 37 | this.setState( { 38 | refsInput: resAsBibTeX 39 | } ); 40 | } 41 | } 42 | 43 | render = () => { 44 | 45 | /** 46 | * Variables definition 47 | */ 48 | const { 49 | onChange, 50 | style, 51 | } = this.props; 52 | const { refsInput } = this.state; 53 | 54 | /** 55 | * Callbacks handlers 56 | */ 57 | const handleBibTeXInputChange = ( value ) => { 58 | this.setState( { 59 | refsInput: value, 60 | } ); 61 | onChange( value ); 62 | }; 63 | 64 | return ( 65 |
68 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | BibRefsEditor.contextTypes = { 78 | t: PropTypes.func, 79 | }; 80 | export default BibRefsEditor; 81 | -------------------------------------------------------------------------------- /src/components/BibRefsEditor/index.js: -------------------------------------------------------------------------------- 1 | import BibRefsEditor from './BibRefsEditor'; 2 | 3 | export default BibRefsEditor; 4 | -------------------------------------------------------------------------------- /src/components/BibliographicPreview/BibliographicPreview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable bibliography preview element component. 3 | * It displays a simple bibliography 4 | * @module fonio/components/BibliographicPreview 5 | */ 6 | /** 7 | * Imports Libraries 8 | */ 9 | import React from 'react'; 10 | import PropTypes from 'prop-types'; 11 | import { 12 | Content 13 | } from 'quinoa-design-library/components'; 14 | import { Bibliography } from 'react-citeproc'; 15 | 16 | /** 17 | * Imports Assets 18 | */ 19 | import './BibliographicPreview.scss'; 20 | const english = require( 'raw-loader!../../sharedAssets/bibAssets/english-locale.xml' ); 21 | const apa = require( 'raw-loader!../../sharedAssets/bibAssets/apa.csl' ); 22 | 23 | /** 24 | * Renders the BibliographicPreview component as a pure function 25 | * @param {object} props - used props (see prop types below) 26 | * @todo: load style and locale from currently set style and locale 27 | * @return {ReactElement} component - the resulting component 28 | */ 29 | const BibliographicPreview = ( { 30 | items, 31 | style = apa, 32 | locale = english 33 | } ) => ( 34 | 35 |
36 | 41 |
42 |
43 | ); 44 | 45 | /** 46 | * Component's properties types 47 | */ 48 | BibliographicPreview.propTypes = { 49 | 50 | /** 51 | * Map of the bibliographic items to render (keys are ids) 52 | */ 53 | items: PropTypes.object, 54 | }; 55 | 56 | export default BibliographicPreview; 57 | -------------------------------------------------------------------------------- /src/components/BibliographicPreview/BibliographicPreview.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * style definitions for the BibliographicPreview component 3 | * 4 | * @module fonio/components/BibliographicPreview 5 | */ 6 | @import '../../parameters.scss'; 7 | -------------------------------------------------------------------------------- /src/components/BibliographicPreview/index.js: -------------------------------------------------------------------------------- 1 | import BibliographicPreview from './BibliographicPreview'; 2 | 3 | export default BibliographicPreview; 4 | -------------------------------------------------------------------------------- /src/components/CitationStyleSelector/CitationStyleSelector.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * style definitions for the CitationStyleSelector component 3 | * 4 | * @module fonio/components/CitationStyleSelector 5 | */ 6 | @import '../../parameters.scss'; 7 | 8 | .citation-style-selector{ 9 | .button{ 10 | margin: .5rem; 11 | min-width: 2.5rem; 12 | } 13 | .content blockquote{ 14 | border: none; 15 | padding: 0; 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/CitationStyleSelector/index.js: -------------------------------------------------------------------------------- 1 | import CitationStyleSelector from './CitationStyleSelector'; 2 | 3 | export default CitationStyleSelector; 4 | -------------------------------------------------------------------------------- /src/components/ConfirmToDeleteModal/ConfirmToDeleteModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a modal for confirming a section deletion 3 | * @module fonio/components/ConfirmToDeleteModal 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | 11 | import { 12 | ModalCard, 13 | Button, 14 | } from 'quinoa-design-library/components/'; 15 | 16 | /** 17 | * Imports Project utils 18 | */ 19 | 20 | import { getResourceTitle } from '../../helpers/resourcesUtils'; 21 | import { translateNameSpacer } from '../../helpers/translateUtils'; 22 | 23 | const ConfirmToDeleteModal = ( { 24 | onClose, 25 | onDeleteConfirm, 26 | id, 27 | story, 28 | deleteType, 29 | isActive, 30 | isDisabled = false 31 | }, { t } ) => { 32 | 33 | /** 34 | * Local functions 35 | */ 36 | const translate = translateNameSpacer( t, 'Components.ConfirmToDeleteModal' ); 37 | 38 | /** 39 | * Computed variables 40 | */ 41 | let message; 42 | let citedContext; 43 | if ( deleteType === 'section' ) { 44 | message = ( story && story.sections[id] ) ? translate( 45 | 'Are you sure you want to delete the section "{s}" ? All its content will be lost without possible recovery.', 46 | { 47 | s: story.sections[id].metadata.title 48 | } 49 | ) : translate( 'Are you sure you want to delete this section ?' ); 50 | } 51 | else { 52 | const { contextualizations } = story; 53 | citedContext = Object.keys( contextualizations ) 54 | .map( ( contextId ) => contextualizations[contextId] ) 55 | .filter( ( d ) => d.resourceId === id ); 56 | 57 | message = ( story && story.resources[id] ) ? translate( 58 | 'Are you sure you want to delete the resource "{s}" ?', 59 | { 60 | s: getResourceTitle( story.resources[id] ) 61 | } 62 | ) : translate( 'Are you sure you want to delete this resource ?' ); 63 | } 64 | 65 | return ( 66 | 72 | {deleteType === 'resource' && citedContext.length > 0 && 73 |
74 | {translate( [ 'You will destroy one item mention in your content if you delete this item.', 'You will destroy {n} item mentions in your content if your delete this item.', 'n' ], 75 | { n: citedContext.length } )} 76 |
} 77 |
{message}
78 | 79 | } 80 | footerContent={ [ 81 | , 90 | , 97 | ] } 98 | /> 99 | ); 100 | }; 101 | 102 | ConfirmToDeleteModal.contextTypes = { 103 | t: PropTypes.func, 104 | }; 105 | 106 | export default ConfirmToDeleteModal; 107 | 108 | -------------------------------------------------------------------------------- /src/components/ConfirmToDeleteModal/index.js: -------------------------------------------------------------------------------- 1 | import ConfirmToDeleteModal from './ConfirmToDeleteModal'; 2 | 3 | export default ConfirmToDeleteModal; 4 | -------------------------------------------------------------------------------- /src/components/DataUrlProvider/DataUrlProvider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides wrapper serving a resource data url get function to its children context 3 | * @module fonio/components/DataUrlProvider 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import { Component } from 'react'; 9 | import PropTypes from 'prop-types'; 10 | 11 | export default class DataUrlProvider extends Component { 12 | 13 | static childContextTypes = { 14 | getResourceDataUrl: PropTypes.func 15 | } 16 | 17 | constructor( props ) { 18 | super( props ); 19 | } 20 | 21 | getChildContext = () => ( { 22 | getResourceDataUrl: this.getResourceDataUrl 23 | } ) 24 | 25 | getResourceDataUrl = ( data ) => { 26 | const { 27 | serverUrl, 28 | } = this.props; 29 | return `${serverUrl}/static${data.filePath}`; 30 | } 31 | 32 | render = () => { 33 | const { children } = this.props; 34 | return children; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/DataUrlProvider/index.js: -------------------------------------------------------------------------------- 1 | import DataUrlProvider from './DataUrlProvider'; 2 | 3 | export default DataUrlProvider; 4 | -------------------------------------------------------------------------------- /src/components/DemoLeaveConfirmModal/DemoLeaveConfirmModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a modal for warning user of demo version behaviour when leaving a story 3 | * @module fonio/components/DemoLeaveConfirmModal 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { 11 | ModalCard, 12 | Notification, 13 | Button, 14 | Title, 15 | } from 'quinoa-design-library/components/'; 16 | 17 | /** 18 | * Imports Project utils 19 | */ 20 | import { translateNameSpacer } from '../../helpers/translateUtils'; 21 | 22 | const DemoLeaveConfirmModal = ( { 23 | onConfirm, 24 | onCancel, 25 | }, { t } ) => { 26 | const translate = translateNameSpacer( t, 'Components.DemoLeaveConfirmModal' ); 27 | return ( 28 | 34 | 35 | {translate( 'As you are in the demonstration version, this means all the contents of this story will be deleted if you leave it.' )} 36 | 37 | 38 |

{translate( 'Before leaving, you can retrieve a publishable version of your by clicking on the export button on the top right corner of the screen (you can also save it as a data file and reimport it later from the home page).' )}

39 |
40 |

41 | {translate( 'So, are you ready to leave this test story and let it be deleted ?' )} 42 |

43 | 44 | } 45 | footerContent={ [ 46 | , 53 | 60 | ] } 61 | /> 62 | ); 63 | }; 64 | 65 | DemoLeaveConfirmModal.contextTypes = { 66 | t: PropTypes.func.isRequired, 67 | }; 68 | 69 | export default DemoLeaveConfirmModal; 70 | -------------------------------------------------------------------------------- /src/components/DemoLeaveConfirmModal/index.js: -------------------------------------------------------------------------------- 1 | import Component from './DemoLeaveConfirmModal'; 2 | 3 | export default Component; 4 | -------------------------------------------------------------------------------- /src/components/EmbedHelpModal/index.js: -------------------------------------------------------------------------------- 1 | import EmbedHelpModal from './EmbedHelpModal'; 2 | 3 | export default EmbedHelpModal; 4 | -------------------------------------------------------------------------------- /src/components/ExplainedLabel/ExplainedLabel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable label with explanation tooltip 3 | * @module fonio/components/ExplainedLabel 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | Label, 11 | HelpPin, 12 | } from 'quinoa-design-library/components/'; 13 | 14 | const ExplainedLabel = ( { 15 | title = '', 16 | explanation 17 | } ) => ( 18 | 24 | ); 25 | 26 | export default ExplainedLabel; 27 | -------------------------------------------------------------------------------- /src/components/ExplainedLabel/index.js: -------------------------------------------------------------------------------- 1 | import ExplainedLabel from './ExplainedLabel'; 2 | 3 | export default ExplainedLabel; 4 | -------------------------------------------------------------------------------- /src/components/ExportModal/ExportModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a modal for exporting a story 3 | * @module fonio/components/ExportModal 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { 11 | Column, 12 | ModalCard, 13 | BigSelect, 14 | Notification, 15 | StretchedLayoutContainer, 16 | HelpPin, 17 | StretchedLayoutItem, 18 | } from 'quinoa-design-library/components/'; 19 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 20 | 21 | /** 22 | * Imports Project utils 23 | */ 24 | import { translateNameSpacer } from '../../helpers/translateUtils'; 25 | 26 | const ExportModal = ( { 27 | activeOptionId, 28 | onClose, 29 | onChange, 30 | status, 31 | isActive 32 | }, { t } ) => { 33 | 34 | const translate = translateNameSpacer( t, 'Components.ExportModal' ); 35 | 36 | return ( 37 | 44 | 45 | 46 | {translate( 'Export your story to publish it' )}{translate( 'multiple-html-help' )}, 55 | iconUrl: activeOptionId === 'html-multi' ? icons.takeAway.white.svg : icons.takeAway.black.svg 56 | }, 57 | { 58 | id: 'html-single', 59 | label: {translate( 'Export your story to archive it' )}{translate( 'single-html-help' )}, 60 | iconUrl: activeOptionId === 'html-single' ? icons.takeAway.white.svg : icons.takeAway.black.svg 61 | }, 62 | { 63 | id: 'json', 64 | label: {translate( 'Export your story to backup it' )}{translate( 'json-help' )}, 65 | iconUrl: activeOptionId === 'json' ? icons.takeAway.white.svg : icons.takeAway.black.svg 66 | } 67 | ] } 68 | /> 69 | 70 | 71 | {status === 'success' && 72 | 73 | 74 | {translate( 'Story was bundled successfully' )} 75 | 76 | 77 | } 78 | 79 | } 80 | /> 81 | ); 82 | }; 83 | 84 | ExportModal.contextTypes = { 85 | t: PropTypes.func, 86 | }; 87 | 88 | export default ExportModal; 89 | 90 | -------------------------------------------------------------------------------- /src/components/ExportModal/index.js: -------------------------------------------------------------------------------- 1 | import ExportModal from './ExportModal'; 2 | 3 | export default ExportModal; 4 | -------------------------------------------------------------------------------- /src/components/GlossaryModal/index.js: -------------------------------------------------------------------------------- 1 | import GlossaryModal from './GlossaryModal'; 2 | 3 | export default GlossaryModal; 4 | -------------------------------------------------------------------------------- /src/components/IconBtn/IconBtn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable button displaying a single pictographic icon 3 | * @module fonio/components/IconBtn 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React, { Component } from 'react'; 9 | import { 10 | Button, 11 | Image, 12 | } from 'quinoa-design-library/'; 13 | 14 | export default class IconBtn extends Component { 15 | constructor( props ) { 16 | super( props ); 17 | this.state = {}; 18 | } 19 | render = () => { 20 | const { 21 | isColor, 22 | onClick, 23 | src, 24 | dataTip, 25 | ...otherProps 26 | } = this.props; 27 | 28 | const bindRef = ( element ) => { 29 | this.element = element; 30 | }; 31 | 32 | return ( 33 | 36 | 60 | 61 | ); 62 | 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/components/IconBtn/index.js: -------------------------------------------------------------------------------- 1 | import IconBtn from './IconBtn'; 2 | 3 | export default IconBtn; 4 | -------------------------------------------------------------------------------- /src/components/IdentificationModal/index.js: -------------------------------------------------------------------------------- 1 | import Component from './IdentificationModal'; 2 | 3 | export default Component; 4 | -------------------------------------------------------------------------------- /src/components/InternalLinkModal/index.js: -------------------------------------------------------------------------------- 1 | import InternalLinkModal from './InternalLinkModal'; 2 | 3 | export default InternalLinkModal; 4 | -------------------------------------------------------------------------------- /src/components/LanguageToggler/LanguageToggler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a language toggling button 3 | * @module fonio/components/LanguageToggler 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React, { Component } from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { bindActionCreators } from 'redux'; 11 | import { connect } from 'react-redux'; 12 | import { setLanguage } from 'redux-i18n'; 13 | 14 | import { 15 | Button, 16 | } from 'quinoa-design-library/components/'; 17 | 18 | @connect( 19 | ( state ) => ( { 20 | lang: state.i18nState.lang, 21 | } ), 22 | ( dispatch ) => ( { 23 | actions: bindActionCreators( { 24 | setLanguage, 25 | }, dispatch ) 26 | } ) 27 | ) 28 | class LanguageToggler extends Component { 29 | 30 | /** 31 | * Context data used by the component 32 | */ 33 | static contextTypes = { 34 | 35 | /** 36 | * Un-namespaced translate function 37 | */ 38 | t: PropTypes.func.isRequired, 39 | 40 | /** 41 | * Redux store 42 | */ 43 | store: PropTypes.object.isRequired 44 | } 45 | 46 | /** 47 | * constructor 48 | * @param {object} props - properties given to instance at instanciation 49 | */ 50 | constructor( props ) { 51 | super( props ); 52 | } 53 | 54 | /** 55 | * Defines whether the component should re-render 56 | * @param {object} nextProps - the props to come 57 | * @param {object} nextState - the state to come 58 | * @return {boolean} shouldUpdate - whether to update or not 59 | */ 60 | shouldComponentUpdate() { 61 | // todo: optimize when the feature is stabilized 62 | return true; 63 | } 64 | 65 | /** 66 | * Renders the component 67 | * @return {ReactElement} component - the component 68 | */ 69 | render() { 70 | 71 | /** 72 | * Variables definition 73 | */ 74 | const { 75 | lang, 76 | actions: { 77 | setLanguage: doSetLanguage 78 | } 79 | } = this.props; 80 | 81 | /** 82 | * Computed variables 83 | */ 84 | const otherLang = lang === 'fr' ? 'en' : 'fr'; 85 | 86 | /** 87 | * Callbacks handlers 88 | */ 89 | const handleClick = () => { 90 | doSetLanguage( otherLang ); 91 | }; 92 | 93 | return ( 94 | 101 | ); 102 | } 103 | } 104 | 105 | export default LanguageToggler; 106 | -------------------------------------------------------------------------------- /src/components/LanguageToggler/index.js: -------------------------------------------------------------------------------- 1 | import LanguageToggler from './LanguageToggler'; 2 | 3 | export default LanguageToggler; 4 | -------------------------------------------------------------------------------- /src/components/LinkModal/index.js: -------------------------------------------------------------------------------- 1 | import LinkModal from './LinkModal'; 2 | 3 | export default LinkModal; 4 | -------------------------------------------------------------------------------- /src/components/LoadingScreen/LoadingScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable loading component 3 | * @module fonio/components/LoadingScreen 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { 11 | AbsoluteContainer, 12 | FlexContainer, 13 | } from 'quinoa-design-library/components'; 14 | 15 | /** 16 | * Imports Project utils 17 | */ 18 | import { translateNameSpacer } from '../../helpers/translateUtils'; 19 | 20 | const LoadingScreen = ( {}, { t } ) => { 21 | const translate = translateNameSpacer( t, 'Components.LoadingScreen' ); 22 | return ( 23 | 24 | 29 | 34 |
{translate( 'loading...' )}
35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | LoadingScreen.contextTypes = { 42 | t: PropTypes.func.isRequired 43 | }; 44 | 45 | export default LoadingScreen; 46 | -------------------------------------------------------------------------------- /src/components/LoadingScreen/index.js: -------------------------------------------------------------------------------- 1 | import LoadingScreen from './LoadingScreen'; 2 | 3 | export default LoadingScreen; 4 | -------------------------------------------------------------------------------- /src/components/MetadataForm/index.js: -------------------------------------------------------------------------------- 1 | import Component from './MetadataForm'; 2 | 3 | export default Component; 4 | -------------------------------------------------------------------------------- /src/components/MovePad/MovePad.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .move-pad{ 4 | $unit : 2.5rem; 5 | position : relative; 6 | .move-item{ 7 | position : absolute; 8 | 9 | &.is-disabled{ 10 | opacity: .1; 11 | pointer-events: none; 12 | cursor: not-allowed; 13 | } 14 | } 15 | 16 | .chevron-icon-up, 17 | .chevron-icon-right, 18 | .chevron-icon-left, 19 | .chevron-icon-down{ 20 | width: $unit; 21 | height: $unit; 22 | display: flex; 23 | flex-flow: row nowrap; 24 | align-items: center; 25 | justify-content: center; 26 | cursor: pointer; 27 | transition: all .3s ease; 28 | 29 | opacity: .5; 30 | 31 | &:not(.is-disabled):hover{ 32 | opacity: 1; 33 | } 34 | 35 | .icon 36 | { 37 | transition: all .3s ease; 38 | } 39 | } 40 | 41 | .chevron-icon-up{ 42 | .icon{ 43 | left: -.1rem; 44 | top: .4rem; 45 | position: relative; 46 | } 47 | &:not(.is-disabled):hover{ 48 | .icon{ 49 | top: 0rem; 50 | } 51 | } 52 | } 53 | 54 | .chevron-icon-down{ 55 | .icon{ 56 | left: -.1rem; 57 | position: relative; 58 | top: -.4rem; 59 | } 60 | &:not(.is-disabled):hover{ 61 | .icon{ 62 | top: 0rem; 63 | } 64 | } 65 | } 66 | 67 | .chevron-icon-left{ 68 | .icon{ 69 | left: .2rem; 70 | position: relative; 71 | } 72 | &:not(.is-disabled):hover{ 73 | .icon{ 74 | left: -.2rem; 75 | } 76 | } 77 | } 78 | .chevron-icon-right{ 79 | .icon{ 80 | left: -.4rem; 81 | position: relative; 82 | } 83 | &:not(.is-disabled):hover{ 84 | .icon{ 85 | left: 0rem; 86 | } 87 | } 88 | } 89 | 90 | .chevron-icon-left { 91 | top: $unit; 92 | left: 0; 93 | } 94 | .chevron-icon-up, 95 | .move-button, 96 | .chevron-icon-down { 97 | left: $unit; 98 | } 99 | .chevron-icon-right{ 100 | left: $unit * 2; 101 | top: $unit; 102 | } 103 | 104 | .chevron-icon-up{ 105 | top: 0; 106 | } 107 | 108 | .move-button{ 109 | top: $unit; 110 | } 111 | 112 | .chevron-icon-down{ 113 | top: $unit * 2; 114 | } 115 | } -------------------------------------------------------------------------------- /src/components/MovePad/index.js: -------------------------------------------------------------------------------- 1 | import MovePad from './MovePad'; 2 | 3 | export default MovePad; 4 | -------------------------------------------------------------------------------- /src/components/NewSectionForm/index.js: -------------------------------------------------------------------------------- 1 | import NewSectionForm from './NewSectionForm'; 2 | 3 | export default NewSectionForm; 4 | -------------------------------------------------------------------------------- /src/components/PageNotFound/PageNotFound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a component for 404-like views 3 | * @module fonio/components/AuthorsManager 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { 11 | Link, 12 | } from 'react-router-dom'; 13 | import { 14 | ModalCard 15 | } from 'quinoa-design-library/components'; 16 | 17 | /** 18 | * Imports Project utils 19 | */ 20 | import { translateNameSpacer } from '../../helpers/translateUtils'; 21 | 22 | const PageNotFound = ( { 23 | pathName 24 | }, { t } ) => { 25 | const translate = translateNameSpacer( t, 'Components.PageNotFound' ); 26 | 27 | return ( 28 | 33 | {pathName ? 34 | translate( 'No match for {u}, go back to ', { u: pathName } ) 35 | : 36 | translate( 'The page you are looking does not exist on this fonio instance, go back to ' ) 37 | } 38 | 39 | {translate( 'home page' )} 40 | 41 |

42 | } 43 | /> 44 | ); 45 | }; 46 | 47 | PageNotFound.contextTypes = { 48 | t: PropTypes.func 49 | }; 50 | 51 | export default PageNotFound; 52 | -------------------------------------------------------------------------------- /src/components/PageNotFound/index.js: -------------------------------------------------------------------------------- 1 | import PageNotFound from './PageNotFound'; 2 | 3 | export default PageNotFound; 4 | -------------------------------------------------------------------------------- /src/components/PaginatedList/PaginatedList.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .fonio-PaginatedList 4 | { 5 | display: flex; 6 | overflow: hidden; 7 | 8 | flex-flow: column nowrap; 9 | justify-content: stretch; 10 | .items-container 11 | { 12 | overflow: auto; 13 | 14 | flex: 1; 15 | // max-height: calc(100% - 2rem); 16 | > div 17 | { 18 | display: flex; 19 | overflow-y: auto; 20 | 21 | flex-flow: row wrap; 22 | } 23 | } 24 | .pagination 25 | { 26 | min-height: 5rem; 27 | } 28 | 29 | .pagination-previous, 30 | .pagination-next{ 31 | &.is-disabled{ 32 | opacity: .5; 33 | cursor: not-allowed; 34 | pointer-events: none; 35 | } 36 | } 37 | 38 | .pagination-list 39 | { 40 | li 41 | { 42 | &.is-current 43 | { 44 | a 45 | { 46 | color: white!important; 47 | background: brown; 48 | } 49 | } 50 | } 51 | } 52 | 53 | 54 | .my-masonry-grid 55 | { 56 | display: flex; 57 | 58 | width: auto; 59 | margin-left: -1rem; /* gutter size offset */ 60 | } 61 | .my-masonry-grid_column 62 | { 63 | padding-left: 1rem; /* gutter size */ 64 | 65 | background-clip: padding-box; 66 | } 67 | 68 | .my-masonry-grid_column > div 69 | { 70 | width: 100%; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/PaginatedList/index.js: -------------------------------------------------------------------------------- 1 | import PaginatedList from './PaginatedList'; 2 | 3 | export default PaginatedList; 4 | -------------------------------------------------------------------------------- /src/components/PasswordInput/PasswordInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable password input component 3 | * @module fonio/components/PasswordInput 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { Text } from 'react-form'; 11 | import { 12 | Control, 13 | StretchedLayoutContainer, 14 | StretchedLayoutItem, 15 | } from 'quinoa-design-library/components/'; 16 | 17 | /** 18 | * Imports Project utils 19 | */ 20 | import { translateNameSpacer } from '../../helpers/translateUtils'; 21 | 22 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 23 | import { faLock } from '@fortawesome/free-solid-svg-icons/faLock'; 24 | 25 | const PasswordInput = ( { id = 'password' }, { t } ) => { 26 | const translate = translateNameSpacer( t, 'Components.PasswordInput' ); 27 | 28 | return ( 29 | 30 | 34 | 35 | 38 | 39 | 43 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | PasswordInput.contextTypes = { 57 | t: PropTypes.func 58 | }; 59 | 60 | export default PasswordInput; 61 | -------------------------------------------------------------------------------- /src/components/PasswordInput/index.js: -------------------------------------------------------------------------------- 1 | import PasswordInput from './PasswordInput'; 2 | 3 | export default PasswordInput; 4 | -------------------------------------------------------------------------------- /src/components/PastingModal/PastingModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a modal displaying what is happening when pasting contents 3 | * @module fonio/components/PastingModal 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { 11 | ModalCard, 12 | } from 'quinoa-design-library/components/'; 13 | 14 | /** 15 | * Imports Project utils 16 | */ 17 | import { translateNameSpacer } from '../../helpers/translateUtils'; 18 | 19 | const UploadModal = ( { 20 | editorPastingStatus = {} 21 | }, { t } ) => { 22 | 23 | /** 24 | * Local functions 25 | */ 26 | const translate = translateNameSpacer( t, 'Components.PastingModal' ); 27 | 28 | /** 29 | * Computed variables 30 | */ 31 | const { statusParameters = {} } = editorPastingStatus; 32 | let message; 33 | switch ( editorPastingStatus.status ) { 34 | case 'duplicating-contents': 35 | message = translate( 'Duplicating contents' ); 36 | break; 37 | case 'updating-contents': 38 | message = translate( 'Updating contents' ); 39 | break; 40 | case 'converting-contents': 41 | message = translate( 'Converting contents' ); 42 | break; 43 | case 'duplicating-contextualizers': 44 | message = translate( 'Duplicating {n} contextualizers', { n: statusParameters.length } ); 45 | break; 46 | case 'duplicating-notes': 47 | message = translate( 'Duplicating {n} notes', { n: statusParameters.length } ); 48 | break; 49 | case 'fetching-images': 50 | if ( statusParameters.iteration ) { 51 | message = translate( 'Creating image {x} of {n}', { x: statusParameters.iteration, n: statusParameters.length } ); 52 | } 53 | else { 54 | message = translate( 'Creating {n} images', { n: statusParameters.length } ); 55 | } 56 | break; 57 | case 'creating-images': 58 | if ( statusParameters.iteration ) { 59 | message = translate( 'Creating image {x} of {n}', { x: statusParameters.iteration, n: statusParameters.length } ); 60 | } 61 | else { 62 | message = translate( 'Creating {n} images', { n: statusParameters.length } ); 63 | } 64 | break; 65 | case 'creating-resources': 66 | if ( statusParameters.iteration ) { 67 | message = translate( 'Creating item {x} of {n}', { x: statusParameters.iteration, n: statusParameters.length } ); 68 | } 69 | else { 70 | message = translate( 'Creating {n} items', { n: statusParameters.length } ); 71 | } 72 | break; 73 | case 'attaching-contextualizers': 74 | if ( statusParameters.iteration ) { 75 | message = translate( 'Attaching contextualizer {x} of {n}', { x: statusParameters.iteration, n: statusParameters.length } ); 76 | } 77 | else { 78 | message = translate( 'Attaching {n} contextualizers', { n: statusParameters.length } ); 79 | } 80 | break; 81 | 82 | default: 83 | break; 84 | } 85 | return ( 86 | 91 | {message && 92 |

93 | {message} 94 |

95 | } 96 | 97 | } 98 | /> 99 | ); 100 | }; 101 | 102 | UploadModal.contextTypes = { 103 | t: PropTypes.func, 104 | }; 105 | 106 | export default UploadModal; 107 | -------------------------------------------------------------------------------- /src/components/PastingModal/index.js: -------------------------------------------------------------------------------- 1 | import PastingModal from './PastingModal'; 2 | export default PastingModal; 3 | -------------------------------------------------------------------------------- /src/components/ResourceForm/index.js: -------------------------------------------------------------------------------- 1 | import ResourceForm from './ResourceForm'; 2 | 3 | export default ResourceForm; 4 | -------------------------------------------------------------------------------- /src/components/SectionEditor/AssetButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a icon button allowing to add/edit assets 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React, { Component } from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 11 | 12 | /** 13 | * Imports Project utils 14 | */ 15 | import { translateNameSpacer } from '../../helpers/translateUtils'; 16 | import { silentEvent } from '../../helpers/misc'; 17 | 18 | /** 19 | * Imports Components 20 | */ 21 | import IconBtn from '../IconBtn'; 22 | 23 | class AssetButton extends Component { 24 | constructor( props ) { 25 | super( props ); 26 | this.state = {}; 27 | } 28 | render = () => { 29 | 30 | /** 31 | * Variables definition 32 | */ 33 | const { 34 | props: { 35 | onClick, 36 | active, 37 | icon 38 | }, 39 | context: { t } 40 | } = this; 41 | 42 | /** 43 | * Computed variables 44 | */ 45 | /** 46 | * Local functions 47 | */ 48 | const translate = translateNameSpacer( t, 'Components.SectionEditor' ); 49 | 50 | /** 51 | * Callbacks handlers 52 | */ 53 | const handleMouseDown = ( event ) => { 54 | silentEvent( event ); 55 | }; 56 | 57 | /** 58 | * References bindings 59 | */ 60 | const bindRef = ( btn ) => { 61 | if ( btn ) { 62 | this.element = btn.element; 63 | } 64 | }; 65 | return ( 66 | 74 | ); 75 | } 76 | } 77 | 78 | AssetButton.contextTypes = { 79 | t: PropTypes.func 80 | }; 81 | 82 | export default AssetButton; 83 | -------------------------------------------------------------------------------- /src/components/SectionEditor/Bibliography.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable bibliography wrapper for the editor component 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | 11 | /** 12 | * Imports Project utils 13 | */ 14 | import { translateNameSpacer } from '../../helpers/translateUtils'; 15 | 16 | /** 17 | * Renders the Bib component as a pure function 18 | * @param {object} props - (un)used props (see prop types below) 19 | * @param {object} context - used context data (see context types below) 20 | * @return {ReactElement} component - the resulting component 21 | */ 22 | const Bib = ( unusedProps, { 23 | bibliography, 24 | t 25 | } ) => { 26 | const translate = translateNameSpacer( t, 'Components.References' ); 27 | return ( 28 |
29 |

{translate( 'References' )}

30 |
{bibliography}
31 |
32 | ); 33 | }; 34 | 35 | /** 36 | * Component's properties types 37 | */ 38 | Bib.propTypes = {}; 39 | 40 | /** 41 | * Component's context used properties 42 | */ 43 | Bib.contextTypes = { 44 | 45 | /** 46 | * The properly formatted bibliography object to be rendered 47 | */ 48 | bibliography: PropTypes.oneOfType( [ 49 | PropTypes.object, 50 | PropTypes.array 51 | ] ), 52 | 53 | /** 54 | * The active language 55 | */ 56 | lang: PropTypes.string, 57 | 58 | /** 59 | * Un-namespaced translate function 60 | */ 61 | t: PropTypes.func.isRequired 62 | }; 63 | 64 | export default Bib; 65 | -------------------------------------------------------------------------------- /src/components/SectionEditor/GlossaryMention.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable inline glossary mention component 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/no-set-state: 0 */ 6 | /* eslint react/prefer-stateless-function : 0 */ 7 | /** 8 | * Imports Libraries 9 | */ 10 | import React, { Component } from 'react'; 11 | import PropTypes from 'prop-types'; 12 | 13 | /** 14 | * GlossaryMention class for building react component instances 15 | */ 16 | class GlossaryMention extends Component { 17 | 18 | /** 19 | * Component's context used properties 20 | */ 21 | static contextTypes = { 22 | t: PropTypes.func.isRequired, 23 | } 24 | 25 | /** 26 | * constructor 27 | * @param {object} props - properties given to instance at instanciation 28 | */ 29 | constructor( props ) { 30 | super( props ); 31 | } 32 | 33 | shouldComponentUpdate = ( nextProps ) => { 34 | return this.props.children !== nextProps.children; 35 | } 36 | 37 | /** 38 | * Renders the component 39 | * @return {ReactElement} component - the component 40 | */ 41 | render() { 42 | const { 43 | children, 44 | } = this.props; 45 | return {children}; 46 | } 47 | } 48 | 49 | /** 50 | * Component's properties types 51 | */ 52 | GlossaryMention.propTypes = { 53 | 54 | /** 55 | * The asset to consume for displaying the glossary mention 56 | */ 57 | asset: PropTypes.object, 58 | 59 | /** 60 | * Children react elements of the component 61 | */ 62 | children: PropTypes.array, 63 | 64 | /** 65 | * Callbacks when an asset is blured 66 | */ 67 | onAssetBlur: PropTypes.func, 68 | 69 | /** 70 | * Callbacks when an asset is changed 71 | */ 72 | onAssetChange: PropTypes.func, 73 | 74 | /** 75 | * Callbacks when an asset is focused 76 | */ 77 | onAssetFocus: PropTypes.func, 78 | }; 79 | 80 | export default GlossaryMention; 81 | -------------------------------------------------------------------------------- /src/components/SectionEditor/LinkContextualization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable inline citation widget component 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/no-set-state: 0 */ 6 | /* eslint react/prefer-stateless-function : 0 */ 7 | 8 | /** 9 | * Imports Libraries 10 | */ 11 | import React, { Component } from 'react'; 12 | import PropTypes from 'prop-types'; 13 | 14 | class LinkContextualization extends Component { 15 | 16 | static contextTypes = { 17 | t: PropTypes.func.isRequired, 18 | } 19 | render = () => { 20 | const { 21 | children, 22 | } = this.props; 23 | 24 | return ( 25 | 28 | {children} 29 | 30 | ); 31 | } 32 | } 33 | 34 | /** 35 | * Component's properties types 36 | */ 37 | LinkContextualization.propTypes = { 38 | 39 | /** 40 | * The asset to consume for displaying the inline citation 41 | */ 42 | asset: PropTypes.object, 43 | 44 | /** 45 | * Children react elements of the component 46 | */ 47 | children: PropTypes.array, 48 | 49 | /** 50 | * Callbacks when an asset is blured 51 | */ 52 | onAssetBlur: PropTypes.func, 53 | 54 | /** 55 | * Callbacks when an asset is changed 56 | */ 57 | onAssetChange: PropTypes.func, 58 | 59 | /** 60 | * Callbacks when an asset is focused 61 | */ 62 | onAssetFocus: PropTypes.func, 63 | }; 64 | 65 | export default LinkContextualization; 66 | -------------------------------------------------------------------------------- /src/components/SectionEditor/NoteButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a reusable note addition button 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 11 | 12 | /** 13 | * Imports Project utils 14 | */ 15 | import { translateNameSpacer } from '../../helpers/translateUtils'; 16 | import { silentEvent } from '../../helpers/misc'; 17 | 18 | /** 19 | * Imports Components 20 | */ 21 | import IconBtn from '../IconBtn'; 22 | 23 | const NoteButton = ( { 24 | onClick, 25 | active 26 | }, { t } ) => { 27 | const translate = translateNameSpacer( t, 'Components.SectionEditor' ); 28 | const handleMouseDown = ( event ) => { 29 | silentEvent( event ); 30 | }; 31 | return ( 32 | 39 | ); 40 | }; 41 | 42 | NoteButton.contextTypes = { 43 | t: PropTypes.func 44 | }; 45 | 46 | export default NoteButton; 47 | -------------------------------------------------------------------------------- /src/components/SectionEditor/SectionEditorWrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a wrapper for converting editor received props 3 | * into context data for its children 4 | * @module fonio/components/SectionEditor 5 | */ 6 | /** 7 | * Imports Libraries 8 | */ 9 | import React, { Component } from 'react'; 10 | import PropTypes from 'prop-types'; 11 | 12 | /** 13 | * Imports Components 14 | */ 15 | import SectionEditor from './SectionEditor'; 16 | 17 | export default class SectionEditorWrapper extends Component { 18 | 19 | static childContextTypes = { 20 | startExistingResourceConfiguration: PropTypes.func, 21 | startNewResourceConfiguration: PropTypes.func, 22 | deleteContextualizationFromId: PropTypes.func, 23 | } 24 | 25 | constructor( props ) { 26 | super( props ); 27 | } 28 | 29 | getChildContext = () => ( { 30 | startExistingResourceConfiguration: this.props.startExistingResourceConfiguration, 31 | startNewResourceConfiguration: this.props.startNewResourceConfiguration, 32 | deleteContextualizationFromId: this.props.deleteContextualizationFromId 33 | } ) 34 | 35 | render = () => { 36 | const { 37 | props 38 | } = this; 39 | 40 | return ; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/AssetButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a icon button allowing to add/edit assets 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import ReactTooltip from 'react-tooltip'; 10 | 11 | const AssetButton = ( { 12 | onClick, 13 | active, 14 | iconMap, 15 | message, 16 | ...otherProps 17 | } ) => { 18 | const onMouseDown = ( event ) => event.preventDefault(); 19 | return ( 20 |
28 | {iconMap.asset} 29 | 32 |
33 | ); 34 | }; 35 | 36 | AssetButton.propTypes = { 37 | 38 | active: PropTypes.bool, 39 | 40 | iconMap: PropTypes.object, 41 | 42 | message: PropTypes.string, 43 | 44 | onClick: PropTypes.func, 45 | 46 | }; 47 | 48 | export default AssetButton; 49 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/BlockButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button wrapper for block modifiers 3 | * @module fonio/components/SectionEditor 4 | */ 5 | import React, { Component } from 'react'; 6 | import { RichUtils } from 'draft-js'; 7 | import PropTypes from 'prop-types'; 8 | 9 | import { 10 | Button 11 | } from 'quinoa-design-library/components'; 12 | 13 | class BlockButton extends Component { 14 | 15 | static propTypes = { 16 | 17 | /** 18 | * The block type this button is responsible for. 19 | */ 20 | blockType: PropTypes.string, 21 | 22 | children: PropTypes.oneOfType( [ 23 | PropTypes.array, 24 | PropTypes.object 25 | ] ), 26 | 27 | /** 28 | * The current editorState. This gets passed down from the editor. 29 | */ 30 | editorState: PropTypes.object, 31 | 32 | /** 33 | * A method that can be called to update the editor's editorState. This 34 | * gets passed down from the editor. 35 | */ 36 | updateEditorState: PropTypes.func, 37 | }; 38 | 39 | isSelected = ( editorState, blockType ) => { 40 | if ( !editorState || !editorState.getSelection ) { 41 | return; 42 | } 43 | const selection = editorState.getSelection(); 44 | const selectedBlock = editorState 45 | .getCurrentContent() 46 | .getBlockForKey( selection.getStartKey() ); 47 | if ( !selectedBlock ) return false; 48 | const selectedBlockType = selectedBlock.getType(); 49 | return selectedBlockType === blockType; 50 | }; 51 | 52 | render = () => { 53 | 54 | const { 55 | editorState, 56 | blockType, 57 | children, 58 | updateEditorState, 59 | tooltip, 60 | 61 | /* 62 | * iconMap, 63 | * ...otherProps 64 | */ 65 | } = this.props; 66 | 67 | const selected = this.isSelected( editorState, blockType ); 68 | // const className = `scholar-draft-BlockButton${selected ? ' active' : ''}`; 69 | 70 | const onMouseDown = ( event ) => { 71 | event.preventDefault(); 72 | updateEditorState( RichUtils.toggleBlockType( editorState, blockType ) ); 73 | }; 74 | 75 | return ( 76 | 89 | ); 90 | } 91 | } 92 | 93 | export default BlockButton; 94 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/BlockQuoteButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for blockquote modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import BlockButton from './BlockButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.quoteblock} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/BoldButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for bold text modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import InlineButton from './InlineButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.bold} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/ButtonStyles.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .scholar-draft-InlineButton, 4 | .scholar-draft-BlockButton, 5 | .scholar-draft-NoteButton, 6 | .scholar-draft-AssetButton 7 | { 8 | display: inline-block; 9 | 10 | width: 24px; 11 | height: 24px; 12 | 13 | cursor: pointer; 14 | transition: all .1s ease; 15 | 16 | background: inherit; 17 | 18 | img 19 | { 20 | transition: all .1s ease; 21 | } 22 | 23 | &:hover 24 | { 25 | background: #ccc; 26 | } 27 | &.active 28 | { 29 | background: #999; 30 | } 31 | } 32 | 33 | 34 | .scholar-draft-NoteButton, 35 | .scholar-draft-AssetButton 36 | { 37 | border-radius: 50%; 38 | } 39 | 40 | .scholar-draft-AssetButton 41 | { 42 | &.active 43 | { 44 | img 45 | { 46 | transform: rotate(45deg); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/CodeBlockButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for codeblock modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import BlockButton from './BlockButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.codeblock} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/GlossaryButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for link modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | 10 | import { 11 | Button, 12 | Image, 13 | } from 'quinoa-design-library/components'; 14 | 15 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 16 | 17 | import { silentEvent } from '../../../helpers/misc'; 18 | 19 | const GlossaryButton = ( { tooltip }, { 20 | // startNewResourceConfiguration, 21 | setGlossaryModalFocusData, 22 | editorFocus 23 | } ) => { 24 | const onClick = ( e ) => { 25 | silentEvent( e ); 26 | setGlossaryModalFocusData( editorFocus ); 27 | }; 28 | return ( 29 | 40 | ); 41 | }; 42 | 43 | GlossaryButton.contextTypes = { 44 | setGlossaryModalFocusData: PropTypes.func, 45 | editorFocus: PropTypes.string, 46 | // startNewResourceConfiguration: PropTypes.func, 47 | }; 48 | 49 | export default GlossaryButton; 50 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/HeaderOneButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for h1 modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import BlockButton from './BlockButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.h1} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/HeaderTwoButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for h2 modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import BlockButton from './BlockButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.h2} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/InlineButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a generic button for inline modifiers 3 | * @module fonio/components/SectionEditor 4 | */ 5 | import React, { Component } from 'react'; 6 | import { RichUtils } from 'draft-js'; 7 | import PropTypes from 'prop-types'; 8 | 9 | import './ButtonStyles.scss'; 10 | 11 | import { 12 | Button 13 | } from 'quinoa-design-library/components'; 14 | 15 | class InlineButton extends Component { 16 | 17 | static propTypes = { 18 | 19 | children: PropTypes.oneOfType( [ PropTypes.array, PropTypes.object ] ), 20 | 21 | /** 22 | * The current editorState. This gets passed down from the editor. 23 | */ 24 | editorState: PropTypes.object, 25 | 26 | iconMap: PropTypes.object, 27 | inlineStyleType: PropTypes.string, 28 | 29 | /** 30 | * The inline style type this button is responsible for. 31 | */ 32 | styleType: PropTypes.string, 33 | 34 | /** 35 | * A method that can be called to update the editor's editorState. This 36 | * gets passed down from the editor. 37 | */ 38 | updateEditorState: PropTypes.func, 39 | }; 40 | 41 | /** 42 | * Checks wether current styling button is selected 43 | * @param {Record} editorState - editorState to check for selection 44 | * @param {string} inlineStyleType - inline style to inspect against the provided editorState 45 | * @return {boolean} isSelected - 46 | */ 47 | isSelected = ( editorState, inlineStyleType ) => { 48 | if ( !editorState || !editorState.getSelection ) { 49 | return; 50 | } 51 | // Check the editor is focused 52 | const selection = editorState.getSelection(); 53 | 54 | const selectedBlock = editorState 55 | .getCurrentContent() 56 | .getBlockForKey( selection.getStartKey() ); 57 | if ( !selectedBlock ) { 58 | return false; 59 | } 60 | 61 | const currentInlineStyle = editorState.getCurrentInlineStyle(); 62 | return currentInlineStyle.has( inlineStyleType ); 63 | }; 64 | 65 | render = () => { 66 | 67 | const { 68 | editorState, 69 | updateEditorState, 70 | inlineStyleType, 71 | tooltip, 72 | iconMap, /* eslint no-unused-vars:0 */ 73 | ...otherProps 74 | } = this.props; 75 | 76 | const selected = this.isSelected( editorState, inlineStyleType ); 77 | // const className = `scholar-draft-InlineButton${selected ? ' active' : ''} `; 78 | 79 | const onMouseDown = ( event ) => { 80 | event.preventDefault(); 81 | updateEditorState( RichUtils.toggleInlineStyle( editorState, inlineStyleType ) ); 82 | }; 83 | 84 | return ( 85 | 100 | ); 101 | }; 102 | } 103 | 104 | export default InlineButton; 105 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/InternalLinkButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for link modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | 10 | import { 11 | Button, 12 | Image, 13 | } from 'quinoa-design-library/components'; 14 | 15 | import icon from '../../../sharedAssets/internal-link.svg'; 16 | 17 | import { silentEvent } from '../../../helpers/misc'; 18 | 19 | const InternalLinkButton = ( { tooltip }, { 20 | // startNewResourceConfiguration, 21 | setInternalLinkModalFocusData, 22 | editorFocus 23 | } ) => { 24 | const onClick = ( e ) => { 25 | silentEvent( e ); 26 | // startNewResourceConfiguration(true, 'webpage'); 27 | setInternalLinkModalFocusData( editorFocus ); 28 | }; 29 | return ( 30 | 41 | ); 42 | }; 43 | 44 | InternalLinkButton.contextTypes = { 45 | setInternalLinkModalFocusData: PropTypes.func, 46 | editorFocus: PropTypes.string, 47 | // startNewResourceConfiguration: PropTypes.func, 48 | }; 49 | 50 | export default InternalLinkButton; 51 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/ItalicButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for italic modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import InlineButton from './InlineButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.italic} 16 | 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/LinkButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for link modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | 10 | import { 11 | Button, 12 | Image, 13 | } from 'quinoa-design-library/components'; 14 | 15 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 16 | 17 | import { silentEvent } from '../../../helpers/misc'; 18 | 19 | const LinkButton = ( { tooltip }, { 20 | // startNewResourceConfiguration, 21 | setLinkModalFocusData, 22 | editorFocus 23 | } ) => { 24 | const onClick = ( e ) => { 25 | silentEvent( e ); 26 | // startNewResourceConfiguration(true, 'webpage'); 27 | setLinkModalFocusData( editorFocus ); 28 | }; 29 | return ( 30 | 41 | ); 42 | }; 43 | 44 | LinkButton.contextTypes = { 45 | setLinkModalFocusData: PropTypes.func, 46 | editorFocus: PropTypes.string, 47 | // startNewResourceConfiguration: PropTypes.func, 48 | }; 49 | 50 | export default LinkButton; 51 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/NoteButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an aside button for note adding 3 | * @module fonio/components/SectionEditor 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import ReactTooltip from 'react-tooltip'; 8 | 9 | const NoteButton = ( { 10 | onClick, 11 | iconMap, 12 | message, 13 | ...otherProps 14 | } ) => { 15 | 16 | const onMouseDown = ( event ) => event.preventDefault(); 17 | 18 | return ( 19 |
26 | {iconMap.note} 27 | 30 |
31 | ); 32 | }; 33 | 34 | NoteButton.propTypes = { 35 | iconMap: PropTypes.object, 36 | message: PropTypes.string, 37 | onClick: PropTypes.func, 38 | }; 39 | 40 | export default NoteButton; 41 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/OrderedListItemButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for ordered list modifier 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import BlockButton from './BlockButton'; 9 | 10 | export default ( props ) => ( 11 | 15 | {props.iconMap.orderedlist} 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/RemoveFormattingButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a toolbar button for remove formatting action 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /* eslint react/prop-types: 0 */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | 10 | import { 11 | Button, 12 | Image, 13 | } from 'quinoa-design-library/components'; 14 | 15 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 16 | 17 | import { silentEvent } from '../../../helpers/misc'; 18 | 19 | const RemoveFormattingButton = ( props, { 20 | removeFormattingForSelection 21 | } ) => { 22 | const onClick = ( e ) => { 23 | silentEvent( e ); 24 | removeFormattingForSelection(); 25 | }; 26 | return ( 27 | 38 | ); 39 | }; 40 | 41 | RemoveFormattingButton.contextTypes = { 42 | removeFormattingForSelection: PropTypes.func, 43 | }; 44 | 45 | export default RemoveFormattingButton; 46 | -------------------------------------------------------------------------------- /src/components/SectionEditor/buttons/Separator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Button, 5 | } from 'quinoa-design-library/components'; 6 | 7 | export default () => ( 8 | 98 | 99 | ); 100 | }; 101 | 102 | export default MainDesignColumn; 103 | -------------------------------------------------------------------------------- /src/features/DesignView/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the design view react components 3 | * @module fonio/features/DesignView 4 | */ 5 | import DesignViewContainer from './DesignViewContainer'; 6 | 7 | export default DesignViewContainer; 8 | -------------------------------------------------------------------------------- /src/features/DesignView/utils/buildCssHelp.js: -------------------------------------------------------------------------------- 1 | export default ( translate ) => [ 2 | { 3 | action: translate( 'Change the paragraphs font size' ), 4 | code: ` 5 | .content-p{ 6 | font-size: 10px; 7 | }` 8 | }, 9 | { 10 | action: translate( 'Change the background color' ), 11 | code: ` 12 | .wrapper, .nav{ 13 | background: white; 14 | }` 15 | }, 16 | { 17 | action: translate( 'Change the titles color' ), 18 | code: ` 19 | .content-h1,.content-h2,.section-title 20 | { 21 | color: blue; 22 | }` 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /src/features/EditionUiWrapper/components/EditionUiWrapperContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a connected component for handling edition ui generals 3 | * @module fonio/features/EditionUi 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React, { Component } from 'react'; 9 | import { bindActionCreators } from 'redux'; 10 | import { connect } from 'react-redux'; 11 | import { withRouter } from 'react-router'; 12 | 13 | /** 14 | * Imports Project utils 15 | */ 16 | /** 17 | * Imports Ducks 18 | */ 19 | import * as duck from '../duck'; 20 | import * as userInfoDuck from '../../UserInfoManager/duck'; 21 | import * as connectionsDuck from '../../ConnectionsManager/duck'; 22 | import * as editedStoryDuck from '../../StoryManager/duck'; 23 | 24 | /** 25 | * Imports Components 26 | */ 27 | import EditionUiWrapperLayout from './EditionUiWrapperLayout'; 28 | 29 | @connect( 30 | ( state ) => ( { 31 | lang: state.i18nState && state.i18nState.lang, 32 | ...connectionsDuck.selector( state.connections ), 33 | ...duck.selector( state.editionUiWrapper ), 34 | ...userInfoDuck.selector( state.userInfo ), 35 | ...editedStoryDuck.selector( state.editedStory ), 36 | } ), 37 | ( dispatch ) => ( { 38 | actions: bindActionCreators( { 39 | 40 | ...duck, 41 | ...userInfoDuck, 42 | ...connectionsDuck, 43 | }, dispatch ) 44 | } ) 45 | ) 46 | 47 | class EditionUiWrapperContainer extends Component { 48 | 49 | constructor( props ) { 50 | super( props ); 51 | } 52 | 53 | getNavLocation = ( path ) => { 54 | switch ( path ) { 55 | case '/story/:storyId/library': 56 | return 'library'; 57 | case '/story/:storyId/design': 58 | return 'design'; 59 | case '/story/:storyId/section/:sectionId': 60 | return 'editor'; 61 | case '/story/:storyId/summary': 62 | case '/story/:storyId': 63 | return 'summary'; 64 | default: 65 | return undefined; 66 | } 67 | } 68 | 69 | getActiveSectionTitle = ( story, sectionId ) => story.sections[sectionId].metadata.title; 70 | 71 | render() { 72 | const navLocation = this.getNavLocation( this.props.match.path ); 73 | let activeSectionTitle; 74 | if ( this.props.match.params.sectionId && this.props.editedStory ) { 75 | activeSectionTitle = this.getActiveSectionTitle( this.props.editedStory, this.props.match.params.sectionId ); 76 | } 77 | return ( 78 | 84 | ); 85 | } 86 | } 87 | 88 | export default withRouter( EditionUiWrapperContainer ); 89 | -------------------------------------------------------------------------------- /src/features/EditionUiWrapper/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the edition ui generic wrapper's react components 3 | * @module fonio/features/AuthManager 4 | */ 5 | import EditionUiWrapperContainer from './EditionUiWrapperContainer'; 6 | 7 | export default EditionUiWrapperContainer; 8 | -------------------------------------------------------------------------------- /src/features/ErrorMessageManager/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to error-related react components 3 | * @module fonio/features/ErrorMessage 4 | */ 5 | import ErrorMessageContainer from './ErrorMessageContainer'; 6 | 7 | export default ErrorMessageContainer; 8 | -------------------------------------------------------------------------------- /src/features/HomeView/assets/user-guide-fr.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/src/features/HomeView/assets/user-guide-fr.pdf -------------------------------------------------------------------------------- /src/features/HomeView/components/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides the footer of the home view 3 | * @module fonio/features/HomeView 4 | */ 5 | /* eslint react/no-danger : 0 */ 6 | /** 7 | * Imports Libraries 8 | */ 9 | import React from 'react'; 10 | import { 11 | Footer, 12 | Container, 13 | Columns, 14 | Column, 15 | Content, 16 | } from 'quinoa-design-library/components/'; 17 | 18 | import medialabLogo from '../assets/logo-medialab.svg'; 19 | import forccastLogo from '../assets/logo-forccast.svg'; 20 | 21 | const logoStyle = { 22 | maxWidth: '150px', 23 | paddingBottom: '.5rem', 24 | paddingTop: '.5rem', 25 | boxSizing: 'content-box', 26 | paddingLeft: '2rem', 27 | display: 'block' 28 | }; 29 | 30 | const FooterComponent = ( { 31 | id, 32 | translate 33 | } ) => ( 34 |
35 | 36 | 37 | 38 | 58 | 59 | 60 | 64 |

FORCCAST program, fostering pedagogical innovations in controversy mapping.' ) 67 | } } 68 | /> 69 |

médialab SciencesPo, a research laboratory that connects social sciences with inventive methods.' ) 72 | } } 73 | /> 74 | 75 |

76 | {translate( 'Avatar icons courtesy of ' )} 77 | 81 | Freepik 82 | . 83 |

84 |

85 | 90 | 94 | AGPL v3 95 | 96 | {translate( ' and is hosted on ' )} 97 | 101 | Github 102 | . 103 |

104 |
105 |
106 |
107 | 108 |
109 |
110 | ); 111 | 112 | export default FooterComponent; 113 | -------------------------------------------------------------------------------- /src/features/HomeView/components/HomeViewContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a connected component for handling the home view 3 | * @module fonio/features/HomeView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React, { Component } from 'react'; 9 | import PropTypes from 'prop-types'; 10 | import { bindActionCreators } from 'redux'; 11 | import { connect } from 'react-redux'; 12 | import { setLanguage } from 'redux-i18n'; 13 | import { withRouter } from 'react-router'; 14 | 15 | /** 16 | * Imports Project utils 17 | */ 18 | import { getEditionHistoryMap } from '../../../helpers/localStorageUtils'; 19 | 20 | /** 21 | * Imports Ducks 22 | */ 23 | import * as duck from '../duck'; 24 | import * as userInfoDuck from '../../UserInfoManager/duck'; 25 | import * as connectionsDuck from '../../ConnectionsManager/duck'; 26 | import * as authDuck from '../../AuthManager/duck'; 27 | import * as editionDuck from '../../EditionUiWrapper/duck'; 28 | import * as errorMessageDuck from '../../ErrorMessageManager/duck'; 29 | 30 | /** 31 | * Imports Components 32 | */ 33 | import HomeViewLayout from './HomeViewLayout'; 34 | 35 | /** 36 | * Redux-decorated component class rendering the takeaway dialog feature to the app 37 | */ 38 | @connect( 39 | ( state ) => ( { 40 | ...editionDuck.selector( state.editionUiWrapper ), 41 | ...duck.selector( state.home ), 42 | lang: state.i18nState.lang, 43 | ...userInfoDuck.selector( state.userInfo ), 44 | ...connectionsDuck.selector( state.connections ), 45 | ...authDuck.selector( state.auth ), 46 | } ), 47 | ( dispatch ) => ( { 48 | actions: bindActionCreators( { 49 | ...editionDuck, 50 | ...userInfoDuck, 51 | ...connectionsDuck, 52 | ...authDuck, 53 | ...errorMessageDuck, 54 | ...duck, 55 | setLanguage, 56 | }, dispatch ) 57 | } ) 58 | ) 59 | class HomeViewContainer extends Component { 60 | 61 | /** 62 | * Context data used by the component 63 | */ 64 | static contextTypes = { 65 | 66 | /** 67 | * Un-namespaced translate function 68 | */ 69 | t: PropTypes.func.isRequired, 70 | 71 | /** 72 | * Redux store 73 | */ 74 | store: PropTypes.object.isRequired 75 | } 76 | 77 | /** 78 | * constructor 79 | * @param {object} props - properties given to instance at instanciation 80 | */ 81 | constructor( props ) { 82 | super( props ); 83 | } 84 | 85 | componentWillMount() { 86 | this.props.actions.fetchStories(); 87 | const editionHistoryMap = getEditionHistoryMap(); 88 | this.props.actions.setEditionHistory( editionHistoryMap ); 89 | this.props.actions.setStoryLoginId( undefined ); 90 | } 91 | 92 | /** 93 | * Defines whether the component should re-render 94 | * @param {object} nextProps - the props to come 95 | * @param {object} nextState - the state to come 96 | * @return {boolean} shouldUpdate - whether to update or not 97 | */ 98 | shouldComponentUpdate() { 99 | // todo: optimize when the feature is stabilized 100 | return true; 101 | } 102 | 103 | /** 104 | * Renders the component 105 | * @return {ReactElement} component - the component 106 | */ 107 | render() { 108 | return ( 109 | 112 | ); 113 | } 114 | } 115 | 116 | export default withRouter( HomeViewContainer ); 117 | -------------------------------------------------------------------------------- /src/features/HomeView/components/NewStoryForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a form for creating a new story 3 | * @module fonio/features/HomeView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | Column, 11 | Columns, 12 | Container, 13 | Delete, 14 | DropZone, 15 | Help, 16 | Tab, 17 | TabLink, 18 | TabList, 19 | Tabs, 20 | Title, 21 | } from 'quinoa-design-library/components'; 22 | 23 | /** 24 | * Imports Components 25 | */ 26 | import MetadataForm from '../../../components/MetadataForm'; 27 | 28 | const NewStoryForm = ( { 29 | createStoryStatus, 30 | importStoryStatus, 31 | mode, 32 | newStory, 33 | onClose, 34 | onCloseNewStory, 35 | onCreateNewStory, 36 | onDropFiles, 37 | onSetModeFile, 38 | onSetModeForm, 39 | translate, 40 | widthRatio, 41 | } ) => { 42 | return ( 43 | 44 | { 45 | 46 | 47 | <Columns> 48 | <Column isSize={ 11 }> 49 | {translate( 'New Story' )} 50 | </Column> 51 | <Column style={ { textAlign: 'right' } }> 52 | <Delete onClick={ onClose } /> 53 | </Column> 54 | </Columns> 55 | 56 | 60 | 61 | 62 | {translate( 'Create a story' )} 66 | 67 | {translate( 'Import an existing story' )} 71 | 72 | 73 | 74 | 75 | {mode === 'form' ? 76 | 83 | : 84 | 85 | 89 | {translate( 'Drop a fonio file' )} 90 | 91 | {importStoryStatus === 'fail' && {translate( 'Story is not valid' )}} 92 | 93 | 94 | } 95 | 96 | } 97 | 98 | 99 | ); 100 | }; 101 | 102 | export default NewStoryForm; 103 | -------------------------------------------------------------------------------- /src/features/HomeView/components/OtherUsersWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a component for representing other users active in the classroom 3 | * @module fonio/features/HomeView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | Title, 11 | StretchedLayoutContainer, 12 | StretchedLayoutItem, 13 | HelpPin, 14 | Image, 15 | Content 16 | } from 'quinoa-design-library/components'; 17 | 18 | const OtherUsersWidget = ( { 19 | translate, 20 | users, 21 | userId, 22 | } ) => { 23 | return ( 24 |
25 | { 26 | users && 27 | Object.keys( users ) 28 | .filter( ( thatUserId ) => userId !== thatUserId ).length > 0 && 29 | 33 | {translate( 'Who else is online ?' )} 34 | <HelpPin>{translate( 'writers connected to this classroom right now' )}</HelpPin> 35 | 36 | } 37 |
38 | {users && 39 | Object.keys( users ) 40 | .filter( ( thatUserId ) => userId !== thatUserId ) 41 | .map( ( thatUserId ) => ( { userId, ...users[thatUserId] } ) ) 42 | .map( ( user, index ) => { 43 | return ( 44 | 49 | 50 | 55 | 56 | 57 | 58 | {user.name} 59 | 60 | 61 | 62 | ); 63 | } ) 64 | } 65 |
66 |
67 | ); 68 | }; 69 | 70 | export default OtherUsersWidget; 71 | -------------------------------------------------------------------------------- /src/features/HomeView/components/ProfileWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a component allowing user to preview and edit its personal identification information 3 | * @module fonio/features/HomeView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | Title, 11 | StretchedLayoutContainer, 12 | StretchedLayoutItem, 13 | Image, 14 | HelpPin, 15 | Button 16 | } from 'quinoa-design-library/components/'; 17 | 18 | const ProfileWidget = ( { 19 | translate, 20 | onEdit, 21 | userInfo, 22 | } ) => { 23 | return ( 24 |
28 | 32 | {translate( 'Your profile' )} 33 | <HelpPin>{translate( 'choose how you will be identified by other writers' )}</HelpPin> 34 | 35 | {userInfo.userId !== undefined && 36 | 37 | 38 | 44 | 45 | 49 | {userInfo.name} 50 | 51 | 52 | 55 | 56 | 57 | } 58 |
59 | ); 60 | }; 61 | 62 | export default ProfileWidget; 63 | -------------------------------------------------------------------------------- /src/features/HomeView/components/StoryCard.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .fonio-StoryCard{ 4 | .users-container{ 5 | max-height: 30rem; 6 | overflow-x: auto; 7 | } 8 | .users-wrapper{ 9 | display: flex; 10 | flex-flow: row wrap; 11 | } 12 | .user-container{ 13 | margin-right: 1rem; 14 | margin-bottom: 1rem; 15 | } 16 | .authors-container{ 17 | padding-bottom: 3rem; 18 | } 19 | .last-update-container{ 20 | align-self: flex-end; 21 | position: absolute; 22 | bottom: 2.5rem; 23 | left: 1.5rem; 24 | } 25 | .inline-icon-container{ 26 | margin-left: .5rem; 27 | margin-right: 1rem; 28 | } 29 | .title,.subtitle{ 30 | color: inherit; 31 | } 32 | &.is-special{ 33 | .card{ 34 | background: #4a4a4a; 35 | color: #fff; 36 | } 37 | } 38 | @media screen and (max-width:789px) { 39 | .card-content{ 40 | padding-bottom: 0; 41 | } 42 | .column.is-two-fifth{ 43 | padding-top: 0; 44 | 45 | } 46 | .aside-actions{ 47 | padding-top: 0; 48 | padding-bottom: 1.5rem; 49 | } 50 | .authors-container{ 51 | padding-bottom: 0; 52 | .content{ 53 | margin-bottom: 0; 54 | } 55 | } 56 | .users-container{ 57 | padding-bottom: 0; 58 | padding-top: 0; 59 | } 60 | .last-update-container{ 61 | padding-top: 1rem; 62 | align-self: unset; 63 | position: relative; 64 | bottom: unset; 65 | left: unset; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/features/HomeView/components/StoryCardWrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a wrapper for story cards as displayed in list within the home view 3 | * @module fonio/features/HomeView 4 | */ 5 | /* eslint react/prefer-stateless-function : 0 */ 6 | /** 7 | * Imports Libraries 8 | */ 9 | import React, { Component } from 'react'; 10 | import { 11 | Level, 12 | Column 13 | } from 'quinoa-design-library/components/'; 14 | 15 | /** 16 | * Imports Components 17 | */ 18 | import StoryCard from './StoryCard'; 19 | 20 | export default class StoryCardWrapper extends Component { 21 | render = () => { 22 | const { 23 | story, 24 | users, 25 | onAction: handleAction, 26 | onClick: handleClick, 27 | } = this.props; 28 | return ( 29 | 30 | 31 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/features/HomeView/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the authentication manager feature react components 3 | * @module fonio/features/HomeView 4 | */ 5 | import HomeViewContainer from './HomeViewContainer'; 6 | 7 | export default HomeViewContainer; 8 | -------------------------------------------------------------------------------- /src/features/HomeView/duck.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { 4 | ui as uiReducer, 5 | SET_TAB_MODE, 6 | SET_SEARCH_STRING, 7 | SET_SORTING_MODE, 8 | SET_IDENTIFICATION_MODAL_SWITCH, 9 | SET_PREVIEWED_STORY_ID, 10 | SET_STORY_DELETE_ID, 11 | SET_CHANGE_PASSWORD_ID, 12 | SET_OVERRIDE_IMPORT, 13 | SET_OVERRIDE_STORY_MODE, 14 | SET_NEW_STORY_OPEN, 15 | SET_NEW_STORY_TAB_MODE, 16 | SET_PASSWORD_MODAL_OPEN 17 | } from './duck'; 18 | 19 | describe( 'HomeView ui reducer test', () => { 20 | let mockState; 21 | let action; 22 | 23 | beforeEach( () => { 24 | mockState = {}; 25 | } ); 26 | 27 | test.each( [ 28 | [ 'stories', SET_TAB_MODE, 'stories', 'tabMode' ], 29 | [ 'searchinput', SET_SEARCH_STRING, 'searchinput', 'searchString' ], 30 | [ 'title', SET_SORTING_MODE, 'title', 'sortingMode' ], 31 | [ false, SET_IDENTIFICATION_MODAL_SWITCH, false, 'identificationModalSwitch' ], 32 | [ 'testStoryId', SET_PREVIEWED_STORY_ID, 'testStoryId', 'previewedStoryId' ], 33 | [ 'testStoryId', SET_STORY_DELETE_ID, 'testStoryId', 'storyDeleteId' ], 34 | [ 'testStoryId', SET_CHANGE_PASSWORD_ID, 'testStoryId', 'changePasswordId' ], 35 | [ false, SET_OVERRIDE_IMPORT, false, 'overrideImport' ], 36 | [ 'create', SET_OVERRIDE_STORY_MODE, 'create', 'overrideStoryMode' ], 37 | [ false, SET_NEW_STORY_OPEN, false, 'newStoryOpen' ], 38 | [ 'form', SET_NEW_STORY_TAB_MODE, 'form', 'newStoryTabMode' ], 39 | [ false, SET_PASSWORD_MODAL_OPEN, false, 'passwordModalOpen' ], 40 | ] )( 41 | 'should return %p when %s with %p', 42 | ( expected, actionName, input, reducerName ) => { 43 | action = { 44 | type: actionName, 45 | payload: input 46 | }; 47 | const resultState = uiReducer( mockState, action ); 48 | expect( resultState[reducerName] ).toEqual( expected ); 49 | }, 50 | ); 51 | } ); 52 | -------------------------------------------------------------------------------- /src/features/LibraryView/components/ConfirmBatchDeleteModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a modal for confirm the deletion of several resources 3 | * @module fonio/features/LibraryView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | ModalCard, 11 | Button 12 | } from 'quinoa-design-library/components'; 13 | 14 | const ConfirmBatchDeleteModal = ( { 15 | translate, 16 | isActive, 17 | actualResourcesPromptedToDelete, 18 | resourcesPromptedToDelete, 19 | endangeredContextualizationsLength, 20 | onDelete, 21 | onCancel 22 | } ) => ( 23 | 29 | { 30 | actualResourcesPromptedToDelete.length !== resourcesPromptedToDelete.length && 31 |

32 | { 33 | translate( '{x} of {y} of the resources you selected cannot be deleted now because they are used by another author.', { x: resourcesPromptedToDelete.length - actualResourcesPromptedToDelete.length, y: resourcesPromptedToDelete.length } ) 34 | } 35 |

36 | } 37 | {endangeredContextualizationsLength > 0 && 38 |

{ 39 | translate( [ 40 | 'You will destroy one item mention in your content if you delete these items.', 41 | 'You will destroy {n} item mentions in your content if your delete these items.', 42 | 'n' 43 | ], 44 | { n: endangeredContextualizationsLength } )} 45 |

46 | } 47 |

48 | {translate( [ 'Are you sure you want to delete this item ?', 'Are you sure you want to delete these items ?', 'n' ], { n: resourcesPromptedToDelete.length } )} 49 |

50 | 51 | } 52 | footerContent={ [ 53 | , 61 | , 68 | ] } 69 | /> 70 | ); 71 | 72 | export default ConfirmBatchDeleteModal; 73 | -------------------------------------------------------------------------------- /src/features/LibraryView/components/ResourceCard.scss: -------------------------------------------------------------------------------- 1 | .fonio-ResourceCard 2 | { 3 | .bib-wrapper 4 | { 5 | .Bibliography 6 | { 7 | .csl-entry 8 | { 9 | position: relative; 10 | 11 | overflow: hidden; 12 | 13 | max-height: 9rem; 14 | 15 | word-break: break-all; 16 | &:before 17 | { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | 22 | width: 100%; 23 | height: 100%; 24 | 25 | content: ''; 26 | } 27 | } 28 | a 29 | { 30 | pointer-events: none; 31 | } 32 | } 33 | } 34 | .preview-container 35 | { 36 | overflow: hidden; 37 | } 38 | 39 | .ReactTable 40 | { 41 | .-pagination 42 | { 43 | display: none; 44 | } 45 | .rt-tbody{ 46 | overflow-y: hidden; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/features/LibraryView/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the library view react components 3 | * @module fonio/features/LibraryView 4 | */ 5 | import LibraryViewContainer from './LibraryViewContainer'; 6 | 7 | export default LibraryViewContainer; 8 | -------------------------------------------------------------------------------- /src/features/ReadStoryView/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the read story view react components 3 | * @module fonio/features/ReadStoryView 4 | */ 5 | import ReadStoryViewContainer from './ReadStoryViewContainer'; 6 | 7 | export default ReadStoryViewContainer; 8 | -------------------------------------------------------------------------------- /src/features/SectionView/components/MoveButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a mini component for representing move possibility 3 | * @module fonio/features/SectionView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | Button, 11 | Icon 12 | } from 'quinoa-design-library/components'; 13 | import icons from 'quinoa-design-library/src/themes/millet/icons'; 14 | 15 | const MoveButton = ( { 16 | 17 | } ) => ( 18 | 28 | ); 29 | 30 | export default MoveButton; 31 | -------------------------------------------------------------------------------- /src/features/SectionView/components/ResourceMiniCard.scss: -------------------------------------------------------------------------------- 1 | .bib-wrapper-mini { 2 | .Bibliography 3 | { 4 | .csl-entry 5 | { 6 | overflow: hidden; 7 | max-height: 4.5rem; 8 | word-break: break-all; 9 | position: relative; 10 | &:before 11 | { 12 | content:''; 13 | width:100%; 14 | height:100%; 15 | position:absolute; 16 | left:0; 17 | top:0; 18 | } 19 | } 20 | a 21 | { 22 | pointer-events: none; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/features/SectionView/components/ShortcutsModal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a modal displaying shortcuts help 3 | * @module fonio/features/SectionView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { 10 | ModalCard 11 | } from 'quinoa-design-library/components'; 12 | 13 | const ShortcutsModal = ( { 14 | translate, 15 | isActive, 16 | onClose, 17 | } ) => ( 18 | 27 |

28 | {translate( 'All the shortcuts presented below are also accessible through the editor graphical interface (move cursor/select text)' )} 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
{translate( 'Shortcut' )}{translate( 'Where' )}{translate( 'Effect' )}
cmd+l{translate( 'Anywhere' )}{translate( 'Open item citation widget' )}
cmd+m{translate( 'Anywhere' )}{translate( 'Add a new note' )}
{translate( '"#" then space' )}{translate( 'Begining of a paragraph' )}{translate( 'Add a title' )}
{translate( '">" then space' )}{translate( 'Begining of a paragraph' )}{translate( 'Add a citation block' )}
{translate( '"*" then content then "*"' )}{translate( 'Anywhere' )}{translate( 'Write italic text' )}
{translate( '"**" then content then "**"' )}{translate( 'Anywhere' )}{translate( 'Write bold text' )}
{translate( '"*" then space' )}{translate( 'Begining of a paragraph' )}{translate( 'Begin a list' )}
76 | 77 | } 78 | /> 79 | ); 80 | 81 | export default ShortcutsModal; 82 | -------------------------------------------------------------------------------- /src/features/SectionView/components/SortableMiniSectionsList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a list of sections for the section view 3 | * @module fonio/features/SectionView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { SortableContainer, SortableElement } from 'react-sortable-hoc'; 10 | import { List, AutoSizer } from 'react-virtualized'; 11 | import ReactTooltip from 'react-tooltip'; 12 | import { 13 | Level, 14 | Column, 15 | } from 'quinoa-design-library/components/'; 16 | 17 | /** 18 | * Imports Components 19 | */ 20 | import SectionMiniCard from './SectionMiniCard'; 21 | 22 | const SortableItem = SortableElement( ( { 23 | value: section, 24 | onOpenSettings, 25 | onDeleteSection, 26 | setSectionLevel, 27 | 28 | storyId, 29 | 30 | setSectionIndex, 31 | sectionIndex, 32 | maxSectionIndex, 33 | history, 34 | 35 | } ) => { 36 | const handleDelete = ( event ) => { 37 | event.stopPropagation(); 38 | onDeleteSection( section.id ); 39 | }; 40 | const handleSelect = () => { 41 | if ( section.lockStatus === 'open' || ( section.lockData && section.lockData.status === 'idle' ) ) { 42 | history.push( `/story/${storyId}/section/${section.id}` ); 43 | } 44 | }; 45 | return ( 46 | 47 | 51 | 62 | 63 | 64 | ); 65 | } ); 66 | 67 | const SortableSectionsList = SortableContainer( ( { 68 | items, 69 | ...props 70 | } ) => { 71 | const rowRenderer = ( { 72 | key, 73 | style, 74 | index, 75 | } ) => { 76 | return ( 77 |
81 | 87 |
88 | ); 89 | }; 90 | const handleRowsRendered = () => 91 | ReactTooltip.rebuild(); 92 | return ( 93 | 94 | {( { width, height } ) => ( 95 | 103 | )} 104 | 105 | ); 106 | } ); 107 | 108 | export default SortableSectionsList; 109 | -------------------------------------------------------------------------------- /src/features/SectionView/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the section view react components 3 | * @module fonio/features/SectionView 4 | */ 5 | import SectionViewContainer from './SectionViewContainer'; 6 | 7 | export default SectionViewContainer; 8 | -------------------------------------------------------------------------------- /src/features/SectionsManager/duck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exports logic-related elements for specific cross-view sections operations 3 | * This module follows the ducks convention for putting in the same place actions, action types, 4 | * state selectors and reducers about a given feature (see https://github.com/erikras/ducks-modular-redux) 5 | * @module fonio/features/SectionsManager 6 | */ 7 | 8 | import { combineReducers } from 'redux'; 9 | import { createStructuredSelector } from 'reselect'; 10 | 11 | import { getStatePropFromActionSet } from '../../helpers/reduxUtils'; 12 | 13 | /** 14 | * =================================================== 15 | * ACTION NAMES 16 | * =================================================== 17 | */ 18 | /** 19 | * UI 20 | */ 21 | const SET_PROMPTED_TO_DELETE_SECTION_ID = 'SET_PROMPTED_TO_DELETE_SECTION_ID'; 22 | 23 | /** 24 | * =================================================== 25 | * ACTION CREATORS 26 | * =================================================== 27 | */ 28 | 29 | export const setPromptedToDeleteSectionId = ( payload ) => ( { 30 | type: SET_PROMPTED_TO_DELETE_SECTION_ID, 31 | payload 32 | } ); 33 | 34 | /** 35 | * =================================================== 36 | * REDUCERS 37 | * =================================================== 38 | */ 39 | 40 | const UI_DEFAULT_STATE = { 41 | promptedToDeleteSectionId: undefined, 42 | }; 43 | 44 | /** 45 | * This redux reducer handles the state of the ui 46 | * @param {object} state - the state given to the reducer 47 | * @param {object} action - the action to use to produce new state 48 | * @return {object} newState - the resulting state 49 | */ 50 | function ui( state = UI_DEFAULT_STATE, action ) { 51 | const { payload } = action; 52 | switch ( action.type ) { 53 | case SET_PROMPTED_TO_DELETE_SECTION_ID: 54 | const propName = getStatePropFromActionSet( action.type ); 55 | return { 56 | ...state, 57 | [propName]: payload 58 | }; 59 | default: 60 | return state; 61 | } 62 | } 63 | 64 | /** 65 | * The module exports a reducer connected to pouchdb thanks to redux-pouchdb 66 | */ 67 | export default combineReducers( { 68 | ui, 69 | } ); 70 | 71 | /** 72 | * =================================================== 73 | * SELECTORS 74 | * =================================================== 75 | */ 76 | const promptedToDeleteSectionId = ( state ) => state.ui.promptedToDeleteSectionId; 77 | 78 | /** 79 | * The selector is a set of functions for accessing this feature's state 80 | * @type {object} 81 | */ 82 | export const selector = createStructuredSelector( { 83 | promptedToDeleteSectionId, 84 | } ); 85 | -------------------------------------------------------------------------------- /src/features/SummaryView/components/SortableSectionsList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a sortable sections cards list 3 | * @module fonio/features/SummaryView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React from 'react'; 9 | import { SortableContainer, SortableElement } from 'react-sortable-hoc'; 10 | import FlipMove from 'react-flip-move'; 11 | import { 12 | Level, 13 | Column, 14 | } from 'quinoa-design-library/components/'; 15 | 16 | /** 17 | * Imports Components 18 | */ 19 | import SectionCard from './SectionCard'; 20 | 21 | const SortableItem = SortableElement( ( { 22 | value: section, 23 | story, 24 | goToSection, 25 | onDelete, 26 | setSectionLevel, 27 | reverseSectionLockMap = {}, 28 | isSorting, 29 | sectionIndex, 30 | // sectionIndex, 31 | maxSectionIndex, 32 | setSectionIndex, 33 | } ) => { 34 | return ( 35 | 36 | 40 | 52 | 53 | 54 | ); 55 | } 56 | ); 57 | 58 | const SortableSectionsList = SortableContainer( ( { 59 | items, 60 | ...props 61 | } ) => { 62 | return ( 63 | 64 | {items 65 | .map( ( section, index ) => { 66 | return ( 67 | 75 | ); 76 | } 77 | )} 78 | 79 | ); 80 | } ); 81 | 82 | export default SortableSectionsList; 83 | -------------------------------------------------------------------------------- /src/features/SummaryView/components/SummaryViewContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a connected component for handling the summary view 3 | * @module fonio/features/SummaryView 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import React, { Component } from 'react'; 9 | import { bindActionCreators } from 'redux'; 10 | import { connect } from 'react-redux'; 11 | 12 | /** 13 | * Imports Ducks 14 | */ 15 | import * as duck from '../duck'; 16 | import * as editedStoryDuck from '../../StoryManager/duck'; 17 | import * as connectionsDuck from '../../ConnectionsManager/duck'; 18 | import * as sectionsManagementDuck from '../../SectionsManager/duck'; 19 | 20 | /** 21 | * Imports Components 22 | */ 23 | import SummaryViewLayout from './SummaryViewLayout'; 24 | import EditionUiWrapper from '../../EditionUiWrapper/components'; 25 | 26 | @connect( 27 | ( state ) => ( { 28 | ...duck.selector( state.summary ), 29 | ...editedStoryDuck.selector( state.editedStory ), 30 | ...connectionsDuck.selector( state.connections ), 31 | ...sectionsManagementDuck.selector( state.sectionsManagement ), 32 | } ), 33 | ( dispatch ) => ( { 34 | actions: bindActionCreators( { 35 | ...connectionsDuck, 36 | ...editedStoryDuck, 37 | ...sectionsManagementDuck, 38 | ...duck 39 | }, dispatch ) 40 | } ) 41 | ) 42 | class SummaryViewContainer extends Component { 43 | 44 | constructor( props ) { 45 | super( props ); 46 | } 47 | 48 | componentWillMount = () => { 49 | const { 50 | match: { 51 | params: { 52 | storyId 53 | } 54 | }, 55 | userId 56 | } = this.props; 57 | this.props.actions.enterBlock( { 58 | storyId, 59 | userId, 60 | blockType: 'summary', 61 | blockId: 'summary', 62 | noLock: true 63 | } ); 64 | } 65 | 66 | shouldComponentUpdate = () => true; 67 | 68 | componentWillUnmount = () => { 69 | 70 | /** 71 | * Leave metadata if it was locked 72 | */ 73 | const { 74 | lockingMap, 75 | userId, 76 | editedStory = {}, 77 | actions: { 78 | leaveBlock 79 | } 80 | } = this.props; 81 | const { id: storyId } = editedStory; 82 | const userLockedOnMetadataId = lockingMap[storyId] && lockingMap[storyId].locks && 83 | Object.keys( lockingMap[storyId].locks ) 84 | .find( ( thatUserId ) => lockingMap[storyId].locks[thatUserId].storyMetadata !== undefined ); 85 | if ( userLockedOnMetadataId && userLockedOnMetadataId === userId ) { 86 | leaveBlock( { 87 | storyId, 88 | userId, 89 | blockType: 'storyMetadata', 90 | blockId: 'storyMetadata', 91 | } ); 92 | } 93 | 94 | this.props.actions.leaveBlock( { 95 | storyId, 96 | userId, 97 | blockType: 'summary', 98 | blockId: 'summary', 99 | noLock: true 100 | } ); 101 | } 102 | 103 | goToSection = ( sectionId ) => { 104 | const { 105 | editedStory: { 106 | id 107 | } 108 | } = this.props; 109 | this.props.history.push( `/story/${id}/section/${sectionId}` ); 110 | } 111 | 112 | render() { 113 | return this.props.editedStory ? 114 | ( 115 | 116 | 120 | 121 | ) 122 | : null; 123 | } 124 | } 125 | 126 | export default SummaryViewContainer; 127 | -------------------------------------------------------------------------------- /src/features/SummaryView/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint to the summary view react components 3 | * @module fonio/features/SummaryView 4 | */ 5 | import SummaryViewContainer from './SummaryViewContainer'; 6 | 7 | export default SummaryViewContainer; 8 | -------------------------------------------------------------------------------- /src/features/UserInfoManager/duck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exports logic-related elements for managing user personal identification 3 | * This module follows the ducks convention for putting in the same place actions, action types, 4 | * state selectors and reducers about a given feature (see https://github.com/erikras/ducks-modular-redux) 5 | * @module fonio/features/UserInfo 6 | */ 7 | 8 | import { createStructuredSelector } from 'reselect'; 9 | import { saveUserInfo, loadUserInfo } from '../../helpers/localStorageUtils'; 10 | 11 | /** 12 | * =================================================== 13 | * ACTION NAMES 14 | * =================================================== 15 | */ 16 | export const SET_USER_INFO = 'SET_USER_INFO'; 17 | export const SET_USER_INFO_TEMP = 'SET_USER_INFO_TEMP'; 18 | 19 | import { SET_IDENTIFICATION_MODAL_SWITCH } from '../HomeView/duck'; 20 | 21 | /** 22 | * =================================================== 23 | * ACTION CREATORS 24 | * =================================================== 25 | */ 26 | export const setUserInfo = ( payload ) => ( { 27 | type: SET_USER_INFO, 28 | payload 29 | } ); 30 | 31 | export const setUserInfoTemp = ( payload ) => ( { 32 | type: SET_USER_INFO_TEMP, 33 | payload 34 | } ); 35 | 36 | const DEFAULT_USER_INFO_STATE = { 37 | 38 | /** 39 | * User info 40 | */ 41 | userInfo: loadUserInfo(), 42 | 43 | /** 44 | * temp value of user info 45 | */ 46 | userInfoTemp: loadUserInfo(), 47 | }; 48 | 49 | /** 50 | * Reducer for the user info function 51 | * @param {object} state 52 | * @param {object} action 53 | * @return {object} newState 54 | */ 55 | export default function userInfo( state = DEFAULT_USER_INFO_STATE, action ) { 56 | switch ( action.type ) { 57 | case SET_USER_INFO: 58 | saveUserInfo( action.payload ); 59 | return { 60 | ...state, 61 | userInfo: action.payload, 62 | userInfoTemp: action.payload, 63 | }; 64 | case SET_USER_INFO_TEMP: 65 | return { 66 | ...state, 67 | userInfoTemp: action.payload, 68 | }; 69 | case SET_IDENTIFICATION_MODAL_SWITCH: 70 | if ( action.payload === false ) { 71 | return { 72 | ...state, 73 | userInfoTemp: loadUserInfo() 74 | }; 75 | } 76 | return state; 77 | default: 78 | return state; 79 | } 80 | } 81 | 82 | export const selector = createStructuredSelector( { 83 | userInfo: ( state ) => state.userInfo, 84 | userInfoTemp: ( state ) => state.userInfoTemp, 85 | } ); 86 | -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/__mocks__/README.md: -------------------------------------------------------------------------------- 1 | # Generate mocked data for copy/paste utils test 2 | 3 | ## copyTests.json (mocked data for editor copied from inside fonio) 4 | 5 | ``` 6 | { 7 | name, // name of test case 8 | editorFocus, // focused editor when copy ('main' or 'note') 9 | data: { 10 | sections, // for test purpose, only one section should inside the story, if editorFocus is note, only one note is required 11 | sectionsOrder, // for test purpose, only one section should inside the story 12 | resources, 13 | contextualizations, 14 | contextualizers, 15 | } 16 | } 17 | ``` 18 | 19 | ## pasteOutsideTests.json (mocked data for paste from outside fonio) 20 | 21 | ``` 22 | { 23 | name, // name of test case 24 | html, // html text copied from outside 25 | resources, // exist resources in story 26 | expectedResourcesToAdd, // number of resources retrieved from html 27 | expectedContextualizationsToAdd, // number of contextualizations should be created from html 28 | expectedContextualizersToAdd, // number of contextualizers created from html 29 | expectedImagesToAdd, // number of images retrieved from html 30 | } 31 | ``` 32 | ## pasteInsideTests.json (mocked data for target editor pasted to inside fonio) 33 | 34 | ``` 35 | { 36 | name, // name of test case 37 | editorFocus, // focused editor when paste ('main' or 'note') 38 | data: { 39 | sections, // for test purpose, only one section should inside the story, if editorFocus is note, only one note is required 40 | sectionsOrder, // for test purpose, only one section should inside the story 41 | } 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/__mocks__/pasteOutsideTests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "single-hyperlink", 4 | "html": "Bold text, Italic text

Example link", 5 | "resources": {}, 6 | "expectedResourcesToAdd": 1, 7 | "expectedContextualizationsToAdd": 1, 8 | "expectedContextualizersToAdd": 1, 9 | "expectedImagesToAdd": 0 10 | } 11 | ] -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an entrypoint for methods handling the complex logic 3 | * required to handle copying and pasting content in/from the editor 4 | * @module fonio/components/SectionEditor 5 | */ 6 | import copyManager from './handleCopy'; 7 | import pasteManager from './handlePaste'; 8 | 9 | /** 10 | * Prepares data within component's state for later pasting 11 | * @param {event} e - the copy event 12 | */ 13 | export const handleCopy = copyManager; 14 | 15 | /** 16 | * Handles pasting command in the editor 17 | * @param {event} e - the copy event 18 | */ 19 | export const handlePaste = pasteManager; 20 | -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/makeReactCitations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides a helper for preparing csl-json citations for their display in editor 3 | * @module fonio/components/SectionEditor 4 | */ 5 | import { Parser } from 'html-to-react'; 6 | 7 | const htmlToReactParser = new Parser(); 8 | 9 | const makeReactCitations = ( processor, cits ) => { 10 | return cits.reduce( ( inputCitations, citationData ) => { 11 | const citations = { ...inputCitations }; 12 | const citation = citationData[0]; 13 | const citationsPre = citationData[1]; 14 | const citationsPost = citationData[2]; 15 | let citationObjects = processor.processCitationCluster( citation, citationsPre, citationsPost ); 16 | citationObjects = citationObjects[1]; 17 | citationObjects.forEach( ( cit ) => { 18 | const order = cit[0]; 19 | const html = cit[1]; 20 | const ThatComponent = htmlToReactParser.parse( cit[1] ); 21 | const citationId = cit[2]; 22 | citations[citationId] = { 23 | order, 24 | html, 25 | Component: ThatComponent 26 | }; 27 | } ); 28 | return citations; 29 | }, {} ); 30 | }; 31 | 32 | export default makeReactCitations; 33 | -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/parsePastedImage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides the logic for handling an image pasting 3 | * @module fonio/components/SectionEditor 4 | */ 5 | import { v4 as generateId } from 'uuid'; 6 | 7 | import { createDefaultResource } from '../schemaUtils'; 8 | 9 | import { 10 | constants, 11 | } from 'scholar-draft'; 12 | 13 | const { 14 | BLOCK_ASSET, 15 | } = constants; 16 | 17 | export default ( 18 | node, 19 | resources = [], 20 | activeSectionId, 21 | ) => { 22 | 23 | let resource; 24 | 25 | const url = node.getAttribute( 'src' ); 26 | let title = node.getAttribute( 'title' ); 27 | const alt = node.getAttribute( 'alt' ); 28 | if ( !title || !alt || alt === 'href' ) { 29 | title = url; 30 | } 31 | if ( !url || url.indexOf( 'http' ) !== 0 ) { 32 | return {}; 33 | } 34 | 35 | const existingResource = [ ...resources ] 36 | .find( ( res ) => 37 | res.metadata.type === 'image' 38 | && res.data.url === url 39 | ); 40 | let resourceId; 41 | if ( existingResource ) { 42 | resourceId = existingResource.id; 43 | } 44 | else { 45 | resourceId = generateId(); 46 | const ext = url.split( '.' ).pop().split( '?' )[0]; 47 | resource = { 48 | ...createDefaultResource(), 49 | id: resourceId, 50 | metadata: { 51 | type: 'image', 52 | createdAt: new Date().getTime(), 53 | lastModifiedAt: new Date().getTime(), 54 | ext, 55 | mimetype: `image/${ext}`, 56 | title, 57 | }, 58 | data: { 59 | url, 60 | } 61 | }; 62 | } 63 | const contextualizerId = generateId(); 64 | const contextualizationId = generateId(); 65 | const contextualizer = { 66 | id: contextualizerId, 67 | type: 'image', 68 | insertionType: 'block' 69 | }; 70 | const contextualization = { 71 | id: contextualizationId, 72 | resourceId, 73 | contextualizerId, 74 | sectionId: activeSectionId, 75 | type: 'image', 76 | }; 77 | 78 | const entity = { 79 | type: BLOCK_ASSET, 80 | mutability: 'IMMUTABLE', 81 | data: { 82 | asset: { 83 | id: contextualizationId 84 | } 85 | } 86 | }; 87 | 88 | return { 89 | resource, 90 | contextualizer, 91 | contextualization, 92 | entity, 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/parsePastedLink.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides the logic for handling a link pasting 3 | * @module fonio/components/SectionEditor 4 | */ 5 | import { v4 as generateId } from 'uuid'; 6 | 7 | import { createDefaultResource } from '../schemaUtils'; 8 | 9 | import { 10 | constants, 11 | } from 'scholar-draft'; 12 | 13 | const { 14 | INLINE_ASSET, 15 | } = constants; 16 | 17 | export default ( 18 | node, 19 | resources = [], 20 | activeSectionId, 21 | ) => { 22 | 23 | let resource; 24 | 25 | const url = node.getAttribute( 'href' ); 26 | const alt = node.getAttribute( 'alt' ); 27 | let title = node.getAttribute( 'title' ); 28 | if ( !title || !alt || alt === 'href' ) { 29 | title = url; 30 | } 31 | if ( !url || url.indexOf( '#' ) === 0 ) { 32 | return {}; 33 | } 34 | 35 | const existingResource = [ ...resources ] 36 | .find( ( res ) => 37 | res.metadata.type === 'webpage' 38 | && res.data.url === url 39 | ); 40 | let resourceId; 41 | if ( existingResource ) { 42 | resourceId = existingResource.id; 43 | } 44 | else { 45 | resourceId = generateId(); 46 | resource = { 47 | ...createDefaultResource(), 48 | id: resourceId, 49 | metadata: { 50 | type: 'webpage', 51 | createdAt: new Date().getTime(), 52 | lastModifiedAt: new Date().getTime(), 53 | title, 54 | }, 55 | data: { 56 | url, 57 | } 58 | }; 59 | } 60 | const contextualizerId = generateId(); 61 | const contextualizationId = generateId(); 62 | const contextualizer = { 63 | id: contextualizerId, 64 | type: 'webpage', 65 | insertionType: 'inline' 66 | }; 67 | const contextualization = { 68 | id: contextualizationId, 69 | resourceId, 70 | contextualizerId, 71 | sectionId: activeSectionId, 72 | type: 'webpage', 73 | }; 74 | 75 | const entity = { 76 | type: INLINE_ASSET, 77 | mutability: 'MUTABLE', 78 | data: { 79 | asset: { 80 | id: contextualizationId 81 | } 82 | } 83 | }; 84 | 85 | return { 86 | resource, 87 | contextualizer, 88 | contextualization, 89 | entity, 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /src/helpers/clipboardUtils/pasteFromOutside.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides unit tests for the assetsUtils module 3 | * @module fonio/helpers/clipboardUtils/handleCopy 4 | * @funciton computePastedData() 5 | * @param {string} html - mock from handleCopy 6 | * @param {object} activeSection - mock from state 7 | * @param {object} resources - mock from story json 8 | */ 9 | 10 | import expect from 'expect'; 11 | import { 12 | } from './__mocks__/services.js'; 13 | import tests from './__mocks__/pasteOutsideTests'; 14 | import { 15 | computePastedData, 16 | } from './pasteFromOutside'; 17 | 18 | const testCases = tests.map( ( item ) => { 19 | return [ item.name ]; 20 | } ); 21 | describe( 'test computePastedData()', () => { 22 | test.each( testCases )( 'test', ( testName ) => { 23 | const { 24 | html, 25 | resources, 26 | expectedResourcesToAdd, 27 | expectedContextualizationsToAdd, 28 | expectedContextualizersToAdd, 29 | expectedImagesToAdd 30 | } = tests.find( ( item ) => item.name === testName ); 31 | const computedData = computePastedData( { 32 | html, 33 | resources, 34 | activeSection: { 35 | id: 'testSectionId' 36 | } 37 | } ); 38 | const { 39 | copiedContentState, 40 | resourcesToAdd, 41 | contextualizationsToAdd, 42 | contextualizersToAdd, 43 | imagesToAdd 44 | } = computedData; 45 | expect( copiedContentState ).toBeDefined(); 46 | expect( resourcesToAdd.length ).toEqual( expectedResourcesToAdd ); 47 | expect( contextualizationsToAdd.length ).toEqual( expectedContextualizationsToAdd ); 48 | expect( contextualizersToAdd.length ).toEqual( expectedContextualizersToAdd ); 49 | expect( imagesToAdd.length ).toEqual( expectedImagesToAdd ); 50 | } ); 51 | } ); 52 | 53 | -------------------------------------------------------------------------------- /src/helpers/draftUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module handles scholar-draft related operations on draft-js states 3 | * @module fonio/utils/draftUtils 4 | */ 5 | 6 | import { 7 | utils 8 | } from 'scholar-draft'; 9 | 10 | const { 11 | insertInlineAssetInEditor, 12 | insertBlockAssetInEditor, 13 | } = utils; 14 | 15 | /** 16 | * Inserts an inline contextualization entity into the given draft editor state 17 | * @param {EditorState} editorState - the editor state before insertion 18 | * @param {object} contextualization - the contextualization to link the entity to 19 | * @return {EditorState} newEditorState - a new editor state 20 | */ 21 | export const insertInlineContextualization = ( editorState, contextualization, mutable = false ) => { 22 | const newEditorState = insertInlineAssetInEditor( editorState, { id: contextualization.id }, editorState.getSelection(), mutable ); 23 | return newEditorState ? newEditorState : editorState; 24 | }; 25 | 26 | /** 27 | * Inserts a block contextualization entity into the given draft editor state 28 | * @param {EditorState} editorState - the editor state before insertion 29 | * @param {object} contextualization - the contextualization to link the entity to 30 | * @return {EditorState} newEditorState - a new editor state 31 | */ 32 | export const insertBlockContextualization = ( editorState, contextualization ) => { 33 | const newEditorState = insertBlockAssetInEditor( editorState, { id: contextualization.id }, editorState.getSelection() ); 34 | return newEditorState ? newEditorState : editorState; 35 | }; 36 | 37 | /** 38 | * Get current selected text 39 | * @param {Draft.ContentState} 40 | * @param {Draft.SelectionState} 41 | * @param {String} 42 | * @return {String} 43 | */ 44 | export const getTextSelection = ( contentState, selection, blockDelimiter ) => { 45 | blockDelimiter = blockDelimiter || '\n'; 46 | const startKey = selection.getStartKey(); 47 | const endKey = selection.getEndKey(); 48 | const blocks = contentState.getBlockMap(); 49 | 50 | let lastWasEnd = false; 51 | const selectedBlock = blocks 52 | .skipUntil( function( block ) { 53 | return block.getKey() === startKey; 54 | } ) 55 | .takeUntil( function( block ) { 56 | const result = lastWasEnd; 57 | 58 | if ( block.getKey() === endKey ) { 59 | lastWasEnd = true; 60 | } 61 | 62 | return result; 63 | } ); 64 | 65 | return selectedBlock 66 | .map( function( block ) { 67 | const key = block.getKey(); 68 | let text = block.getText(); 69 | 70 | let start = 0; 71 | let end = text.length; 72 | 73 | if ( key === startKey ) { 74 | start = selection.getStartOffset(); 75 | } 76 | if ( key === endKey ) { 77 | end = selection.getEndOffset(); 78 | } 79 | 80 | text = text.slice( start, end ); 81 | return text; 82 | } ) 83 | .join( blockDelimiter ); 84 | }; 85 | -------------------------------------------------------------------------------- /src/helpers/editorToStoryUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides utils for infering story modifications from the editor state 3 | * @module fonio/components/SectionEditor 4 | */ 5 | /** 6 | * Imports Libraries 7 | */ 8 | import { 9 | utils, 10 | } from 'scholar-draft'; 11 | 12 | /** 13 | * Shared variables 14 | */ 15 | const { 16 | getUsedAssets, 17 | updateNotesFromEditor, 18 | } = utils; 19 | 20 | /** 21 | * Deletes notes that are not any more linked 22 | * to an entity in the editor 23 | * and update notes numbers if their order has changed. 24 | * @param {object} props - properties to use 25 | */ 26 | export const updateNotesFromSectionEditor = ( props ) => { 27 | const { 28 | editorStates, 29 | sectionId, 30 | activeStoryId, 31 | activeSection, 32 | updateSection, 33 | } = props; 34 | const { 35 | // newNotes, 36 | notesOrder 37 | } = updateNotesFromEditor( editorStates[sectionId], { ...activeSection.notes } ); 38 | const newSection = activeSection; 39 | // newSection.notes = newNotes; 40 | newSection.notesOrder = notesOrder; 41 | // if (newNotes !== activeSection.notes) { 42 | updateSection( activeStoryId, sectionId, newSection ); 43 | // } 44 | }; 45 | 46 | /** 47 | * Deletes contextualizations that are not any more linked 48 | * to an entity in the editor. 49 | * @param {object} props - properties to use 50 | */ 51 | export const updateContextualizationsFromEditor = ( props ) => { 52 | const { 53 | activeSection, 54 | editorStates, 55 | deleteContextualization, 56 | deleteContextualizer, 57 | // sectionId, 58 | story, 59 | userId 60 | } = props; 61 | const activeStoryId = story.id; 62 | const activeSectionId = activeSection.id; 63 | // regroup all eligible editorStates 64 | const notesEditorStates = activeSection.notesOrder.reduce( ( result, noteId ) => { 65 | return { 66 | ...result, 67 | [noteId]: editorStates[noteId] 68 | }; 69 | }, {} ); 70 | // regroup all eligible contextualizations 71 | const sectionContextualizations = Object.keys( story.contextualizations ) 72 | .filter( ( id ) => { 73 | return story.contextualizations[id].sectionId === activeSectionId; 74 | } ) 75 | .reduce( ( final, id ) => ( { 76 | ...final, 77 | [id]: story.contextualizations[id], 78 | } ), {} ); 79 | 80 | // look for used contextualizations in main 81 | let used = getUsedAssets( editorStates[activeSectionId], sectionContextualizations ); 82 | // look for used contextualizations in notes 83 | Object.keys( notesEditorStates ) 84 | .forEach( ( noteId ) => { 85 | const noteEditor = notesEditorStates[noteId]; 86 | used = used.concat( getUsedAssets( noteEditor, sectionContextualizations ) ); 87 | } ); 88 | 89 | /* 90 | * compare list of contextualizations with list of used contextualizations 91 | * to track all unused contextualizations 92 | */ 93 | const unusedAssets = Object.keys( sectionContextualizations ).filter( ( id ) => !used.includes( id ) ); 94 | // delete contextualizations 95 | unusedAssets.forEach( ( id ) => { 96 | const { contextualizerId } = sectionContextualizations[id]; 97 | deleteContextualization( { storyId: activeStoryId, contextualizationId: id, userId } ); 98 | deleteContextualizer( { storyId: activeStoryId, contextualizerId, userId } ); 99 | } ); 100 | }; 101 | -------------------------------------------------------------------------------- /src/helpers/fileDownloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module handles downloading a file from string content 3 | * @module fonio/utils/fileDownloader 4 | */ 5 | import FileSaver from 'file-saver'; 6 | 7 | /** 8 | * @param {string} text - the text to put in the file 9 | * @param {string} extension - the extension to append to file name 10 | * @param {string} fileName - the name to attribute to the downloaded file 11 | */ 12 | export default function downloadFile( text, extension = 'txt', fileName = 'fonio' ) { 13 | let type; 14 | switch ( extension ) { 15 | case 'zip': 16 | type = 'octet/stream'; 17 | break; 18 | case 'html': 19 | type = 'text/html;charset=utf-8'; 20 | break; 21 | default: 22 | type = 'text/plain;charset=utf-8'; 23 | break; 24 | } 25 | const blob = new Blob( [ text ], { type } ); 26 | FileSaver.saveAs( blob, `${fileName }.${ extension}` ); 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/fileLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module helps to load files from app server or user own file system 3 | * @module fonio/utils/fileLoader 4 | */ 5 | 6 | /** 7 | * Validates whether the extension of a file is valid against its visualization model 8 | * @param {string} fileName - the name of the file to validate 9 | * @param {object} visualizationModel - the model of the visualization to validate the filename against 10 | * @return {boolean} isValid - whether the filename is valid 11 | */ 12 | export function validateFileExtensionForVisType ( fileName = '', visualizationModel ) { 13 | const fileExtension = fileName.split( '.' ).pop(); 14 | return visualizationModel.acceptedFileExtensions.find( ( ext ) => ext === fileExtension ) !== undefined; 15 | } 16 | 17 | /** 18 | * Reads the raw string content of a file from user file system 19 | * @param {File} fileToRead - the file to read 20 | * @param {function} callback 21 | */ 22 | export function getFileAsText( file ) { 23 | return new Promise( ( resolve, reject ) => { 24 | let reader = new FileReader(); 25 | reader.onload = ( event ) => { 26 | resolve( event.target.result ); 27 | reader = undefined; 28 | }; 29 | reader.onerror = ( event ) => { 30 | reject( event.target.error ); 31 | reader = undefined; 32 | }; 33 | return reader.readAsText( file ); 34 | } ); 35 | } 36 | -------------------------------------------------------------------------------- /src/helpers/fileLoader.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides unit tests for the fileLoader module 3 | * @module fonio/utils/fileLoader 4 | */ 5 | import { expect } from 'chai'; 6 | 7 | import { 8 | validateFileExtensionForVisType 9 | } from './fileLoader'; 10 | 11 | describe( 'fileLoader helpers', () => { 12 | describe( 'validateFileExtensionForVisType', () => { 13 | const visualizationModel = { 14 | acceptedFileExtensions: [ 'csv', 'tsv', 'dsv' ] 15 | }; 16 | const validFileNames = [ 'myfile.csv', 'myfile.tsv', 'myfile.dsv', 'myfile.doc.csv' ]; 17 | const invalidFileNames = [ '', 'myfile', 'myfile_csv', 'myfile.csv.psd' ]; 18 | 19 | it( 'should accept valid extensions', () => { 20 | validFileNames.forEach( ( fileName ) => { 21 | const valid = validateFileExtensionForVisType( fileName, visualizationModel ); 22 | return expect( valid ).to.be.true; 23 | } ); 24 | } ); 25 | 26 | it( 'should not accept invalid extensions', () => { 27 | invalidFileNames.forEach( ( fileName ) => { 28 | const valid = validateFileExtensionForVisType( fileName, visualizationModel ); 29 | return expect( valid ).to.be.false; 30 | } ); 31 | } ); 32 | } ); 33 | } ); 34 | -------------------------------------------------------------------------------- /src/helpers/localStorageUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module helps to manage local storage data 3 | * @module fonio/utils/localStorageUtils 4 | */ 5 | export const loadStoryToken = ( storyId ) => { 6 | return localStorage.getItem( `fonio/storyToken/${storyId}` ); 7 | }; 8 | export const saveStoryToken = ( storyId, token ) => { 9 | localStorage.setItem( `fonio/storyToken/${storyId}`, token ); 10 | }; 11 | export const deleteStoryToken = ( storyId ) => { 12 | localStorage.removeItem( `fonio/storyToken/${storyId}` ); 13 | }; 14 | 15 | export const updateEditionHistoryMap = ( storyId ) => { 16 | const existing = localStorage.getItem( 'fonio/editionStoryMap' ); 17 | let previousMap; 18 | try { 19 | if ( existing ) { 20 | previousMap = JSON.parse( existing ); 21 | } 22 | else previousMap = {}; 23 | } 24 | catch ( e ) { 25 | previousMap = {}; 26 | } 27 | const newMap = { 28 | ...previousMap, 29 | [storyId]: new Date().getTime() 30 | }; 31 | localStorage.setItem( 'fonio/editionStoryMap', JSON.stringify( newMap ) ); 32 | }; 33 | 34 | const getJSONFromStorage = ( key ) => { 35 | const existing = localStorage.getItem( key ); 36 | let result; 37 | try { 38 | if ( existing ) { 39 | result = JSON.parse( existing ); 40 | } 41 | } 42 | catch ( e ) { 43 | result = undefined; 44 | } 45 | return result; 46 | }; 47 | 48 | export const getEditionHistoryMap = () => { 49 | return getJSONFromStorage( 'fonio/editionStoryMap' ) || {}; 50 | }; 51 | 52 | export const saveUserInfo = ( userInfo ) => { 53 | localStorage.setItem( 'fonio/user_info', JSON.stringify( userInfo ) ); 54 | }; 55 | 56 | export const loadUserInfo = () => { 57 | return getJSONFromStorage( 'fonio/user_info' ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/helpers/misc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides miscellaneous utils 3 | * @module fonio/utils/misc 4 | */ 5 | import trunc from 'unicode-byte-truncate'; 6 | 7 | export const abbrevString = ( str = '', maxLength = 10 ) => { 8 | if ( str.length > maxLength ) { 9 | return `${trunc( str, maxLength ) }...`; 10 | } 11 | return str; 12 | }; 13 | 14 | export const splitPathnameForSockets = ( url ) => { 15 | const h = url.split( '//' ); 16 | const p = h.slice( -1 )[0].split( '/' ); 17 | 18 | return [ 19 | ( h.length > 1 ? ( `${h[0] }//` ) : '' ) + p[0], 20 | p.slice( 1 ).filter( ( i ) => i ) 21 | ]; 22 | }; 23 | 24 | export const bytesToBase64Length = ( bytes ) => bytes * ( 4 / 3 ); 25 | export const base64ToBytesLength = ( bytes ) => bytes / ( 4 / 3 ); 26 | 27 | export const getBrowserInfo = () => { 28 | const ua = navigator.userAgent; 29 | let tem, M = ua.match( /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i ) || []; 30 | if ( /trident/i.test( M[1] ) ) { 31 | tem = /\brv[ :]+(\d+)/g.exec( ua ) || []; 32 | return { name: 'IE ', version: ( tem[1] || '' ) }; 33 | } 34 | if ( /edge/gi.test( ua ) ) { 35 | return { 36 | name: 'Edge', 37 | version: 'unknown', 38 | }; 39 | } 40 | if ( M[1] === 'Chrome' ) { 41 | tem = ua.match( /\bOPR\/(\d+)/ ); 42 | if ( tem != null ) { /* eslint eqeqeq : 0 */ 43 | return { name: 'Opera', version: tem[1] }; 44 | } 45 | } 46 | M = M[2] ? [ M[1], M[2] ] : [ navigator.appName, navigator.appVersion, '-?' ]; 47 | if ( ( tem = ua.match( /version\/(\d+)/i ) ) != null ) { /* eslint eqeqeq : 0 */ 48 | M.splice( 1, 1, tem[1] ); 49 | } 50 | return { 51 | name: M[0], 52 | version: M[1] 53 | }; 54 | }; 55 | 56 | export const computeSectionFirstWords = ( section, maxLength = 100 ) => { 57 | if ( section.contents 58 | && section.contents.blocks 59 | && section.contents.blocks[0] 60 | && section.contents.blocks[0].text 61 | ) { 62 | return section.contents.blocks[0].text.length > maxLength ? 63 | `${section.contents.blocks[0].text.substr( 0, maxLength )}...` 64 | : 65 | section.contents.blocks[0].text; 66 | } 67 | return ''; 68 | }; 69 | 70 | export const silentEvent = ( event ) => { 71 | if ( event ) { 72 | event.stopPropagation(); 73 | event.preventDefault(); 74 | } 75 | }; 76 | 77 | const getContentsRawText = ( contentObj ) => { 78 | return contentObj.blocks ? contentObj.blocks.map( ( b ) => b.text ).join( '\n' ) : ''; 79 | }; 80 | 81 | const getSectionRawText = ( section ) => { 82 | return section.notesOrder.reduce( ( sum, noteId ) => { 83 | return `${sum} ${getContentsRawText( section.notes[noteId].contents )} `; 84 | }, getContentsRawText( section.contents ) ); 85 | }; 86 | 87 | export const getStoryStats = ( story ) => { 88 | const rawText = story.sectionsOrder.reduce( ( sum, sectionId ) => { 89 | return `${sum} ${story.sections[sectionId].metadata.title} ${getSectionRawText( story.sections[sectionId] )}`; 90 | }, '' ); 91 | const numberOfWords = rawText.match( /\S+/g ) ? rawText.match( /\S+/g ).length : 0; 92 | return { 93 | numberOfCharacters: rawText.length, 94 | numberOfWords, 95 | numberOfPages: Math.ceil( numberOfWords / 300 ) 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /src/helpers/postcss.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module helps to prefix css style 3 | * @module fonio/utils/postcss 4 | */ 5 | import postcss from 'postcss'; 6 | import prefixer from 'postcss-prefix-selector'; 7 | 8 | export const processCustomCss = ( css = '' ) => { 9 | try { 10 | return postcss().use( prefixer( { 11 | prefix: '.quinoa-story-player', 12 | // exclude: ['.c'], 13 | 14 | // Optional transform callback for case-by-case overrides 15 | transform ( prefix, selector, prefixedSelector ) { 16 | if ( selector === 'body' ) { 17 | return `body ${ prefix}`; 18 | } 19 | else { 20 | return prefixedSelector; 21 | } 22 | } 23 | } ) ) 24 | .process( css ).css; 25 | } 26 | catch ( e ) { 27 | return undefined; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/helpers/projectBundler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module helps for processes related to the exports of a story 3 | * @module fonio/utils/projectBundler 4 | */ 5 | import { 6 | convertFromRaw 7 | } from 'draft-js'; 8 | 9 | import { stateToMarkdown } from 'draft-js-export-markdown'; 10 | 11 | /** 12 | * Prepares a story data for a clean version to export 13 | * @param {object} story - the input data to clean 14 | * @return {object} newStory - the cleaned story 15 | */ 16 | export const cleanStoryForExport = ( story ) => { 17 | return story; 18 | }; 19 | 20 | /** 21 | * Consumes story data to produce a representation in markdown syntax 22 | * @todo: for now this does not handle assets, it should be broadly improved to do that 23 | * @param {object} story - the story to consume 24 | * @return {string} markdown - the markdown representation of the story 25 | */ 26 | export const convertStoryToMarkdown = ( story ) => { 27 | const header = `${story.metadata.title} 28 | ==== 29 | ${story.metadata.authors.join( ', ' )} 30 | --- 31 | `; 32 | return header + story.sectionsOrder.map( ( id ) => { 33 | const content = convertFromRaw( story.sections[id].contents ); 34 | return stateToMarkdown( content ); 35 | } ).join( '\n \n' ); 36 | }; 37 | 38 | /** 39 | * Cleans and serializes a story representation 40 | * @param {object} story - the story to bundle 41 | * @return {string} result - the resulting serialized story 42 | */ 43 | export const bundleProjectAsJSON = ( story ) => { 44 | return JSON.stringify( cleanStoryForExport( story ) ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/helpers/reduxUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module helps to optimize redux-related code 3 | * @module fonio/utils/reduxUtils 4 | */ 5 | 6 | export const getStatePropFromActionSet = ( actionName ) => { 7 | return actionName.replace( 'SET_', '' ).toLowerCase().replace( /(_[a-z])/gi, ( a, b ) => b.substr( 1 ).toUpperCase() ); 8 | }; 9 | -------------------------------------------------------------------------------- /src/helpers/translateUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provide helpers to handle translations in the app 3 | * @module bulgur/utils/translateUtils 4 | */ 5 | 6 | /** 7 | * Automatically functions a translate function 8 | * @param {function} translateFn - the translate function to namespace 9 | * @param {string} nameSpace - the namespace to use 10 | * @return {function} translateFnBis - a new function using the namespace 11 | */ 12 | export const translateNameSpacer = ( translateFn, nameSpace ) => { 13 | return function( key, props ) { 14 | if ( Array.isArray( key ) ) { 15 | return translateFn( key.map( ( k ) => { 16 | if ( k.length > 1 ) { 17 | return `${nameSpace }.${ k}`; 18 | } 19 | return k; 20 | } ), props ); 21 | } 22 | return translateFn( `${nameSpace }.${ key}`, props ); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/helpers/userInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module helps to manage users identification data 3 | * @module fonio/utils/userInfo 4 | */ 5 | import userNames from '../sharedAssets/userNames'; 6 | import avatars from '../sharedAssets/avatars'; 7 | 8 | /** 9 | * Generates random values for user name 10 | * @param {string} lang - the lang to use to generate info 11 | */ 12 | export default function generateRandomUserInfo ( lang ) { 13 | const { adjectives, names, pattern } = userNames[lang]; 14 | const adjective = adjectives[parseInt( Math.random() * adjectives.length, 10 )]; 15 | const name = names[parseInt( Math.random() * names.length, 10 )].toLowerCase(); 16 | const avatar = avatars[parseInt( Math.random() * avatars.length, 10 )]; 17 | return { 18 | name: pattern.replace( 'adjective', adjective ).replace( 'name', name ), 19 | avatar, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fonio Application Endpoint 3 | * ====================================== 4 | * 5 | * Rendering the application. 6 | * @module fonio 7 | */ 8 | import React from 'react'; 9 | import { render } from 'react-dom'; 10 | import { Provider } from 'react-redux'; 11 | import I18n from 'redux-i18n'; 12 | 13 | import configureStore from './redux/configureStore'; 14 | import Application from './Application'; 15 | 16 | import translations from './translations/index.js'; 17 | 18 | let CurrentApplication = Application; 19 | 20 | const initialState = {}; 21 | 22 | const store = configureStore( initialState ); 23 | window.store = store; 24 | 25 | const mountNode = document.getElementById( 'mount' ); 26 | 27 | let browserLang = ( navigator.language || navigator.userLanguage ).split( '-' )[0]; 28 | if ( browserLang !== 'en' || browserLang !== 'fr' ) { 29 | browserLang = 'en'; 30 | } 31 | const initialLang = localStorage.getItem( 'fonio-lang' ) || browserLang; 32 | 33 | /** 34 | * Mounts the application to the given mount node 35 | */ 36 | export function renderApplication() { 37 | const group = ( 38 | 39 | 43 | 44 | 45 | 46 | ); 47 | render( group, mountNode ); 48 | } 49 | 50 | renderApplication(); 51 | 52 | /** 53 | * Hot-reloading. 54 | */ 55 | if ( module.hot ) { 56 | module.hot.accept( './Application', function() { 57 | CurrentApplication = require( './Application' ).default; 58 | renderApplication(); 59 | } ); 60 | } 61 | -------------------------------------------------------------------------------- /src/parameters.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * global style parameters for the whole app 3 | * 4 | * @module fonio 5 | */ 6 | 7 | 8 | $gutter-small : 1rem; 9 | -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fonio store configuration 3 | * =================================== 4 | * Configuring store with appropriate middlewares 5 | */ 6 | import { 7 | applyMiddleware, 8 | createStore, 9 | compose 10 | } from 'redux'; 11 | import rootReducer from './rootReducer'; 12 | import promiseMiddleware from './promiseMiddleware'; 13 | import Validator from './payloadValidatorMiddleware'; 14 | import { loadingBarMiddleware } from 'react-redux-loading-bar'; 15 | 16 | import { CONNECT_ERROR, RECONNECT } from '../features/ErrorMessageManager/duck'; 17 | 18 | import config from '../config'; 19 | 20 | import io from 'socket.io-client'; 21 | import createSocketIoMiddleware from './socketIoMiddleware'; 22 | 23 | import { splitPathnameForSockets } from '../helpers/misc'; 24 | 25 | /** 26 | * @todo: fetch that from config 27 | */ 28 | const [ apiOrigin, apiPathname ] = splitPathnameForSockets( config.apiUrl ); 29 | const path = `/${ apiPathname.concat( 'sockets' ).join( '/' )}`; 30 | 31 | const socket = io( apiOrigin, { path } ); 32 | 33 | const socketIoMiddleware = createSocketIoMiddleware( socket ); 34 | 35 | /** 36 | * redux action validator middleware 37 | */ 38 | const validatorMiddleware = Validator(); 39 | 40 | /** 41 | * Configures store with a possible inherited state and appropriate reducers 42 | * @param initialState - the state to use to bootstrap the reducer 43 | * @return {object} store - the configured store 44 | */ 45 | export default function configureStore ( initialState = {} ) { 46 | // Compose final middleware with thunk and promises handling 47 | const middleware = applyMiddleware( 48 | validatorMiddleware, 49 | socketIoMiddleware, 50 | promiseMiddleware(), 51 | loadingBarMiddleware( { 52 | promiseTypeSuffixes: [ 'PENDING', 'SUCCESS', 'FAIL' ], 53 | } ) 54 | ); 55 | 56 | // Create final store and subscribe router in debug env ie. for devtools 57 | const createStoreWithMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose( 58 | // related middlewares 59 | middleware, 60 | // connection to redux dev tools 61 | window.__REDUX_DEVTOOLS_EXTENSION__() )( createStore ) : compose( middleware )( createStore ); 62 | 63 | const store = createStoreWithMiddleware( 64 | rootReducer, 65 | initialState, 66 | ); 67 | 68 | const connectionErrors = [ 'connect_error', 'reconnect_failed', 'reconnect_error', 'disconnect' ]; 69 | connectionErrors.forEach( ( message ) => { 70 | socket.on( message, ( error ) => { 71 | store.dispatch( { 72 | type: CONNECT_ERROR, 73 | error 74 | } ); 75 | } ); 76 | } ); 77 | 78 | socket.on( 'reconnect', ( error ) => { 79 | store.dispatch( { 80 | type: RECONNECT, 81 | error 82 | } ); 83 | } ); 84 | 85 | // live-reloading handling 86 | if ( module.hot ) { 87 | module.hot.accept( './rootReducer', () => { 88 | const nextRootReducer = require( './rootReducer' ).default; 89 | store.replaceReducer( nextRootReducer ); 90 | } ); 91 | } 92 | return store; 93 | } 94 | -------------------------------------------------------------------------------- /src/redux/payloadValidatorMiddleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Payload validator middleware 3 | * =================================== 4 | * Modified from: 5 | * https://github.com/MaxLi1994/redux-validator 6 | * Author: https://github.com/MaxLi1994 7 | */ 8 | const options = { 9 | validatorKey: 'meta', 10 | paramKey: 'payload' 11 | }; 12 | 13 | export default () => ( store ) => ( next ) => ( action ) => { 14 | if ( !action[options.validatorKey] || !action[options.validatorKey].validator || action[options.validatorKey].disableValidate ) { 15 | // thunk compatible 16 | if ( action[options.paramKey] && action[options.paramKey].thunk ) { 17 | return next( action[options.paramKey].thunk ); 18 | } 19 | else { 20 | return next( action ); 21 | } 22 | } 23 | 24 | let flag = true; 25 | let errorParam, errorId, errorMsg; 26 | 27 | const validators = action[options.validatorKey].validator || {}; 28 | const runValidator = ( param, func, msg, id, key ) => { 29 | let valid; 30 | if ( func ) { 31 | valid = func( param, store.getState(), action.payload ); 32 | } 33 | else { 34 | throw new Error( 'validator func is needed' ); 35 | } 36 | if 37 | ( typeof valid !== 'boolean' ) { 38 | throw new Error( 'validator func must return boolean type' ); 39 | } 40 | if ( !valid ) { 41 | errorParam = key; 42 | errorId = id; 43 | errorMsg = ( typeof msg === 'function' 44 | ? msg( param, store.getState(), action.payload ) 45 | : msg ) 46 | || ''; 47 | } 48 | 49 | return valid; 50 | }; 51 | 52 | const runValidatorContainer = ( validator, param, key ) => { 53 | let valid; 54 | if ( Array.prototype.isPrototypeOf( validator ) ) { 55 | for ( const j in validator ) { 56 | if ( validator.hasOwnProperty( j ) ) { 57 | const item = validator[j]; 58 | valid = runValidator( param, item.func, item.msg, j, key ); 59 | if ( !valid ) break; 60 | } 61 | } 62 | } 63 | else { 64 | valid = runValidator( param, validator.func, validator.msg, 0, key ); 65 | } 66 | return valid; 67 | }; 68 | 69 | const params = action[options.paramKey] || {}; 70 | for ( const i in validators ) { 71 | if ( validators.hasOwnProperty( i ) ) { 72 | if ( i === options.paramKey || i === 'thunk' ) continue; 73 | const validator = validators[i]; 74 | 75 | flag = runValidatorContainer( validator, params[i], i ); 76 | if ( !flag ) break; 77 | } 78 | } 79 | 80 | // param object itself 81 | const paramObjValidator = validators[options.paramKey]; 82 | if ( paramObjValidator && flag ) { 83 | flag = runValidatorContainer( paramObjValidator, action[options.paramKey], options.paramKey ); 84 | } 85 | // ------- 86 | 87 | if ( flag ) { 88 | // thunk compatible 89 | if ( action[options.paramKey] && action[options.paramKey].thunk ) { 90 | return next( action[options.paramKey].thunk ); 91 | } 92 | else { 93 | return next( action ); 94 | } 95 | } 96 | else { 97 | return next( { errors: errorMsg, type: `${action.type}_FAIL`, param: errorParam, id: errorId } ); 98 | 99 | /* 100 | * return { 101 | * err: 'validator', 102 | * msg: errorMsg, 103 | * param: errorParam, 104 | * id: errorId 105 | * }; 106 | */ 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /src/redux/promiseMiddleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promise middleware 3 | * =================================== 4 | * If a promise is passed in an action, 5 | * this middleware will resolve it and dispatch related actions names 6 | * (ACTION_NAME when started, then ACTION_NAME_SUCCESS or ACTION_NAME_FAIL depending on promise outcome) 7 | */ 8 | 9 | import config from '../../config'; 10 | const { timers } = config; 11 | 12 | export default () => ( { dispatch, getState } ) => ( next ) => ( action ) => { 13 | // If the action is a function, execute it 14 | if ( typeof action === 'function' ) { 15 | return action( dispatch, getState ); 16 | } 17 | 18 | const { promise, type, ...rest } = action; 19 | 20 | // If there is no promise in the action, ignore it 21 | if ( !promise ) { 22 | // pass the action to the next middleware 23 | return next( action ); 24 | } 25 | else if ( typeof promise !== 'function' || !Promise.resolve( promise ) ) { 26 | console.warn( 'passed an action with a "promise" prop which is not a promise function, action:', action );/* eslint no-console : 0 */ 27 | return next( action ); 28 | } 29 | // build constants that will be used to dispatch actions 30 | const REQUEST = `${type }_PENDING`; 31 | const SUCCESS = `${type }_SUCCESS`; 32 | const FAIL = `${type }_FAIL`; 33 | const RESET = `${type }_RESET`; 34 | 35 | /* 36 | * Trigger the action once to dispatch 37 | * the fact promise is starting resolving (for loading indication for instance) 38 | */ 39 | next( { ...rest, type: REQUEST } ); 40 | // resolve promise 41 | return promise( dispatch, getState ).then( 42 | ( result ) => { 43 | setTimeout( () => 44 | next( { ...rest, type: RESET } ) 45 | , timers.long ); 46 | return next( { ...rest, result, type: SUCCESS } ); 47 | } ).catch( ( error ) => 48 | next( { ...rest, error, type: FAIL } ) 49 | ); 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fonio Reducers Endpoint 3 | * =================================== 4 | * 5 | * Combining the app's reducers. 6 | */ 7 | import { combineReducers } from 'redux'; 8 | 9 | import { i18nState } from 'redux-i18n'; 10 | 11 | import { loadingBarReducer } from 'react-redux-loading-bar'; 12 | 13 | import { reducer as toastrReducer } from 'react-redux-toastr'; 14 | 15 | import home from '../features/HomeView/duck'; 16 | import summary from '../features/SummaryView/duck'; 17 | import section from '../features/SectionView/duck'; 18 | import library from '../features/LibraryView/duck'; 19 | import design from '../features/DesignView/duck'; 20 | 21 | import connections from '../features/ConnectionsManager/duck'; 22 | import userInfo from '../features/UserInfoManager/duck'; 23 | import auth from '../features/AuthManager/duck'; 24 | import editionUiWrapper from '../features/EditionUiWrapper/duck'; 25 | import editedStory from '../features/StoryManager/duck'; 26 | import sectionsManagement from '../features/SectionsManager/duck'; 27 | import errorMessage from '../features/ErrorMessageManager/duck'; 28 | 29 | const saveLang = ( state = {}, action ) => { 30 | if ( action.type === 'REDUX_I18N_SET_LANGUAGE' ) { 31 | localStorage.setItem( 'fonio-lang', action.lang ); 32 | return state; 33 | } 34 | else return state; 35 | }; 36 | 37 | export default combineReducers( { 38 | i18nState, 39 | saveLang, 40 | 41 | loadingBar: loadingBarReducer, 42 | toastr: toastrReducer, 43 | 44 | connections, 45 | userInfo, 46 | auth, 47 | editionUiWrapper, 48 | editedStory, 49 | sectionsManagement, 50 | errorMessage, 51 | 52 | home, 53 | summary, 54 | section, 55 | library, 56 | design, 57 | } ); 58 | -------------------------------------------------------------------------------- /src/redux/socketIoMiddleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Socket middleware 3 | * Catch socket-related actions (triggered if they contain a meta property) 4 | * and pass them through the socket 5 | */ 6 | import { loadStoryToken } from '../helpers/localStorageUtils'; 7 | 8 | export default ( socket ) => { 9 | const eventName = 'action'; 10 | 11 | return ( store ) => { 12 | socket.on( eventName, store.dispatch ); 13 | return ( next ) => ( action ) => { 14 | if ( action.meta && action.meta.remote ) { 15 | 16 | // passing jwt token if a story content is involved 17 | const { storyId } = action.payload; 18 | let token; 19 | if ( storyId ) { 20 | token = loadStoryToken( storyId ); 21 | } 22 | 23 | if ( action.callback && typeof action.callback === 'function' ) { 24 | socket.emit( eventName, { ...action, token }, action.callback ); 25 | } 26 | else { 27 | socket.emit( eventName, { ...action, token } ); 28 | } 29 | } 30 | return next( action ); 31 | }; 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/disappointed-boy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 23 | 24 | 25 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/embarrased-boy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 13 | 16 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/fat-boy-smiling.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 22 | 23 | 24 | 27 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/happy-baby.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "amazed-man.svg", 3 | "happy-baby.svg", 4 | "angry-man.svg", 5 | "happy-woman.svg", 6 | "angry-woman.svg", 7 | "hipster-smiling.svg", 8 | "baby-crying.svg", 9 | "hypnotized-hipster.svg", 10 | "baby-love.svg", 11 | "inexpressive-girl.svg", 12 | "boy-broad-smile.svg", 13 | "kissing-girl.svg", 14 | "boy-happy-smile.svg", 15 | "man-with-moustache-smiling.svg", 16 | "boy-smiling.svg", 17 | "naughty-girl.svg", 18 | "boy-suffering.svg", 19 | "perplexed-man.svg", 20 | "delighted-granny.svg", 21 | "philosophizing-boy.svg", 22 | "disappointed-boy.svg", 23 | "sad-baby.svg", 24 | "embarrased-boy.svg", 25 | "sad-girl.svg", 26 | "embarrased-girl.svg", 27 | "sad-hipster.svg", 28 | "embarrased-granny.svg", 29 | "sad-woman.svg", 30 | "emotive-granny.svg", 31 | "satisfied-woman.svg", 32 | "fat-boy-angry.svg", 33 | "shocked-girl.svg", 34 | "fat-boy-shocked.svg", 35 | "sleeping-granny.svg", 36 | "fat-boy-smiling.svg", 37 | "sleepy-boy.svg", 38 | "fat-boy-sorry.svg", 39 | "smiling-baby.svg", 40 | "fat-boy.svg", 41 | "smiling-girl.svg", 42 | "frightened-hipster.svg", 43 | "surprised-girl.svg", 44 | "girl-air-kissing.svg", 45 | "suspicious-man.svg", 46 | "girl-crying.svg", 47 | "teasing-boy.svg", 48 | "girl-embarrased.svg", 49 | "upset-girl.svg", 50 | "girl-laughing-to-tears.svg", 51 | "upset-granny.svg", 52 | "girl-showing-tongue.svg", 53 | "winking-boy.svg", 54 | "girl-smiling.svg", 55 | "wondered-hipster.svg", 56 | "girl-with-poker-face.svg" 57 | ] -------------------------------------------------------------------------------- /src/sharedAssets/avatars/sad-baby.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 21 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/smiling-baby.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/sharedAssets/avatars/winking-boy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 10 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/sharedAssets/cover_forccast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/src/sharedAssets/cover_forccast.jpg -------------------------------------------------------------------------------- /src/sharedAssets/internal-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/sharedAssets/logo-quinoa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medialab/fonio/974f497357f401250d69cad69d05c501f58c5c3d/src/sharedAssets/logo-quinoa.png -------------------------------------------------------------------------------- /src/translations/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translation data map 3 | * Exports available language data maps 4 | */ 5 | 6 | import fr from './locales/fr'; 7 | import en from './locales/en'; 8 | 9 | const translations = { 10 | en, 11 | fr 12 | }; 13 | 14 | export default translations; 15 | -------------------------------------------------------------------------------- /translationScripts/addTranslationLanguage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translations language adder 3 | */ 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var colors = require('colors'); 7 | var argv = require('optimist').argv; 8 | 9 | var addTranslationLanguage = function() { 10 | const lang = argv._[0]; 11 | const poPath = argv.poPath || './translations'; 12 | const translationsPath = path.resolve(__dirname, argv['translations-folder']); 13 | if (lang) { 14 | // copy translation json 15 | const modelPath = fs.readdirSync(translationsPath + '/locales')[0]; 16 | const model = fs.readFileSync(translationsPath + '/locales/' + modelPath, 'utf-8'); 17 | fs.writeFileSync(translationsPath + '/locales/' + lang + '.json', model); 18 | // update translations index script 19 | const indexContent = fs.readFileSync(translationsPath + '/index.js', 'utf-8'); 20 | const defRegex = /translations = {/; 21 | const match = indexContent.match(defRegex); 22 | if (match) { 23 | const newIndexContent = 'import ' + lang + ' from \'./locales/' + lang + '\';\n' + indexContent.substr(0, match.index) + match[0] + '\n ' + lang + ',' + indexContent.substr(match.index + match[0].length); 24 | fs.writeFileSync(translationsPath + '/index.js', newIndexContent); 25 | fs.writeFileSync(poPath + '/' + lang + '.po', ''); 26 | console.log(colors.green('done updating code for language ' + lang)); 27 | } 28 | } else { 29 | console.log('you must indicate a language code, e.g. "npm run translations:addlanguage it"') 30 | } 31 | } 32 | 33 | addTranslationLanguage(); 34 | 35 | module.exports = addTranslationLanguage; -------------------------------------------------------------------------------- /translationScripts/backfillTranslations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * written by Robin de Mourat 3 | * Backfills untranslated keys with default language. 4 | * Lets the user know about the process 5 | */ 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var colors = require('colors'); 10 | var argv = require('optimist').argv; 11 | 12 | var backfillTranslations = function() { 13 | console.log(colors.green('Begining to check all translations')); 14 | const localesPath = path.resolve(__dirname, argv.locales); 15 | const localesFiles = fs.readdirSync(localesPath); 16 | const locales = localesFiles.map(fileName => { 17 | return { 18 | fileName, 19 | translations: require(argv.locales + '/' + fileName) 20 | } 21 | }); 22 | const msgs = []; 23 | 24 | locales.forEach(locale1 => { 25 | Object.keys(locale1.translations).map(key => { 26 | locales.forEach(locale2 => { 27 | if (locale1.fileName !== locale2.fileName) { 28 | if (locale2.translations[key] === undefined) { 29 | msgs.push({ 30 | type: 'untranslated', 31 | key: key, 32 | from: locale1.fileName, 33 | to: locale2.fileName, 34 | backfill: locale1.translations[key] 35 | }); 36 | locale2.translations[key] = locale1.translations[key]; 37 | } 38 | } 39 | }); 40 | }); 41 | }); 42 | 43 | locales.forEach(locale => { 44 | fs.writeFileSync(localesPath + '/' + locale.fileName, JSON.stringify(locale.translations, null, 2)); 45 | }); 46 | msgs.forEach(msg => { 47 | if (msg.type === 'untranslated') { 48 | var msgC ='Message with key "' + 49 | msg.key + 50 | '" was not translated in language ' + 51 | msg.to.split('.')[0] + 52 | ', it has been backfilled with ' + 53 | msg.from.split('.')[0] + 54 | ' message "' + 55 | msg.backfill + 56 | '". \nGo to ' + 57 | localesPath + 58 | '/' + msg.to + 59 | ' to translate it properly.\n\n'; 60 | console.log(colors.red(msgC)); 61 | } 62 | }) 63 | console.log(colors.green('Translation maintenance is complete')); 64 | } 65 | 66 | backfillTranslations(); 67 | 68 | module.exports = backfillTranslations; -------------------------------------------------------------------------------- /translationScripts/exportTranslationsToPo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translations exporter 3 | */ 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var colors = require('colors'); 7 | var argv = require('optimist').argv; 8 | var json2po = require('json2po'); 9 | 10 | var exportTranslationsToPo = function() { 11 | console.log(colors.green('Begining to export all translations to .po translation files')); 12 | const localesPath = path.resolve(__dirname, argv.locales); 13 | const localesFiles = fs.readdirSync(localesPath); 14 | const destPath = path.resolve(__dirname, argv.dest); 15 | localesFiles.forEach(function (fileName) { 16 | const lang = fileName.split('.')[0]; 17 | const data = require(localesPath + '/' + fileName); 18 | const po = json2po( 19 | JSON.stringify(data), 20 | { 21 | "Project-Id-Version": "Sample Project", 22 | "PO-Revision-Date": new Date(), 23 | "Language": lang 24 | } 25 | ); 26 | const output = destPath + '/' + lang + '.po'; 27 | fs.writeFileSync(output, po); 28 | }) 29 | console.log(colors.green('Export done for all translations files !')); 30 | } 31 | 32 | exportTranslationsToPo(); 33 | 34 | module.exports = exportTranslationsToPo; -------------------------------------------------------------------------------- /translationScripts/importTranslationsFromPo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translations importer 3 | */ 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var colors = require('colors'); 7 | var argv = require('optimist').argv; 8 | var po2json = require('po2json'); 9 | 10 | var importTranslationsFromPo = function() { 11 | console.log(colors.green('Begining to import all translations from .po translation files')); 12 | const localesPath = path.resolve(__dirname, argv.locales); 13 | const localesFiles = fs.readdirSync(localesPath); 14 | const srcPath = path.resolve(__dirname, argv.src); 15 | const localesSources = fs.readdirSync(srcPath); 16 | localesSources.forEach(function (fileName) { 17 | const lang = fileName.split('.')[0]; 18 | console.log(colors.green('importing locale files for ', lang)); 19 | const json = po2json.parseFileSync(srcPath + '/' + fileName); 20 | const clean = Object.keys(json).reduce(function(results, key) { 21 | if (key.length > 0) { 22 | results[key] = json[key][1]; 23 | } 24 | return results; 25 | }, {}); 26 | if (clean) { 27 | fs.writeFileSync(localesPath + '/' + lang + '.json', JSON.stringify(clean, null, 2)); 28 | } 29 | }) 30 | console.log(colors.green('Import done for all translations files !')); 31 | } 32 | 33 | importTranslationsFromPo(); 34 | 35 | module.exports = importTranslationsFromPo; -------------------------------------------------------------------------------- /translationScripts/updateTranslationsToPo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Translations reconcilier 3 | */ 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var colors = require('colors'); 7 | var argv = require('optimist').argv; 8 | var json2po = require('json2po'); 9 | var po2json = require('po2json'); 10 | 11 | var updateTranslationsToPo = function() { 12 | console.log(colors.green('Begining to update po files with translations to .po translation files')); 13 | const localesPath = path.resolve(__dirname, argv.locales); 14 | const localesFiles = fs.readdirSync(localesPath); 15 | const destPath = path.resolve(__dirname, argv.dest); 16 | const poFiles = fs.readdirSync(destPath); 17 | poFiles.forEach(function (fileName) { 18 | const lang = fileName.split('.')[0]; 19 | console.log(colors.green('inspecting locale files for ', lang)); 20 | const json = po2json.parseFileSync(destPath + '/' + fileName); 21 | const data = Object.keys(json).reduce(function(results, key) { 22 | if (key.length > 0) { 23 | results[key] = json[key][1]; 24 | } 25 | return results; 26 | }, {}); 27 | if (data) { 28 | const js = require(localesPath + '/' + lang + '.json'); 29 | var toUpdate = false; 30 | Object.keys(js).forEach(function(key) { 31 | if (data[key] === undefined) { 32 | console.log(colors.red(key + ' key is not present in po files for lang ' + lang + ', adding it')); 33 | data[key] = js[key]; 34 | toUpdate = true; 35 | } 36 | }); 37 | 38 | if (toUpdate) { 39 | console.log(colors.green('updating po file ' + fileName)); 40 | const po = json2po( 41 | JSON.stringify(data), 42 | { 43 | "Project-Id-Version": "Sample Project", 44 | "PO-Revision-Date": new Date(), 45 | "Language": lang 46 | } 47 | ); 48 | const output = destPath + '/' + lang + '.po'; 49 | fs.writeFileSync(output, po); 50 | console.log(colors.green('update done for po file ' + fileName)); 51 | } 52 | } 53 | }) 54 | console.log(colors.green('Update done for all translations files ! You can now work with the Po files')); 55 | } 56 | 57 | updateTranslationsToPo(); 58 | 59 | module.exports = updateTranslationsToPo; -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration for handling the application's source code 3 | * in development mode (standard) 4 | */ 5 | var webpack = require('webpack'); 6 | var config = require('config'); 7 | var sharedConfig = require('./webpack.config.shared'); 8 | 9 | module.exports = { 10 | module: sharedConfig.module, 11 | plugins: sharedConfig.plugins.concat([ 12 | new webpack.DefinePlugin({ 13 | 'process.env': { 14 | NODE_ENV: JSON.stringify('development') 15 | }, 16 | 'FONIO_CONFIG': JSON.stringify(config) 17 | }) 18 | ]) 19 | }; 20 | -------------------------------------------------------------------------------- /webpack.config.docker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration for handling the applicatino's source code 3 | * in production mode (standard + minify) 4 | */ 5 | var webpack = require('webpack'); 6 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 7 | 8 | var sharedConfig = require('./webpack.config.shared'); 9 | 10 | module.exports = { 11 | 12 | module: sharedConfig.module, 13 | 14 | plugins: sharedConfig.plugins 15 | .concat(new UglifyJsPlugin()) 16 | .concat(new webpack.DefinePlugin({ 17 | 'process.env': { 18 | NODE_ENV: JSON.stringify('production') 19 | } 20 | })), 21 | 22 | devtool: 'source-map', 23 | 24 | output: { 25 | path: '/build', 26 | publicPath: '@@URL_PREFIX@@/build/' 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration for handling the applicatino's source code 3 | * in production mode (standard + minify) 4 | */ 5 | var webpack = require('webpack'); 6 | const config = require('config'); 7 | const urlPrefix = config.get('urlPrefix'); 8 | 9 | var sharedConfig = require('./webpack.config.shared'); 10 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 11 | module.exports = { 12 | 13 | module: sharedConfig.module, 14 | 15 | mode: 'production', 16 | 17 | plugins: sharedConfig.plugins 18 | /*.concat(new UglifyJsPlugin())*/ 19 | .concat(new webpack.DefinePlugin({ 20 | 'process.env': { 21 | NODE_ENV: JSON.stringify('production') 22 | } 23 | })) 24 | .concat(new BundleAnalyzerPlugin({ 25 | openAnalyzer: false, 26 | generateStatsFile: true, 27 | analyzerMode: 'disabled' 28 | })) 29 | , 30 | 31 | // devtool: 'source-map', 32 | 33 | output: { 34 | path: '/build', 35 | publicPath: urlPrefix && urlPrefix.length ? urlPrefix + '/build/' : '/build/' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.config.shared.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration base for handling the application's source code 3 | */ 4 | var webpack = require('webpack'); 5 | 6 | module.exports = { 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.(woff|ttf|otf|eot|woff2)$/i, 11 | use: [ 12 | { 13 | loader: 'file-loader', 14 | options: { 15 | query: { 16 | name:'assets/[name].[ext]' 17 | } 18 | } 19 | }, 20 | ] 21 | }, 22 | { 23 | test: /\.(jpe?g|png|gif|svg)$/i, 24 | use: [ 25 | 'url-loader?limit=10000', 26 | 'img-loader' 27 | ] 28 | }, 29 | /*{ 30 | test: /\.scss$/, 31 | use: ['style-loader', 'css-loader', 'sass-loader'] 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: ['style-loader', 'css-loader'] 36 | }*/ 37 | ] 38 | }, 39 | plugins: [] 40 | }; 41 | --------------------------------------------------------------------------------