├── app ├── app.tsx ├── icons │ ├── icons.scss │ ├── next-page.svg │ ├── double-arrow-down.svg │ ├── full-name-icon.svg │ ├── copy-doi.svg │ ├── complete.svg │ ├── small-plus.svg │ ├── bookmark-thin.svg │ ├── password-icon.svg │ ├── email-verification-complete.svg │ ├── email-icon.svg │ ├── clock-outline.svg │ ├── activity-outline.svg │ ├── email-verification-fail.svg │ ├── cited.svg │ ├── bookmark-gray.svg │ ├── cloud-upload-outline.svg │ ├── mask.svg │ ├── minus.svg │ ├── twitter-logo.svg │ ├── link.svg │ ├── feedback-label.svg │ ├── matched-paper.svg │ ├── impact-factor.svg │ ├── ellipsis.svg │ ├── password.svg │ ├── source Copy.svg │ ├── copy.svg │ ├── trash-can.svg │ ├── arrow-up.svg │ ├── error.svg │ ├── arrow-down.svg │ ├── pen-only.svg │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── plus.svg │ ├── new-tab.svg │ └── check.svg ├── __mocks__ │ ├── fos.json │ ├── paperSource.json │ ├── paperAuthor.json │ ├── member.json │ ├── currentUser.json │ ├── journal.json │ ├── index.tsx │ └── author.json ├── __tests__ │ ├── fileMock.js │ ├── withStyles.js │ ├── mockStore.tsx │ └── preload.tsx ├── constants │ ├── auth.tsx │ ├── abTestGlobalValue.tsx │ ├── profileRequest.tsx │ ├── paperRequestDialog.tsx │ ├── paperFigure.tsx │ ├── paperSearch.tsx │ ├── scinapse-extension.tsx │ ├── common.tsx │ ├── ga.tsx │ ├── abTestObject.tsx │ ├── feedback.tsx │ └── actionTicket.tsx ├── containers │ ├── refCitedPapersContainer │ │ └── refCitedPapersContainer.scss │ ├── authorShow │ │ ├── types.tsx │ │ └── sideEffect.tsx │ ├── collectionShow │ │ ├── types.tsx │ │ └── sideEffect.tsx │ ├── profileOnboarding │ │ ├── components │ │ │ ├── onboardingHeader │ │ │ │ ├── onboardingHeader.scss │ │ │ │ └── index.tsx │ │ │ └── onboardingFooter │ │ │ │ └── onboardingFooter.scss │ │ ├── types.tsx │ │ ├── helper.tsx │ │ └── profileOnboarding.scss │ ├── admin │ │ ├── admin.scss │ │ └── index.tsx │ ├── profile │ │ ├── components │ │ │ ├── gsImportForm │ │ │ │ └── gsImportForm.scss │ │ │ ├── citationStringImportForm │ │ │ │ └── citationStringImportForm.scss │ │ │ ├── pendingDescriptionDialog │ │ │ │ └── pendingDescriptionDialog.scss │ │ │ ├── authorUrlsImportField │ │ │ │ └── authorUrlsImportField.scss │ │ │ ├── pendingPaperItem │ │ │ │ └── pendingPaperItem.scss │ │ │ ├── pendingPaperList │ │ │ │ └── pendingPaperList.scss │ │ │ ├── resolvedPendingPaperDialog │ │ │ │ └── resolvedPendingPaperDialog.scss │ │ │ └── allRepresentativePaperDialog │ │ │ │ └── allRepresentativePaperDialog.scss │ │ ├── types.tsx │ │ └── sideEffects.tsx │ ├── paperShow │ │ ├── types.tsx │ │ └── select.tsx │ ├── filterContainer │ │ └── filterButton │ │ │ └── filterButton.scss │ ├── authorSearch │ │ └── records.tsx │ ├── keywordSettings │ │ └── keywordSettings.scss │ ├── userSettings │ │ └── index.tsx │ ├── filterBox │ │ └── autocompleteFilter │ │ │ └── filterItem.tsx │ └── profileForm │ │ └── profileForm.scss ├── components │ ├── yearRangeSlider │ │ └── constants.tsx │ ├── auth │ │ ├── types │ │ │ └── index.ts │ │ ├── authButton │ │ │ └── authButtons.scss │ │ ├── signUp │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── helpers │ │ │ │ └── checkDuplicateEmail.tsx │ │ │ └── components │ │ │ │ └── firstForm │ │ │ │ └── firstForm.scss │ │ ├── emailVerification │ │ │ ├── records.tsx │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── authContextText │ │ │ ├── constants.tsx │ │ │ └── authContextText.scss │ │ ├── signIn │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── auth.scss │ │ ├── authTabs │ │ │ └── authTabs.scss │ │ ├── resetPassword │ │ │ └── resetPassword.scss │ │ └── authGuideContext │ │ │ └── authGuideContext.scss │ ├── paperShow │ │ ├── constants.tsx │ │ ├── common.scss │ │ ├── components │ │ │ ├── otherPaperList.scss │ │ │ ├── keyword.scss │ │ │ ├── viewFullTextBtn.scss │ │ │ ├── searchingPDFBtn.scss │ │ │ ├── searchingPDFBtn.tsx │ │ │ ├── paperShowFigureList.scss │ │ │ ├── relatedPaperItem.scss │ │ │ └── doiInPaperShow.scss │ │ ├── refCitedPapers │ │ │ └── mobileRefCitedPapers.scss │ │ ├── backButton │ │ │ └── backButton.scss │ │ └── refCitedTab │ │ │ └── types.tsx │ ├── journalShow │ │ └── types.tsx │ ├── findInLibraryDialog │ │ ├── types │ │ │ └── index.ts │ │ ├── successRequestContext.tsx │ │ └── alreadyRequestContext.tsx │ ├── pdfViewer │ │ ├── types.tsx │ │ └── component │ │ │ └── progressSpinner.tsx │ ├── common │ │ ├── paperItem │ │ │ ├── simplePaperItemButtonGroup.scss │ │ │ ├── moreDropdownItem.scss │ │ │ ├── figures.scss │ │ │ ├── noteButton.scss │ │ │ ├── sourceButton.scss │ │ │ ├── paperItem.scss │ │ │ ├── citeButton.scss │ │ │ ├── moreDropdownItem.tsx │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── blockVenue_spec.tsx.snap │ │ │ │ └── blockVenue_spec.tsx │ │ │ ├── abstract.scss │ │ │ ├── mobileVenueAuthors.scss │ │ │ ├── searchPaperItem.scss │ │ │ ├── mobileAuthors.scss │ │ │ ├── savedCollections.scss │ │ │ ├── paperItemButtonGroup.scss │ │ │ ├── blockVenueAuthor.tsx │ │ │ ├── moreDropdownButton.scss │ │ │ └── title.scss │ │ ├── button │ │ │ └── types.tsx │ │ ├── progressStepper │ │ │ └── progressStepper.scss │ │ ├── autoSizeTextarea │ │ │ └── autoSizeTextarea.scss │ │ ├── roundImageIcon │ │ │ ├── roundImageIcon.scss │ │ │ └── index.tsx │ │ ├── scinapseInput │ │ │ └── scinapseCheckbox.scss │ │ ├── hIndexBox │ │ │ ├── hIndexBox.scss │ │ │ └── index.tsx │ │ ├── separator │ │ │ ├── separator.scss │ │ │ └── index.tsx │ │ ├── scinapseSnackbar │ │ │ └── scinapseSnackbar.scss │ │ ├── status │ │ │ └── index.tsx │ │ ├── scinapseButton │ │ │ └── scinapseButton.scss │ │ ├── formikInput │ │ │ └── index.tsx │ │ ├── spinner │ │ │ ├── buttonSpinner.tsx │ │ │ ├── articleSpinner.tsx │ │ │ └── articleSpinner.scss │ │ ├── InputWithSuggestionList │ │ │ ├── types.tsx │ │ │ └── inputWithSuggestionList.scss │ │ ├── tabNavigationBar │ │ │ └── tabNavigationBar.scss │ │ ├── groupButton │ │ │ ├── index.tsx │ │ │ └── groupButton.scss │ │ ├── mobileSearchBox │ │ │ └── mobileSearchBox.scss │ │ ├── bubblePopover │ │ │ ├── bubblePopover.scss │ │ │ └── index.tsx │ │ ├── sourceURLPopover │ │ │ └── sourceURLPopover.scss │ │ ├── paperFigure │ │ │ ├── smallPaperFigure.scss │ │ │ └── smallPaperFigure.tsx │ │ ├── mobilePagination │ │ │ └── pagination.scss │ │ └── sortBox │ │ │ └── sortBox.scss │ ├── recommendPool │ │ └── constants.tsx │ ├── articleSearch │ │ ├── types │ │ │ ├── index.ts │ │ │ └── actions.ts │ │ ├── common.scss │ │ └── components │ │ │ ├── emailBanner │ │ │ └── emailBanner.scss │ │ │ └── searchList │ │ │ └── searchList.scss │ ├── mobileRelatedPapers │ │ ├── mobileRelatedPapers.scss │ │ └── mobileRelatedPapers.tsx │ ├── snackbar │ │ ├── createKeywordSnackBar │ │ │ └── createKeywordSnackBar.scss │ │ └── collectionSnackBar │ │ │ └── collectionSnackBar.scss │ ├── sortingDropdown │ │ └── sortingDropdown.scss │ ├── createKeywordInput │ │ └── createKeywordInput.scss │ ├── authorCV │ │ └── affiliationBox.scss │ ├── simplePaperItem │ │ └── simplePaperItem.scss │ ├── filterContainer │ │ └── filterResetButton.scss │ ├── mobilePaperShowButtonGroup │ │ └── mobilePaperShowButtonGroup.scss │ ├── paperShowTabItem │ │ ├── paperShowTabItem.scss │ │ └── paperShowTabItem.tsx │ ├── profileRegister │ │ └── profileRegiser.scss │ ├── profilePaperItem │ │ └── profilePaperItem.scss │ ├── emailSettings │ │ ├── emailToggleTitle.scss │ │ ├── emailToggleTitle.tsx │ │ ├── emailSettings.scss │ │ └── emailToggleButton.scss │ ├── yearGraph │ │ └── yearGraph.scss │ ├── layouts │ │ ├── types │ │ │ └── header.tsx │ │ └── reducer.tsx │ ├── improvedHome │ │ └── constants.tsx │ ├── dialog │ │ ├── types │ │ │ └── index.tsx │ │ └── dialog.scss │ ├── termsOfService │ │ └── termsOfService.scss │ ├── profileVerifyEmail │ │ └── profileVerifyEmail.scss │ ├── privacyPolicy │ │ └── privacyPolicy.scss │ ├── collectionPaperItem │ │ └── collectionPaperItem.scss │ ├── collectionShow │ │ ├── collectionPapersControlBtns.scss │ │ └── relatedPaperInCollectionShow.scss │ ├── alertCreateButton │ │ └── alertCreateButton.scss │ ├── error │ │ └── errorPage.scss │ ├── popupConsentBanner │ │ └── popupConsentBanner.scss │ └── copyDOIButton │ │ └── copyDOIButton.tsx ├── index.tsx ├── model │ ├── domain.ts │ ├── savedInCollection.tsx │ ├── affiliation.tsx │ ├── paperSource.tsx │ ├── Institute.tsx │ ├── fos.tsx │ ├── oauth.tsx │ ├── conferenceSeries.tsx │ ├── suggestion.tsx │ ├── profileAffiliation.ts │ ├── paperInCollection.tsx │ ├── author.tsx │ ├── journal.tsx │ ├── error.tsx │ ├── collection.tsx │ ├── member.tsx │ ├── conferenceInstance.tsx │ ├── aggregation.tsx │ ├── currentUser.tsx │ └── author │ │ └── author.tsx ├── helpers │ ├── withStylesHelper.tsx │ ├── __mocks__ │ │ ├── makePlutoToastAction.ts │ │ └── handleGA.ts │ ├── abTestHelper │ │ └── index.tsx │ ├── multiRemoveElementFromArray.tsx │ ├── safeURIStringHandler.tsx │ ├── getQueryParamsObject.tsx │ ├── axiosCancelTokenManager.ts │ ├── validateEmail.ts │ ├── toggleElementFromArray.tsx │ ├── makePlutoToastAction.ts │ ├── getBubbleContextType.tsx │ ├── pageMapper │ │ └── index.tsx │ ├── trackSelectFilter.tsx │ ├── formatNumber.tsx │ ├── scrollRestoration.tsx │ ├── exportCitationText.tsx │ ├── userAgentHelper.ts │ ├── getPDFLink.tsx │ ├── highlightContent │ │ └── __tests__ │ │ │ └── index_spec.tsx │ └── displayFormula.ts ├── hooks │ ├── useThunkDispatch.tsx │ ├── useEnvHook.tsx │ ├── FBisLoadingHook.tsx │ ├── useIntervalProgressHook.tsx │ └── useIntersectionHook.tsx ├── store │ ├── types.tsx │ └── serverStore.tsx ├── selectors │ ├── getLayout.tsx │ ├── getPaperShow.tsx │ ├── getCurrentUser.tsx │ ├── getConfiguration.tsx │ ├── getPDFViewer.tsx │ ├── getSearchFilter.tsx │ └── papersSelector.tsx ├── index.ejs ├── api │ ├── types │ │ ├── member.ts │ │ ├── recommendation.ts │ │ ├── author.ts │ │ └── paper.ts │ ├── home.ts │ ├── getHost.ts │ ├── author │ │ └── types.ts │ ├── recommendation.ts │ └── suggest.ts ├── actions │ ├── searchFilter.tsx │ ├── profileInfo.tsx │ ├── pdfViewer.tsx │ └── relatedPapers.tsx └── reducers │ ├── signUpModal.tsx │ ├── profileEntity.tsx │ ├── findInLibraryDialog.tsx │ ├── searchQuery.tsx │ ├── realtedPapers.tsx │ └── configuration.tsx ├── scripts ├── index.ts ├── deploy │ ├── config.ts │ └── config.js ├── package.json └── tsconfig.json ├── .eslintignore ├── config.yml ├── env ├── dev.yml ├── production.yml └── stage.yml ├── e2eTest ├── constants │ └── setting.ts ├── helpers │ └── getHost.ts └── jest.setup.js ├── lint-staged.config.js ├── typings └── actionType │ └── index.d.ts ├── .gitignore ├── jest └── jestReporter.js ├── server ├── localServer.tsx ├── routes │ ├── robots.tsx │ ├── openSearchXML.tsx │ ├── manifest.tsx │ └── sitemap.tsx ├── prodHandler.tsx ├── fallbackRender.tsx └── helpers │ └── setABTest.tsx ├── .circleci └── setup_puppeteer.sh ├── jest.e2e.config.js ├── tsconfig.test.json ├── tsconfig.json └── .babelrc.js /app/app.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/icons/icons.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.scss 2 | **/*.css 3 | **/*.ejs 4 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | service_name: serverless-unviversal-app 2 | -------------------------------------------------------------------------------- /app/__mocks__/fos.json: -------------------------------------------------------------------------------- 1 | { "id": "7159", "fos": "Engineering" } 2 | -------------------------------------------------------------------------------- /app/__tests__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = "test-file-stub"; 2 | -------------------------------------------------------------------------------- /app/constants/auth.tsx: -------------------------------------------------------------------------------- 1 | export const MINIMUM_PASSWORD_LENGTH = 8; 2 | -------------------------------------------------------------------------------- /app/containers/refCitedPapersContainer/refCitedPapersContainer.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/__tests__/withStyles.js: -------------------------------------------------------------------------------- 1 | module.exports = () => component => component; 2 | -------------------------------------------------------------------------------- /app/components/yearRangeSlider/constants.tsx: -------------------------------------------------------------------------------- 1 | export const MIN_YEAR = 1960; 2 | -------------------------------------------------------------------------------- /app/constants/abTestGlobalValue.tsx: -------------------------------------------------------------------------------- 1 | export const DUMMY_TEST = 'twins'; 2 | -------------------------------------------------------------------------------- /app/constants/profileRequest.tsx: -------------------------------------------------------------------------------- 1 | export const ProfileRequestKey = 'r_p_k_id'; 2 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import app from '../server'; 2 | 3 | export const ssr = app; 4 | -------------------------------------------------------------------------------- /app/constants/paperRequestDialog.tsx: -------------------------------------------------------------------------------- 1 | export const LAST_SUCCEEDED_EMAIL_KEY = 'l_s_e_k'; 2 | -------------------------------------------------------------------------------- /app/components/auth/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum SIGN_TYPE { 2 | SIGN_UP, 3 | SIGN_IN, 4 | } 5 | -------------------------------------------------------------------------------- /app/constants/paperFigure.tsx: -------------------------------------------------------------------------------- 1 | export const FIGURE_PREFIX = 'https://asset-pdf.scinapse.io/'; 2 | -------------------------------------------------------------------------------- /app/components/auth/authButton/authButtons.scss: -------------------------------------------------------------------------------- 1 | .authButtonWrapper { 2 | margin-top: 8px; 3 | } 4 | -------------------------------------------------------------------------------- /app/constants/paperSearch.tsx: -------------------------------------------------------------------------------- 1 | export type FILTER_BOX_TYPE = 'PUBLISHED_YEAR' | 'FOS' | 'JOURNAL'; 2 | -------------------------------------------------------------------------------- /app/model/domain.ts: -------------------------------------------------------------------------------- 1 | export type Domain = { 2 | id: number | null; 3 | domain: string; 4 | } 5 | -------------------------------------------------------------------------------- /env/dev.yml: -------------------------------------------------------------------------------- 1 | HANDLER: handler.ssr 2 | API_ENDPOINT: 'https://stage-api.scinapse.io:8443/{proxy}' 3 | -------------------------------------------------------------------------------- /env/production.yml: -------------------------------------------------------------------------------- 1 | HANDLER: server/main.ssr 2 | API_ENDPOINT: 'https://api.scinapse.io/{proxy}' 3 | -------------------------------------------------------------------------------- /app/components/paperShow/constants.tsx: -------------------------------------------------------------------------------- 1 | export type REF_CITED_CONTAINER_TYPE = 'reference' | 'cited'; 2 | -------------------------------------------------------------------------------- /app/constants/scinapse-extension.tsx: -------------------------------------------------------------------------------- 1 | export const EXTENSION_APP_ID = 'goiligimjimdmphfkcjmojaooghndgak'; 2 | -------------------------------------------------------------------------------- /e2eTest/constants/setting.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SCREEN_SHOT_OUTPUT_DIRECTORY = './output/screenshots'; 2 | -------------------------------------------------------------------------------- /env/stage.yml: -------------------------------------------------------------------------------- 1 | HANDLER: server/main.ssr 2 | API_ENDPOINT: 'https://stage-api.scinapse.io:8443/{proxy}' 3 | -------------------------------------------------------------------------------- /app/components/journalShow/types.tsx: -------------------------------------------------------------------------------- 1 | export interface JournalShowMatchParams { 2 | journalId: string; 3 | } 4 | -------------------------------------------------------------------------------- /app/containers/authorShow/types.tsx: -------------------------------------------------------------------------------- 1 | export interface AuthorShowMatchParams { 2 | authorId: string; 3 | } 4 | -------------------------------------------------------------------------------- /app/model/savedInCollection.tsx: -------------------------------------------------------------------------------- 1 | export interface SavedInCollection { 2 | id: number; 3 | title: string; 4 | } 5 | -------------------------------------------------------------------------------- /app/containers/collectionShow/types.tsx: -------------------------------------------------------------------------------- 1 | export interface CollectionShowMatchParams { 2 | collectionId: string; 3 | } 4 | -------------------------------------------------------------------------------- /app/components/findInLibraryDialog/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum REQUEST_STEP { 2 | REQUEST_FORM, 3 | SUCCESS, 4 | ALREADY, 5 | } 6 | -------------------------------------------------------------------------------- /app/containers/profileOnboarding/components/onboardingHeader/onboardingHeader.scss: -------------------------------------------------------------------------------- 1 | .onboardingHeaderWrapper { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /app/__mocks__/paperSource.json: -------------------------------------------------------------------------------- 1 | { "id": "124957841", "paperId": 101, "url": "http://dblp.uni-trier.de/db/conf/midi/midi2013.html#MottusLPT13" } 2 | -------------------------------------------------------------------------------- /app/helpers/withStylesHelper.tsx: -------------------------------------------------------------------------------- 1 | export const withStyles: (styles: any) => (arg: T) => T = require('isomorphic-style-loader/withStyles'); 2 | -------------------------------------------------------------------------------- /app/model/affiliation.tsx: -------------------------------------------------------------------------------- 1 | export interface Affiliation { 2 | id: string | null; 3 | name: string; 4 | nameAbbrev?: string | null; 5 | } 6 | -------------------------------------------------------------------------------- /app/model/paperSource.tsx: -------------------------------------------------------------------------------- 1 | export interface PaperSource { 2 | id: number; 3 | paperId: string; 4 | url: string; 5 | isPdf: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /app/components/pdfViewer/types.tsx: -------------------------------------------------------------------------------- 1 | import { Paper } from '../../model/paper'; 2 | 3 | export interface PDFViewerProps { 4 | paper: Paper; 5 | } 6 | -------------------------------------------------------------------------------- /app/constants/common.tsx: -------------------------------------------------------------------------------- 1 | const styles = require('../_variables.scss'); 2 | 3 | export const NAVBAR_HEIGHT = parseInt(styles.navbarHeight, 10) + 1; 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | linters: { 3 | 'app/**/*.+(ts|tsx)': ['eslint app --ext .ts,.tsx', 'git add'], 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /app/model/Institute.tsx: -------------------------------------------------------------------------------- 1 | export interface Institute { 2 | id: string; 3 | name: string; 4 | nameAbbrev: string; 5 | journalSubCount?: number; 6 | } 7 | -------------------------------------------------------------------------------- /app/containers/admin/admin.scss: -------------------------------------------------------------------------------- 1 | .linkItem { 2 | display: block; 3 | margin-top: 12px; 4 | 5 | &:hover { 6 | text-decoration: underline; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/components/common/paperItem/simplePaperItemButtonGroup.scss: -------------------------------------------------------------------------------- 1 | .groupWrapper { 2 | display: flex; 3 | justify-content: space-between; 4 | margin: 0px -10px; 5 | } 6 | -------------------------------------------------------------------------------- /app/components/auth/signUp/types/index.ts: -------------------------------------------------------------------------------- 1 | export enum SIGN_UP_STEP { 2 | FIRST, 3 | WITH_EMAIL, 4 | WITH_SOCIAL, 5 | FINAL_WITH_EMAIL, 6 | FINAL_WITH_SOCIAL, 7 | } 8 | -------------------------------------------------------------------------------- /app/components/recommendPool/constants.tsx: -------------------------------------------------------------------------------- 1 | export const RECOMMENDED_PAPER_LOGGING_FOR_NON_USER = 'r_p_l'; 2 | export const RECOMMENDED_PAPER_LOGGING_LENGTH_FOR_NON_USER = 50; 3 | -------------------------------------------------------------------------------- /app/model/fos.tsx: -------------------------------------------------------------------------------- 1 | export interface Fos { 2 | id: string; 3 | fos: string; 4 | } 5 | 6 | export interface NewFOS { 7 | id: string; 8 | name: string; 9 | } 10 | -------------------------------------------------------------------------------- /typings/actionType/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "redux"; 2 | 3 | declare global { 4 | interface ReduxAction extends Action { 5 | payload?: T; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | node_modules 4 | dist 5 | .serverless 6 | dist-sls 7 | dist.zip 8 | version 9 | output 10 | .DS_Store 11 | tmp 12 | .awcache 13 | debug.log 14 | -------------------------------------------------------------------------------- /e2eTest/helpers/getHost.ts: -------------------------------------------------------------------------------- 1 | export default function getHost() { 2 | if (process.env.STAGE === 'stage') { 3 | return 'stage.scinapse.io'; 4 | } 5 | return 'scinapse.io'; 6 | } 7 | -------------------------------------------------------------------------------- /app/components/articleSearch/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface SearchPageQueryParams { 2 | query?: string; 3 | filter?: string; 4 | page?: string; 5 | sort?: Scinapse.ArticleSearch.SEARCH_SORT_OPTIONS; 6 | } 7 | -------------------------------------------------------------------------------- /app/components/paperShow/common.scss: -------------------------------------------------------------------------------- 1 | .paperContentBlockHeader { 2 | width: 100%; 3 | font-weight: 700; 4 | color: $black1; 5 | padding-bottom: 4px; 6 | margin-bottom: 8px; 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /app/components/articleSearch/types/actions.ts: -------------------------------------------------------------------------------- 1 | export interface FetchSearchItemsParams { 2 | query?: string; 3 | filter?: string; 4 | paperId?: string; 5 | cognitiveId?: number | null; 6 | page: number; 7 | } 8 | -------------------------------------------------------------------------------- /app/components/common/button/types.tsx: -------------------------------------------------------------------------------- 1 | export type ButtonSize = 'small' | 'medium' | 'large'; 2 | export type ButtonVariant = 'text' | 'outlined' | 'contained'; 3 | export type ButtonColor = 'blue' | 'gray' | 'black'; 4 | -------------------------------------------------------------------------------- /app/components/mobileRelatedPapers/mobileRelatedPapers.scss: -------------------------------------------------------------------------------- 1 | .relatedPaperItem { 2 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 3 | } 4 | 5 | .relatedPaperItem + .relatedPaperItem { 6 | margin-top: 12px; 7 | } 8 | -------------------------------------------------------------------------------- /app/helpers/__mocks__/makePlutoToastAction.ts: -------------------------------------------------------------------------------- 1 | export default function alertToast(notificationActionPayload: Scinapse.Alert.NotificationActionPayload): void { 2 | if (!!notificationActionPayload) { 3 | return; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/helpers/abTestHelper/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Cookies from 'js-cookie'; 2 | import { ABTest } from '../../constants/abTest'; 3 | 4 | export function getUserGroupName(testName: ABTest) { 5 | return Cookies.get(testName); 6 | } 7 | -------------------------------------------------------------------------------- /app/components/auth/signUp/helpers/checkDuplicateEmail.tsx: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | import { checkDuplicatedEmail } from '../actions'; 3 | 4 | export const debouncedCheckDuplicate = debounce(checkDuplicatedEmail, 200); 5 | -------------------------------------------------------------------------------- /app/components/snackbar/createKeywordSnackBar/createKeywordSnackBar.scss: -------------------------------------------------------------------------------- 1 | .seeAllBtn { 2 | margin-right: 8px; 3 | } 4 | 5 | .snackbarContext { 6 | font-size: 16px; 7 | color: white; 8 | padding-right: 48px; 9 | } 10 | -------------------------------------------------------------------------------- /app/components/common/progressStepper/progressStepper.scss: -------------------------------------------------------------------------------- 1 | .stepWrapper { 2 | svg { 3 | color: $gray400; 4 | } 5 | } 6 | 7 | .optionalLabel { 8 | color: $gray600; 9 | font-size: 13px; 10 | font-weight: 300; 11 | } 12 | -------------------------------------------------------------------------------- /app/__mocks__/paperAuthor.json: -------------------------------------------------------------------------------- 1 | { 2 | "paperId": "127708505", 3 | "order": 1, 4 | "name": "Jingbo Shao", 5 | "organization": "College of Computer Science and Technology, Harbin Engineering University, Harbin 150001, China#TAB#" 6 | } 7 | -------------------------------------------------------------------------------- /app/helpers/multiRemoveElementFromArray.tsx: -------------------------------------------------------------------------------- 1 | export function multiRemoveElementFromArray(targetArray: T[], originalArray: T[]) { 2 | return originalArray.filter(element => { 3 | return !targetArray.includes(element); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /app/hooks/useThunkDispatch.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import StoreManager from '../store/store'; 3 | 4 | export const useThunkDispatch = () => { 5 | return useDispatch(); 6 | }; 7 | -------------------------------------------------------------------------------- /app/components/common/paperItem/moreDropdownItem.scss: -------------------------------------------------------------------------------- 1 | .moreButtonItem { 2 | font-size: 14px !important; 3 | border-radius: 4px; 4 | color: $gray800; 5 | 6 | &:hover { 7 | background-color: $gray30 !important; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/components/sortingDropdown/sortingDropdown.scss: -------------------------------------------------------------------------------- 1 | .dropBoxWrapper { 2 | width: 175px; 3 | border-radius: 4px; 4 | box-shadow: 0 8px 24px 2px rgba(0, 0, 0, 0.2); 5 | background-color: white; 6 | padding: 24px; 7 | margin-top: 8px; 8 | } 9 | -------------------------------------------------------------------------------- /app/model/oauth.tsx: -------------------------------------------------------------------------------- 1 | import { OAUTH_VENDOR } from '../api/types/auth'; 2 | 3 | export interface MemberOAuth { 4 | connected: boolean; 5 | oauthId: string; 6 | userData: {}; 7 | uuid: string; 8 | vendor: OAUTH_VENDOR | null; 9 | } 10 | -------------------------------------------------------------------------------- /app/store/types.tsx: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { ThunkAction } from 'redux-thunk'; 3 | import { AppState } from '../reducers'; 4 | 5 | export type AppThunkAction = ThunkAction; 6 | -------------------------------------------------------------------------------- /app/selectors/getLayout.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | import { AppState } from '../reducers'; 3 | 4 | export const getMemoizedLayout = createSelector([(state: AppState) => state.layout], layout => { 5 | return layout; 6 | }); 7 | -------------------------------------------------------------------------------- /jest/jestReporter.js: -------------------------------------------------------------------------------- 1 | var jasmineReporters = require("jasmine-reporters"); 2 | jasmine.VERBOSE = true; 3 | jasmine.getEnv().addReporter( 4 | new jasmineReporters.JUnitXmlReporter({ 5 | consolidateAll: false, 6 | savePath: "output/", 7 | }), 8 | ); 9 | -------------------------------------------------------------------------------- /app/constants/ga.tsx: -------------------------------------------------------------------------------- 1 | export const LOCAL_GA_ID = 'UA-109822865-3'; 2 | export const DEV_GA_ID = 'UA-109822865-2'; 3 | export const PROD_GA_ID = 'UA-109822865-1'; 4 | export const LOCAL_OPTIMIZE_ID = 'GTM-5JPRM2G'; 5 | export const PROD_OPTIMIZE_ID = 'GTM-5QR7T6H'; 6 | -------------------------------------------------------------------------------- /app/selectors/getPaperShow.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { AppState } from '../reducers'; 3 | 4 | export const getMemoizedPaperShow = createSelector([(state: AppState) => state.paperShow], paperShow => { 5 | return paperShow; 6 | }); 7 | -------------------------------------------------------------------------------- /app/constants/abTestObject.tsx: -------------------------------------------------------------------------------- 1 | import { Test } from './abTest'; 2 | import { DUMMY_TEST } from './abTestGlobalValue'; 3 | 4 | export const dummy: Test = { 5 | name: DUMMY_TEST, 6 | userGroup: [{ groupName: 'a', weight: 1 }, { groupName: 'b', weight: 1 }], 7 | }; 8 | -------------------------------------------------------------------------------- /app/selectors/getCurrentUser.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { AppState } from '../reducers'; 3 | 4 | export const getMemoizedCurrentUser = createSelector( 5 | (state: AppState) => state.currentUser, 6 | currentUser => currentUser 7 | ); 8 | -------------------------------------------------------------------------------- /app/containers/profileOnboarding/types.tsx: -------------------------------------------------------------------------------- 1 | export enum CURRENT_ONBOARDING_PROGRESS_STEP { 2 | UPLOAD_PUB_LIST, 3 | MATCH_UNSYNCED_PUBS, 4 | SELECT_REPRESENTATIVE_PUBS, 5 | } 6 | 7 | export const ONBOARDING_STEPS = ['Upload', 'Match Unsynced', 'Select Representative']; 8 | -------------------------------------------------------------------------------- /app/components/common/paperItem/figures.scss: -------------------------------------------------------------------------------- 1 | .smallPaperFiguresContainer { 2 | display: flex; 3 | margin-top: 16px; 4 | margin-bottom: 16px; 5 | } 6 | 7 | @media (max-width: $mobile_width) { 8 | .smallPaperFiguresContainer { 9 | display: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/icons/next-page.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/selectors/getConfiguration.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { AppState } from '../reducers'; 3 | 4 | export const getMemoizedConfiguration = createSelector([(state: AppState) => state.configuration], configuration => { 5 | return configuration; 6 | }); 7 | -------------------------------------------------------------------------------- /app/selectors/getPDFViewer.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { AppState } from '../reducers'; 3 | 4 | export const getMemoizedPDFViewerState = createSelector([(state: AppState) => state.PDFViewerState], PDFViewerState => { 5 | return PDFViewerState; 6 | }); 7 | -------------------------------------------------------------------------------- /app/helpers/safeURIStringHandler.tsx: -------------------------------------------------------------------------------- 1 | export default class SafeURIStringHandler { 2 | public static decode(rawString: string): string { 3 | try { 4 | return decodeURIComponent(rawString); 5 | } catch (_err) { 6 | return rawString; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2eTest/jest.setup.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp'); 2 | const { DEFAULT_SCREEN_SHOT_OUTPUT_DIRECTORY } = require('./constants/setting'); 3 | 4 | mkdirp(DEFAULT_SCREEN_SHOT_OUTPUT_DIRECTORY, function (err) { 5 | if (err) console.error(err) 6 | }); 7 | 8 | jest.setTimeout(30000); 9 | -------------------------------------------------------------------------------- /app/components/common/paperItem/noteButton.scss: -------------------------------------------------------------------------------- 1 | .collectionNoteForm { 2 | border-radius: 8px; 3 | margin-top: 4px; 4 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); 5 | } 6 | 7 | @media (max-width: $mobile_width) { 8 | .addNoteButton { 9 | display: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/components/common/autoSizeTextarea/autoSizeTextarea.scss: -------------------------------------------------------------------------------- 1 | .textarea { 2 | width: 100%; 3 | border: solid 1px $gray400; 4 | font-size: 14px; 5 | border-radius: 4px; 6 | line-height: 1.4; 7 | margin: 0; 8 | 9 | &::placeholder { 10 | color: $gray500; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/components/createKeywordInput/createKeywordInput.scss: -------------------------------------------------------------------------------- 1 | .formWrapper { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .inputFieldWrapper { 7 | width: 100%; 8 | margin-right: 8px; 9 | } 10 | 11 | .submitButton { 12 | height: 44px; 13 | margin-top: 4px; 14 | } 15 | -------------------------------------------------------------------------------- /app/containers/profile/components/gsImportForm/gsImportForm.scss: -------------------------------------------------------------------------------- 1 | .formWrapper { 2 | padding: 16px 0; 3 | } 4 | 5 | .submitBtn { 6 | margin-top: 16px; 7 | text-align: right; 8 | } 9 | 10 | .guideContext { 11 | font-size: 14px; 12 | color: $gray800; 13 | margin-top: 16px; 14 | } 15 | -------------------------------------------------------------------------------- /app/helpers/getQueryParamsObject.tsx: -------------------------------------------------------------------------------- 1 | import { parse } from 'qs'; 2 | 3 | export default function getQueryParamsObject(queryParams?: string | object) { 4 | if (typeof queryParams === 'string') { 5 | return parse(queryParams, { ignoreQueryPrefix: true }); 6 | } 7 | return queryParams; 8 | } 9 | -------------------------------------------------------------------------------- /app/components/articleSearch/common.scss: -------------------------------------------------------------------------------- 1 | %right-item-wrapper { 2 | max-width: 354px; 3 | width: 100%; 4 | height: 100%; 5 | border-radius: 4px; 6 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 7 | border: 1px solid $gray300; 8 | background-color: white; 9 | padding: 24px; 10 | } 11 | -------------------------------------------------------------------------------- /app/icons/double-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/components/auth/emailVerification/records.tsx: -------------------------------------------------------------------------------- 1 | export interface EmailVerificationState 2 | extends Readonly<{ 3 | isLoading: boolean; 4 | hasError: boolean; 5 | }> {} 6 | 7 | export const EMAIL_VERIFICATION_INITIAL_STATE = { 8 | isLoading: false, 9 | hasError: false, 10 | }; 11 | -------------------------------------------------------------------------------- /app/components/authorCV/affiliationBox.scss: -------------------------------------------------------------------------------- 1 | @import '../../components/dialog/components/modifyProfile/affiliationSelectBox/affiliationSelectBox'; 2 | 3 | .affiliationSelectBox { 4 | display: inline-flex; 5 | } 6 | 7 | .inputWrapper { 8 | .errorMessage { 9 | margin-top: 4px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/helpers/axiosCancelTokenManager.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class AxiosCancelTokenManager { 4 | private cancelToken = axios.CancelToken; 5 | 6 | public getCancelTokenSource() { 7 | return this.cancelToken.source(); 8 | } 9 | } 10 | 11 | export default AxiosCancelTokenManager; 12 | -------------------------------------------------------------------------------- /app/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | <% htmlWebpackPlugin.files.js.forEach(function(js) { %> 9 | 10 | <% }) %> 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/api/types/member.ts: -------------------------------------------------------------------------------- 1 | export interface KeywordSettingItemResponse { 2 | id: string; 3 | keyword: string; 4 | lastSendAt: Date; 5 | } 6 | 7 | export interface KeywordSettingsResponse { 8 | data: { 9 | content: KeywordSettingItemResponse[]; 10 | page: null; 11 | }; 12 | error: null; 13 | } 14 | -------------------------------------------------------------------------------- /app/api/home.ts: -------------------------------------------------------------------------------- 1 | import PlutoAxios from './pluto'; 2 | 3 | class HomeAPI extends PlutoAxios { 4 | public async getPapersFoundCount() { 5 | const res = await this.get(`/papers/found-count`); 6 | 7 | return res.data; 8 | } 9 | } 10 | 11 | const homeAPI = new HomeAPI(); 12 | 13 | export default homeAPI; 14 | -------------------------------------------------------------------------------- /app/components/common/roundImageIcon/roundImageIcon.scss: -------------------------------------------------------------------------------- 1 | .imgWrapper { 2 | display: inline-block; 3 | vertical-align: top; 4 | border: solid 2px $gray400; 5 | border-radius: 50%; 6 | } 7 | 8 | .roundImage { 9 | border-radius: 50%; 10 | width: 100%; 11 | height: 100%; 12 | object-fit: contain; 13 | } 14 | -------------------------------------------------------------------------------- /app/icons/full-name-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/paperShow/components/otherPaperList.scss: -------------------------------------------------------------------------------- 1 | .paperListWrapper { 2 | margin-top: 40px; 3 | padding-top: 16px; 4 | border-top: solid 1px $gray400; 5 | } 6 | 7 | .title { 8 | font-size: 14px; 9 | font-weight: 500; 10 | letter-spacing: 1px; 11 | color: $gray800; 12 | margin-bottom: 20px; 13 | } 14 | -------------------------------------------------------------------------------- /app/components/simplePaperItem/simplePaperItem.scss: -------------------------------------------------------------------------------- 1 | .itemWrapper { 2 | border-radius: 4px; 3 | border: solid 1px $gray300; 4 | background-color: white; 5 | padding: 16px 16px 12px 16px; 6 | } 7 | 8 | .btnGroupWrapper { 9 | margin-top: 12px; 10 | padding-top: 8px; 11 | border-top: 1px solid $gray200; 12 | } 13 | -------------------------------------------------------------------------------- /app/components/snackbar/collectionSnackBar/collectionSnackBar.scss: -------------------------------------------------------------------------------- 1 | .viewCollectionBtn { 2 | margin-right: 8px; 3 | } 4 | 5 | .collectionName { 6 | font-weight: 700; 7 | color: white; 8 | } 9 | 10 | .snackbarContext { 11 | font-size: 16px; 12 | color: rgba(256, 256, 256, 0.8); 13 | padding-right: 48px; 14 | } 15 | -------------------------------------------------------------------------------- /app/selectors/getSearchFilter.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { AppState } from '../reducers'; 3 | 4 | export const getMemoizedSearchFilterState = createSelector( 5 | [(state: AppState) => state.searchFilterState], 6 | searchFilterState => { 7 | return searchFilterState; 8 | } 9 | ); 10 | -------------------------------------------------------------------------------- /server/localServer.tsx: -------------------------------------------------------------------------------- 1 | import app from './express'; 2 | 3 | const port: number = Number(process.env.PORT) || 3000; 4 | 5 | app 6 | .listen(port, () => console.log(`Express server listening at ${port}! Visit https://localhost:${port}`)) 7 | .on('error', err => console.error('LOCAL_SERVER_ERROR =======================', err)); 8 | -------------------------------------------------------------------------------- /app/icons/copy-doi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/components/paperShow/components/keyword.scss: -------------------------------------------------------------------------------- 1 | .fosBtnWrapper { 2 | display: inline-flex; 3 | vertical-align: top; 4 | padding: 8px 0; 5 | margin-right: 8px; 6 | 7 | &:last-of-type { 8 | margin-right: 0; 9 | } 10 | } 11 | 12 | .alertIcon { 13 | svg { 14 | width: 16px; 15 | height: 16px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/model/conferenceSeries.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | 3 | export interface ConferenceSeries { 4 | id: string; 5 | name: string; 6 | paperCount: number; 7 | citationCount: number; 8 | nameAbbrev: string | null; 9 | } 10 | 11 | export const conferenceSeriesSchema = new schema.Entity('conferenceSeries'); 12 | -------------------------------------------------------------------------------- /app/model/suggestion.tsx: -------------------------------------------------------------------------------- 1 | export interface RawSuggestion { 2 | highlighted: string; 3 | keyword: string; 4 | original_query: string; 5 | suggest_query: string; 6 | suggestion: string; 7 | } 8 | 9 | export interface Suggestion { 10 | highlighted: string; 11 | originalQuery: string; 12 | suggestQuery: string; 13 | } 14 | -------------------------------------------------------------------------------- /app/components/common/paperItem/sourceButton.scss: -------------------------------------------------------------------------------- 1 | .sourceHostInfo { 2 | @extend %one-line-overflow-ellipsis; 3 | } 4 | 5 | @media (max-width: $mobile_width) { 6 | .extSourceIcon { 7 | display: none !important; 8 | } 9 | } 10 | 11 | @media (max-width: 374px) { 12 | .sourceHostInfo { 13 | display: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/model/profileAffiliation.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from "./domain"; 2 | 3 | export type ProfileAffiliation = { 4 | id: string; 5 | name: string; 6 | nameAbbrev: string | null; 7 | officialPage: string | null; 8 | paperCount: number | null; 9 | citationCount: number | null; 10 | wikiPage: string | null; 11 | domains: Domain[]; 12 | } -------------------------------------------------------------------------------- /app/hooks/useEnvHook.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import EnvChecker from '../helpers/envChecker'; 3 | 4 | export function useEnvHook() { 5 | const [isOnClient, setIsOnClient] = React.useState(false); 6 | React.useEffect(() => { 7 | setIsOnClient(!EnvChecker.isOnServer()); 8 | }, []); 9 | 10 | return { isOnClient }; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/deploy/config.ts: -------------------------------------------------------------------------------- 1 | export const AWS_S3_BUCKET = 'pluto-web-client'; 2 | export const APP_DEST = './dist/'; 3 | export const AWS_S3_PRODUCTION_FOLDER_PREFIX = 'production'; 4 | export const AWS_S3_DEV_FOLDER_PREFIX = 'dev'; 5 | export const BUNDLE_BASE_PATH = '/bundle'; 6 | export const AWS_SSM_PARAM_STORE_NAME = '/scinapse-web-client/dev/branch-mapper'; 7 | -------------------------------------------------------------------------------- /app/icons/complete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/routes/robots.tsx: -------------------------------------------------------------------------------- 1 | export default function getRobotTxt(isProd: boolean) { 2 | if (isProd) { 3 | return ` 4 | User-agent: * 5 | Disallow: /search 6 | Disallow: /search/* 7 | Disallow: /papers/*/cited 8 | Disallow: /papers/*/ref 9 | `; 10 | } 11 | return ` 12 | User-agent: * 13 | Disallow: / 14 | `; 15 | } 16 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pluto-web-client", 3 | "devDependencies": {}, 4 | "dependencies": { 5 | "@auth0/s3": "^1.0.0", 6 | "@babel/plugin-proposal-class-properties": "7.4.4", 7 | "@babel/plugin-transform-runtime": "7.4.4", 8 | "@babel/runtime": "7.4.4", 9 | "aws-sdk": "2.403.0", 10 | "core-js": "2.6.5" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/helpers/validateEmail.ts: -------------------------------------------------------------------------------- 1 | export default function validateEmail(email: string) { 2 | // tslint:disable-next-line:max-line-length 3 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 4 | return reg.test(email) && email !== '' && email.length > 0; 5 | } 6 | -------------------------------------------------------------------------------- /app/helpers/toggleElementFromArray.tsx: -------------------------------------------------------------------------------- 1 | export function toggleElementFromArray(elem: T, array: T[], prepend?: boolean) { 2 | const i = array.indexOf(elem); 3 | if (i > -1) { 4 | return [...array.slice(0, i), ...array.slice(i + 1)]; 5 | } 6 | 7 | if (prepend) { 8 | return [elem, ...array]; 9 | } else { 10 | return [...array, elem]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/containers/profile/components/citationStringImportForm/citationStringImportForm.scss: -------------------------------------------------------------------------------- 1 | .formWrapper { 2 | padding: 16px 0; 3 | } 4 | 5 | .submitBtn { 6 | margin-top: 16px; 7 | text-align: right; 8 | } 9 | 10 | .guideContext { 11 | font-size: 14px; 12 | color: $gray800; 13 | margin-top: 16px; 14 | } 15 | 16 | .citationTextArea { 17 | height: 200px; 18 | } 19 | -------------------------------------------------------------------------------- /app/__mocks__/member.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "39", 3 | "email": "honeyjasdds@postech.ac.kr", 4 | "emailVerified": false, 5 | "firstName": "Tylor", 6 | "lastName": "Shin", 7 | "profileImage": null, 8 | "affiliation": "eeee", 9 | "major": null, 10 | "reputation": 0, 11 | "articleCount": 0, 12 | "reviewCount": 0, 13 | "commentCount": 0, 14 | "oauth": null 15 | } 16 | -------------------------------------------------------------------------------- /app/components/common/scinapseInput/scinapseCheckbox.scss: -------------------------------------------------------------------------------- 1 | .checkboxIcon { 2 | display: inline-flex; 3 | padding: 0; 4 | padding-bottom: 4px; 5 | margin-right: 4px; 6 | width: 20px; 7 | height: 20px; 8 | color: $gray600 !important; 9 | 10 | svg { 11 | font-size: 17px; 12 | } 13 | } 14 | 15 | .checkedCheckboxIcon { 16 | color: $main_blue0 !important; 17 | } 18 | -------------------------------------------------------------------------------- /app/icons/small-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/__mocks__/currentUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "affiliation": "eeee", 3 | "articleCount": 0, 4 | "email": "honeyjasdds@postech.ac.kr", 5 | "emailVerified": false, 6 | "id": "39", 7 | "isLoggedIn": true, 8 | "major": null, 9 | "name": "academey", 10 | "oauth": null, 11 | "oauthLoggedIn": true, 12 | "profileImage": null, 13 | "reputation": 0, 14 | "reviewCount": 0 15 | } 16 | -------------------------------------------------------------------------------- /app/icons/bookmark-thin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/common/hIndexBox/hIndexBox.scss: -------------------------------------------------------------------------------- 1 | .hIndexBox { 2 | display: inline-block; 3 | vertical-align: top; 4 | height: 15.5px; 5 | border-radius: 7.7px; 6 | background-color: $main_blue_light1; 7 | font-size: 8.5px; 8 | font-weight: 500; 9 | letter-spacing: 0.3px; 10 | text-align: center; 11 | color: $main_blue0; 12 | padding: 3px 6px; 13 | margin-left: 4px; 14 | } 15 | -------------------------------------------------------------------------------- /app/icons/password-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/components/filterContainer/filterResetButton.scss: -------------------------------------------------------------------------------- 1 | .resetButtonWrapper { 2 | position: absolute; 3 | display: inline-flex; 4 | justify-content: center; 5 | align-items: center; 6 | right: 0; 7 | bottom: 0; 8 | top: 2px; 9 | font-size: 13px; 10 | color: $gray600; 11 | cursor: pointer; 12 | font-weight: normal; 13 | 14 | &:hover { 15 | color: $gray500; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/icons/email-verification-complete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/constants/feedback.tsx: -------------------------------------------------------------------------------- 1 | export enum FEEDBACK_SOURCE { 2 | EMAIL = 1, 3 | PORTAL, 4 | PHONE, 5 | CHAT = 7, 6 | MOBILHELP, 7 | FEEDBACK_WIDGET, 8 | OUTBOUND_EMAIL, 9 | } 10 | 11 | export enum FEEDBACK_STATUS { 12 | OPEN = 2, 13 | PENDING, 14 | RESOLVED, 15 | CLOSED, 16 | } 17 | 18 | export enum FEEDBACK_PRIORITY { 19 | LOW = 1, 20 | MEDIUM, 21 | HIGH, 22 | URGENT, 23 | } 24 | -------------------------------------------------------------------------------- /app/containers/profile/types.tsx: -------------------------------------------------------------------------------- 1 | export enum IMPORT_SOURCE_TAB { 2 | GS = 'GS', 3 | BIBTEX = 'BIBTEX', 4 | CITATION = 'CITATION', 5 | AUTHOR_URLS = 'AUTHOR_URLS', 6 | } 7 | 8 | export enum CURRENT_IMPORT_PROGRESS_STEP { 9 | PROGRESS, 10 | RESULT, 11 | } 12 | 13 | export interface HandleImportPaperListParams { 14 | type: IMPORT_SOURCE_TAB; 15 | importedContext: string | string[]; 16 | } 17 | -------------------------------------------------------------------------------- /app/helpers/makePlutoToastAction.ts: -------------------------------------------------------------------------------- 1 | import StoreManager from '../store/store'; 2 | import { ACTION_TYPES } from '../actions/actionTypes'; 3 | 4 | export default function alertToast(notificationActionPayload: Scinapse.Alert.NotificationActionPayload): void { 5 | StoreManager.store.dispatch({ 6 | type: ACTION_TYPES.GLOBAL_ALERT_NOTIFICATION, 7 | payload: notificationActionPayload, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /app/model/paperInCollection.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | import { Paper } from './paper'; 3 | 4 | export interface PaperInCollection { 5 | note: string | null; 6 | collectionId: number; 7 | paperId: string; 8 | paper: Paper; 9 | } 10 | 11 | export const paperInCollectionSchema = new schema.Entity('papersInCollection', undefined, { 12 | idAttribute: value => value.paperId, 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/common/separator/separator.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | 5 | .dash { 6 | width: 100%; 7 | height: 1px; 8 | border-top: dashed 2px $gray400; 9 | margin-left: 4x; 10 | margin-right: 4px; 11 | } 12 | 13 | .content { 14 | font-size: 14px; 15 | text-align: center; 16 | color: $gray500; 17 | padding: 0 8px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/actions/searchFilter.tsx: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES, SetActiveFilterBoxButtonAction } from './actionTypes'; 2 | import { FILTER_BUTTON_TYPE } from '../components/filterButton'; 3 | 4 | export function setActiveFilterButton(button: FILTER_BUTTON_TYPE | null): SetActiveFilterBoxButtonAction { 5 | return { 6 | type: ACTION_TYPES.ARTICLE_SEARCH_SET_ACTIVE_FILTER_BOX_BUTTON, 7 | payload: { button }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /app/containers/profile/components/pendingDescriptionDialog/pendingDescriptionDialog.scss: -------------------------------------------------------------------------------- 1 | .iconWrapper { 2 | position: absolute; 3 | top: 16px; 4 | right: 16px; 5 | display: inline-flex; 6 | justify-content: center; 7 | align-items: center; 8 | width: 20px; 9 | height: 20px; 10 | cursor: pointer; 11 | 12 | svg { 13 | width: 20px; 14 | height: 20px; 15 | color: $gray500; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/icons/email-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/prodHandler.tsx: -------------------------------------------------------------------------------- 1 | import handler from '.'; 2 | 3 | export const ssr = async (event: LambdaProxy.Event) => { 4 | try { 5 | const resBody = handler(event); 6 | return resBody; 7 | } catch (err) { 8 | return { 9 | statusCode: 500, 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify({ error: err.message }), 14 | }; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /app/helpers/getBubbleContextType.tsx: -------------------------------------------------------------------------------- 1 | export const BUBBLE_CONTEXT_TYPE = 'b_c_t'; 2 | 3 | const store = require('store'); 4 | 5 | export function setBubbleContextTypeHelper() { 6 | const bubbleContextType: number = store.get(BUBBLE_CONTEXT_TYPE) || 1; 7 | 8 | if (bubbleContextType < 3) { 9 | store.set(BUBBLE_CONTEXT_TYPE, bubbleContextType + 1); 10 | } else { 11 | store.set(BUBBLE_CONTEXT_TYPE, 1); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/model/author.tsx: -------------------------------------------------------------------------------- 1 | import { Affiliation } from './affiliation'; 2 | import { Profile } from './profile'; 3 | 4 | export interface BasePaperAuthor { 5 | id: string; 6 | name: string; 7 | isLayered: boolean; 8 | profile: Profile | null; 9 | hindex?: number; 10 | } 11 | 12 | export interface PaperAuthor extends BasePaperAuthor { 13 | order: number; 14 | organization: string; 15 | affiliation: Affiliation | null; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/mobilePaperShowButtonGroup/mobilePaperShowButtonGroup.scss: -------------------------------------------------------------------------------- 1 | .buttonWrapper { 2 | flex: 1 0 auto; 3 | 4 | button, 5 | a { 6 | width: 100%; 7 | justify-content: center; 8 | } 9 | } 10 | 11 | .buttonWrapper + .buttonWrapper { 12 | margin-left: 8px; 13 | } 14 | 15 | .citeButton { 16 | width: 100%; 17 | height: 100%; 18 | justify-content: center; 19 | 20 | button { 21 | width: 100%; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/hooks/FBisLoadingHook.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | declare var FB: any; 3 | 4 | export default function useFBIsLoading() { 5 | const [FBisLoading, setFBisLoading] = React.useState(typeof FB === 'undefined'); 6 | 7 | React.useEffect( 8 | () => { 9 | if (typeof FB !== 'undefined') { 10 | setFBisLoading(false); 11 | } 12 | }, 13 | [typeof FB] 14 | ); 15 | 16 | return FBisLoading; 17 | } 18 | -------------------------------------------------------------------------------- /app/components/paperShowTabItem/paperShowTabItem.scss: -------------------------------------------------------------------------------- 1 | .tabItem { 2 | display: inline-block; 3 | vertical-align: top; 4 | padding: 16px 8px 12px 8px; 5 | font-size: 16px; 6 | font-weight: 500; 7 | color: $gray600; 8 | cursor: pointer; 9 | 10 | &.active { 11 | color: $black0; 12 | border-bottom: 2px solid $black0; 13 | } 14 | } 15 | 16 | @media (max-width: 375px) { 17 | .tabItem { 18 | font-size: 14px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/icons/clock-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/components/common/scinapseSnackbar/scinapseSnackbar.scss: -------------------------------------------------------------------------------- 1 | .snackbarWrapper { 2 | @extend %snackbar-wrapper; 3 | } 4 | 5 | .snackbarContext { 6 | font-size: 16px; 7 | color: rgba(256, 256, 256, 0.8); 8 | padding-right: 48px; 9 | } 10 | 11 | @media (max-width: $mui-md-width) { 12 | .snackbarContext { 13 | padding-right: 0px; 14 | } 15 | 16 | .closeBtn { 17 | position: absolute; 18 | right: 8px; 19 | top: 14px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/api/types/recommendation.ts: -------------------------------------------------------------------------------- 1 | export type RecommendationActionTag = 2 | | 'paperShow' 3 | | 'copyDoi' 4 | | 'downloadPdf' 5 | | 'citePaper' 6 | | 'addToCollection' 7 | | 'source' 8 | | 'viewMorePDF'; 9 | 10 | export interface RecommendationActionAPIParams { 11 | paper_id: string; 12 | action: RecommendationActionTag; 13 | } 14 | 15 | export interface RecommendationActionParams { 16 | paperId: string; 17 | action: RecommendationActionTag; 18 | } 19 | -------------------------------------------------------------------------------- /app/model/journal.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | import { NewFOS } from './fos'; 3 | 4 | export interface Journal { 5 | id: string; 6 | citationCount: number; 7 | fosList: NewFOS[]; 8 | impactFactor: number | null; 9 | issn: string | null; 10 | paperCount: number; 11 | title: string; 12 | webPage: string | null; 13 | titleAbbrev: string | null; 14 | sci: boolean; 15 | } 16 | 17 | export const journalSchema = new schema.Entity('journals'); 18 | -------------------------------------------------------------------------------- /app/helpers/pageMapper/index.tsx: -------------------------------------------------------------------------------- 1 | import { RawPageObjectV2, PageObjectV2 } from '../../api/types/common'; 2 | 3 | export default function mapPageObject(rawPage: RawPageObjectV2): PageObjectV2 { 4 | return { 5 | size: rawPage.size, 6 | page: rawPage.page, 7 | first: rawPage.first, 8 | last: rawPage.last, 9 | numberOfElements: rawPage.number_of_elements, 10 | totalElements: rawPage.total_elements, 11 | totalPages: rawPage.total_pages, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /app/components/common/status/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | interface StatusProps { 5 | code: number; 6 | } 7 | 8 | const Status: React.SFC = ({ code, children }) => ( 9 | { 11 | if (staticContext) { 12 | staticContext.statusCode = code; 13 | } 14 | return children; 15 | }} 16 | /> 17 | ); 18 | 19 | export default Status; 20 | -------------------------------------------------------------------------------- /app/containers/profile/components/authorUrlsImportField/authorUrlsImportField.scss: -------------------------------------------------------------------------------- 1 | .inputWrapper { 2 | display: flex; 3 | } 4 | 5 | .removeUrlBtn { 6 | margin-left: 8px; 7 | } 8 | 9 | .errorMessage { 10 | font-size: 14px; 11 | padding: 0 10px; 12 | color: $red0; 13 | line-height: 1.5; 14 | margin-top: 4px; 15 | } 16 | 17 | .authorNameText { 18 | font-size: 14px; 19 | padding: 0 10px; 20 | color: $gray600; 21 | line-height: 1.5; 22 | margin-top: 4px; 23 | } 24 | -------------------------------------------------------------------------------- /app/icons/activity-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/icons/email-verification-fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/icons/cited.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/model/error.tsx: -------------------------------------------------------------------------------- 1 | export interface CommonError { 2 | errors: MappedError[] | null; 3 | exception: string; 4 | message: string; 5 | path: string; 6 | reason: string; 7 | status: number; 8 | timestamp: string; // ISO 8601 with Z 9 | } 10 | 11 | interface MappedError { 12 | codes: string[]; 13 | arguments: any[]; 14 | defaultMessage: string; 15 | objectName: string; 16 | field: string; 17 | rejectedValue: string; 18 | bindingFailure: boolean; 19 | code: string; 20 | } 21 | -------------------------------------------------------------------------------- /app/actions/profileInfo.tsx: -------------------------------------------------------------------------------- 1 | import { ProfileInfo } from '../model/profileInfo'; 2 | import { getAxiosInstance } from '../api/axios'; 3 | 4 | export async function getProfileCVInformation(profileSlug: string) { 5 | // WARN: working at client side only 6 | const axios = getAxiosInstance(); 7 | try { 8 | const res = await axios.get(`/profiles/${profileSlug}/information`); 9 | return res.data.data.content as ProfileInfo; 10 | } catch (err) { 11 | // TODO: handle error state 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/components/common/paperItem/paperItem.scss: -------------------------------------------------------------------------------- 1 | .paperItemWrapper { 2 | border-radius: 4px; 3 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 4 | border: solid 1px $gray200; 5 | background-color: white; 6 | padding: 16px 16px 0 16px; 7 | 8 | & + & { 9 | margin-top: 16px; 10 | } 11 | } 12 | 13 | @media (max-width: $mobile_width) { 14 | .paperItemWrapper { 15 | width: 100%; 16 | border: solid 1px #eaedf4; 17 | 18 | & + & { 19 | margin-top: 12px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/components/profileRegister/profileRegiser.scss: -------------------------------------------------------------------------------- 1 | $wrapper-vertical-margin: 128px; 2 | 3 | .container { 4 | background-color: $gray30; 5 | min-height: calc(100vh - #{$navbar_height}); 6 | padding-top: $navbar_height; 7 | } 8 | 9 | .wrapper { 10 | display: flex; 11 | align-items: center; 12 | background-color: white; 13 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 14 | width: 100%; 15 | max-width: 578px; 16 | min-height: calc(100vh - #{$navbar_height}); 17 | margin: 0 auto; 18 | } 19 | -------------------------------------------------------------------------------- /app/components/auth/authContextText/constants.tsx: -------------------------------------------------------------------------------- 1 | export const positiveSignUpContext: { [key: string]: string } = { 2 | downloadPdf: '📜 To download PDF more, you need to be a Scinapse member.', 3 | viewMorePDF: '📜 To view full text, you need to be a Scinapse member.', 4 | addToCollection: '🔍 To add papers to collection, you need to be a Scinapse member.', 5 | query: '🔍 To search more, you need to be a Scinapse member.', 6 | paperShow: '📈 To view paper information more, you need to be a Scinapse member.', 7 | }; 8 | -------------------------------------------------------------------------------- /app/components/profilePaperItem/profilePaperItem.scss: -------------------------------------------------------------------------------- 1 | .paperItemWrapper { 2 | border-radius: 4px; 3 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 4 | border: solid 1px $gray200; 5 | background-color: white; 6 | padding: 16px 16px 0 16px; 7 | 8 | & + & { 9 | margin-top: 16px; 10 | } 11 | } 12 | 13 | @media (max-width: $mobile_width) { 14 | .paperItemWrapper { 15 | width: 100%; 16 | border: solid 1px #eaedf4; 17 | 18 | & + & { 19 | margin-top: 12px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/containers/paperShow/types.tsx: -------------------------------------------------------------------------------- 1 | import { PAPER_LIST_SORT_TYPES } from '../../components/common/sortBox'; 2 | 3 | export interface PaperShowMatchParams { 4 | paperId: string; 5 | } 6 | 7 | export interface PaperShowPageQueryParams { 8 | 'ref-page'?: number; 9 | 'ref-sort'?: PAPER_LIST_SORT_TYPES; 10 | 'ref-query'?: string; 11 | 'cited-page'?: number; 12 | 'cited-sort'?: PAPER_LIST_SORT_TYPES; 13 | 'cited-query'?: string; 14 | } 15 | 16 | export type RefCitedTabItem = 'fullText' | 'ref' | 'cited'; 17 | -------------------------------------------------------------------------------- /app/components/common/scinapseButton/scinapseButton.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | padding: 7px 8px; 3 | border-radius: 5px; 4 | font-size: 14px; 5 | font-weight: bold; 6 | text-align: center; 7 | color: white; 8 | cursor: pointer; 9 | 10 | &:disabled { 11 | cursor: not-allowed; 12 | opacity: 0.2; 13 | } 14 | } 15 | 16 | .spinnerWrapper { 17 | display: flex; 18 | height: 100%; 19 | align-items: center; 20 | justify-content: center; 21 | 22 | .loadingSpinner { 23 | color: white; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/containers/profileOnboarding/helper.tsx: -------------------------------------------------------------------------------- 1 | import { CURRENT_ONBOARDING_PROGRESS_STEP, ONBOARDING_STEPS } from './types'; 2 | 3 | export const isStepOptional = (step: CURRENT_ONBOARDING_PROGRESS_STEP) => { 4 | return ( 5 | step === CURRENT_ONBOARDING_PROGRESS_STEP.MATCH_UNSYNCED_PUBS || 6 | step === CURRENT_ONBOARDING_PROGRESS_STEP.SELECT_REPRESENTATIVE_PUBS 7 | ); 8 | }; 9 | 10 | export const isStepFinal = (step: CURRENT_ONBOARDING_PROGRESS_STEP) => { 11 | return step === ONBOARDING_STEPS.length; 12 | }; 13 | -------------------------------------------------------------------------------- /app/model/collection.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | import { Member } from './member'; 3 | 4 | export interface Collection { 5 | id: number; 6 | createdBy: Member; 7 | title: string; 8 | description: string; 9 | paperCount: number; 10 | createdAt: string; 11 | updatedAt: string; 12 | isDefault: boolean; 13 | containsSelected?: boolean; 14 | note?: string | null; 15 | // used on client only 16 | noteUpdated?: boolean; 17 | } 18 | 19 | export const collectionSchema = new schema.Entity('collections'); 20 | -------------------------------------------------------------------------------- /app/components/auth/signIn/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | import { GLOBAL_DIALOG_TYPE } from '../../../dialog/reducer'; 4 | import { OAUTH_VENDOR } from '../../../../api/types/auth'; 5 | 6 | export interface SignInContainerProps extends RouteComponentProps { 7 | dispatch: Dispatch; 8 | handleChangeDialogType?: (type: GLOBAL_DIALOG_TYPE) => void; 9 | } 10 | 11 | export interface SignInSearchParams { 12 | code?: string; 13 | vendor?: OAUTH_VENDOR; 14 | } 15 | -------------------------------------------------------------------------------- /app/actions/pdfViewer.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, PaperPdf } from '../model/paper'; 2 | import PaperAPI from '../api/paper'; 3 | 4 | export async function getBestPdf(paper: Paper) { 5 | if (paper.bestPdf) return paper.bestPdf; 6 | return await PaperAPI.getBestPdfOfPaper({ paperId: paper.id }); 7 | } 8 | 9 | export async function getPDFPathOrBlob(pdf: PaperPdf) { 10 | if (!pdf) return null; 11 | 12 | if (pdf.path) return pdf.path; 13 | 14 | if (pdf.hasBest) { 15 | const blob = await PaperAPI.getPDFBlob(pdf.url); 16 | return blob; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/components/emailSettings/emailToggleTitle.scss: -------------------------------------------------------------------------------- 1 | .toggleItemContext { 2 | max-width: 377px; 3 | width: 100%; 4 | margin-right: 8px; 5 | } 6 | 7 | .toggleItemTitle { 8 | font-size: 18px; 9 | font-weight: 500; 10 | line-height: 1.33; 11 | color: $black0; 12 | margin-bottom: 8px; 13 | } 14 | 15 | .toggleItemSubtitle { 16 | font-size: 15px; 17 | line-height: 1.33; 18 | color: $gray800; 19 | } 20 | 21 | @media (max-width: $mobile_width) { 22 | .toggleItemContext { 23 | margin-right: 0px; 24 | max-width: 100%; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/helpers/__mocks__/handleGA.ts: -------------------------------------------------------------------------------- 1 | export function trackAndOpenLink(from: string) { 2 | if (!from) throw new Error('mockError'); 3 | } 4 | 5 | export function trackAction(path: string, from: string) { 6 | if (!path || !from) throw new Error('mockError'); 7 | } 8 | 9 | export function trackDialogView(name: string) { 10 | if (!name) throw new Error('mockError'); 11 | } 12 | 13 | export function measureTiming(category: string, variable: string, consumedTime: number) { 14 | if (!category || !variable || !consumedTime) throw new Error('mockError'); 15 | } 16 | -------------------------------------------------------------------------------- /app/containers/profileOnboarding/profileOnboarding.scss: -------------------------------------------------------------------------------- 1 | $wrapper-vertical-margin: 128px; 2 | 3 | .container { 4 | background-color: $gray30; 5 | min-height: calc(100vh - #{$navbar_height}); 6 | padding-top: $navbar_height; 7 | } 8 | 9 | .wrapper { 10 | position: relative; 11 | display: flex; 12 | flex-direction: column; 13 | background-color: white; 14 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 15 | width: 100%; 16 | max-width: 768px; 17 | min-height: calc(100vh - #{$navbar_height}); 18 | margin: 0 auto; 19 | padding-top: 24px; 20 | } 21 | -------------------------------------------------------------------------------- /.circleci/setup_puppeteer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-get update 4 | sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 5 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 6 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ 7 | libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ 8 | ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget --fix-missing 9 | -------------------------------------------------------------------------------- /app/components/auth/auth.scss: -------------------------------------------------------------------------------- 1 | .pageWrapper { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .authWrapper { 7 | width: 100%; 8 | min-height: 100vh; 9 | background-color: $gray30; 10 | } 11 | 12 | .contentWrapper { 13 | margin: 180px auto; 14 | width: 100%; 15 | max-width: 794px; 16 | border-radius: 1.7px; 17 | background-color: #fff; 18 | border: solid 1px #d8dde7; 19 | position: relative; 20 | overflow-y: hidden; 21 | } 22 | 23 | @media (max-width: $tablet_width) { 24 | .contentWrapper { 25 | max-width: 384px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/components/common/paperItem/citeButton.scss: -------------------------------------------------------------------------------- 1 | .citeButton { 2 | display: flex; 3 | align-items: center; 4 | border-radius: 4px; 5 | border: solid 1px $gray400; 6 | background-color: white; 7 | font-size: 14px; 8 | line-height: 16px; 9 | padding: 8px; 10 | color: $main_blue0; 11 | cursor: pointer; 12 | } 13 | 14 | .citationIcon { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | margin-right: 4px; 19 | width: 16px; 20 | height: 16px; 21 | 22 | svg { 23 | width: 16px; 24 | height: 16px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/icons/bookmark-gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/fallbackRender.tsx: -------------------------------------------------------------------------------- 1 | import { Helmet } from "react-helmet"; 2 | import { initialState } from "../app/reducers"; 3 | import { generateFullHTML } from "../app/helpers/htmlWrapper"; 4 | 5 | export default function fallbackJSOnlyRender(scriptTags: string, version: string) { 6 | const helmet = Helmet.renderStatic(); 7 | const fullHTML: string = generateFullHTML({ 8 | reactDom: "", 9 | linkTags: "", 10 | scriptTags, 11 | helmet, 12 | initialState: JSON.stringify(initialState), 13 | css: "", 14 | version, 15 | }); 16 | 17 | return fullHTML; 18 | } 19 | -------------------------------------------------------------------------------- /app/api/types/author.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from 'axios'; 2 | import { Author } from '../../model/author/author'; 3 | 4 | export interface GetAuthorsParam { 5 | sort: string; 6 | page: number; 7 | query: string; 8 | filter?: string; 9 | size?: number; 10 | cancelToken?: CancelToken; 11 | } 12 | 13 | export interface GetAuthorsResult { 14 | authors: Author[]; 15 | first: boolean; 16 | last: boolean; 17 | number: number; 18 | numberOfElements: number; 19 | size: number; 20 | sort: string | null; 21 | totalElements: number; 22 | totalPages: number; 23 | } 24 | -------------------------------------------------------------------------------- /app/model/member.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | import { MemberOAuth } from './oauth'; 3 | 4 | export interface Member { 5 | id: number; 6 | email: string; 7 | emailVerified: boolean; 8 | firstName: string; 9 | lastName: string; 10 | profileSlug: string | null; 11 | profileImageUrl: string; 12 | affiliationId: string | null; 13 | affiliationName: string | null; 14 | oauth: MemberOAuth | null; 15 | isAuthorConnected: boolean; 16 | authorId: string; 17 | profileLink: string; 18 | } 19 | 20 | export const memberSchema = new schema.Entity('members'); 21 | -------------------------------------------------------------------------------- /app/components/auth/authContextText/authContextText.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | max-width: 792px; 4 | height: 100%; 5 | min-height: 44px; 6 | border-radius: 4px; 7 | background-color: $gray30; 8 | } 9 | 10 | .contentWrapper { 11 | padding: 12px 32px; 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | 16 | .contentText { 17 | font-size: 14px; 18 | line-height: 1.43; 19 | color: $black0; 20 | } 21 | } 22 | 23 | @media (max-width: $mobile_width) { 24 | .container { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/components/common/paperItem/moreDropdownItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem from '@material-ui/core/MenuItem'; 3 | const useStyles = require('isomorphic-style-loader/useStyles'); 4 | const s = require('./moreDropdownItem.scss'); 5 | 6 | const PaperItemMoreDropdownItem: React.FC<{ onClick: () => void; content: string }> = ({ onClick, content }) => { 7 | useStyles(s); 8 | return ( 9 | 10 | {content} 11 | 12 | ); 13 | }; 14 | 15 | export default PaperItemMoreDropdownItem; 16 | -------------------------------------------------------------------------------- /app/components/paperShow/components/viewFullTextBtn.scss: -------------------------------------------------------------------------------- 1 | .btnStyle { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | padding: 10px 16px 10px 12px; 6 | width: 100%; 7 | height: 40px; 8 | border-radius: 4px; 9 | font-size: 14px; 10 | font-weight: bold; 11 | line-height: 1.43; 12 | background-color: $main_blue1; 13 | color: white; 14 | 15 | &:hover { 16 | background-color: $main_blue_light0; 17 | } 18 | } 19 | 20 | .pdfIcon { 21 | display: flex; 22 | margin-right: 8px; 23 | svg { 24 | width: 20px; 25 | height: 20px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/components/common/hIndexBox/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withStyles } from '../../../helpers/withStylesHelper'; 3 | const styles = require('./hIndexBox.scss'); 4 | 5 | interface HIndexBoxProps { 6 | hIndex?: number; 7 | } 8 | 9 | class HIndexBox extends React.PureComponent { 10 | public render() { 11 | if (!this.props.hIndex) { 12 | return null; 13 | } 14 | 15 | return {`H-index : ${this.props.hIndex}`}; 16 | } 17 | } 18 | 19 | export default withStyles(styles)(HIndexBox); 20 | -------------------------------------------------------------------------------- /app/helpers/trackSelectFilter.tsx: -------------------------------------------------------------------------------- 1 | import { FILTER_BOX_TYPE } from '../constants/paperSearch'; 2 | import { trackEvent } from './handleGA'; 3 | import ActionTicketManager from './actionTicketManager'; 4 | 5 | export function trackSelectFilter(actionType: FILTER_BOX_TYPE, actionValue: string | number) { 6 | trackEvent({ category: 'Filter', action: actionType, label: String(actionValue) }); 7 | ActionTicketManager.trackTicket({ 8 | pageType: 'searchResult', 9 | actionType: 'fire', 10 | actionArea: 'filter', 11 | actionTag: actionType, 12 | actionLabel: String(actionValue), 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /app/components/auth/emailVerification/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | import { EmailVerificationState } from '../records'; 4 | import { GLOBAL_DIALOG_TYPE } from '../../../dialog/reducer'; 5 | 6 | export interface EmailVerificationContainerProps extends RouteComponentProps { 7 | handleChangeDialogType?: (type: GLOBAL_DIALOG_TYPE) => void; 8 | emailVerificationState: EmailVerificationState; 9 | dispatch: Dispatch; 10 | } 11 | 12 | export interface EmailVerificationParams { 13 | token?: string; 14 | email?: string; 15 | } 16 | -------------------------------------------------------------------------------- /app/components/paperShow/refCitedPapers/mobileRefCitedPapers.scss: -------------------------------------------------------------------------------- 1 | .loadingSection { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 350px; 6 | } 7 | 8 | .titleWrapper { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | padding-top: 12px; 13 | } 14 | 15 | .title { 16 | font-size: 18px; 17 | font-weight: 500; 18 | color: $black_light_0; 19 | margin-bottom: 12px; 20 | } 21 | 22 | .itemWrapper { 23 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 24 | } 25 | 26 | .itemWrapper + .itemWrapper { 27 | margin-top: 12px; 28 | } 29 | -------------------------------------------------------------------------------- /app/icons/cloud-upload-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/components/common/formikInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FieldProps } from 'formik'; 3 | import { InputProps, InputField } from '@pluto_network/pluto-design-elements'; 4 | 5 | const FormikInput: React.FC = props => { 6 | const { field, form, ...inputProps } = props; 7 | const { touched, errors } = form; 8 | const error = errors[field.name] as string | undefined; 9 | const touch = touched[field.name] as boolean | undefined; 10 | 11 | return ; 12 | }; 13 | 14 | export default FormikInput; 15 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../dist-sls", 4 | "allowSyntheticDefaultImports": true, 5 | "noImplicitAny": true, 6 | "allowUnusedLabels": false, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "module": "commonjs", 10 | "target": "es5", 11 | "lib": ["es2016", "dom", "esnext.asynciterable"], 12 | "typeRoots": ["../node_modules/@types", "../typings"], 13 | "removeComments": true, 14 | "skipLibCheck": true, 15 | "experimentalDecorators": true 16 | }, 17 | "include": ["**/*.ts"], 18 | "exclude": ["../node_modules/**/*"] 19 | } 20 | -------------------------------------------------------------------------------- /app/components/yearGraph/yearGraph.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | } 4 | 5 | .yearLabel { 6 | position: absolute; 7 | color: $gray700; 8 | font-size: 12px; 9 | width: 28px; 10 | height: 14px; 11 | top: 8px; 12 | } 13 | 14 | .column { 15 | fill: $gray600; 16 | } 17 | 18 | .boxWrapper { 19 | &:hover { 20 | .column { 21 | fill: $navy0; 22 | } 23 | } 24 | } 25 | 26 | .labelWrapper { 27 | position: relative; 28 | height: 30px; 29 | display: flex; 30 | justify-content: center; 31 | padding: 8px 0; 32 | } 33 | 34 | .labelText { 35 | color: $gray700; 36 | font-size: 12px; 37 | } 38 | -------------------------------------------------------------------------------- /app/containers/filterContainer/filterButton/filterButton.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | width: 99px; 3 | border-radius: 4px; 4 | border: solid 1px $gray400; 5 | padding: 8px 0; 6 | text-align: center; 7 | font-size: 13px; 8 | text-align: center; 9 | color: $gray600; 10 | cursor: pointer; 11 | 12 | & + & { 13 | margin-left: 10px; 14 | } 15 | 16 | &:hover { 17 | border-color: $gray500; 18 | } 19 | 20 | &.isActive { 21 | color: $main_blue1; 22 | border-color: $main_blue1; 23 | } 24 | 25 | &.isActive:hover { 26 | color: $main_blue_light0; 27 | border-color: $main_blue_light0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/containers/paperShow/select.tsx: -------------------------------------------------------------------------------- 1 | import { denormalize } from 'normalizr'; 2 | import { createSelector } from 'reselect'; 3 | import { getPaperEntities } from '../../selectors/papersSelector'; 4 | import { paperSchema, Paper } from '../../model/paper'; 5 | import { AppState } from '../../reducers'; 6 | 7 | function getPaperId(state: AppState) { 8 | return state.paperShow.paperId; 9 | } 10 | 11 | export const getMemoizedPaper = createSelector([getPaperId, getPaperEntities], (paperId, paperEntities) => { 12 | const paper: Paper | null = denormalize(paperId, paperSchema, { papers: paperEntities }); 13 | return paper; 14 | }); 15 | -------------------------------------------------------------------------------- /app/__mocks__/journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2764552960", 3 | "title": "Applied Mathematics and Computation", 4 | "issn": null, 5 | "webPage": null, 6 | "impactFactor": null, 7 | "paperCount": 19555, 8 | "citationCount": 253265, 9 | "fosList": [ 10 | { "id": "33923547", "name": "Mathematics" }, 11 | { "id": "48753275", "name": "Numerical analysis" }, 12 | { "id": "126255220", "name": "Mathematical optimization" }, 13 | { "id": "134306372", "name": "Mathematical analysis" }, 14 | { "id": "182310444", "name": "Boundary value problem" } 15 | ], 16 | "fullTitle": "Applied Mathematics and Computation" 17 | } 18 | -------------------------------------------------------------------------------- /app/components/layouts/types/header.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { RouteComponentProps } from 'react-router'; 3 | import { LayoutState } from '../reducer'; 4 | import { CurrentUser } from '../../../model/currentUser'; 5 | import { MyCollectionsState } from '../../../containers/paperShowCollectionControlButton/reducer'; 6 | import { Paper } from '../../../model/paper'; 7 | 8 | export interface HeaderProps extends RouteComponentProps { 9 | layoutState: LayoutState; 10 | currentUserState: CurrentUser; 11 | myCollectionsState: MyCollectionsState; 12 | paper: Paper | null; 13 | dispatch: Dispatch; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/improvedHome/constants.tsx: -------------------------------------------------------------------------------- 1 | export const JOURNALS = [ 2 | 'nature', 3 | 'science', 4 | 'ieee', 5 | 'cell', 6 | 'acs', 7 | 'aps', 8 | 'lancet', 9 | 'acm', 10 | 'jama', 11 | 'bmj', 12 | 'pnas', 13 | 'more-journals', 14 | ]; 15 | 16 | export const MOBILE_JOURNALS = ['nature', 'science', 'lancet', 'acm', 'ieee', 'cell', 'more-journal-mobile']; 17 | 18 | export const AFFILIATIONS = [ 19 | 'peking', 20 | 'noaa', 21 | 'hkpc', 22 | 'intel', 23 | 'roche', 24 | 'oxford', 25 | 'harvard', 26 | 'stanford', 27 | 'california', 28 | 'google', 29 | 'tokyo', 30 | 'cambridge', 31 | 'ncgm', 32 | ]; 33 | -------------------------------------------------------------------------------- /app/components/common/spinner/buttonSpinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import CircularProgress from '@material-ui/core/CircularProgress'; 3 | 4 | interface ButtonSpinnerProps { 5 | className?: string; 6 | size?: number; 7 | thickness?: number; 8 | color?: string; 9 | } 10 | 11 | // WARNING: DEPRECATED 12 | const ButtonSpinner = ({ size = 13.5, thickness = 2, className, color }: ButtonSpinnerProps) => { 13 | return ( 14 |
15 | 16 |
17 | ); 18 | }; 19 | 20 | export default ButtonSpinner; 21 | -------------------------------------------------------------------------------- /app/__tests__/mockStore.tsx: -------------------------------------------------------------------------------- 1 | import thunk, { ThunkDispatch } from 'redux-thunk'; 2 | import configureMockStore, { MockStoreEnhanced } from 'redux-mock-store'; 3 | import { AppState, initialState } from '../reducers'; 4 | import { AnyAction } from 'redux'; 5 | 6 | export type EnhancedMockStore = MockStoreEnhanced>; 7 | 8 | export const generateMockStore = (state: AppState = initialState) => { 9 | const mockStore = configureMockStore>([thunk]); 10 | const store = mockStore(state); 11 | 12 | store.clearActions(); 13 | return store; 14 | }; 15 | -------------------------------------------------------------------------------- /app/components/dialog/types/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from 'react-router-dom'; 2 | import { Dispatch } from 'redux'; 3 | import { DialogState } from '../reducer'; 4 | import { CurrentUser } from '../../../model/currentUser'; 5 | import { Collection } from '../../../model/collection'; 6 | import { LayoutState } from '../../layouts/reducer'; 7 | 8 | export interface DialogContainerProps 9 | extends Readonly<{ 10 | layout: LayoutState; 11 | dialogState: DialogState; 12 | myCollections: Collection[]; 13 | currentUser: CurrentUser; 14 | dispatch: Dispatch; 15 | }>, 16 | RouteComponentProps {} 17 | -------------------------------------------------------------------------------- /app/reducers/signUpModal.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export interface SignUpModalState { 4 | email: string; 5 | } 6 | 7 | export const SIGN_UP_MODAL_INITIAL_STATE = { email: '' }; 8 | 9 | const signUpModalSlice = createSlice({ 10 | name: 'signUpModal', 11 | initialState: SIGN_UP_MODAL_INITIAL_STATE, 12 | reducers: { 13 | setSignUpModalEmail(state, action: PayloadAction<{ email: string }>) { 14 | state.email = action.payload.email; 15 | }, 16 | }, 17 | }); 18 | 19 | export const { setSignUpModalEmail } = signUpModalSlice.actions; 20 | 21 | export default signUpModalSlice.reducer; 22 | -------------------------------------------------------------------------------- /app/hooks/useIntervalProgressHook.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function useIntervalProgress(callback: () => void, delay: number | null) { 4 | const savedCallback = React.useRef(() => {}); 5 | 6 | React.useEffect( 7 | () => { 8 | savedCallback.current = callback; 9 | }, 10 | [callback] 11 | ); 12 | 13 | React.useEffect( 14 | () => { 15 | function tick() { 16 | savedCallback.current(); 17 | } 18 | 19 | if (delay !== null) { 20 | const timer = setInterval(tick, delay); 21 | return () => clearInterval(timer); 22 | } 23 | }, 24 | [delay] 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/__tests__/preload.tsx: -------------------------------------------------------------------------------- 1 | window.alert = msg => { 2 | console.log(msg); 3 | }; 4 | (window as any).matchMedia = () => ({}); 5 | window.scrollTo = () => {}; 6 | 7 | const localStorageMock = (function() { 8 | let store: any = {}; 9 | return { 10 | getItem(key: string) { 11 | return store[key] || null; 12 | }, 13 | setItem(key: string, value: any) { 14 | store[key] = value.toString(); 15 | }, 16 | removeItem(key: string) { 17 | delete store[key]; 18 | }, 19 | clear() { 20 | store = {}; 21 | }, 22 | }; 23 | })(); 24 | 25 | Object.defineProperty(window, 'localStorage', { 26 | value: localStorageMock, 27 | }); 28 | -------------------------------------------------------------------------------- /app/components/emailSettings/emailToggleTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const useStyles = require('isomorphic-style-loader/useStyles'); 3 | const s = require('./emailToggleTitle.scss'); 4 | 5 | interface EmailToggleTitleProps { 6 | title: string; 7 | subtitle: string; 8 | } 9 | 10 | const EmailToggleTitle: React.FC = ({ title, subtitle }) => { 11 | useStyles(s); 12 | 13 | return ( 14 |
15 |
{title}
16 |
{subtitle}
17 |
18 | ); 19 | }; 20 | 21 | export default EmailToggleTitle; 22 | -------------------------------------------------------------------------------- /app/helpers/formatNumber.tsx: -------------------------------------------------------------------------------- 1 | export default function formatNumber(rawNumber: number | undefined | null): string { 2 | if (!rawNumber) { 3 | return '0'; 4 | } 5 | 6 | if (rawNumber < 10000) { 7 | return rawNumber.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); 8 | } else if (rawNumber >= 10000 && rawNumber < 100000) { 9 | return `${Math.round(rawNumber / 100) / 10}k`; 10 | } else if (rawNumber >= 100000 && rawNumber < 1000000) { 11 | return `${Math.round(rawNumber / 1000)}k`; 12 | } else if (rawNumber >= 1000000) { 13 | return `${Math.round(rawNumber / 100000) / 10}m`; 14 | } else { 15 | return rawNumber.toString(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/helpers/scrollRestoration.tsx: -------------------------------------------------------------------------------- 1 | import EnvChecker from './envChecker'; 2 | import { HistoryInformation, HISTORY_SESSION_KEY } from '../components/locationListener'; 3 | 4 | export default function restoreScroll(locationKey?: string) { 5 | if (!EnvChecker.isOnServer()) { 6 | const histories: HistoryInformation[] = JSON.parse(window.sessionStorage.getItem(HISTORY_SESSION_KEY) || '[]'); 7 | const targetHistory = histories.find(h => h.key === locationKey || (h.key === 'initial' && !locationKey)); 8 | 9 | if (targetHistory) { 10 | window.scrollTo(0, targetHistory.scrollPosition); 11 | } else { 12 | window.scrollTo(0, 0); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/icons/mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/api/getHost.ts: -------------------------------------------------------------------------------- 1 | import EnvChecker from '../helpers/envChecker'; 2 | 3 | const CLIENT_API_PREFIX = '/api'; // This API HOST is used for a REAL service. 4 | const PROD_SERVER_API_HOST = 'https://api.scinapse.io'; 5 | const STAGE_SERVER_API_HOST = 'https://stage-api.scinapse.io:8443'; 6 | const DEV_SERVER_API_HOST = 'https://dev.scinapse.io'; 7 | 8 | export default function getAPIPrefix() { 9 | if (EnvChecker.isOnServer()) { 10 | if (process.env.NODE_ENV === 'production') return PROD_SERVER_API_HOST; 11 | if (process.env.NODE_ENV === 'development') return DEV_SERVER_API_HOST; 12 | return STAGE_SERVER_API_HOST; 13 | } 14 | 15 | return CLIENT_API_PREFIX; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/common/paperItem/__tests__/__snapshots__/blockVenue_spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BlockVenue Component when paper is from conference should render correctly 1`] = ` 4 |
7 | 10 | JOURNAL 11 | 12 | 15 | 18 | May 1, 2016 19 | 20 | 23 | in 24 | IEEE Symposium on Security and Privacy 25 | 26 | 27 |
28 | `; 29 | -------------------------------------------------------------------------------- /app/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/minus 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/components/common/paperItem/abstract.scss: -------------------------------------------------------------------------------- 1 | .abstract { 2 | display: block; 3 | margin-top: 8px; 4 | font-size: 14px; 5 | line-height: 1.4; 6 | color: $gray800; 7 | word-break: break-word; 8 | overflow-wrap: break-word; 9 | 10 | .searchQuery { 11 | font-weight: 900; 12 | color: $black1; 13 | } 14 | } 15 | 16 | .moreOrLess { 17 | color: $navy0; 18 | font-weight: 500; 19 | margin-left: 4px; 20 | 21 | &:hover { 22 | cursor: pointer; 23 | text-decoration: underline; 24 | } 25 | } 26 | 27 | // Mobile 28 | @media (max-width: $mobile_width) { 29 | .abstract { 30 | margin-top: 12px; 31 | font-size: 14px; 32 | color: $black0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/icons/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/components/paperShow/components/searchingPDFBtn.scss: -------------------------------------------------------------------------------- 1 | .loadingBtnStyle { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | padding: 10px 16px 10px 12px; 6 | width: 100%; 7 | height: 40px; 8 | border-radius: 4px; 9 | font-size: 14px; 10 | font-weight: bold; 11 | color: white; 12 | background-color: $gray500; 13 | 14 | &:disabled { 15 | cursor: not-allowed; 16 | } 17 | 18 | &:hover { 19 | background-color: $gray400; 20 | } 21 | } 22 | 23 | .spinnerWrapper { 24 | display: flex; 25 | height: 100%; 26 | align-items: center; 27 | justify-content: center; 28 | margin-right: 8px; 29 | 30 | .loadingSpinner { 31 | color: white; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/model/conferenceInstance.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | import { ConferenceSeries } from './conferenceSeries'; 3 | 4 | export interface ConferenceInstance { 5 | id: string; 6 | conferenceSeries: ConferenceSeries | null; 7 | name: string; 8 | location: string | null; 9 | officialUrl: string | null; 10 | startDate: string | null; 11 | endDate: string | null; 12 | abstractRegistrationDate: string | null; 13 | submissionDeadlineDate: string | null; 14 | notificationDueDate: string | null; 15 | finalVersionDueDate: string | null; 16 | paperCount: number; 17 | citationCount: number; 18 | } 19 | 20 | export const conferenceInstanceSchema = new schema.Entity('conferenceInstances'); 21 | -------------------------------------------------------------------------------- /app/store/serverStore.tsx: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios'; 2 | import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; 3 | import { rootReducer, initialState } from '../reducers'; 4 | 5 | interface ThunkExtraArgument { 6 | axios: AxiosInstance; 7 | } 8 | 9 | export default class ServerStoreManager { 10 | public static getStore(extraArgument: ThunkExtraArgument) { 11 | return configureStore({ 12 | reducer: rootReducer, 13 | devTools: false, 14 | preloadedState: initialState, 15 | middleware: [ 16 | ...getDefaultMiddleware({ 17 | thunk: { 18 | extraArgument, 19 | }, 20 | }), 21 | ], 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/containers/authorShow/sideEffect.tsx: -------------------------------------------------------------------------------- 1 | import { LoadDataParams } from '../../routes'; 2 | import { AuthorShowMatchParams } from '.'; 3 | import { fetchAuthorShowRelevantData } from '../../actions/author'; 4 | import { ActionCreators } from '../../actions/actionTypes'; 5 | 6 | export async function fetchAuthorShowPageData(params: LoadDataParams) { 7 | const { dispatch, match } = params; 8 | const authorId = match.params.authorId; 9 | 10 | if (isNaN(parseInt(authorId, 10))) { 11 | return dispatch(ActionCreators.failedToLoadAuthorShowPageData({ statusCode: 400 })); 12 | } 13 | 14 | await dispatch( 15 | fetchAuthorShowRelevantData({ 16 | authorId, 17 | }) 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/api/author/types.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from 'axios'; 2 | import { PageObjectV2 } from '../types/common'; 3 | import { Paper } from '../../model/paper'; 4 | import { AUTHOR_PAPER_LIST_SORT_TYPES } from '../../components/common/sortBox'; 5 | 6 | export interface GetAuthorPapersParams { 7 | authorId: string; 8 | page: number; 9 | sort: AUTHOR_PAPER_LIST_SORT_TYPES; 10 | cancelToken?: CancelToken; 11 | query?: string; 12 | size?: number; 13 | } 14 | 15 | export interface AuthorPapersResponse { 16 | content: Paper[]; 17 | page: PageObjectV2; 18 | } 19 | 20 | export interface GetAuthorPaperResult extends PageObjectV2 { 21 | entities: { papers: { [paperId: string]: Paper } }; 22 | result: string[]; 23 | } 24 | -------------------------------------------------------------------------------- /jest.e2e.config.js: -------------------------------------------------------------------------------- 1 | const { defaults: tsjPreset } = require('ts-jest/presets'); 2 | 3 | module.exports = { 4 | preset: 'jest-puppeteer', 5 | testURL: 'https://scinapse.io/', 6 | setupFilesAfterEnv: ['/jest/jestReporter.js', '/e2eTest/jest.setup.js'], 7 | verbose: true, 8 | rootDir: '', 9 | transform: { 10 | ...tsjPreset.transform, 11 | }, 12 | moduleFileExtensions: ['js', 'ts', 'tsx'], 13 | coveragePathIgnorePatterns: ['/node_modules/'], 14 | watchPathIgnorePatterns: ['/node_modules/'], 15 | coverageDirectory: 'output/e2e', 16 | testMatch: null, 17 | testRegex: '.*_spec.ts$', 18 | roots: ['/e2eTest/'], 19 | transformIgnorePatterns: ['/node_modules/'], 20 | }; 21 | -------------------------------------------------------------------------------- /app/components/termsOfService/termsOfService.scss: -------------------------------------------------------------------------------- 1 | .termsOfServiceContainer { 2 | max-width: $small_container_width; 3 | margin: $navbar_height auto 40px auto; 4 | text-align: left; 5 | padding-top: 56px; 6 | } 7 | 8 | .title { 9 | color: $black0; 10 | font-size: 24px; 11 | font-weight: 600; 12 | margin-bottom: 32px; 13 | } 14 | 15 | .subtitle { 16 | color: $black0; 17 | font-size: 20px; 18 | font-weight: 600; 19 | margin-bottom: 16px; 20 | } 21 | 22 | .contents { 23 | color: $gray800; 24 | font-size: 14px; 25 | line-height: 20px; 26 | margin-bottom: 40px; 27 | } 28 | 29 | .bulletPoint { 30 | color: $gray800; 31 | font-size: 14px; 32 | line-height: 24px; 33 | margin: -24px 0px 16px 0px; 34 | } 35 | -------------------------------------------------------------------------------- /server/helpers/setABTest.tsx: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { getRandomUserGroup, LIVE_TESTS } from '../../app/constants/abTest'; 3 | 4 | export default function setABTestCookie(req: express.Request, res: express.Response) { 5 | if (req.cookies) { 6 | const keys = Object.keys(req.cookies); 7 | 8 | LIVE_TESTS.forEach(test => { 9 | if (!keys.includes(test.name)) { 10 | const randomUserGroup = getRandomUserGroup(test.name); 11 | res.cookie(test.name, randomUserGroup, { 12 | maxAge: 2592000000, 13 | }); 14 | } else { 15 | res.cookie(test.name, req.cookies[test.name], { 16 | maxAge: 2592000000, 17 | }); 18 | } 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/helpers/exportCitationText.tsx: -------------------------------------------------------------------------------- 1 | import getAPIPrefix from '../api/getHost'; 2 | import { AvailableExportCitationType } from '../containers/paperShow/records'; 3 | import axios from 'axios'; 4 | 5 | export async function exportCitationText(type: AvailableExportCitationType, selectedPaperIds: string[]) { 6 | const paperIds = selectedPaperIds.join(','); 7 | const enumValue = AvailableExportCitationType[type]; 8 | 9 | const exportUrl = getAPIPrefix() + `/citations/export?pids=${paperIds}&format=${enumValue}`; 10 | 11 | await axios 12 | .get(exportUrl) 13 | .then(() => { 14 | window.location.href = exportUrl; 15 | }) 16 | .catch(() => { 17 | window.alert('Selected papers can not export citation.'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /app/containers/profile/sideEffects.tsx: -------------------------------------------------------------------------------- 1 | import { LoadDataParams } from '../../routes'; 2 | import { 3 | fetchProfileData, 4 | fetchProfilePapers, 5 | fetchProfilePendingPapers, 6 | fetchRepresentativePapers, 7 | } from '../../actions/profile'; 8 | 9 | export async function fetchAuthorShowPageData(params: LoadDataParams<{ profileSlug: string }>) { 10 | const { dispatch, match, queryParams } = params; 11 | const profileSlug = match.params.profileSlug; 12 | 13 | await Promise.all([ 14 | dispatch(fetchProfileData(profileSlug)), 15 | dispatch(fetchProfilePendingPapers(profileSlug)), 16 | dispatch(fetchRepresentativePapers({ profileSlug })), 17 | dispatch(fetchProfilePapers({ profileSlug, page: queryParams?.page || 0 })), 18 | ]); 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "allowSyntheticDefaultImports": true, 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "allowUnusedLabels": false, 8 | "allowJs": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "target": "es2015", 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "jsx": "react", 15 | "lib": ["es6", "es2016", "dom", "esnext.asynciterable"], 16 | "removeComments": true, 17 | "experimentalDecorators": true, 18 | "strictNullChecks": true, 19 | "typeRoots": ["./node_modules/@types", "./typings"] 20 | }, 21 | "include": ["app/**/*", "e2eTest/**/*"], 22 | "exclude": ["node_modules/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /app/components/auth/authTabs/authTabs.scss: -------------------------------------------------------------------------------- 1 | .authTab { 2 | width: 100%; 3 | display: flex; 4 | text-align: center; 5 | justify-content: space-evenly; 6 | 7 | .authTabItem { 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | height: 73px; 12 | border-radius: 2px; 13 | font-size: 16px; 14 | font-weight: 500; 15 | letter-spacing: 2px; 16 | cursor: pointer; 17 | background-color: $gray200; 18 | color: $gray400; 19 | width: 100%; 20 | 21 | &.active { 22 | background-color: white; 23 | color: $main_blue0; 24 | } 25 | } 26 | } 27 | 28 | @media (max-width: $mobile_width) { 29 | .authTab { 30 | .authTabItem { 31 | height: 52px; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/components/common/InputWithSuggestionList/types.tsx: -------------------------------------------------------------------------------- 1 | import { RouteComponentProps } from 'react-router-dom'; 2 | import { FilterObject } from '../../../helpers/searchQueryManager'; 3 | 4 | export type SearchQueryInputProps = React.InputHTMLAttributes & 5 | RouteComponentProps & { 6 | actionArea: 'home' | 'topBar' | 'paperShow'; 7 | maxCount: number; 8 | currentFilter?: FilterObject; 9 | wrapperClassName?: string; 10 | listWrapperClassName?: string; 11 | inputClassName?: string; 12 | sort?: Scinapse.ArticleSearch.SEARCH_SORT_OPTIONS; 13 | }; 14 | 15 | export type SearchSourceType = 'history' | 'suggestion' | 'raw'; 16 | 17 | export interface SubmitParams { 18 | from: SearchSourceType; 19 | query: string; 20 | } 21 | -------------------------------------------------------------------------------- /app/model/aggregation.tsx: -------------------------------------------------------------------------------- 1 | export interface AggregationJournal { 2 | id: string; 3 | title: string; 4 | docCount: number; 5 | impactFactor: number; 6 | abbrev: string | null; 7 | sci: boolean; 8 | jc: 'JOURNAL' | 'CONFERENCE'; 9 | // added by client 10 | missingDocCount?: boolean; 11 | } 12 | 13 | export interface AggregationFos { 14 | id: string; 15 | name: string; 16 | level: number; 17 | docCount: number; 18 | // added by client 19 | missingDocCount?: boolean; 20 | } 21 | 22 | export interface Year { 23 | year: number; 24 | docCount: number; 25 | } 26 | 27 | export interface AggregationData { 28 | fosList: AggregationFos[]; 29 | journals: AggregationJournal[]; 30 | yearAll: Year[] | null; 31 | yearFiltered: Year[] | null; 32 | } 33 | -------------------------------------------------------------------------------- /app/helpers/userAgentHelper.ts: -------------------------------------------------------------------------------- 1 | import { UAParser } from 'ua-parser-js'; 2 | import EnvChecker from './envChecker'; 3 | 4 | class UserAgentHelper { 5 | private parser: any; 6 | 7 | public constructor(userAgent: string) { 8 | this.parser = new UAParser(userAgent); 9 | } 10 | 11 | public getBrowser() { 12 | if (this.parser) { 13 | return this.parser.getBrowser(); 14 | } 15 | } 16 | 17 | public getDevice() { 18 | if (this.parser) { 19 | return this.parser.getDevice(); 20 | } 21 | } 22 | } 23 | 24 | let userAgent = ''; 25 | if (!EnvChecker.isOnServer()) { 26 | if (navigator) { 27 | userAgent = navigator.userAgent; 28 | } 29 | } 30 | 31 | const userAgentHelper = new UserAgentHelper(userAgent); 32 | 33 | export default userAgentHelper; 34 | -------------------------------------------------------------------------------- /app/components/common/paperItem/mobileVenueAuthors.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin-top: 12px; 3 | } 4 | 5 | .authorBox { 6 | margin-top: 2px; 7 | position: relative; 8 | display: block; 9 | } 10 | 11 | .arrowIcon { 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | width: 32px; 16 | height: 32px; 17 | color: $navy0; 18 | padding-left: 16px; 19 | padding-top: 16px; 20 | cursor: pointer; 21 | 22 | svg { 23 | width: 14px; 24 | height: 14px; 25 | } 26 | 27 | &.isExpanded { 28 | svg { 29 | transform: rotate(180deg); 30 | } 31 | } 32 | 33 | &:active { 34 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 35 | } 36 | } 37 | 38 | .arrowIcon { 39 | position: absolute; 40 | right: 0; 41 | bottom: 0; 42 | } 43 | -------------------------------------------------------------------------------- /app/components/profileVerifyEmail/profileVerifyEmail.scss: -------------------------------------------------------------------------------- 1 | $card-container-max-width: 768px; 2 | 3 | .container { 4 | background-color: $gray30; 5 | min-height: calc(100vh - #{$navbar_height}); 6 | padding-top: $navbar_height; 7 | } 8 | 9 | .wrapper { 10 | display: flex; 11 | align-items: center; 12 | background-color: white; 13 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 14 | width: 100%; 15 | max-width: 578px; 16 | min-height: calc(100vh - #{$navbar_height}); 17 | margin: 0 auto; 18 | } 19 | 20 | .cardContainer { 21 | max-width: $card-container-max-width; 22 | padding: 24px 32px; 23 | } 24 | 25 | .title { 26 | color: $black0; 27 | font-size: 40px; 28 | margin-top: 0; 29 | margin-bottom: 32px; 30 | line-height: 1.5; 31 | text-align: center; 32 | } 33 | -------------------------------------------------------------------------------- /app/components/common/roundImageIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withStyles } from '../../../helpers/withStylesHelper'; 3 | const styles = require('./roundImage.scss'); 4 | 5 | export interface RoundImageProps 6 | extends React.DetailedHTMLProps, HTMLImageElement> { 7 | width: number | string; 8 | height: number | string; 9 | } 10 | 11 | const RoundImage = ({ width, height }: RoundImageProps) => { 12 | return ( 13 |
14 | 21 |
22 | ); 23 | }; 24 | 25 | export default withStyles(styles)(RoundImage); 26 | -------------------------------------------------------------------------------- /app/containers/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const useStyles = require('isomorphic-style-loader/useStyles'); 5 | const s = require('./admin.scss'); 6 | 7 | const DesignPage: FC = () => { 8 | useStyles(s); 9 | 10 | return ( 11 |
12 |

PLUTO DESIGN SYSTEM 🚀

13 | 14 | Button Demo Page 15 | 16 | 17 | Button Builder Page 18 | 19 | 20 | Paper Item Demo Page 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default DesignPage; 27 | -------------------------------------------------------------------------------- /app/constants/actionTicket.tsx: -------------------------------------------------------------------------------- 1 | export const MAXIMUM_TICKET_COUNT_IN_QUEUE = 5; 2 | export const TIME_INTERVAL_TO_SEND_TICKETS = 1000 * 5; 3 | export const DEVICE_ID_KEY = 'd_id'; 4 | export const DEVICE_ID_INITIALIZED_KEY = 'd_id_i'; 5 | export const SESSION_ID_KEY = 's_id'; 6 | export const SESSION_ID_INITIALIZED_KEY = 's_id_i'; 7 | export const SESSION_COUNT_KEY = 's_c'; 8 | export const USER_ID_KEY = 'u_id'; 9 | export const TICKET_QUEUE_KEY = 'a_q'; 10 | export const DEAD_LETTER_QUEUE_KEY = 'd_a_q'; 11 | export const LIVE_SESSION_LENGTH = 1000 * 60 * 30; 12 | export const MAXIMUM_RETRY_COUNT = 3; 13 | export const DESTINATION_URL = 'https://scinapse-logger.azurewebsites.net/api/sclogprod'; 14 | export const AWS_DESTINATION_URL = 'https://1cgir0gy5d.execute-api.us-east-1.amazonaws.com/prod/actionticket'; 15 | -------------------------------------------------------------------------------- /app/components/privacyPolicy/privacyPolicy.scss: -------------------------------------------------------------------------------- 1 | .termsOfServiceContainer { 2 | max-width: $small_container_width; 3 | margin: $navbar_height auto 40px auto; 4 | text-align: left; 5 | padding-top: 56px; 6 | } 7 | 8 | .title { 9 | color: $black0; 10 | font-size: 24px; 11 | font-weight: 600; 12 | margin-bottom: 32px; 13 | } 14 | 15 | .subtitle { 16 | color: $black0; 17 | font-size: 20px; 18 | font-weight: 600; 19 | margin-bottom: 16px; 20 | margin-top: 24px; 21 | } 22 | 23 | .contents { 24 | color: $gray800; 25 | font-size: 14px; 26 | line-height: 20px; 27 | margin-bottom: 20px; 28 | 29 | b { 30 | font-size: 18px; 31 | } 32 | } 33 | 34 | .bulletPoint { 35 | color: $gray800; 36 | font-size: 14px; 37 | line-height: 24px; 38 | margin: -14px 0px 16px 0px; 39 | } 40 | -------------------------------------------------------------------------------- /app/icons/feedback-label.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/components/emailSettings/emailSettings.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 24px; 3 | font-weight: 600; 4 | line-height: 1.83; 5 | letter-spacing: -0.67px; 6 | color: $black0; 7 | margin-bottom: 46px; 8 | } 9 | 10 | .divider { 11 | width: 100%; 12 | height: 1px; 13 | background-color: $gray300; 14 | margin: 40px 0 32px 0; 15 | } 16 | 17 | .toggleItemWrapper { 18 | display: flex; 19 | justify-content: space-between; 20 | 21 | & + & { 22 | margin-top: 40px; 23 | } 24 | } 25 | 26 | .toggleButtonWrapper { 27 | display: flex; 28 | align-items: center; 29 | width: 100px; 30 | } 31 | 32 | @media (max-width: $mobile_width) { 33 | .toggleItemWrapper { 34 | flex-direction: column; 35 | } 36 | 37 | .toggleButtonWrapper { 38 | margin-top: 24px; 39 | width: 100%; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/helpers/getPDFLink.tsx: -------------------------------------------------------------------------------- 1 | import { PaperSource } from '../model/paperSource'; 2 | 3 | export function isPDFLink(source: PaperSource) { 4 | if (source.isPdf) return true; 5 | 6 | // TODO: Remove below logic after done checking pdf sources 7 | const url = source.url; 8 | return ( 9 | url.startsWith('https://arxiv.org/pdf/') || 10 | (url.startsWith('http') && url.endsWith('.pdf') && !url.includes('springer')) 11 | ); 12 | } 13 | 14 | export function getPDFLink(urls: PaperSource[]): PaperSource | undefined { 15 | const pdfLink = urls.find(url => url.isPdf); 16 | 17 | if (pdfLink) { 18 | return pdfLink; 19 | } 20 | 21 | // TODO: Remove below logic after done checking pdf sources 22 | return urls.find((paperSource: PaperSource) => { 23 | return isPDFLink(paperSource); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /app/icons/matched-paper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Match! 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/reducers/profileEntity.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { Profile } from '../model/profile'; 3 | 4 | export interface ProfileEntities { 5 | [profileSlug: string]: Profile; 6 | } 7 | export const PROFILE_ENTITIES_INITIAL_STATE: ProfileEntities = {}; 8 | 9 | const profileEntitiesSlice = createSlice({ 10 | name: 'profileEntitiesSlice', 11 | initialState: PROFILE_ENTITIES_INITIAL_STATE, 12 | reducers: { 13 | addProfileEntities(state, action: PayloadAction<{ profileEntities: ProfileEntities }>) { 14 | return { 15 | ...state, 16 | ...action.payload.profileEntities, 17 | }; 18 | }, 19 | }, 20 | }); 21 | 22 | export const { addProfileEntities } = profileEntitiesSlice.actions; 23 | 24 | export default profileEntitiesSlice.reducer; 25 | -------------------------------------------------------------------------------- /app/model/currentUser.tsx: -------------------------------------------------------------------------------- 1 | import { Member } from './member'; 2 | import { Institute } from './Institute'; 3 | 4 | export interface CurrentUser 5 | extends Member, 6 | Readonly<{ 7 | isLoggedIn: boolean; 8 | oauthLoggedIn: boolean; 9 | isLoggingIn: boolean; 10 | ipInstitute: Institute | null; 11 | }> {} 12 | 13 | export const CURRENT_USER_INITIAL_STATE: CurrentUser = { 14 | isLoggedIn: false, 15 | isLoggingIn: false, 16 | oauthLoggedIn: false, 17 | email: '', 18 | firstName: '', 19 | lastName: '', 20 | id: 0, 21 | profileSlug: null, 22 | profileImageUrl: '', 23 | affiliationId: null, 24 | affiliationName: null, 25 | emailVerified: false, 26 | oauth: null, 27 | isAuthorConnected: false, 28 | authorId: '', 29 | profileLink: '', 30 | ipInstitute: null, 31 | }; 32 | -------------------------------------------------------------------------------- /app/components/emailSettings/emailToggleButton.scss: -------------------------------------------------------------------------------- 1 | .loadingSpinnerWrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | margin-right: -24px; 6 | } 7 | 8 | .toggleButtonWrapper { 9 | display: flex; 10 | align-items: center; 11 | width: 100px; 12 | } 13 | 14 | .blockedToggleButtonWrapper { 15 | @extend .toggleButtonWrapper; 16 | 17 | opacity: 0.8; 18 | button { 19 | cursor: not-allowed !important; 20 | } 21 | } 22 | 23 | .loadingSpinner { 24 | color: $gray400 !important; 25 | margin-right: 8px; 26 | } 27 | 28 | .buttonsWrapper { 29 | width: 100%; 30 | } 31 | 32 | @media (max-width: $mobile_width) { 33 | .toggleButtonWrapper { 34 | margin-top: 24px; 35 | width: 100%; 36 | } 37 | 38 | .loadingSpinnerWrapper { 39 | display: none; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/deploy/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.S3_CLIENT_OPTIONS = { 4 | maxAsyncS3: 20, 5 | s3RetryCount: 3, 6 | s3RetryDelay: 1000, 7 | multipartUploadThreshold: 20971520, 8 | multipartUploadSize: 15728640, 9 | s3Options: { 10 | region: "us-east-1" 11 | } 12 | }; 13 | // NOTE: Change below options following your environment 14 | // Local defined Constants 15 | exports.DEPLOY_VERSION = process.env.DEPLOY_VERSION; 16 | exports.PRODUCTION_GIT_TAG = "production"; 17 | exports.AWS_S3_BUCKET = "pluto-web-client"; 18 | exports.APP_DEST = "./dist/"; 19 | exports.AWS_S3_PRODUCTION_FOLDER_PREFIX = "production"; 20 | exports.AWS_S3_DEV_FOLDER_PREFIX = "dev"; 21 | exports.VERSION_FILE_NAME = "version"; 22 | exports.CDN_BASE_HOST = "https://search-bundle.pluto.network"; 23 | -------------------------------------------------------------------------------- /app/components/collectionPaperItem/collectionPaperItem.scss: -------------------------------------------------------------------------------- 1 | .paperItemWrapper { 2 | display: flex; 3 | 4 | & + & { 5 | margin-top: 16px; 6 | } 7 | } 8 | 9 | .itemWrapper { 10 | @extend %default-paper-item-wrapper; 11 | position: relative; 12 | width: 100%; 13 | 14 | .removeIcon { 15 | position: absolute; 16 | right: 16px; 17 | top: 16px; 18 | width: 20px; 19 | height: 20px; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | cursor: pointer; 24 | color: $gray600; 25 | 26 | svg { 27 | width: 12px; 28 | height: 12px; 29 | } 30 | } 31 | } 32 | 33 | .paperCheckBox { 34 | margin: 8px 16px 0 0; 35 | } 36 | 37 | @media (max-width: $mobile_width) { 38 | .itemWrapper { 39 | width: 100%; 40 | padding: 16px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/components/common/spinner/articleSpinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withStyles } from '../../../helpers/withStylesHelper'; 3 | const styles = require('./articleSpinner.scss'); 4 | 5 | interface ArticleSpinnerProps { 6 | className?: string; 7 | style?: React.CSSProperties; 8 | } 9 | 10 | const ArticleSpinner = (props: ArticleSpinnerProps) => { 11 | let className = styles.spinner; 12 | if (props.className) { 13 | className = `${className} ${props.className}`; 14 | } 15 | 16 | return ( 17 |
18 |
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default withStyles(styles)(ArticleSpinner); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "allowSyntheticDefaultImports": true, 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "allowUnusedLabels": false, 8 | "allowJs": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "jsx": "preserve", 15 | "lib": ["es6", "es2016", "dom", "esnext.asynciterable"], 16 | "removeComments": false, 17 | "skipLibCheck": true, 18 | "experimentalDecorators": true, 19 | "strictNullChecks": true, 20 | "typeRoots": ["./node_modules/@types", "./typings"] 21 | }, 22 | "include": ["app/**/*", "e2eTest/**/*", "localServer/**/*", "server/**/*", "./typings"], 23 | "exclude": ["node_modules/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /app/icons/impact-factor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/impact-factor 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/selectors/papersSelector.tsx: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { denormalize } from 'normalizr'; 3 | import { AppState } from '../reducers'; 4 | import { paperSchema } from '../model/paper'; 5 | 6 | export function getPaperEntities(state: AppState) { 7 | return state.entities.papers; 8 | } 9 | 10 | export const getMemoizedReferencePaperIds = (state: AppState) => { 11 | return state.paperShow.referencePaperIds; 12 | }; 13 | 14 | export const getMemoizedCitedPaperIds = (state: AppState) => { 15 | return state.paperShow.citedPaperIds; 16 | }; 17 | 18 | export const makeGetMemoizedPapers = (getPaperIds: (state: AppState) => string[]) => { 19 | return createSelector([getPaperIds, getPaperEntities], (paperIds, paperEntities) => { 20 | return denormalize(paperIds, [paperSchema], { papers: paperEntities }); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /app/components/collectionShow/collectionPapersControlBtns.scss: -------------------------------------------------------------------------------- 1 | .collectionControlBtnsWrapper { 2 | display: flex; 3 | align-items: center; 4 | 5 | .allCheckBox { 6 | margin-right: 16px; 7 | } 8 | } 9 | 10 | .collectionControlBtnsDivider { 11 | width: 100%; 12 | height: 1px; 13 | background-color: $gray400; 14 | margin-top: 8px; 15 | } 16 | 17 | .citationExportDropdownMenu { 18 | z-index: 2; 19 | border-radius: 4px; 20 | box-shadow: 0 5px 17px 0 rgba(0, 0, 0, 0.15); 21 | background-color: white; 22 | } 23 | 24 | .menuItem { 25 | width: 144px; 26 | height: 40px; 27 | font-size: 16px; 28 | line-height: 1.5; 29 | color: $black1; 30 | padding: 10px 0 10px 16px; 31 | cursor: pointer; 32 | 33 | & + & { 34 | border-top: 1px solid $gray200; 35 | } 36 | 37 | &:hover { 38 | color: $main_blue0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/components/common/paperItem/searchPaperItem.scss: -------------------------------------------------------------------------------- 1 | .paperItemWrapper { 2 | @extend %default-paper-item-wrapper; 3 | } 4 | 5 | .venueAndAuthorWrapper { 6 | margin-top: 8px; 7 | } 8 | 9 | .visitedHistory { 10 | padding-bottom: 12px; 11 | border-bottom: 1px solid $gray200; 12 | margin-bottom: 12px; 13 | color: $gray700; 14 | font-size: 14px; 15 | } 16 | 17 | .missingWordsWrapper { 18 | color: $black0; 19 | margin-top: 8px; 20 | font-size: 12px; 21 | } 22 | 23 | .missingWord { 24 | text-decoration: line-through; 25 | font-weight: 500; 26 | } 27 | 28 | // Mobile 29 | @media (max-width: $mobile_width) { 30 | .visitedHistory { 31 | width: 100%; 32 | padding-bottom: 8px; 33 | margin-top: -4px; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | white-space: nowrap; 37 | border-bottom: 1px solid #f8f9fb; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/helpers/highlightContent/__tests__/index_spec.tsx: -------------------------------------------------------------------------------- 1 | jest.unmock('..'); 2 | 3 | import { STOP_WORDS, getWordsArraySplitBySpaceWithoutStopWords } from '..'; 4 | 5 | describe('SearchQueryHighlightedContent Component', () => { 6 | const mockSearchQuery = 'Principles superconductive of devices circuits Tools'; 7 | 8 | describe('getWordsArraySplitBySpaceWithoutStopWords function', () => { 9 | let result: string[]; 10 | 11 | beforeEach(() => { 12 | result = getWordsArraySplitBySpaceWithoutStopWords(mockSearchQuery); 13 | }); 14 | 15 | it('should return array of the words separated by space', () => { 16 | expect(typeof result).toEqual('object'); 17 | }); 18 | 19 | it('should not return element included in STOP_WORD', () => { 20 | expect(result.every(word => !STOP_WORDS.includes(word))).toBeTruthy(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | let babelPresetEnv = [ 2 | '@babel/preset-env', 3 | { 4 | targets: { 5 | browsers: ['last 2 versions'], 6 | }, 7 | modules: false, 8 | }, 9 | ]; 10 | 11 | if (process.env.TARGET === 'server') { 12 | babelPresetEnv = [ 13 | '@babel/preset-env', 14 | { 15 | useBuiltIns: 'usage', 16 | targets: { 17 | node: '12', 18 | }, 19 | exclude: ['@babel/plugin-transform-classes', 'babel-plugin-transform-classes'], 20 | modules: false, 21 | }, 22 | ]; 23 | } 24 | 25 | module.exports = { 26 | plugins: [ 27 | 'lodash', 28 | '@loadable/babel-plugin', 29 | '@babel/plugin-syntax-dynamic-import', 30 | '@babel/plugin-proposal-class-properties', 31 | '@babel/plugin-transform-runtime', 32 | ], 33 | presets: ['@babel/preset-react', babelPresetEnv], 34 | exclude: '/node_modules/', 35 | }; 36 | -------------------------------------------------------------------------------- /app/components/collectionShow/relatedPaperInCollectionShow.scss: -------------------------------------------------------------------------------- 1 | .relatedPaperContainer { 2 | border-radius: 4px; 3 | border: solid 1px $gray300; 4 | background-color: $gray30; 5 | padding: 24px; 6 | 7 | .titleContext { 8 | font-size: 20px; 9 | font-weight: 500; 10 | color: $black1; 11 | margin: 8px 0 24px 0; 12 | } 13 | } 14 | 15 | .paperItemWrapper { 16 | background-color: white; 17 | margin-bottom: 12px; 18 | } 19 | 20 | .loadingContainer { 21 | display: flex; 22 | flex-direction: column; 23 | width: 100%; 24 | padding-top: 50px; 25 | align-items: center; 26 | min-height: 120px; 27 | 28 | .loadingSpinner { 29 | div { 30 | background-color: $gray400; 31 | } 32 | } 33 | 34 | .loadingContent { 35 | margin-top: 27.5px; 36 | font-size: 15px; 37 | line-height: 2.4; 38 | color: $black0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/components/paperShow/components/searchingPDFBtn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import CircularProgress from '@material-ui/core/CircularProgress'; 3 | import { withStyles } from '../../../helpers/withStylesHelper'; 4 | const styles = require('./searchingPDFBtn.scss'); 5 | 6 | interface SearchingPDFBtnProps { 7 | isLoading: boolean; 8 | } 9 | 10 | const SearchingPDFBtn: React.FunctionComponent = props => { 11 | const { isLoading } = props; 12 | 13 | return ( 14 | 20 | ); 21 | }; 22 | 23 | export default withStyles(styles)(SearchingPDFBtn); 24 | -------------------------------------------------------------------------------- /app/api/types/paper.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from 'axios'; 2 | import { Paper } from '../../model/paper'; 3 | import { PAPER_LIST_SORT_TYPES } from '../../components/common/sortBox'; 4 | 5 | export interface SearchPapersParams { 6 | sort: string; 7 | page: number; 8 | query: string; 9 | filter: string; 10 | size?: number; 11 | cancelToken?: CancelToken; 12 | detectYear?: boolean; 13 | } 14 | 15 | export interface GetRefOrCitedPapersParams { 16 | size?: number; 17 | paperId: string; 18 | page: number; 19 | query: string; 20 | sort: PAPER_LIST_SORT_TYPES | null; 21 | } 22 | 23 | export interface GetPapersResult { 24 | papers: Paper[]; 25 | first: boolean; 26 | last: boolean; 27 | number: number; 28 | numberOfElements: number; 29 | size: number; 30 | sort: PAPER_LIST_SORT_TYPES | null; 31 | totalElements: number; 32 | totalPages: number; 33 | } 34 | -------------------------------------------------------------------------------- /app/components/alertCreateButton/alertCreateButton.scss: -------------------------------------------------------------------------------- 1 | .arrowTopTooltip { 2 | background-color: $black0 !important; 3 | color: white !important; 4 | font-size: 14px !important; 5 | margin-bottom: 6px !important; 6 | width: 235px !important; 7 | padding: 12px !important; 8 | 9 | &:after { 10 | content: ''; 11 | position: absolute; 12 | left: 70%; 13 | width: 0; 14 | height: 0; 15 | border: 12px solid transparent; 16 | border-bottom-color: $black0; 17 | border-top: 0; 18 | margin-top: -40px; 19 | z-index: 2; 20 | } 21 | } 22 | 23 | .alertIcon { 24 | svg { 25 | width: 16px; 26 | height: 16px; 27 | } 28 | } 29 | 30 | // Mobile 31 | @media (max-width: $mobile_width) { 32 | .arrowTopTooltipWrapper { 33 | display: none; 34 | } 35 | 36 | .arrowTopTooltip { 37 | &:after { 38 | left: 50%; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/containers/authorSearch/records.tsx: -------------------------------------------------------------------------------- 1 | import { Author } from '../../model/author/author'; 2 | 3 | export interface AuthorSearchState 4 | extends Readonly<{ 5 | sort: Scinapse.ArticleSearch.SEARCH_SORT_OPTIONS; 6 | isLoading: boolean; 7 | pageErrorCode: number | null; 8 | searchInput: string; 9 | page: number; 10 | numberOfElements: number; 11 | totalElements: number; 12 | totalPages: number; 13 | isFirst: boolean; 14 | isEnd: boolean; 15 | searchItemsToShow: Author[]; 16 | }> {} 17 | 18 | export const AUTHOR_SEARCH_INITIAL_STATE: AuthorSearchState = { 19 | sort: 'RELEVANCE', 20 | isLoading: false, 21 | pageErrorCode: null, 22 | searchInput: '', 23 | page: 1, 24 | numberOfElements: 0, 25 | totalElements: 0, 26 | totalPages: 0, 27 | isFirst: true, 28 | isEnd: false, 29 | searchItemsToShow: [], 30 | }; 31 | -------------------------------------------------------------------------------- /app/containers/profile/components/pendingPaperItem/pendingPaperItem.scss: -------------------------------------------------------------------------------- 1 | .pendingPaperItemWrapper { 2 | display: flex; 3 | width: 100%; 4 | padding: 16px 8px; 5 | align-items: center; 6 | 7 | & + & { 8 | border-top: 1px solid $gray300; 9 | } 10 | } 11 | 12 | .pendingPaperContentsWrapper { 13 | display: inline-block; 14 | width: 100%; 15 | margin-right: 8px; 16 | 17 | .pendingPaperItemTitle { 18 | font-size: 16px; 19 | line-height: 1.4; 20 | color: $black_light_0; 21 | font-weight: 500; 22 | } 23 | 24 | .pendingPaperItemVenueAndAuthor { 25 | font-size: 13px; 26 | line-height: 1.54; 27 | color: $gray700; 28 | } 29 | } 30 | 31 | .resolveBtnWrapper, 32 | .removeBtnWrapper { 33 | margin-left: 8px; 34 | } 35 | 36 | @media (max-width: $mobile_width) { 37 | .resolveBtnWrapper, 38 | .removeBtnWrapper { 39 | display: none; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/reducers/findInLibraryDialog.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | interface FindInLibraryDialogState { 4 | isOpen: boolean; 5 | } 6 | 7 | export const FIND_IN_LIBRARY_DIALOG_INITIAL_STATE: FindInLibraryDialogState = { 8 | isOpen: false, 9 | }; 10 | 11 | const findInLibraryDialogSlice = createSlice({ 12 | name: 'findInLibraryDialog', 13 | initialState: FIND_IN_LIBRARY_DIALOG_INITIAL_STATE, 14 | reducers: { 15 | openFindInLibraryDialog(state) { 16 | return { 17 | ...state, 18 | isOpen: true, 19 | }; 20 | }, 21 | closeFindInLibraryDialog(state) { 22 | return { 23 | ...state, 24 | isOpen: false, 25 | }; 26 | }, 27 | }, 28 | }); 29 | 30 | export const { openFindInLibraryDialog, closeFindInLibraryDialog } = findInLibraryDialogSlice.actions; 31 | 32 | export default findInLibraryDialogSlice.reducer; 33 | -------------------------------------------------------------------------------- /app/components/common/tabNavigationBar/tabNavigationBar.scss: -------------------------------------------------------------------------------- 1 | .tabItemWrapper { 2 | background-color: #ffffff; 3 | } 4 | 5 | .tabItemContainer { 6 | max-width: 1200px; 7 | display: flex; 8 | align-items: center; 9 | margin: $navbar_height auto 0 auto; 10 | padding: 0 16px; 11 | position: relative; 12 | height: 40px; 13 | z-index: 2; 14 | box-shadow: 0 2px 4px -3px rgba(0, 0, 0, 0.1); 15 | border-top: solid 1px $gray200; 16 | } 17 | 18 | .nonActiveTabItem { 19 | padding: 8px 24px; 20 | font-size: 16px; 21 | font-weight: bold; 22 | color: $gray600; 23 | line-height: 24px; 24 | 25 | &:hover { 26 | color: $gray800; 27 | } 28 | } 29 | 30 | .activeTabItem { 31 | @extend .nonActiveTabItem; 32 | box-shadow: inset 0 -4px 0 -1px $black1; 33 | color: $black1; 34 | 35 | &:hover { 36 | color: $gray800; 37 | box-shadow: inset 0 -4px 0 -1px $gray800; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/containers/profileOnboarding/components/onboardingHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import ProgressStepper from '../../../../components/common/progressStepper'; 3 | import { ONBOARDING_STEPS, CURRENT_ONBOARDING_PROGRESS_STEP } from '../../types'; 4 | 5 | const useStyles = require('isomorphic-style-loader/useStyles'); 6 | const s = require('./onboardingHeader.scss'); 7 | 8 | interface OnboardingHeaderProps { 9 | activeStep: CURRENT_ONBOARDING_PROGRESS_STEP; 10 | skipped: CURRENT_ONBOARDING_PROGRESS_STEP[]; 11 | } 12 | 13 | const OnboardingHeader: FC = ({ activeStep, skipped }) => { 14 | useStyles(s); 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | }; 22 | 23 | export default OnboardingHeader; 24 | -------------------------------------------------------------------------------- /app/model/author/author.tsx: -------------------------------------------------------------------------------- 1 | import { schema } from 'normalizr'; 2 | import { Paper, paperSchema } from '../paper'; 3 | import { Affiliation } from '../affiliation'; 4 | import { NewFOS } from '../fos'; 5 | import { Profile } from '../profile'; 6 | 7 | export interface Author { 8 | id: string; 9 | name: string; 10 | hindex: number; 11 | lastKnownAffiliation?: Affiliation; 12 | paperCount: number; 13 | citationCount: number; 14 | bio: string | null; 15 | representativePapers: Paper[]; 16 | topPapers: Paper[]; 17 | email: string; 18 | webPage: string | null; 19 | profileImageUrl: string | null; 20 | isLayered: boolean; 21 | isEmailHidden: boolean; 22 | fosList: NewFOS[]; 23 | profile: Profile | null; 24 | } 25 | 26 | export const authorSchema = new schema.Entity('authors', { 27 | representativePapers: [paperSchema], 28 | }); 29 | export const authorListSchema = [authorSchema]; 30 | -------------------------------------------------------------------------------- /app/components/common/groupButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames/bind'; 3 | import { ButtonVariant, ButtonColor } from '../button/types'; 4 | const useStyles = require('isomorphic-style-loader/useStyles'); 5 | const s = require('./groupButton.scss'); 6 | 7 | interface GroupButtonProps { 8 | variant?: ButtonVariant; 9 | color?: ButtonColor; 10 | disabled?: boolean; 11 | className?: string; 12 | } 13 | 14 | const GroupButton: React.FC = props => { 15 | useStyles(s); 16 | const { children, variant = 'contained', color = 'blue', disabled, className } = props; 17 | 18 | const cx = classNames.bind(s); 19 | const groupButtonClassName = cx(variant, color, { [s.disabled]: disabled }, [s.groupButtonWrapper], [className!]); 20 | 21 | return
{children}
; 22 | }; 23 | 24 | export default GroupButton; 25 | -------------------------------------------------------------------------------- /app/components/common/mobileSearchBox/mobileSearchBox.scss: -------------------------------------------------------------------------------- 1 | .mobileSearchBoxWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: $gray300; 5 | position: fixed; 6 | width: 100%; 7 | height: 100vh; 8 | top: 0px; 9 | left: 0px; 10 | z-index: 999; 11 | border-radius: 0; 12 | overflow: auto; 13 | } 14 | 15 | .mobileSearchBoxFooter { 16 | position: fixed; 17 | bottom: 0; 18 | width: 100%; 19 | padding: 8px; 20 | z-index: 3; 21 | background-color: $gray300; 22 | border-top: 1px solid $gray400; 23 | } 24 | 25 | .searchWrapper { 26 | width: 100%; 27 | background-color: $gray300; 28 | } 29 | 30 | .searchInput { 31 | display: flex; 32 | width: 100%; 33 | height: 44px; 34 | border: 0; 35 | line-height: 1.5; 36 | color: $black0; 37 | background-color: white; 38 | overflow: hidden; 39 | align-items: center; 40 | padding: 0 88px 0 44px; 41 | } 42 | -------------------------------------------------------------------------------- /app/components/common/paperItem/__tests__/blockVenue_spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as renderer from 'react-test-renderer'; 3 | import BlockVenue from '../blockVenue'; 4 | import { RAW } from '../../../../__mocks__'; 5 | 6 | describe('BlockVenue Component', () => { 7 | describe('when paper is from conference', () => { 8 | it('should render correctly', () => { 9 | const paper = RAW.PAPER_FROM_CONFERENCE; 10 | 11 | const tree = renderer 12 | .create( 13 | 21 | ) 22 | .toJSON(); 23 | expect(tree).toMatchSnapshot(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/components/layouts/reducer.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export const enum UserDevice { 4 | DESKTOP = 'DESKTOP', 5 | TABLET = 'TABLET', 6 | MOBILE = 'MOBILE', 7 | } 8 | 9 | export interface LayoutState { 10 | userDevice: UserDevice; 11 | } 12 | 13 | export const LAYOUT_INITIAL_STATE = { userDevice: UserDevice.DESKTOP }; 14 | 15 | type SetDeviceAction = PayloadAction<{ userDevice: UserDevice }>; 16 | 17 | const layoutSlice = createSlice({ 18 | name: 'layout', 19 | initialState: LAYOUT_INITIAL_STATE, 20 | reducers: { 21 | setDeviceType(state, action: SetDeviceAction) { 22 | if (state.userDevice === action.payload.userDevice) return state; 23 | return { ...state, userDevice: action.payload.userDevice }; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { setDeviceType } = layoutSlice.actions; 29 | 30 | export default layoutSlice.reducer; 31 | -------------------------------------------------------------------------------- /app/components/paperShow/components/paperShowFigureList.scss: -------------------------------------------------------------------------------- 1 | .paperFigureContainer { 2 | display: flex; 3 | flex-flow: wrap; 4 | margin-top: 52px; 5 | } 6 | 7 | .paperFigureHeader { 8 | width: 100%; 9 | font-weight: 500; 10 | font-size: 21px; 11 | color: $black_light_0; 12 | padding-bottom: 4px; 13 | margin-bottom: 8px; 14 | position: relative; 15 | } 16 | 17 | .showAllBtn { 18 | display: none; 19 | } 20 | 21 | @media (max-width: $mobile_width) { 22 | .paperFigureContainer { 23 | justify-content: space-around; 24 | } 25 | 26 | .showAllBtn { 27 | display: block; 28 | width: 100%; 29 | height: 44px; 30 | border-radius: 4px; 31 | background-color: $gray50; 32 | font-size: 14px; 33 | color: $gray600; 34 | margin-top: 28px; 35 | 36 | &:hover { 37 | background-color: $gray300; 38 | transition: $button-hover-transition; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/common/paperItem/mobileAuthors.scss: -------------------------------------------------------------------------------- 1 | .oneLineAuthors { 2 | @extend %one-line-overflow-ellipsis; 3 | 4 | width: calc(100% - 24px); 5 | font-size: 14px; 6 | color: $gray800; 7 | } 8 | 9 | .blockAuthorList { 10 | margin-top: 12px; 11 | } 12 | 13 | .blockAuthorItem { 14 | display: block; 15 | position: relative; 16 | font-size: 14px; 17 | color: $gray800; 18 | padding: 12px 0; 19 | 20 | .authorName { 21 | font-weight: 500; 22 | text-decoration: underline; 23 | } 24 | 25 | .authorName, 26 | .affiliation { 27 | @extend %one-line-overflow-ellipsis; 28 | 29 | width: calc(100% - 64px); 30 | } 31 | 32 | .affiliation { 33 | color: $gray600; 34 | } 35 | 36 | .rightBox { 37 | position: absolute; 38 | right: 0; 39 | top: 12px; 40 | } 41 | } 42 | 43 | .authorCount { 44 | height: 16px; 45 | margin-top: 8px; 46 | font-size: 14px; 47 | color: $gray800; 48 | } 49 | -------------------------------------------------------------------------------- /app/components/dialog/dialog.scss: -------------------------------------------------------------------------------- 1 | .dialogPaper { 2 | display: flex; 3 | align-items: center; 4 | padding: 0; 5 | border-radius: 4px; 6 | overflow-y: visible !important; 7 | max-width: 100%; 8 | } 9 | 10 | @media (max-width: $tablet_width) { 11 | .figureDetailDialog { 12 | display: flex; 13 | align-items: center; 14 | padding: 0; 15 | border-radius: 4px; 16 | overflow-y: visible !important; 17 | max-width: 100%; 18 | position: static; 19 | } 20 | 21 | .mobileFigureDetailDialog { 22 | @extend .figureDetailDialog; 23 | position: absolute; 24 | bottom: 0; 25 | left: 0; 26 | margin: 0 auto; 27 | } 28 | 29 | .mobileFigureDetailDialog { 30 | width: 100% !important; 31 | } 32 | } 33 | 34 | @media (max-width: $mobile_width) { 35 | .dialogPaper { 36 | margin: 0; 37 | max-height: unset; 38 | } 39 | 40 | .container { 41 | overflow: auto; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/containers/keywordSettings/keywordSettings.scss: -------------------------------------------------------------------------------- 1 | $wrapper-vertical-margin: 128px; 2 | 3 | %base-divider { 4 | width: 100%; 5 | height: 1px; 6 | background-color: $gray300; 7 | margin: 16px 0; 8 | } 9 | 10 | .wrapper { 11 | width: 100%; 12 | max-width: 578px; 13 | min-height: calc(100vh - #{$footer-height} - #{$wrapper-vertical-margin} - #{$wrapper-vertical-margin}); 14 | margin: $wrapper-vertical-margin auto; 15 | } 16 | 17 | .title { 18 | font-size: 32px; 19 | font-weight: 500; 20 | line-height: 1.22; 21 | letter-spacing: -1px; 22 | color: $black0; 23 | margin-bottom: 16px; 24 | } 25 | 26 | .context { 27 | font-size: 15px; 28 | line-height: 1.5; 29 | color: $gray800; 30 | margin-bottom: 32px; 31 | } 32 | 33 | .divider { 34 | @extend %base-divider; 35 | 36 | margin: 28px 0 40px 0; 37 | } 38 | 39 | @media (max-width: $mobile_width) { 40 | .wrapper { 41 | padding: 0 16px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/icons/ellipsis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/elipsis 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/components/common/bubblePopover/bubblePopover.scss: -------------------------------------------------------------------------------- 1 | .speechBubble { 2 | position: relative; 3 | margin-top: 20px; 4 | background-color: white; 5 | } 6 | 7 | .contentWrapper { 8 | overflow: unset; 9 | border: solid 1px $gray400; 10 | border-radius: 4px; 11 | 12 | &:before { 13 | content: ''; 14 | position: absolute; 15 | top: 0; 16 | left: 88%; 17 | width: 0; 18 | height: 0; 19 | border: 11px solid transparent; 20 | border-bottom-color: $gray400; 21 | border-top: 0; 22 | margin-left: -14px; 23 | margin-top: -10px; 24 | z-index: 2; 25 | } 26 | 27 | &:after { 28 | content: ''; 29 | position: absolute; 30 | top: 0; 31 | left: 88%; 32 | width: 0; 33 | height: 0; 34 | border: 10px solid transparent; 35 | border-bottom-color: white; 36 | border-top: 0; 37 | margin-left: -13px; 38 | margin-top: -9px; 39 | z-index: 2; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/helpers/displayFormula.ts: -------------------------------------------------------------------------------- 1 | declare var KaTeX: any; 2 | 3 | export function formulaeToHTMLStr(rawString: string | null): string { 4 | if (!rawString) { 5 | return ''; 6 | } 7 | 8 | const result = []; 9 | const latexRegex = RegExp(/\$((.|\n)+?)\$/, 'g'); 10 | 11 | let lastIdx = 0; 12 | let match; 13 | 14 | while (true) { 15 | match = latexRegex.exec(rawString); 16 | 17 | if (match === null) { 18 | break; 19 | } 20 | 21 | if (lastIdx < match.index) { 22 | result.push(rawString.substring(lastIdx, match.index)); 23 | } 24 | 25 | try { 26 | result.push(KaTeX.renderToString(match[1])); 27 | } catch (_err) { 28 | result.push(match[1]); 29 | } 30 | 31 | lastIdx = latexRegex.lastIndex + 1; 32 | } 33 | 34 | if (lastIdx < rawString.length) { 35 | result.push(rawString.substring(lastIdx, rawString.length)); 36 | } 37 | 38 | return result.join(''); 39 | } 40 | -------------------------------------------------------------------------------- /app/icons/password.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/password 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/icons/source Copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/source Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/auth/signUp/components/firstForm/firstForm.scss: -------------------------------------------------------------------------------- 1 | .authContainer { 2 | display: flex; 3 | width: 100%; 4 | } 5 | 6 | .authFormWrapper { 7 | width: 384px; 8 | } 9 | 10 | .formWrapper { 11 | padding: 22px 32px 32px 24px; 12 | } 13 | 14 | .googleIconWrapper { 15 | position: absolute; 16 | left: 14px; 17 | width: 24px; 18 | height: 24px; 19 | } 20 | 21 | .errorContent { 22 | height: 14px; 23 | font-size: 12px; 24 | color: $red0; 25 | margin-top: 6px; 26 | margin-left: 4px; 27 | } 28 | 29 | .dialogCloseBtnWrapper { 30 | position: absolute; 31 | bottom: -32px; 32 | right: 12px; 33 | cursor: pointer; 34 | } 35 | 36 | .authButtonWrapper { 37 | margin-top: 8px; 38 | } 39 | 40 | .dashSeparatorBox { 41 | margin: 12px 0; 42 | } 43 | 44 | @media (max-width: $mobile_width) { 45 | .authFormWrapper { 46 | max-width: 100%; 47 | } 48 | 49 | .googleIconWrapper { 50 | display: none; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/components/common/paperItem/savedCollections.scss: -------------------------------------------------------------------------------- 1 | .itemWrapper { 2 | margin-bottom: 8px; 3 | padding-bottom: 8px; 4 | border-bottom: 1px solid $gray200; 5 | } 6 | 7 | .bookmarkIcon { 8 | display: inline-flex; 9 | justify-content: center; 10 | vertical-align: middle; 11 | align-items: center; 12 | width: 20px; 13 | height: 20px; 14 | margin-top: -4px; 15 | margin-right: 4px; 16 | 17 | svg { 18 | display: inline-block; 19 | width: 20px; 20 | height: 20px; 21 | vertical-align: top; 22 | color: $main_blue0; 23 | } 24 | } 25 | 26 | .subContext { 27 | color: $gray600; 28 | font-size: 14px; 29 | line-height: 20px; 30 | } 31 | 32 | .collectionTitle { 33 | color: $gray700; 34 | font-weight: 500; 35 | font-size: 14px; 36 | 37 | &:hover { 38 | text-decoration: underline; 39 | } 40 | } 41 | 42 | @media (max-width: $mobile_width) { 43 | .itemWrapper { 44 | display: none; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/containers/profile/components/pendingPaperList/pendingPaperList.scss: -------------------------------------------------------------------------------- 1 | .description { 2 | font-size: 16px; 3 | color: $gray800; 4 | text-align: center; 5 | padding: 16px 0; 6 | margin-bottom: 32px; 7 | line-height: 1.5; 8 | 9 | .highlightKeyword { 10 | color: $main_blue0; 11 | font-weight: 500; 12 | 13 | &:hover { 14 | cursor: pointer; 15 | text-decoration: underline; 16 | } 17 | } 18 | } 19 | 20 | .contentBox { 21 | width: 100%; 22 | margin-bottom: 24px; 23 | } 24 | 25 | .paperLoadIcon { 26 | width: 20px; 27 | height: 20px; 28 | margin-left: 4px; 29 | 30 | svg { 31 | width: 20px; 32 | height: 20px; 33 | } 34 | } 35 | 36 | .paperLoadButton { 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | color: $main_blue1; 41 | letter-spacing: 1px; 42 | line-height: 1.5; 43 | text-align: center; 44 | margin: 16px 0; 45 | cursor: pointer; 46 | } 47 | -------------------------------------------------------------------------------- /app/icons/trash-can.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/trash-can 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /server/routes/openSearchXML.tsx: -------------------------------------------------------------------------------- 1 | export default function getOpenSearchXML() { 2 | return ` 3 | 4 | Scinapse 5 | Search for academic information on scinapse.io 6 | UTF-8 7 | https://assets.pluto.network/scinapse/favicon.ico 8 | 9 | 10 | https://scinapse.io 11 | 12 | `; 13 | } 14 | -------------------------------------------------------------------------------- /app/components/articleSearch/components/emailBanner/emailBanner.scss: -------------------------------------------------------------------------------- 1 | @import '../../common'; 2 | 3 | .wrapper { 4 | @extend %right-item-wrapper; 5 | height: auto; 6 | } 7 | 8 | .title { 9 | font-size: 24px; 10 | font-weight: bold; 11 | line-height: 1; 12 | color: $black0; 13 | } 14 | 15 | .subtitle { 16 | font-size: 16px; 17 | color: $gray800; 18 | margin-top: 8px; 19 | line-height: 1.5; 20 | } 21 | 22 | .inputWrapper { 23 | margin-top: 16px; 24 | display: flex; 25 | justify-content: space-between; 26 | } 27 | 28 | .emailInput { 29 | border-radius: 4px; 30 | border: solid 1px $gray400; 31 | background-color: white; 32 | padding: 4px 12px; 33 | line-height: 1.5; 34 | font-size: 16px; 35 | color: $black0; 36 | flex: 1; 37 | min-width: 0; 38 | margin-right: 8px; 39 | 40 | &::placeholder { 41 | color: $gray500; 42 | } 43 | } 44 | 45 | .bannerImage { 46 | width: 100%; 47 | margin-top: 16px; 48 | } 49 | -------------------------------------------------------------------------------- /app/components/common/sourceURLPopover/sourceURLPopover.scss: -------------------------------------------------------------------------------- 1 | .sourcesWrapper { 2 | @include popper-paper(198px); 3 | max-height: 202px; 4 | overflow-y: auto; 5 | border-radius: 4px; 6 | } 7 | 8 | .sourceItem { 9 | @extend %popper-item; 10 | position: relative; 11 | white-space: pre; 12 | 13 | .sourceIcon { 14 | @include flex-center-vertical-horizontal(flex); 15 | position: absolute; 16 | top: 12px; 17 | right: 8px; 18 | width: 20px; 19 | height: 20px; 20 | color: $gray600; 21 | opacity: 0; 22 | transition: opacity 150ms ease-in-out; 23 | 24 | svg { 25 | width: 13px; 26 | height: 13px; 27 | } 28 | } 29 | 30 | &:hover { 31 | .sourceIcon { 32 | opacity: 1; 33 | } 34 | } 35 | 36 | .host { 37 | @extend %one-line-overflow-ellipsis; 38 | display: inline-block; 39 | width: 150px; 40 | 41 | &.pdfHost { 42 | width: 100px; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/arrow-up 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/icons/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/error 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/components/mobileRelatedPapers/mobileRelatedPapers.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import SimplePaperItemContainer from '../simplePaperItem/simplePaperItemContainer'; 3 | const s = require('./mobileRelatedPapers.scss'); 4 | const useStyles = require('isomorphic-style-loader/useStyles'); 5 | 6 | interface Props { 7 | paperIds: string[]; 8 | className?: string; 9 | } 10 | 11 | const MobileRelatedPapers: FC = ({ className, paperIds }) => { 12 | useStyles(s); 13 | if (!paperIds || !paperIds.length) return null; 14 | 15 | return ( 16 |
17 | {paperIds.map(paperId => ( 18 | 25 | ))} 26 |
27 | ); 28 | }; 29 | 30 | export default MobileRelatedPapers; 31 | -------------------------------------------------------------------------------- /app/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/arrow-down 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/icons/pen-only.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/pen-only 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/arrow-left 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/components/common/bubblePopover/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Popper, { PopperProps } from '@material-ui/core/Popper'; 3 | import { withStyles } from '../../../helpers/withStylesHelper'; 4 | const styles = require('./bubblePopover.scss'); 5 | 6 | // tslint:disable-next-line:no-empty-interface 7 | interface BubblePopoverProps extends PopperProps {} 8 | 9 | const BubblePopover: React.SFC = props => { 10 | const popperProps: BubblePopoverProps = { 11 | ...props, 12 | modifiers: { 13 | flip: { 14 | enabled: false, 15 | }, 16 | }, 17 | }; 18 | 19 | return ( 20 | 21 |
22 |
{props.children}
23 |
24 |
25 | ); 26 | }; 27 | 28 | export default withStyles(styles)(BubblePopover); 29 | -------------------------------------------------------------------------------- /app/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/arrow-right 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/components/articleSearch/components/searchList/searchList.scss: -------------------------------------------------------------------------------- 1 | .searchItems { 2 | margin-top: 16px; 3 | } 4 | 5 | .searchItemWrapper { 6 | position: relative; 7 | margin-bottom: 12px; 8 | max-width: $search_container_width; 9 | background-color: white; 10 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.05); 11 | padding: 16px 16px 8px 16px; 12 | border-radius: 4px; 13 | border: solid 1px $gray300; 14 | } 15 | 16 | .loadingContainer { 17 | display: flex; 18 | flex-direction: column; 19 | width: 100%; 20 | padding-top: 250px; 21 | align-items: center; 22 | min-height: 738.5px; 23 | 24 | .loadingSpinner { 25 | div { 26 | background-color: $gray400; 27 | } 28 | } 29 | 30 | .loadingContent { 31 | margin-top: 27.5px; 32 | font-size: 15px; 33 | line-height: 2.4; 34 | color: $black0; 35 | } 36 | } 37 | 38 | // Mobile 39 | @media (max-width: $mobile_width) { 40 | .searchItems { 41 | margin-top: 16px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/containers/userSettings/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProfileForm from '../profileForm'; 3 | import AuthEditForm from '../authEditForm'; 4 | import { withStyles } from '../../helpers/withStylesHelper'; 5 | import ImprovedFooter from '../../components/layouts/improvedFooter'; 6 | import EmailSettings from '../../components/emailSettings/emailSettings'; 7 | 8 | const s = require('./userSettings.scss'); 9 | 10 | const UserSettings: React.FC = () => { 11 | return ( 12 | <> 13 |
14 |

Settings

15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 | ); 26 | }; 27 | 28 | export default withStyles(s)(UserSettings); 29 | -------------------------------------------------------------------------------- /app/components/common/separator/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classNames from 'classnames'; 3 | const useStyles = require('isomorphic-style-loader/useStyles'); 4 | const styles = require('./separator.scss'); 5 | 6 | interface DashedDividerWithContentProps { 7 | content: string; 8 | wrapperClassName?: string; 9 | contentClassName?: string; 10 | } 11 | const DashedDividerWithContent: React.FC = React.memo( 12 | ({ content, wrapperClassName, contentClassName }) => { 13 | useStyles(styles); 14 | return ( 15 |
16 |
17 |
{content}
18 |
19 |
20 | ); 21 | } 22 | ); 23 | 24 | export default DashedDividerWithContent; 25 | -------------------------------------------------------------------------------- /app/components/paperShowTabItem/paperShowTabItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | const s = require('./paperShowTabItem.scss'); 5 | const useStyles = require('isomorphic-style-loader/useStyles'); 6 | 7 | export const enum AvailablePaperShowTab { 8 | ref = 1, 9 | cited, 10 | related, 11 | } 12 | 13 | interface Props { 14 | type: AvailablePaperShowTab; 15 | onClick: (target: AvailablePaperShowTab) => void; 16 | content: string; 17 | active: boolean; 18 | className?: string; 19 | } 20 | 21 | const PaperShowTabItem: FC = ({ onClick, type, content, className, active }) => { 22 | useStyles(s); 23 | return ( 24 | onClick(type)} 26 | className={classNames({ 27 | [s.tabItem]: true, 28 | [s.active]: active, 29 | [className!]: !!className, 30 | })} 31 | > 32 | {content} 33 | 34 | ); 35 | }; 36 | export default PaperShowTabItem; 37 | -------------------------------------------------------------------------------- /app/components/common/paperItem/paperItemButtonGroup.scss: -------------------------------------------------------------------------------- 1 | .groupWrapper { 2 | display: flex; 3 | margin-top: 11.5px; 4 | padding: 8px 0; 5 | align-items: center; 6 | justify-content: space-between; 7 | border-top: 1px solid $gray200; 8 | } 9 | 10 | .buttonListBox { 11 | display: flex; 12 | align-items: center; 13 | 14 | & > a { 15 | margin-right: 8px; 16 | } 17 | } 18 | 19 | .buttonWrapper { 20 | & + & { 21 | margin-left: 8px; 22 | } 23 | } 24 | 25 | @media (max-width: $mobile_width) { 26 | .groupWrapper { 27 | border-top: 0; 28 | margin-top: 16px; 29 | } 30 | 31 | .mobileWrapper { 32 | display: flex; 33 | align-items: center; 34 | justify-content: space-between; 35 | margin: 16px 0; 36 | 37 | .buttonWrapper { 38 | flex: 1; 39 | 40 | button, 41 | a { 42 | width: 100%; 43 | justify-content: center; 44 | } 45 | } 46 | } 47 | 48 | .citeButton { 49 | display: none; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/plus 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/reducers/searchQuery.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export interface SearchQueryState { 4 | query: string; 5 | isOpenMobileBox: boolean; 6 | } 7 | 8 | export const SEARCH_QUERY_INITIAL_STATE: SearchQueryState = { 9 | query: '', 10 | isOpenMobileBox: false, 11 | }; 12 | 13 | const SearchQuerySlice = createSlice({ 14 | name: 'searchQuery', 15 | initialState: SEARCH_QUERY_INITIAL_STATE, 16 | reducers: { 17 | changeSearchQuery(state, action: PayloadAction<{ query: string }>) { 18 | return { ...state, query: action.payload.query }; 19 | }, 20 | openMobileSearchBox(state) { 21 | return { ...state, isOpenMobileBox: true }; 22 | }, 23 | closeMobileSearchBox(state) { 24 | return { ...state, isOpenMobileBox: false }; 25 | }, 26 | }, 27 | }); 28 | 29 | export const { changeSearchQuery, openMobileSearchBox, closeMobileSearchBox } = SearchQuerySlice.actions; 30 | 31 | export default SearchQuerySlice.reducer; 32 | -------------------------------------------------------------------------------- /app/components/paperShow/components/relatedPaperItem.scss: -------------------------------------------------------------------------------- 1 | .paperItemWrapper + .paperItemWrapper { 2 | margin-top: 16px; 3 | } 4 | 5 | .title { 6 | display: block; 7 | font-size: 16px; 8 | line-height: 1.4; 9 | color: $navy0; 10 | margin-bottom: 4px; 11 | font-weight: 500; 12 | cursor: pointer; 13 | 14 | &:hover { 15 | text-decoration: underline; 16 | } 17 | 18 | &:visited { 19 | color: $purple0; 20 | } 21 | 22 | &.notVisitedTitle { 23 | &:visited { 24 | color: $navy0; 25 | } 26 | } 27 | } 28 | 29 | .description { 30 | font-size: 13px; 31 | line-height: 1.4; 32 | color: $gray500; 33 | } 34 | 35 | .authorLink, 36 | .journalLink { 37 | color: $gray600; 38 | 39 | &:hover { 40 | text-decoration: underline; 41 | } 42 | } 43 | 44 | .author, 45 | .journal { 46 | display: block; 47 | margin-bottom: 2px; 48 | 49 | svg { 50 | width: 12px; 51 | height: 12px; 52 | margin-right: 4px; 53 | margin-bottom: -2px; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/hooks/useIntersectionHook.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ActionTicketManager from '../helpers/actionTicketManager'; 3 | import { ActionTicketParams } from '../helpers/actionTicketManager/actionTicket'; 4 | 5 | export function useObserver(threshold: number | number[] | undefined, ticketParams: ActionTicketParams) { 6 | const elRef = React.useRef(null); 7 | 8 | React.useEffect(() => { 9 | const intersectionObserver = new IntersectionObserver( 10 | (entries, observer) => { 11 | entries.forEach(entry => { 12 | if (entry.isIntersecting) { 13 | ActionTicketManager.trackTicket(ticketParams); 14 | observer.unobserve(entry.target); 15 | } 16 | }); 17 | }, 18 | { threshold } 19 | ); 20 | 21 | if (elRef.current) { 22 | intersectionObserver.observe(elRef.current); 23 | } 24 | 25 | return () => intersectionObserver.disconnect(); 26 | }, []); 27 | return { elRef }; 28 | } 29 | -------------------------------------------------------------------------------- /server/routes/manifest.tsx: -------------------------------------------------------------------------------- 1 | const manifestJSON = { 2 | short_name: "Scinapse", 3 | name: "Scinapse", 4 | background_color: "#6096ff", 5 | theme_color: "#3e7fff", 6 | display: "standalone", 7 | orientation: "portrait", 8 | icons: [ 9 | { 10 | src: "https://assets.pluto.network/scinapse/app_icon/launcher-icon-1x.png", 11 | type: "image/png", 12 | sizes: "48x48", 13 | }, 14 | { 15 | src: "https://assets.pluto.network/scinapse/app_icon/launcher-icon-2x.png", 16 | type: "image/png", 17 | sizes: "96x96", 18 | }, 19 | { 20 | src: "https://assets.pluto.network/scinapse/app_icon/launcher-icon-4x.png", 21 | type: "image/png", 22 | sizes: "192x192", 23 | }, 24 | { 25 | src: "https://assets.pluto.network/scinapse/app_icon/launcher-icon-4x.png", 26 | type: "image/png", 27 | sizes: "512x512", 28 | }, 29 | ], 30 | start_url: "/?app_launcher=true&utm_source=pwa_app", 31 | }; 32 | 33 | export default manifestJSON; 34 | -------------------------------------------------------------------------------- /app/components/auth/resetPassword/resetPassword.scss: -------------------------------------------------------------------------------- 1 | .signInContainer { 2 | max-width: $container_width; 3 | margin: 0 auto; 4 | 5 | .formContainer { 6 | width: 403px; 7 | margin: 0 auto; 8 | padding-bottom: 54px; 9 | border-radius: 2px; 10 | background-color: white; 11 | border: solid 1px $gray400; 12 | } 13 | } 14 | 15 | .content { 16 | padding: 0 36px; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | 22 | .title { 23 | font-size: 15px; 24 | font-weight: 500; 25 | letter-spacing: 2.7px; 26 | text-align: center; 27 | color: $gray800; 28 | margin-top: 50px; 29 | margin-bottom: 23px; 30 | } 31 | 32 | .subtitle { 33 | font-size: 15px; 34 | line-height: 1.47; 35 | text-align: center; 36 | color: $gray600; 37 | margin-bottom: 27px; 38 | white-space: pre-wrap; 39 | } 40 | 41 | .errorContent { 42 | font-size: 14px; 43 | color: $red0; 44 | text-align: center; 45 | margin-top: 8px; 46 | margin-bottom: -25px; 47 | } 48 | -------------------------------------------------------------------------------- /app/components/pdfViewer/component/progressSpinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useIntervalProgress } from '../../../hooks/useIntervalProgressHook'; 3 | import { withStyles } from '../../../helpers/withStylesHelper'; 4 | import CircularProgress from '@material-ui/core/CircularProgress'; 5 | const styles = require('../pdfViewer.scss'); 6 | 7 | const ProgressSpinner: React.FC = () => { 8 | const [percent, setPercent] = React.useState(0); 9 | 10 | useIntervalProgress(() => { 11 | setPercent(percent + 10); 12 | }, percent < 90 ? 750 : null); 13 | 14 | return ( 15 |
16 |
17 | 18 | {`${percent}%`} 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default withStyles(styles)(ProgressSpinner); 25 | -------------------------------------------------------------------------------- /app/components/common/groupButton/groupButton.scss: -------------------------------------------------------------------------------- 1 | .groupButtonWrapper { 2 | display: inline-flex; 3 | 4 | a:not(:last-of-type), 5 | button:not(:last-of-type) { 6 | border-top-right-radius: 0; 7 | border-bottom-right-radius: 0; 8 | } 9 | 10 | a:not(:first-child), 11 | button:not(:first-child) { 12 | margin-left: -1px; 13 | border-top-left-radius: 0; 14 | border-bottom-left-radius: 0; 15 | } 16 | } 17 | 18 | .outlined.groupButtonWrapper { 19 | @include combined-button-borderline-color($gray300); 20 | } 21 | 22 | .contained.blue.groupButtonWrapper { 23 | @include combined-button-borderline-color($main-blue0); 24 | } 25 | 26 | .contained.gray.groupButtonWrapper, 27 | .contained.black.groupButtonWrapper { 28 | @include combined-button-borderline-color($gray400); 29 | } 30 | 31 | .contained.blue.disabled.groupButtonWrapper, 32 | .contained.gray.disabled.groupButtonWrapper, 33 | .contained.black.disabled.groupButtonWrapper { 34 | @include combined-button-borderline-color($gray300); 35 | } 36 | -------------------------------------------------------------------------------- /app/reducers/realtedPapers.tsx: -------------------------------------------------------------------------------- 1 | import { Actions, ACTION_TYPES } from '../actions/actionTypes'; 2 | 3 | export interface RelatedPapersState { 4 | paperIds: string[]; 5 | isLoading: boolean; 6 | hasFailed: boolean; 7 | } 8 | 9 | export const RELATED_PAPERS_INITIAL_STATE: RelatedPapersState = { 10 | paperIds: [], 11 | isLoading: false, 12 | hasFailed: false, 13 | }; 14 | 15 | export function reducer(state = RELATED_PAPERS_INITIAL_STATE, action: Actions): RelatedPapersState { 16 | switch (action.type) { 17 | case ACTION_TYPES.RELATED_PAPERS_START_TO_GET_PAPERS: { 18 | return { ...state, isLoading: true, hasFailed: false }; 19 | } 20 | 21 | case ACTION_TYPES.RELATED_PAPERS_SUCCEEDED_TO_GET_PAPERS: { 22 | return { ...state, isLoading: false, hasFailed: false, paperIds: action.payload.paperIds }; 23 | } 24 | 25 | case ACTION_TYPES.RELATED_PAPERS_FAILED_TO_GET_PAPERS: { 26 | return { ...state, isLoading: false, hasFailed: true }; 27 | } 28 | } 29 | 30 | return state; 31 | } 32 | -------------------------------------------------------------------------------- /app/components/common/paperFigure/smallPaperFigure.scss: -------------------------------------------------------------------------------- 1 | .figureContainer { 2 | margin-top: 8px; 3 | margin-bottom: 16px; 4 | } 5 | 6 | .figureImageWrapper { 7 | position: relative; 8 | display: inline-flex; 9 | align-items: center; 10 | justify-content: center; 11 | width: 88px; 12 | height: 88px; 13 | margin-right: 8px; 14 | border-radius: 1.8px; 15 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08); 16 | border: solid 0.5px $gray500; 17 | background-color: white; 18 | overflow: hidden; 19 | cursor: pointer; 20 | 21 | &:hover { 22 | .figureImageBackground { 23 | z-index: 1; 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | width: 100%; 28 | height: 100%; 29 | background-color: rgba(52, 73, 94, 0.1); 30 | transition: $button-hover-transition; 31 | } 32 | } 33 | } 34 | 35 | .figureImage { 36 | max-width: 100%; 37 | max-height: 100%; 38 | position: absolute; 39 | top: 50%; 40 | left: 50%; 41 | transform: translate(-50%, -50%); 42 | } 43 | -------------------------------------------------------------------------------- /app/containers/profile/components/resolvedPendingPaperDialog/resolvedPendingPaperDialog.scss: -------------------------------------------------------------------------------- 1 | .dialogPaper { 2 | position: relative; 3 | margin: 8px; 4 | } 5 | 6 | .boxContainer { 7 | font-family: $fallback !important; 8 | width: 666px; 9 | } 10 | 11 | .boxWrapper { 12 | padding: 24px 16px; 13 | } 14 | 15 | .header { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin-bottom: 8px; 20 | } 21 | 22 | .title { 23 | font-size: 18px; 24 | font-weight: 700; 25 | color: $black_light_0; 26 | } 27 | 28 | .iconWrapper { 29 | display: inline-flex; 30 | justify-content: center; 31 | align-items: center; 32 | width: 20px; 33 | height: 20px; 34 | cursor: pointer; 35 | 36 | svg { 37 | width: 20px; 38 | height: 20px; 39 | color: $gray500; 40 | } 41 | } 42 | 43 | .dialogBody { 44 | overflow-y: auto; 45 | max-height: 500px; 46 | height: 100%; 47 | } 48 | 49 | @media (max-width: $tablet_width) { 50 | .boxContainer { 51 | width: 100%; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/reducers/configuration.tsx: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../actions/actionTypes'; 2 | 3 | export interface Configuration 4 | extends Readonly<{ 5 | succeedAPIFetchAtServer: boolean; 6 | renderedAtClient: boolean; 7 | initialPageType: Scinapse.ActionTicket.PageType; 8 | }> {} 9 | 10 | export const CONFIGURATION_INITIAL_STATE: Configuration = { 11 | succeedAPIFetchAtServer: false, 12 | renderedAtClient: false, 13 | initialPageType: 'unknown', 14 | }; 15 | 16 | export function reducer(state: Configuration = CONFIGURATION_INITIAL_STATE, action: ReduxAction): Configuration { 17 | switch (action.type) { 18 | case ACTION_TYPES.GLOBAL_SUCCEEDED_TO_INITIAL_DATA_FETCHING: { 19 | return { ...state, succeedAPIFetchAtServer: true }; 20 | } 21 | 22 | case ACTION_TYPES.GLOBAL_SUCCEEDED_TO_RENDER_AT_THE_CLIENT_SIDE: { 23 | return { ...state, renderedAtClient: true, initialPageType: action.payload.initialPageType }; 24 | } 25 | 26 | default: 27 | return state; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/components/common/mobilePagination/pagination.scss: -------------------------------------------------------------------------------- 1 | .buttonWrapper { 2 | margin-top: 17.5px; 3 | } 4 | 5 | .pageButton { 6 | display: block; 7 | padding: 0; 8 | width: 100%; 9 | height: 46px; 10 | line-height: 46px; 11 | border-radius: 5px; 12 | border: solid 1px $gray400; 13 | font-size: 14px; 14 | text-align: center; 15 | color: $gray800; 16 | } 17 | 18 | .pageButton + .pageButton { 19 | margin-top: 10px; 20 | } 21 | 22 | .pageIconWrapper { 23 | display: inline-block; 24 | width: 32px; 25 | height: 100%; 26 | margin: 0 8px; 27 | } 28 | 29 | .pageIcon { 30 | display: inline-flex; 31 | align-items: center; 32 | width: 12px; 33 | height: 100%; 34 | 35 | svg { 36 | display: inline-block; 37 | vertical-align: top; 38 | width: 100%; 39 | height: 12px; 40 | } 41 | } 42 | 43 | .pageNumber { 44 | display: inline-block; 45 | vertical-align: top; 46 | height: 100%; 47 | margin: 0 18px; 48 | } 49 | 50 | .prevButton { 51 | svg { 52 | transform: rotate(180deg); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/components/common/paperItem/blockVenueAuthor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import BlockVenue from './blockVenue'; 3 | import BlockAuthorList from './blockAuthorList'; 4 | import { Paper } from '../../../model/paper'; 5 | 6 | interface BlockVenueAuthorProps { 7 | paper: Paper; 8 | pageType: Scinapse.ActionTicket.PageType; 9 | actionArea: Scinapse.ActionTicket.ActionArea; 10 | ownProfileSlug?: string; 11 | } 12 | 13 | const BlockVenueAuthor: React.FC = ({ paper, pageType, actionArea, ownProfileSlug }) => { 14 | return ( 15 | <> 16 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default BlockVenueAuthor; 30 | -------------------------------------------------------------------------------- /app/containers/profile/components/allRepresentativePaperDialog/allRepresentativePaperDialog.scss: -------------------------------------------------------------------------------- 1 | .dialogPaper { 2 | position: relative; 3 | margin: 8px; 4 | } 5 | 6 | .boxContainer { 7 | font-family: $fallback !important; 8 | width: 666px; 9 | } 10 | 11 | .boxWrapper { 12 | padding: 24px 16px; 13 | } 14 | 15 | .header { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | margin-bottom: 8px; 20 | } 21 | 22 | .title { 23 | font-size: 18px; 24 | font-weight: 700; 25 | color: $black_light_0; 26 | } 27 | 28 | .iconWrapper { 29 | display: inline-flex; 30 | justify-content: center; 31 | align-items: center; 32 | width: 20px; 33 | height: 20px; 34 | cursor: pointer; 35 | 36 | svg { 37 | width: 20px; 38 | height: 20px; 39 | color: $gray500; 40 | } 41 | } 42 | 43 | .dialogBody { 44 | overflow-y: auto; 45 | max-height: 500px; 46 | height: 100%; 47 | } 48 | 49 | @media (max-width: $tablet_width) { 50 | .boxContainer { 51 | width: 100%; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/containers/profileOnboarding/components/onboardingFooter/onboardingFooter.scss: -------------------------------------------------------------------------------- 1 | .onboardingFooterWrapper { 2 | position: absolute; 3 | bottom: 0; 4 | width: 100%; 5 | display: flex; 6 | justify-content: space-between; 7 | padding: 16px 24px; 8 | border-top: 1px solid $gray400; 9 | } 10 | 11 | .countSection { 12 | .totalCount { 13 | font-size: 20px; 14 | color: $black1; 15 | font-weight: 500; 16 | margin-bottom: 4px; 17 | 18 | .highlightNumber { 19 | margin-left: 8px; 20 | color: $main_blue1; 21 | } 22 | } 23 | 24 | .eachCount { 25 | font-size: 14px; 26 | color: $gray800; 27 | 28 | .highlightNumber { 29 | margin-left: 2px; 30 | color: $main_blue0; 31 | } 32 | } 33 | } 34 | 35 | @media (max-width: $mobile_width) { 36 | .onboardingFooterWrapper { 37 | padding: 16px 4px; 38 | } 39 | 40 | .countSection { 41 | .totalCount { 42 | font-size: 16px; 43 | } 44 | 45 | .eachCount { 46 | font-size: 13px; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/routes/sitemap.tsx: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | 3 | export default async function getSitemap(pathname: string): Promise<{ body: string }> { 4 | const s3 = new AWS.S3(); 5 | 6 | let s3ObjKey: string; 7 | if (pathname === '/sitemap') { 8 | s3ObjKey = 'sitemap.xml.gz'; 9 | } else { 10 | const reqPathToken = pathname.split('/'); 11 | s3ObjKey = `${reqPathToken[reqPathToken.length - 1]}.gz`; 12 | } 13 | 14 | const body = await new Promise((resolve, reject) => { 15 | s3.getObject( 16 | { 17 | Bucket: 'scinapse-sitemap', 18 | Key: s3ObjKey, 19 | }, 20 | (err: AWS.AWSError, data: any) => { 21 | if (err) { 22 | console.error('Error occurred while retrieving sitemap object from S3', err); 23 | reject(err); 24 | } else { 25 | resolve(data.Body as Buffer); 26 | } 27 | } 28 | ); 29 | }); 30 | 31 | const encodedBody = (body as Buffer).toString('base64'); 32 | 33 | return { 34 | body: encodedBody, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /app/components/auth/authGuideContext/authGuideContext.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 408px; 3 | min-height: 520px; 4 | padding: 24px 32px; 5 | overflow: hidden; 6 | position: relative; 7 | } 8 | 9 | .logoIcon { 10 | svg { 11 | width: 88px; 12 | height: 28px; 13 | } 14 | } 15 | 16 | .mainText { 17 | width: 344px; 18 | margin-top: 36px; 19 | font-family: Georgia; 20 | font-size: 46px; 21 | font-weight: bold; 22 | letter-spacing: 0.8px; 23 | color: $black0; 24 | white-space: pre-line; 25 | } 26 | 27 | .subText { 28 | width: 344px; 29 | height: 22px; 30 | margin-top: 8px; 31 | font-family: Georgia; 32 | font-size: 20px; 33 | color: $black0; 34 | } 35 | 36 | .generalGuideImage { 37 | position: absolute; 38 | bottom: -25px; 39 | width: 376px; 40 | height: 242px; 41 | left: -26px; 42 | } 43 | 44 | .guideImage { 45 | @extend .generalGuideImage; 46 | left: 20px; 47 | } 48 | 49 | @media (max-width: $tablet_width) { 50 | .container { 51 | display: none; 52 | min-height: unset; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/components/error/errorPage.scss: -------------------------------------------------------------------------------- 1 | .errorPageContainer { 2 | max-width: $container_width; 3 | margin: $navbar_height auto 229.7px auto; 4 | max-height: 100%; 5 | text-align: center; 6 | } 7 | 8 | .errorNum { 9 | margin-top: 160px; 10 | font-size: 120px; 11 | font-weight: bold; 12 | letter-spacing: 8px; 13 | color: $gray400; 14 | } 15 | 16 | .errorContent { 17 | margin-top: 16px; 18 | font-size: 30px; 19 | font-weight: bold; 20 | color: $gray800; 21 | } 22 | 23 | .goBackButton { 24 | margin-top: 62px; 25 | display: inline-block; 26 | width: 120px; 27 | height: 44px; 28 | background-color: white; 29 | color: $black0; 30 | border-radius: 5px; 31 | line-height: 44px; 32 | border: 1px solid $gray400; 33 | cursor: pointer; 34 | } 35 | 36 | .homeButton { 37 | margin-left: 16px; 38 | margin-top: 62px; 39 | display: inline-block; 40 | width: 120px; 41 | height: 44px; 42 | background-color: $main_blue0; 43 | color: white; 44 | border-radius: 5px; 45 | line-height: 44px; 46 | cursor: pointer; 47 | } 48 | -------------------------------------------------------------------------------- /app/components/popupConsentBanner/popupConsentBanner.scss: -------------------------------------------------------------------------------- 1 | .bannerWrapper { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: fixed; 6 | bottom: 0; 7 | background-color: rgba(30, 42, 53, 0.8); 8 | color: white; 9 | width: 100%; 10 | padding: 12px 24px; 11 | line-height: 1.33; 12 | z-index: 101; 13 | } 14 | 15 | .bannerText { 16 | margin: 0 36px 0 0; 17 | } 18 | 19 | .title { 20 | font-size: 18px; 21 | font-weight: bold; 22 | margin-bottom: 4px; 23 | } 24 | 25 | .context { 26 | font-size: 14px; 27 | } 28 | 29 | .link { 30 | text-decoration: underline; 31 | 32 | &:hover { 33 | text-decoration: underline; 34 | } 35 | } 36 | 37 | @media (max-width: $mobile_width) { 38 | .bannerWrapper { 39 | flex-direction: column; 40 | } 41 | 42 | .bannerText { 43 | margin: 0 0 12px 0; 44 | } 45 | 46 | .bannerButton { 47 | width: 100%; 48 | margin-top: 8px; 49 | 50 | button { 51 | width: 100%; 52 | justify-content: center; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/components/common/sortBox/sortBox.scss: -------------------------------------------------------------------------------- 1 | .sortBoxWrapper { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .currentOption { 7 | display: flex; 8 | align-items: center; 9 | cursor: pointer; 10 | color: $gray800; 11 | margin-right: 8px; 12 | 13 | span { 14 | display: inline-block; 15 | vertical-align: top; 16 | } 17 | } 18 | 19 | .downArrow { 20 | display: inline-block; 21 | vertical-align: top; 22 | width: 16px; 23 | height: 16px; 24 | object-fit: contain; 25 | 26 | svg { 27 | display: inline-block; 28 | vertical-align: top; 29 | transform: rotate(180deg); 30 | width: 100%; 31 | height: 100%; 32 | } 33 | } 34 | 35 | .sortByText { 36 | font-size: 14px; 37 | color: $gray600; 38 | margin-right: 8px; 39 | } 40 | 41 | .sortOptionText { 42 | font-size: 14px; 43 | margin-right: 8px; 44 | } 45 | 46 | body .menuItem { 47 | @extend %default-sort-dropdown-menuitem; 48 | } 49 | 50 | @media (max-width: $mobile_width) { 51 | .sortByText { 52 | visibility: hidden; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/components/paperShow/backButton/backButton.scss: -------------------------------------------------------------------------------- 1 | .goBackBtn { 2 | font-size: 14px; 3 | width: 200px; 4 | font-weight: 500; 5 | letter-spacing: 0.5px; 6 | color: $gray600; 7 | cursor: pointer; 8 | vertical-align: top; 9 | 10 | .backIcon { 11 | svg { 12 | width: 16px; 13 | height: 16px; 14 | margin-right: 4px; 15 | vertical-align: top; 16 | } 17 | } 18 | 19 | &:hover { 20 | color: $gray800; 21 | } 22 | } 23 | 24 | @media (max-width: $mobile_width) { 25 | .goBackBtn { 26 | display: flex; 27 | align-items: center; 28 | font-size: 14px; 29 | font-weight: 500; 30 | width: auto; 31 | font-style: normal; 32 | font-stretch: normal; 33 | line-height: normal; 34 | letter-spacing: normal; 35 | color: $gray500; 36 | 37 | .backIcon { 38 | width: 16px; 39 | height: 13px; 40 | margin-right: 10px; 41 | 42 | svg { 43 | width: 16px; 44 | height: 13px; 45 | padding-top: 0; 46 | margin-right: 0; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/components/common/paperItem/moreDropdownButton.scss: -------------------------------------------------------------------------------- 1 | .moreDropdown { 2 | position: relative; 3 | margin-top: 8px; 4 | background-color: white; 5 | z-index: 999; 6 | } 7 | 8 | .contentWrapper { 9 | overflow: unset; 10 | border: solid 1px $gray400; 11 | border-radius: 4px; 12 | 13 | &:before { 14 | content: ''; 15 | position: absolute; 16 | top: 0; 17 | left: 91%; 18 | width: 0; 19 | height: 0; 20 | border: 11px solid transparent; 21 | border-bottom-color: $gray400; 22 | border-top: 0; 23 | margin-left: -14px; 24 | margin-top: -10px; 25 | z-index: 2; 26 | } 27 | 28 | &:after { 29 | content: ''; 30 | position: absolute; 31 | top: 0; 32 | left: 91%; 33 | width: 0; 34 | height: 0; 35 | border: 10px solid transparent; 36 | border-bottom-color: white; 37 | border-top: 0; 38 | margin-left: -13px; 39 | margin-top: -9px; 40 | z-index: 2; 41 | } 42 | } 43 | 44 | // Mobile 45 | @media (max-width: $mobile_width) { 46 | .moreButton { 47 | display: none; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/api/recommendation.ts: -------------------------------------------------------------------------------- 1 | import PlutoAxios from './pluto'; 2 | import { Paper } from '../model/paper'; 3 | import { Collection } from '../model/collection'; 4 | import { RecommendationActionAPIParams } from './types/recommendation'; 5 | 6 | export interface BasedOnCollectionPapersParams { 7 | collection: Collection; 8 | recommendations: Paper[]; 9 | } 10 | 11 | class RecommendationAPI extends PlutoAxios { 12 | public async addPaperToRecommendationPool(params: RecommendationActionAPIParams) { 13 | const res = await this.post(`/recommendations/log/paper-action`, { 14 | paper_id: String(params.paper_id), 15 | action: params.action, 16 | }); 17 | return res.data.data.content; 18 | } 19 | 20 | public async syncRecommendationPool(params: RecommendationActionAPIParams[]) { 21 | const safeParams = params.map(param => ({ ...param, paper_id: param.paper_id })); 22 | await this.post(`/recommendations/log/paper-action-init`, safeParams); 23 | } 24 | } 25 | 26 | const recommendationAPI = new RecommendationAPI(); 27 | 28 | export default recommendationAPI; 29 | -------------------------------------------------------------------------------- /app/icons/new-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/new-tab 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/components/common/spinner/articleSpinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | margin: 0 auto; 3 | text-align: center; 4 | } 5 | 6 | .spinner > div { 7 | width: 16px; 8 | height: 16px; 9 | margin-right: 5px; 10 | background-color: $main_blue0; 11 | border-radius: 100%; 12 | display: inline-block; 13 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 14 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 15 | } 16 | 17 | .spinner .bounce1 { 18 | -webkit-animation-delay: -0.32s; 19 | animation-delay: -0.32s; 20 | } 21 | 22 | .spinner .bounce2 { 23 | -webkit-animation-delay: -0.16s; 24 | animation-delay: -0.16s; 25 | } 26 | 27 | @-webkit-keyframes sk-bouncedelay { 28 | 0%, 29 | 80%, 30 | 100% { 31 | -webkit-transform: scale(0); 32 | } 33 | 40% { 34 | -webkit-transform: scale(1); 35 | } 36 | } 37 | 38 | @keyframes sk-bouncedelay { 39 | 0%, 40 | 80%, 41 | 100% { 42 | -webkit-transform: scale(0); 43 | transform: scale(0); 44 | } 45 | 40% { 46 | -webkit-transform: scale(1); 47 | transform: scale(1); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/actions/relatedPapers.tsx: -------------------------------------------------------------------------------- 1 | import Axios, { CancelToken } from 'axios'; 2 | import { normalize } from 'normalizr'; 3 | import { ActionCreators } from './actionTypes'; 4 | import { AppThunkAction } from '../store/types'; 5 | import { getIdSafePaper } from '../helpers/getIdSafeData'; 6 | import { paperSchema } from '../model/paper'; 7 | 8 | export function getRelatedPapers(paperId: string, cancelToken?: CancelToken): AppThunkAction { 9 | return async (dispatch, _getState, { axios }) => { 10 | dispatch(ActionCreators.startToGetRelatedPapers()); 11 | 12 | try { 13 | const getPapersResponse = await axios.get(`/papers/${paperId}/related`, { cancelToken }); 14 | const papers = getPapersResponse.data.data.map(getIdSafePaper); 15 | const res = normalize(papers, [paperSchema]); 16 | 17 | dispatch(ActionCreators.addEntity(res)); 18 | dispatch(ActionCreators.getRelatedPapers({ paperIds: res.result })); 19 | } catch (err) { 20 | if (!Axios.isCancel(err)) { 21 | dispatch(ActionCreators.failedToGetRelatedPapers()); 22 | } 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Icon/check 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/__mocks__/index.tsx: -------------------------------------------------------------------------------- 1 | import { Member } from '../model/member'; 2 | import { Fos } from '../model/fos'; 3 | import { Journal } from '../model/journal'; 4 | import { Paper } from '../model/paper'; 5 | import { PaperSource } from '../model/paperSource'; 6 | import { PaperAuthor } from '../model/author'; 7 | import { CurrentUser } from '../model/currentUser'; 8 | import { camelCaseKeys } from '../helpers/camelCaseKeys'; 9 | 10 | export const RAW = { 11 | AUTHOR_IN_PAPER: require('./paperAuthor.json') as PaperAuthor, 12 | AUTHOR: require('./author.json'), 13 | CURRENT_USER: require('./currentUser.json') as CurrentUser, 14 | FOS: require('./fos.json') as Fos, 15 | JOURNAL: require('./journal.json') as Journal, 16 | MEMBER: require('./member.json') as Member, 17 | PAPER: require('./paper.json') as Paper, 18 | PAPER_SOURCE: require('./paperSource.json') as PaperSource, 19 | AGGREGATION_RESPONSE: require('./aggregation.json'), 20 | JOURNAL_PAPERS_RESPONSE: require('./journalPapersResponse.json'), 21 | PAPER_FROM_CONFERENCE: camelCaseKeys(require('./paperFromConference.json')) as Paper, 22 | }; 23 | -------------------------------------------------------------------------------- /app/components/findInLibraryDialog/successRequestContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '../../icons'; 3 | const useStyles = require('isomorphic-style-loader/useStyles'); 4 | const s = require('./findInLibraryDialog.scss'); 5 | 6 | const SuccessRequestContext: React.FC<{ count: number; affiliationName: string }> = ({ count, affiliationName }) => { 7 | useStyles(s); 8 | 9 | return ( 10 |
11 |
12 | 13 |
SUCCESS
14 |
15 | Thank you.
We will notify you when the paper is available through your institution 16 |
17 | There are currently {count} requests
for{' '} 18 | {affiliationName}. 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default SuccessRequestContext; 26 | -------------------------------------------------------------------------------- /app/components/paperShow/refCitedTab/types.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'redux'; 2 | import { Paper } from '../../../model/paper'; 3 | import { CurrentUser } from '../../../model/currentUser'; 4 | import { RefCitedTabItem } from '../../../containers/paperShow/types'; 5 | 6 | export interface PaperShowRefCitedTabProps { 7 | paper: Paper; 8 | isFixed: boolean; 9 | isOnRef: boolean; 10 | isOnCited: boolean; 11 | isOnFullText: boolean; 12 | isLoading: boolean; 13 | currentUser: CurrentUser; 14 | canShowFullPDF: boolean; 15 | 16 | afterDownloadPDF: () => void; 17 | onClickDownloadPDF: () => void; 18 | onClickTabItem: (section: RefCitedTabItem) => () => void; 19 | } 20 | 21 | export interface TabItemProps { 22 | active: boolean; 23 | text: string; 24 | onClick: () => void; 25 | } 26 | 27 | export interface PDFButtonProps { 28 | dispatch: Dispatch; 29 | paper: Paper; 30 | isLoading: boolean; 31 | canShowFullPDF: boolean; 32 | actionBtnEl: HTMLDivElement | null; 33 | currentUser: CurrentUser; 34 | afterDownloadPDF: () => void; 35 | onClickDownloadPDF: () => void; 36 | } 37 | -------------------------------------------------------------------------------- /app/containers/collectionShow/sideEffect.tsx: -------------------------------------------------------------------------------- 1 | import { LoadDataParams } from '../../routes'; 2 | import { CollectionShowMatchParams } from './types'; 3 | import { getCollection, getPapers } from './actions'; 4 | import { ActionCreators } from '../../actions/actionTypes'; 5 | import { Dispatch } from 'redux'; 6 | 7 | export async function fetchCollectionShowData(params: LoadDataParams) { 8 | const { dispatch, match } = params; 9 | 10 | const collectionId = parseInt(match.params.collectionId); 11 | if (isNaN(collectionId)) { 12 | return dispatch( 13 | ActionCreators.failedToGetCollectionInCollectionShow({ 14 | statusCode: 400, 15 | }) 16 | ); 17 | } else { 18 | const promiseArr: ((dispatch: Dispatch) => Promise)[] = []; 19 | promiseArr.push(dispatch(getCollection(collectionId))); 20 | promiseArr.push( 21 | dispatch( 22 | getPapers({ 23 | collectionId, 24 | sort: 'RECENTLY_ADDED', 25 | page: 1, 26 | }) 27 | ) 28 | ); 29 | await Promise.all(promiseArr); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/common/paperFigure/smallPaperFigure.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LazyImage } from '@pluto_network/pluto-design-elements'; 3 | import { PaperFigure } from '../../../model/paper'; 4 | import { withStyles } from '../../../helpers/withStylesHelper'; 5 | import { FIGURE_PREFIX } from '../../../constants/paperFigure'; 6 | 7 | const styles = require('./smallPaperFigure.scss'); 8 | 9 | interface SmallPaperFigureProps { 10 | figure: PaperFigure; 11 | handleOpenFigureDetailDialog: () => void; 12 | } 13 | 14 | const SmallPaperFigure: React.FC = ({ figure, handleOpenFigureDetailDialog }) => { 15 | return ( 16 |
17 |
18 | 24 |
25 | ); 26 | }; 27 | 28 | export default withStyles(styles)(SmallPaperFigure); 29 | -------------------------------------------------------------------------------- /app/components/common/InputWithSuggestionList/inputWithSuggestionList.scss: -------------------------------------------------------------------------------- 1 | .inputWithListWrapper { 2 | position: relative; 3 | width: 100%; 4 | } 5 | 6 | .suggestionList { 7 | position: absolute; 8 | list-style: none; 9 | padding: 0; 10 | margin: 0; 11 | left: 0; 12 | right: 0; 13 | border-radius: 4px; 14 | 15 | &:focus { 16 | outline: none; 17 | } 18 | } 19 | 20 | .keywordCompletionItem { 21 | position: relative; 22 | text-align: left; 23 | width: 100%; 24 | overflow: hidden; 25 | background-color: white; 26 | font-size: 15px; 27 | font-weight: 400; 28 | color: $black1; 29 | border-bottom: solid 1px $gray200; 30 | cursor: pointer; 31 | white-space: pre; 32 | 33 | &:last-of-type { 34 | border-bottom: none; 35 | border-radius: 4px; 36 | } 37 | 38 | &:focus, 39 | &:hover { 40 | outline: none; 41 | background-color: $gray30; 42 | } 43 | 44 | b { 45 | color: $main_blue0; 46 | font-weight: bold; 47 | } 48 | } 49 | 50 | .highLightKeywordCompletionItem { 51 | @extend .keywordCompletionItem; 52 | 53 | background-color: $gray30; 54 | } 55 | -------------------------------------------------------------------------------- /app/containers/filterBox/autocompleteFilter/filterItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import * as classNames from 'classnames'; 4 | import Checkbox from '@material-ui/core/Checkbox'; 5 | import { withStyles } from '../../../helpers/withStylesHelper'; 6 | const s = require('./autocompleteFilter.scss'); 7 | 8 | interface FilterItemProps { 9 | content: string; 10 | checked: boolean; 11 | to: string; 12 | isHighlight: boolean; 13 | } 14 | 15 | const FilterItem: React.FunctionComponent = props => { 16 | return ( 17 | 24 | 31 | {props.content} 32 | 33 | ); 34 | }; 35 | 36 | export default withStyles(s)(FilterItem); 37 | -------------------------------------------------------------------------------- /app/api/suggest.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import PlutoAxios from './pluto'; 3 | import { PaginationResponseV2 } from './types/common'; 4 | 5 | export interface SuggestAffiliation { 6 | type: string; 7 | keyword: string; 8 | affiliationId: string; 9 | } 10 | 11 | class SuggestAPI extends PlutoAxios { 12 | public async getAffiliationSuggest(q: string): Promise> { 13 | const res: AxiosResponse = await this.get(`/complete/affiliation`, { 14 | params: { 15 | q, 16 | }, 17 | }); 18 | 19 | const suggestionData: PaginationResponseV2 = res.data; 20 | const safeSuggestionData = { 21 | ...suggestionData, 22 | data: { 23 | ...suggestionData.data, 24 | content: suggestionData.data.content.map(affiliation => ({ 25 | ...affiliation, 26 | affiliationId: String(affiliation.affiliationId), 27 | })), 28 | }, 29 | }; 30 | 31 | return safeSuggestionData; 32 | } 33 | } 34 | 35 | const suggestAPI = new SuggestAPI(); 36 | 37 | export default suggestAPI; 38 | -------------------------------------------------------------------------------- /app/components/paperShow/components/doiInPaperShow.scss: -------------------------------------------------------------------------------- 1 | .doiContext { 2 | font-size: 16px; 3 | line-height: 1.6; 4 | color: $gray600; 5 | } 6 | 7 | .doiTitle { 8 | @extend .doiContext; 9 | color: $gray800; 10 | font-weight: 500; 11 | } 12 | 13 | .doiWrapper { 14 | display: inline-block; 15 | margin-left: 2px; 16 | } 17 | 18 | .tinyButton { 19 | display: inline-flex; 20 | justify-content: center; 21 | align-items: center; 22 | float: right; 23 | margin-left: 4px; 24 | outline: none; 25 | border: none; 26 | cursor: pointer; 27 | border-radius: 8px; 28 | padding: 4px 6px; 29 | color: $main_blue1; 30 | 31 | &:hover { 32 | background-color: $gray50; 33 | } 34 | 35 | &:active { 36 | filter: brightness(95%); 37 | } 38 | 39 | i { 40 | display: inline-flex; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | 45 | svg { 46 | width: 16px; 47 | height: 16px; 48 | vertical-align: middle; 49 | margin-right: 4px; 50 | } 51 | 52 | span { 53 | color: $main_blue1; 54 | font-size: 14px; 55 | vertical-align: middle; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/containers/profileForm/profileForm.scss: -------------------------------------------------------------------------------- 1 | @import '../userSettings/userSettings'; 2 | 3 | .title { 4 | font-weight: 600; 5 | line-height: 1.83; 6 | letter-spacing: -0.67px; 7 | font-size: 24px; 8 | color: $black0; 9 | margin-bottom: 24px; 10 | } 11 | 12 | .formRow { 13 | display: flex; 14 | } 15 | 16 | .formWrapper { 17 | width: 100%; 18 | } 19 | 20 | .formWrapper + .formWrapper { 21 | margin-left: 21px; 22 | } 23 | 24 | .inputForm { 25 | @extend %base-input-style; 26 | 27 | &:disabled { 28 | background-color: transparent; 29 | border-color: $gray400; 30 | color: $gray500; 31 | } 32 | } 33 | 34 | .formLabel { 35 | @extend %base-label; 36 | } 37 | 38 | .affiliationFormWrapper { 39 | margin-top: 44px; 40 | margin-bottom: 36px; 41 | } 42 | 43 | .errorMsg { 44 | font-family: $fallback; 45 | font-size: 13px; 46 | color: $red0; 47 | margin: 8px 0 0 8px; 48 | } 49 | 50 | .affiliationErrorMsg { 51 | font-family: $fallback; 52 | font-size: 13px; 53 | color: $red0; 54 | margin-top: 8px; 55 | } 56 | 57 | .divider { 58 | @extend %base-divider; 59 | 60 | margin: 40px 0 16px 0; 61 | } 62 | -------------------------------------------------------------------------------- /app/components/copyDOIButton/copyDOIButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import Icon from '../../icons'; 3 | import copySelectedTextToClipboard from '../../helpers/copySelectedTextToClipboard'; 4 | import ActionTicketManager from '../../helpers/actionTicketManager'; 5 | 6 | interface Props { 7 | doi: string; 8 | paperId: string; 9 | pageType: Scinapse.ActionTicket.PageType; 10 | actionArea: Scinapse.ActionTicket.ActionArea; 11 | className?: string; 12 | } 13 | 14 | const CopyDOIButton: FC = ({ doi, paperId, className, pageType, actionArea }) => { 15 | if (!doi) return null; 16 | 17 | const clickDOIButton = () => { 18 | copySelectedTextToClipboard(`https://doi.org/${doi}`); 19 | ActionTicketManager.trackTicket({ 20 | pageType, 21 | actionType: 'fire', 22 | actionArea, 23 | actionTag: 'copyDoi', 24 | actionLabel: String(paperId), 25 | }); 26 | }; 27 | 28 | return ( 29 | 33 | ); 34 | }; 35 | 36 | export default CopyDOIButton; 37 | -------------------------------------------------------------------------------- /app/components/findInLibraryDialog/alreadyRequestContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '../../icons'; 3 | const useStyles = require('isomorphic-style-loader/useStyles'); 4 | const s = require('./findInLibraryDialog.scss'); 5 | 6 | const AlreadyRequestContext: React.FC<{ count: number; affiliationName: string }> = ({ count, affiliationName }) => { 7 | useStyles(s); 8 | 9 | return ( 10 |
11 |
12 | 13 |
IN PROGRESS
14 |
15 | You have already submitted a request to your institution to link with Scinapse.
16 | We will notify you when it’s complete. 17 |
18 | There are currently {count} requests
for{' '} 19 | {affiliationName}. 20 |
21 |
22 |
23 | ); 24 | }; 25 | 26 | export default AlreadyRequestContext; 27 | -------------------------------------------------------------------------------- /app/components/common/paperItem/title.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | vertical-align: top; 3 | font-size: 20px; 4 | line-height: 1.4; 5 | color: $navy0; 6 | font-weight: 500; 7 | 8 | b { 9 | font-weight: 900; 10 | vertical-align: top; 11 | } 12 | 13 | &:hover { 14 | text-decoration: underline; 15 | } 16 | 17 | &:visited { 18 | color: $purple0; 19 | } 20 | } 21 | 22 | .externalIconWrapper { 23 | display: inline-flex; 24 | height: 24px; 25 | margin-top: 2px; 26 | margin-left: 2px; 27 | justify-content: center; 28 | align-items: center; 29 | color: $navy0; 30 | 31 | &:visited { 32 | color: $purple0; 33 | } 34 | 35 | .externalIcon { 36 | width: 20px; 37 | height: 20px; 38 | 39 | svg { 40 | width: 20px; 41 | height: 20px; 42 | } 43 | } 44 | 45 | .newLabel { 46 | font-size: 10px; 47 | color: $main_blue0; 48 | margin-left: 4px; 49 | } 50 | } 51 | 52 | // Mobile 53 | @media (max-width: $mobile_width) { 54 | .title { 55 | font-size: 16px; 56 | line-height: 1; 57 | font-weight: 500; 58 | } 59 | 60 | .externalIconWrapper { 61 | display: none; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/__mocks__/author.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2258681040", 3 | "name": "H. Goldschmidt", 4 | "email": "scshinjr@gmail.com", 5 | "bio": 6 | "Local server runs with Express and webpack-dev-server.\nBecause Scinapse is the universal rendering web-app, it needs both server-side app file and client-side app file. \nLocal server runs with Express and webpack-dev-server.\nBecause Scinapse is the universal rendering web-app, it needs both server-side app file and client-side app file.\nLocal server runs with Express and webpack-dev-server.\nBecause Scinapse is the universal rendering web-app, it needs both server-side app file and client-side app file.\nLocal server runs with Express and webpack-dev-server.\nBecause Scinapse is the universal rendering web-app, it needs both server-side app file and client-side app file.", 7 | "web_page": null, 8 | "hindex": null, 9 | "last_known_affiliation": { "id": "223822909", "name": "Heidelberg University" }, 10 | "paper_count": 19, 11 | "citation_count": 3398, 12 | "is_layered": true, 13 | "representative_papers": [], 14 | "fos_list": [], 15 | "top_papers": [], 16 | "profile_id": null, 17 | "is_profile_connected": false 18 | } 19 | --------------------------------------------------------------------------------