diff --git a/packages/admin-frontend/components/EditBook/EditBookForm.js b/packages/admin-frontend/components/EditBook/EditBookForm.js index 23e68948a..414b259f3 100644 --- a/packages/admin-frontend/components/EditBook/EditBookForm.js +++ b/packages/admin-frontend/components/EditBook/EditBookForm.js @@ -6,7 +6,8 @@ import { Button, TextField, Typography, - Select + Select, + Snackbar } from '@material-ui/core'; import * as React from 'react'; import Link from 'next/link'; @@ -20,14 +21,28 @@ import Container from '../Container'; import Row from '../Row'; import BookCover from './BookCover'; +import { saveFeaturedContent } from '../../lib/fetch'; +import type { FeaturedContent } from '../../types'; +import getConfig from 'next/config'; + +const { + publicRuntimeConfig: { baseUrl } +} = getConfig(); + const PUBLISHING_STATUS = ['PUBLISHED', 'FLAGGED', 'UNLISTED']; const PAGE_ORIENTATIONS = ['PORTRAIT', 'LANDSCAPE']; type Props = { book: BookDetails }; +type State = { + snackbarMessage: ?string +}; -export default class EditBookForm extends React.Component { +export default class EditBookForm extends React.Component { + state = { + snackbarMessage: null + }; handleSubmit = (content: BookDetails) => { this.updateBook(content); }; @@ -43,8 +58,33 @@ export default class EditBookForm extends React.Component { } }; + postNewFeaturedContent = async (content: FeaturedContent) => { + const result = await saveFeaturedContent( + content, + this.props.book.language.code + ); + if (result.isOk) { + this.setState({ + snackbarMessage: `${this.props.book.title} is added to featured content` + }); + } + }; + render() { const book = this.props.book; + const { snackbarMessage } = this.state; + const content = { + id: 0, + title: book.title, + description: book.description, + language: book.language, + //default image (Grace in Space) if book does not have cover image + imageUrl: + book.coverImage !== undefined + ? book.coverImage.url + : 'https://res.cloudinary.com/dwqxoowxi/f_auto,q_auto/e7ad2d851664f1485743e157c46f7142', + link: `${baseUrl}/${book.language.code}/books/details/${book.id}` + }; return ( {' '} @@ -150,7 +190,6 @@ export default class EditBookForm extends React.Component { > Discard changes - + )} /> + this.setState({ snackbarMessage: null })} + ContentProps={{ + 'aria-describedby': 'feature-content-snack-msg' + }} + message={ + + {snackbarMessage} + + } + /> ); } diff --git a/packages/admin-frontend/config.js b/packages/admin-frontend/config.js index 4f5f8de93..71c68cf1b 100644 --- a/packages/admin-frontend/config.js +++ b/packages/admin-frontend/config.js @@ -46,6 +46,19 @@ const imageApiUrl = () => { } }; +const baseUrl = () => { + switch (GDL_ENVIRONMENT) { + case 'local': + return 'http://localhost:3000'; + case 'dev': + return 'https://test.digitallibrary.io'; + case 'prod': + return 'https://digitallibrary.io'; + default: + return `https://${GDL_ENVIRONMENT}.digitallibrary.io`; + } +}; + module.exports = { serverRuntimeConfig: { port: process.env.ADMIN_FRONTEND_PORT || 3010 @@ -53,6 +66,7 @@ module.exports = { publicRuntimeConfig: { imageApiUrl: imageApiUrl(), bookApiUrl: bookApiUrl(), - statisticsUrl: statisticsUrl() + statisticsUrl: statisticsUrl(), + baseUrl: baseUrl() } }; diff --git a/packages/admin-frontend/pages/_app.js b/packages/admin-frontend/pages/_app.js index 0ff7cee22..8a8d307bf 100644 --- a/packages/admin-frontend/pages/_app.js +++ b/packages/admin-frontend/pages/_app.js @@ -51,7 +51,6 @@ class App extends NextApp { } const userHasAuthToken = hasAuthToken(ctx.req); - const userHasAdminPrivileges = hasClaim(claims.readAdmin, ctx.req); // If we have response object, set a proper HTTP status code diff --git a/packages/admin-frontend/pages/admin/featured.js b/packages/admin-frontend/pages/admin/featured.js index ddb604e37..5d32f8374 100644 --- a/packages/admin-frontend/pages/admin/featured.js +++ b/packages/admin-frontend/pages/admin/featured.js @@ -10,13 +10,18 @@ import * as React from 'react'; import { Select, Button, - FormHelperText, InputLabel, FormControl, - TextField, - Typography + Typography, + Card, + CardContent, + CardMedia, + CardActions, + Dialog, + DialogActions, + DialogContent, + DialogTitle } from '@material-ui/core'; -import { Form, Field, FormSpy } from 'react-final-form'; import { fetchLanguages, fetchFeaturedContent, @@ -24,22 +29,25 @@ import { saveFeaturedContent, deleteFeaturedContent } from '../../lib/fetch'; -import UploadFileDialog from '../../components/UploadFileDialog'; -import FeaturedImage from '../../components/FeaturedImage'; import Layout from '../../components/Layout'; -import Row from '../../components/Row'; import Container from '../../components/Container'; -import isEmptyString from '../../lib/isEmptyString'; import type { FeaturedContent, Language } from '../../types'; +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import FeaturedEdit from './featuredEdit'; +import FeatureAdd from './featuredAdd'; type Props = { languages: Array }; type State = { - featuredContent: ?FeaturedContent, + featuredContentList: Array, selectedLanguage: string, - file: ?File + file: ?File, + openDeleteDialog: boolean, + placementOfSelectedContent: number, + selectedContent: null | FeaturedContent }; export default class EditFeaturedContent extends React.Component { @@ -52,26 +60,26 @@ export default class EditFeaturedContent extends React.Component { } state = { - featuredContent: null, selectedLanguage: '', croppedParameters: null, - file: null + file: null, + featuredContentList: [], + openDeleteDialog: false, + selectedContent: null, + placementOfSelectedContent: 0 }; getFeaturedContent = async (languageCode: string) => { const featuredContentRes = await fetchFeaturedContent(languageCode); - const featuredContent = featuredContentRes.isOk - ? featuredContentRes.data[0] - : null; - if (featuredContent) { - if (featuredContent.language.code !== languageCode) { + if (featuredContentRes.isOk && featuredContentRes.data[0]) { + if (featuredContentRes.data[0].language.code !== languageCode) { this.setState({ - featuredContent: null + featuredContentList: [] }); } else { this.setState({ - featuredContent: featuredContent + featuredContentList: featuredContentRes.data }); } } @@ -87,20 +95,29 @@ export default class EditFeaturedContent extends React.Component { this.state.selectedLanguage ); if (result.isOk) { - this.setState(prevState => ({ - featuredContent: { ...prevState.featuredContent, id: result.data.id } - })); + this.setState(() => { + const featuredContentList = this.state.featuredContentList.map(item => { + if (item.id === content.id) { + return content; + } else { + return item; + } + }); + return { + featuredContentList + }; + }); } }; - - handleSaveButtonClick = (defaultReturned: boolean) => ( - content: FeaturedContent - ) => { - defaultReturned - ? this.postFeaturedContent(content) - : this.putFeaturedContent(content); - - this.setState({ featuredContent: content }); + postNewFeaturedContent = async (content: FeaturedContent) => { + console.log(content); + const result = await saveFeaturedContent( + content, + this.state.selectedLanguage + ); + if (result.isOk) { + this.getFeaturedContent(this.state.selectedLanguage); + } }; handleLanguageSelect = (event: SyntheticInputEvent) => { @@ -114,12 +131,32 @@ export default class EditFeaturedContent extends React.Component { deleteFeaturedContent = async (id: number) => { await deleteFeaturedContent(id); }; + handleCloseDeleteDialog = () => { + this.setState({ + openDeleteDialog: false, + selectedContent: null + }); + }; - handleDelete = () => { - if (this.state.featuredContent) { - this.deleteFeaturedContent(this.state.featuredContent.id); - this.setState({ featuredContent: null }); - } + handleOpenDeleteDialog = (content: FeaturedContent, i: number) => { + this.setState({ + openDeleteDialog: true, + selectedContent: content, + placementOfSelectedContent: i + }); + }; + handleDelete = (id: number, index: number) => { + this.deleteFeaturedContent(this.state.featuredContentList[index].id); + this.setState(state => { + const featuredContentList = this.state.featuredContentList.filter( + item => item.id !== id + ); + return { + featuredContentList, + openDeleteDialog: false, + selectedContent: null + }; + }); }; handleOnUpload = ( @@ -139,18 +176,83 @@ export default class EditFeaturedContent extends React.Component { file: event.target.files[0] }); }; + handleSaveButtonClick = ( + defaultReturned: boolean, + content: FeaturedContent + ) => { + defaultReturned + ? this.postFeaturedContent(content) + : this.putFeaturedContent(content); + + this.setState(() => { + const featuredContentList = this.state.featuredContentList.map(item => { + if (item.id === content.id) { + return content; + } else { + return item; + } + }); + return { + featuredContentList + }; + }); + }; + + handleSaveButtonClickNew = ( + defaultReturned: boolean, + content: FeaturedContent + ) => { + this.postNewFeaturedContent(content); + }; + + getDialog = () => { + if (this.state.selectedContent) { + const contentId = this.state.selectedContent.id; + return ( + + + + Do you want to delete {this.state.selectedContent.title} from + featured content? + + + + + + + + ); + } + }; render() { - const { featuredContent, selectedLanguage } = this.state; + const { selectedLanguage, featuredContentList } = this.state; // If the language of the featured content is different from what we expected to fetch, there is no featured content for that language. A request defaults to english if it does not exist. let defaultReturned = true; if ( - featuredContent && - featuredContent.language && - featuredContent.language.code + featuredContentList[0] && + featuredContentList[0].language && + featuredContentList[0].language.code ) { - defaultReturned = featuredContent.language.code !== selectedLanguage; + defaultReturned = + featuredContentList[0].language.code !== selectedLanguage; } return ( @@ -180,168 +282,83 @@ export default class EditFeaturedContent extends React.Component { ; -
( - - ( - - )} - /> - ( - - )} - /> - ( - <> - - {meta.error && meta.touched && ( - {meta.error} - )} - - )} - /> - + There is no featured content for the language{' '} + {selectedLanguage} +

+ ) : null} +
+ {featuredContentList.map((content, i) => { + return ( + -
- ( - <> - - {meta.error && meta.touched && ( - {meta.error} - )} - - )} - /> -
- - or - - this.handleFileChosen(event)} + - - {this.state.file && ( - this.handleOnUpload(url, form.change)} + +

{content.title}

+

{content.description}

+
+ + Edit} + featuredContentList={featuredContentList} + selectedLanguage={selectedLanguage} + i={i} + defaultReturned={defaultReturned} + handleSaveButtonClick={this.handleSaveButtonClick} + handleFileChosen={this.handleFileChosen} + handleOnCancel={this.handleOnCancel} + file={this.state.file} + handleOnUpload={this.handleOnUpload} /> - )} - - - // $FlowFixMe - values.imageUrl ? ( - - ) : null - } - /> - - - - - - )} - /> + + +
+ ); + })} + {selectedLanguage !== '' + ? FeatureAdd( + defaultReturned, + this.handleSaveButtonClickNew, + this.handleFileChosen, + this.handleOnCancel, + this.state.file, + this.handleOnUpload, + this.state.featuredContentList, + this.state.selectedLanguage + ) + : null} +
+ {this.getDialog()} ); } } -function handleValidate(values) { - const errors = {}; - - if (isEmptyString(values.title)) { - errors.title = 'Required'; - } - - if (isEmptyString(values.description)) { - errors.description = 'Required'; - } - - const regex = /http(s)?:\/\/.*/; - if (isEmptyString(values.link) || !values.link.match(regex)) { - errors.link = 'Must be a valid URL e.g "https://digitallibrary.io"'; - } - - if (isEmptyString(values.imageUrl) || !values.imageUrl.match(regex)) { - errors.imageUrl = - 'Must be a valid URL image url e.g "https://images.digitallibrary.io/imageId.png'; - } - - return errors; -} diff --git a/packages/admin-frontend/pages/admin/featuredAdd.js b/packages/admin-frontend/pages/admin/featuredAdd.js new file mode 100644 index 000000000..250df3b9c --- /dev/null +++ b/packages/admin-frontend/pages/admin/featuredAdd.js @@ -0,0 +1,66 @@ +//@flow +import { Card } from '@material-ui/core'; +import { Add } from '@material-ui/icons'; +import FeaturedEdit from './featuredEdit'; +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import type { FeaturedContent } from '../../types'; + +export default function FeatureAdd( + defaultReturned: boolean, + handleSaveButtonClick: (boolean, FeaturedContent) => void, + handleFileChosen: (SyntheticInputEvent) => void, + handleOnCancel: () => void, + file: ?File, + handleOnUpload: (string, (string, any) => void) => void, + featuredContentList: Array, + selectedLanguage: string +) { + return ( + +
+ +

+ ADD +

+
+ + } + i={featuredContentList.length} + featuredContentList={featuredContentList} + defaultReturned={defaultReturned} + handleSaveButtonClick={handleSaveButtonClick} + handleFileChosen={handleFileChosen} + handleOnCancel={handleOnCancel} + file={file} + handleOnUpload={handleOnUpload} + selectedLanguage={selectedLanguage} + /> + ); +} diff --git a/packages/admin-frontend/pages/admin/featuredEdit.js b/packages/admin-frontend/pages/admin/featuredEdit.js new file mode 100644 index 000000000..75fcd14fd --- /dev/null +++ b/packages/admin-frontend/pages/admin/featuredEdit.js @@ -0,0 +1,235 @@ +//@flow +import React from 'react'; +import { Button, Dialog, TextField, FormHelperText } from '@material-ui/core'; +import { Form, Field, FormSpy } from 'react-final-form'; +import isEmptyString from '../../lib/isEmptyString'; +import UploadFileDialog from '../../components/UploadFileDialog'; +import Row from '../../components/Row'; +import FeaturedImage from '../../components/FeaturedImage'; +import type { FeaturedContent } from '../../types'; + +type Props = { + i: number, + button: any, + featuredContentList: Array, + selectedLanguage: string, + defaultReturned: boolean, + handleSaveButtonClick: (boolean, FeaturedContent) => void, + handleFileChosen: (SyntheticInputEvent) => void, + handleOnCancel: () => void, + file: ?File, + handleOnUpload: (string, (string, any) => void) => void +}; +type State = { + open: boolean +}; +function handleValidate(values) { + const errors = {}; + + if (isEmptyString(values.title)) { + errors.title = 'Required'; + } + + if (isEmptyString(values.description)) { + errors.description = 'Required'; + } + + const regex = /http(s)?:\/\/.*/; + if (isEmptyString(values.link) || !values.link.match(regex)) { + errors.link = 'Must be a valid URL e.g "https://digitallibrary.io"'; + } + + if (isEmptyString(values.imageUrl) || !values.imageUrl.match(regex)) { + errors.imageUrl = + 'Must be a valid URL image url e.g "https://images.digitallibrary.io/imageId.png'; + } + + return errors; +} + +export default class FeaturedEdit extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + open: false + }; + } + + handleClickOpen = () => { + this.setState({ + open: true + }); + }; + handleClose() { + this.setState({ + open: false + }); + } + + handleSaveButtonClick = (defaultReturned: boolean) => ( + content: FeaturedContent + ) => { + this.props.handleSaveButtonClick(defaultReturned, content); + this.setState({ open: false }); + }; + render() { + const { + button, + featuredContentList, + selectedLanguage, + i, + defaultReturned, + file + } = this.props; + return ( + <> +
{button}
+ + +
+ <> +
( + + ( + + )} + /> + ( + + )} + /> + ( + <> + + {meta.error && meta.touched && ( + {meta.error} + )} + + )} + /> + + +
+ ( + <> + + {meta.error && meta.touched && ( + + {meta.error} + + )} + + )} + /> +
+ + or + + this.props.handleFileChosen(event)} + /> + + {file && ( + + this.props.handleOnUpload(url, form.change) + } + /> + )} +
+ + + // $FlowFixMe + values.imageUrl ? ( + + ) : null + } + /> + + + + + )} + /> + +
+
+ + ); + } +}