Commit 8891132b authored by Vincent's avatar Vincent

List endorsements on the website

parent 79a774b1
......@@ -7,7 +7,11 @@ services:
context: .
dockerfile: docker/app/Dockerfile
ports:
# Website:
- 8000:8000
# API:
- 5000:5000
# Widget:
- 3000:3000
volumes:
- .:/usr/src/app/:rw
......
......@@ -8,6 +8,7 @@ RUN apk add --no-cache python make g++
COPY ["./package.json", "yarn.lock", "/usr/src/app/"]
COPY ["./server/package.json", "server/yarn.lock", "/usr/src/app/server/"]
COPY ["./client/package.json", "client/yarn.lock", "/usr/src/app/client/"]
COPY ["./website/package.json", "website/yarn.lock", "/usr/src/app/website/"]
EXPOSE 3000
EXPOSE 5000
RUN ["yarn"]
......
......@@ -7,7 +7,7 @@
"build-client": "cd client; yarn run build; yarn run build-storybook; cd ../; mkdir -p dist/client; cp -r client/build/* dist/client; cp -r client/node_modules dist/client/; mkdir -p dist/storybook; cp -r client/build_storybook/* dist/storybook;",
"build-website": "cd website; rm -rf dist/; yarn run build; cd ../; mkdir -p dist/website; cp -r website/dist/* dist/website;",
"build": "rm -rf dist/; yarn run build-server; yarn run build-embed; yarn run build-client; yarn run build-website;",
"watch": "concurrently \"cd server; yarn watch;\" \"cd client; yarn watch;\"",
"watch": "concurrently \"cd server; yarn watch;\" \"cd client; yarn watch;\" \"cd website; yarn watch;\"",
"start": "cd dist/server; node index.js",
"postinstall": "cd server; yarn; cd ../embed; yarn; cd ../client; yarn; cd ../website; yarn;",
"heroku-postbuild": "yarn run build"
......
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 256 256" style="enable-background:new 0 0 256 256;" xml:space="preserve">
<style type="text/css">
.st0{fill:#A6CE39;}
.st1{fill:#FFFFFF;}
</style>
<path class="st0" d="M256,128c0,70.7-57.3,128-128,128C57.3,256,0,198.7,0,128C0,57.3,57.3,0,128,0C198.7,0,256,57.3,256,128z"/>
<g>
<path class="st1" d="M86.3,186.2H70.9V79.1h15.4v48.4V186.2z"/>
<path class="st1" d="M108.9,79.1h41.6c39.6,0,57,28.3,57,53.6c0,27.5-21.5,53.6-56.8,53.6h-41.8V79.1z M124.3,172.4h24.5
c34.9,0,42.9-26.5,42.9-39.7c0-21.5-13.7-39.7-43.7-39.7h-23.7V172.4z"/>
<path class="st1" d="M88.7,56.8c0,5.5-4.5,10.1-10.1,10.1c-5.6,0-10.1-4.6-10.1-10.1c0-5.6,4.5-10.1,10.1-10.1
C84.2,46.7,88.7,51.3,88.7,56.8z"/>
</g>
</svg>
\ No newline at end of file
......@@ -5,7 +5,7 @@
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "react-static start",
"watch": "react-static start",
"stage": "react-static build --staging",
"build": "react-static build",
"serve": "serve dist -p 3000"
......@@ -33,7 +33,7 @@
"sass-loader": "^7.1.0",
"serve": "^6.5.3",
"ts-loader": "^3.5.0",
"typescript": "^2.7.2"
"typescript": "^3.2.1"
},
"prettier": {
"semi": false,
......
import React from 'react'
import { Router, Link, Head } from 'react-static'
import { Router, Link, Head, Route, Switch } from 'react-static'
import { hot } from 'react-hot-loader'
import Routes from 'react-static-routes'
import universal from 'react-universal-component';
import './app.scss'
const eLifeLogo = require('../assets/partners/elife.png');
const cosLogo = require('../assets/partners/cos.png');
const Work = universal(import('./containers/Work'));
const App = () => (
<Router>
<div>
......@@ -17,7 +20,10 @@ const App = () => (
<meta name="keywords" content="plaudit, orcid, crossref, open access, open science, doi"/>
</Head>
<main role="main">
<Routes />
<Switch>
<Route path="/endorsements/:pid+" component={Work} />
<Routes />
</Switch>
</main>
<footer className="footer">
<div className="container">
......
......@@ -5,7 +5,8 @@
$cyan: #0288d1;
$grey-dark: #30434e;
$grey: #f8fafb;
$grey-light: #f8fafb;
$grey: darken($grey-light, 10%);
$orange: #ff8d02;
$red: #ff3f02;
$yellow: #ffbc02;
......@@ -41,3 +42,55 @@ $primary: $cyan;
border-bottom-color: $primary;
}
}
.orcidIcon {
display: inline-block;
width: 1rem;
height: 1rem;
margin: 0;
padding: 0;
margin-right: 0.5rem;
}
img.orcidIcon {
filter: grayscale(1);
a:hover &,
a:focus & {
filter: grayscale(0);
}
}
.hero.is-primary .loadingTitle {
height: $size-1;
background-image: linear-gradient( $grey $size-1, transparent 0);
background-size: percentage(0.5 + random()/2) $size-1;
background-repeat: no-repeat;
margin-bottom: 0.5em;
}
.hero.is-primary .loadingSubTitle {
height: $size-3;
background-image: linear-gradient( $grey $size-3, transparent 0);
background-size: percentage(random()/2) $size-3;
background-repeat: no-repeat;
}
.section .loadingHeading {
height: $size-4;
background-image: linear-gradient( $grey $size-4, transparent 0);
background-size: percentage(0.5 + random()/2) $size-4;
background-repeat: no-repeat;
margin-bottom: 0.5em;
}
.section .loadingEndorsement {
height: $size-4;
background-image:
linear-gradient( $grey $size-4, transparent 0),
linear-gradient( $grey $size-4, transparent 0);
background-size: $size-4, percentage(0.2 + random()/2) $size-4;
background-repeat: no-repeat;
background-position: 0 0, ($size-4 + 0.5rem) 0;
margin-bottom: 0.5rem;
}
.loadingFullwidthButton {
height: $size-4;
background-image: linear-gradient( $grey $size-4, transparent 0);
}
import React from 'react';
import { Endorsement as EndorsementType } from '../../../lib/endpoints';
const orcidIcon = require('../../assets/icons/orcid.svg');
interface Props {
endorsement: EndorsementType & { userName?: string };
}
export const Endorsement: React.SFC<Props> = (props) => (
<a
href={`https://orcid.org/${props.endorsement.orcid}`}
title={`View the ORCID profile of ${props.endorsement.userName || props.endorsement.orcid}`}
className="content is-medium"
>
<img
src={orcidIcon}
className="orcidIcon"
alt=""
/>
{props.endorsement.userName || props.endorsement.orcid}
</a>
);
import React from 'react'
export default () => (
<div>
<h1>This page could not be found</h1>
</div>
)
// To prevent the 404 page from being prerendered during the build, we only render it after it has
// been mounted (which doesn't happen in the build). We need to prevent it from being prerendered,
// because otherwise it will be shown for dynamic routes (i.e. routes that are not prerendered
// during the build, such as the page that lists the latest endorsements) just before the component
// on that route is rendered.
// See https://github.com/nozzle/react-static/blob/master/examples/non-static-routing/src/containers/404.js
interface State {
hasMounted: boolean;
}
export default class extends React.Component<{}, State> {
state = {
hasMounted: false,
}
componentDidMount () {
this.logMount()
}
logMount = () => {
if (!this.state.hasMounted) {
this.setState({
hasMounted: true,
})
}
}
render () {
return this.state.hasMounted ? (
<div>
<h1>This page could not be found</h1>
</div>
) : null
}
}
import React from 'react'
import { RouteComponentProps } from 'react-static';
import NotFound from './404';
import { fetchDoiMetadata, CslJson } from '../data/doi';
import Loading from './Work/Loading';
import WorkDetails from './Work/WorkDetails';
import { GetEndorsementsResponse } from '../../../lib/endpoints';
import { getEndorsements } from '../data/plaudit';
interface OwnProps {}
interface RouteParams {
pid: string;
}
type Props = OwnProps & RouteComponentProps<RouteParams>;
interface State {
workDetails?: CslJson;
endorsementData?: GetEndorsementsResponse;
}
// A DOI is `10.`, followed by a number of digits, then a slash, and then any number of characters
const doiRegex = /^10\.\d{4,}\/.*$/;
export default class extends React.Component<Props, State> {
state: State = {};
componentDidMount() {
this.fetchDetails();
this.fetchEndorsements();
}
async fetchDetails() {
const [ identifierType, identifier ] = this.props.match.params.pid.split(':');
if (
identifierType !== 'doi' ||
typeof identifier === 'undefined' ||
!doiRegex.test(identifier)
) {
return;
}
const details = await fetchDoiMetadata(identifier);
this.setState({ workDetails: details });
}
async fetchEndorsements() {
const [ identifierType, identifier ] = this.props.match.params.pid.split(':');
if (
identifierType !== 'doi' ||
typeof identifier === 'undefined' ||
!doiRegex.test(identifier)
) {
return;
}
const endorsementData = await getEndorsements(identifier);
this.setState({ endorsementData: endorsementData });
}
render () {
const [ identifierType, identifier ] = this.props.match.params.pid.split(':');
if (
identifierType !== 'doi' ||
typeof identifier === 'undefined' ||
!doiRegex.test(identifier)
) {
return <NotFound/>;
}
if (
typeof this.state.workDetails === 'undefined'
|| typeof this.state.endorsementData === 'undefined'
) {
return <Loading/>;
}
return (
<WorkDetails
work={this.state.workDetails}
endorsements={this.state.endorsementData}
/>
);
}
};
import React from 'react'
export default () => (
<div>
<header className="hero is-primary is-medium">
<div className="hero-body">
<div className="container">
<div className="loadingTitle"/>
<div className="loadingSubTitle"/>
</div>
</div>
</header>
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-half-widescreen">
<div className="loadingHeading"/>
<div className="loadingEndorsement"/>
<div className="loadingEndorsement"/>
</div>
<div className="column is-one-quarter-widescreen is-offset-one-quarter-widescreen">
<div className="loadingFullwidthButton"/>
</div>
</div>
</div>
</section>
</div>
)
import React from 'react'
import { CslJson, CiteProcName, isCiteProcInstitution } from '../../data/doi';
import { GetEndorsementsResponse } from '../../../../lib/endpoints';
import { Endorsement } from '../../components/Endorsement';
interface Props {
work: CslJson;
endorsements: GetEndorsementsResponse;
}
export default (props: Props) => (
<div>
<header className="hero is-primary is-medium">
<div className="hero-body">
<div className="container">
<h1 className="title">{props.work.title}</h1>
<p className="subtitle">{formatAuthors(props.work.author)}</p>
</div>
</div>
</header>
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-half-widescreen">
{renderEndorsements(props.endorsements)}
</div>
<div className="column is-one-quarter-widescreen is-offset-one-quarter-widescreen">
<a
href={props.work.URL || `https://doi.org/${props.work.DOI}`}
className="button is-text is-fullwidth"
title="View this article"
>View this article</a>
</div>
</div>
</div>
</section>
</div>
)
function formatAuthors(authors: CiteProcName[]) {
return authors
.map(author => {
if (isCiteProcInstitution(author)) {
return author.literal;
}
if (typeof author.given !== 'undefined') {
// This will probably be displayed incorrectly in some locales;
// not sure how to deal with the data as given in this way:
return `${author.given} ${author.family}`;
}
return author.family;
})
.join(', ');
}
function renderEndorsements(data: GetEndorsementsResponse) {
if (data.endorsements.length === 0) {
return (
<div>
<h2 className="title is-size-4">No endorsements</h2>
<p className="content is-medium">This article has not been endorsed yet.</p>
</div>
);
}
const list = data.endorsements.map((endorsement) => {
return (
<li key={`${endorsement.doi}-${endorsement.orcid}-${endorsement.type}`}>
<Endorsement endorsement={endorsement}/>
</li>
);
})
const title = (data.endorsements.length === 1)
? '1 endorsement:'
: `${data.endorsements.length} endorsements:`;
return (
<div>
<h2 className="title is-size-4">{title}</h2>
<ul>
{list}
</ul>
</div>
);
}
// See https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#date-fields
export type CiteProcDate =
// [year, month, day of month]:
[ [ number, number, number ] ] |
// [start year, start month, day of start month], [end year, end month, day of end month]:
[ [ number, number, number], [ number, number, number ] ]
;
// See https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#name-fields
export interface CiteProcPerson {
family: string;
given?: string;
suffix?: string;
'non-dropping-particle'?: string;
}
export interface CiteProcInstitution {
literal: string;
}
export type CiteProcName = CiteProcPerson | CiteProcInstitution;
export function isCiteProcInstitution(person: CiteProcName): person is CiteProcInstitution {
return typeof (person as CiteProcInstitution).literal !== 'undefined';
}
// See https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#items
interface CiteProcItem {
id: string;
DOI: string;
type: 'article-journal' | 'article' | 'book' | 'dataset' | string;
title: string;
author: CiteProcName[];
issued: { 'date-parts': CiteProcDate };
URL?: string;
abstract?: string;
}
// See https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html
export type CslJson = CiteProcItem;
// See https://crosscite.org/docs.html
export async function fetchDoiMetadata(doi:string) {
// Note: some example DOIs from different registration agencies can be found at
// https://www.doi.org/demos.html
const response = await fetch(
`https://doi.org/${doi}`,
{
headers: {
// CrossRef, DataCite and mEDRA all supposedly support CSL-JSON, so that should cover most works:
// https://crosscite.org/docs.html#sec-4
Accept: 'application/vnd.citationstyles.csl+json',
},
},
);
const data: CslJson = await response.json();
return data;
}
import { GetEndorsementsResponse } from '../../../lib/endpoints';
export const getEndorsements = async (pid: string) => {
const referrer = encodeURIComponent((document.location || '').toString())
const response = await fetch(
`/api/endorsements/${pid}?embedder_id=plaudit&referrer=${referrer}`,
{
credentials: 'same-origin',
},
);
const data: GetEndorsementsResponse = await response.json();
return data;
};
export interface Post {
body: string
id: number
title: string
}
......@@ -131,6 +131,12 @@ export default {
]
return config
},
devServer: {
port: 8000,
proxy: {
'/api': 'http://localhost:5000',
},
},
// Hook into the React-Static lifecycles to add favicons
// See https://www.npmjs.com/package/@kuroku/react-static-favicons#usage
renderToHtml: async (render, C, meta) => {
......
......@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2015",
"module": "esnext",
"lib": ["es2015"],
"lib": ["es2015", "dom"],
"allowJs": true,
"jsx": "preserve",
"sourceMap": true,
......
......@@ -9592,10 +9592,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^2.7.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
typescript@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.1.tgz#0b7a04b8cf3868188de914d9568bd030f0c56192"
integrity sha512-jw7P2z/h6aPT4AENXDGjcfHTu5CSqzsbZc6YlUIebTyBAq8XaKp78x7VcSh30xwSCcsu5irZkYZUSFP1MrAMbg==
uglify-js@3.4.x:
version "3.4.9"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment