<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Tooltip from '@material-ui/core/Tooltip';

// button to trigger search in browser
class SearchButton extends React.Component {
  state = {};

  buttonLabel = (this.props.variant === 'search')? 'Suchen' : 'gesamten Korpus herunterladen';
  buttonVariant = (this.props.variant === 'search')? 'contained' : 'text';

  // display tip on hover
  searchTip = 'Alle Texte abfragen, die den eingegebenen Kriterien entsprechen';
  getAllTip = 'Alle verfügbaren Texte abfragen';
  buttonTips = this.props.variant === 'search'? this.searchTip : this.getAllTip;

  render() {
    const { classes } = this.props;
    return(
      <div className={classes.root}>
        <Tooltip title={this.buttonTips} placement={'bottom-start'}>
          <Button
            color={'primary'}
            variant={this.buttonVariant}
            disabled={this.props.getLoading()}
            onClick={this.props.handleSubmit}
          >
            {this.buttonLabel}
          </Button>
        </Tooltip>
      </div>
    )
  }
}

SearchButton.propTypes = {
  classes: PropTypes.object.isRequired,
  variant: PropTypes.string,
  getLoading: PropTypes.func.isRequired,
  handleSubmit: PropTypes.func.isRequired,
};

const styles = theme => ({
  root:{
    margin: theme.spacing.unit,
  }
});

export default withStyles(styles)(SearchButton);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import InfoCard from '../InfoCard';
import NotificationContext from '../notifications/NotificationContext';
import Link from '@material-ui/core/Link';


class Downloads extends React.Component {
  state = {
    filenames: [],
  };

  handleChange = name => event => {
    this.setState({
      [name]: event.target.value,
    });
  };

  componentDidMount() {
    this.requestStatus();
  };

  logout = () => {
    this.handleStateChange('loggedIn', false);
    this.handleStateChange('isAdmin', false);
  };

  requestFilenames = () => {
    fetch("/backend/lib/functions.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({getFiles: true})
    })
      .then(res => {
        if(res.ok) {
          res.json().then(data => {
            if (data && data.status === "success") {
              this.context.handleNotificationChange(true, 'Die Dateien wurden erfolgreich abgerufen.', 'getFiles', 'success');
              this.setState({filenames: data.files})
            } else {
              this.context.handleNotificationChange(true, 'Die Dateien konnten nicht vom Server geladen werden.', 'getFiles', 'error');
            }
          })
        } else {
          this.context.handleNotificationChange(true, 'Die Dateien konnten nicht vom Server geladen werden.', 'getFiles', 'error');
        }
      })
      .catch(error => {
          this.context.handleNotificationChange(true, 'Die Dateien konnten nicht vom Server geladen werden.', 'getFiles', 'error');
        }
      );
  };

  requestStatus = () => {
    // check if session is still valid otherwise logout user
    fetch("/backend/lib/sessionManagement.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        loginStatus: true,
        loginID: this.props.sessionID
      })
    }).then(response => {
        if(response.ok) {
          response.json().then(data => {
            if(!data || data.status === 'error') {
              this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
              this.props.handleAppChange('loggedIn', false);
              this.props.handleAppChange('isAdmin', false);
            } else {
              this.requestFilenames();
            }
          });
        } else {
          this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
          this.logout();
        }
      }
    ).catch(error => {
        this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error', 404);
        this.logout();
      }
    );
  };

  render() {
    const {classes} = this.props;
    const {filenames} = this.state;

    return (
      <div className={classes.root}>
        <InfoCard
          message='Auf dieser Seite können Sie die Nutzerdokumentation mit Hinweisen zur Seite und
                    Skripte zur weiteren Verarbeitung der Texte herunterladen.'
        />
        <div className={classes.textContainer}>
          <Typography variant={'h5'} color={'primary'}>Hinweise für Nutzer*innen</Typography>
          <ul>
            {filenames
              .filter(name => name.includes('.pdf'))
              .map(name => <li><Link key={name} href={'/backend/scripts/' + name} download>{name}</Link></li>)}
          </ul>
          <Typography variant={'h5'} color={'primary'}>Skripte</Typography>
          <ul>
            {filenames.map(name => {
                if(!name.includes('.pdf')) {
                  return <li><Link key={name} href={'/backend/scripts/' + name} download>{name}</Link></li>
                }
            }
            )}
          </ul>
        </div>
      </div>
    )
  }

}

Downloads.propTypes = {
  classes: PropTypes.object.isRequired,
  handleAppChange: PropTypes.func.isRequired,
  sessionID: PropTypes.any.isRequired,
};

const styles = theme => ({
  root:{
    margin: theme.spacing.unit * 3,
    marginTop: theme.spacing.unit * 4,
  },
  textContainer: {
    minWidth: 500,
    maxWidth: 1000,
    marginLeft: theme.spacing.unit,
  }
});
Downloads.contextType = NotificationContext;

export default withStyles(styles)(Downloads);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import Paper from "@material-ui/core/Paper/Paper";
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import InfoIcon from '@material-ui/icons/Info';
import CircularProgress from '@material-ui/core/CircularProgress';
import NotificationContext from "../../notifications/NotificationContext";

class Login extends React.Component {
  state = {
    username: '',
    password: '',
    loading: false,
  };

  onInputChange = (event) => {
    this.setState({[event.target.name]: event.target.value});
  };

  onKeyPress = (event) => {
    if(event.key === 'Enter') {
      this.requestLogin();
      event.preventDefault();
    }
  };

  // send request to server + change app state or display error
  requestLogin = () => {
    if(this.state.username.length < 1 || this.state.password.length < 1) {
      this.context.handleNotificationChange(true, 'Bitte geben Sie einen Username und ein Passwort ein.', 'login', 'error');
      return;
    }
    this.setState({
      loading: true,
      error: false,
    });
    fetch('/backend/lib/sessionManagement.php',{
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        login: true,
        username: this.state.username,
        password: <PASSWORD>,
      }),
    })
      .then(response => {
        this.setState({statusCode: response.status});
        if (response.ok) {
          response.json().then(data => {
            if(data && data.status === 'success' && typeof data.isadmin !== 'undefined') {
              this.props.handleAppStateChange('loggedIn', true);
              this.props.handleAppStateChange('isAdmin', data.isadmin !== 0);
              this.props.handleAppStateChange('sessionID', data.id);

              this.context.handleNotificationChange(true, 'Login erfolgreich.', 'login', 'success');
            } else {
              // server error
              this.context.handleNotificationChange(true, 'Es ist ein Fehler beim Login aufgetreten.', 'login', 'error', data.error);
            }
          });
        } else {
          this.setState({
            loginError: true,
            errorMessage: response.statusText,
          });
          this.context.handleNotificationChange(true, response.statusText, 'login', 'error', response.statusCode);
        }
      })
      .catch(error => {
        this.setState({
          loginError: true,
          errorMessage: error.message,
        });
        this.context.handleNotificationChange(true, error.message, 'login', 'error', 404);
      });
    this.setState({loading: false});
  };

  render() {
    const {classes } = this.props;
    const { username, password, loading } = this.state;

    return (
      <div className={classes.root}>
        <Paper className={classes.infoBox}>
          <InfoIcon color={"primary"} className={classes.infoIcon}/>
          <Typography color={"primary"}>
            Falls Sie sich registrieren möchten, Ihr Passwort vergessen haben
            oder Ihr Nutzerkonto löschen möchten wenden Sie sich bitte an den/die Administrator/in.
          </Typography>
        </Paper>
        <Paper className={classes.loginContainer}>
          <div className={classes.formContainer}>
            <Typography variant={'h6'} color={'primary'}>Bitte melden Sie sich an</Typography>
            <TextField
              disabled={loading}
              autoFocus={true}
              className={classes.textField}
              label={'Benutzername'}
              type={'text'}
              name={'username'}
              value={username}
              onChange={this.onInputChange}
              onKeyPress={this.onKeyPress}
            />
            <TextField
              className={classes.textField}
              disabled={loading}
              label={'Passwort'}
              type={'password'}
              name={'password'}
              value={<PASSWORD>}
              onChange={this.onInputChange}
              onKeyPress={this.onKeyPress}
            />
            <div className={classes.flexContainer}>
              <Button
                disabled={loading}
                size="small"
                color="primary"
                variant={"contained"}
                className={classes.button}
                onClick={this.requestLogin}
              >
                Anmelden
              </Button>
              {loading && <CircularProgress className={classes.loadingAnimation} size={30}/>}
            </div>
          </div>
        </Paper>
      </div>
    )
  }
}

Login.propTypes = {
  classes: PropTypes.object.isRequired,
  url: PropTypes.string,
  handleAppStateChange: PropTypes.func,
};

Login.contextType = NotificationContext;

const styles = theme => ({
  root: {
    marginTop: theme.spacing.unit * 4
  },
  loginContainer:{
    width: 400,
    padding: theme.spacing.unit * 6,
    margin: 'auto',
  },
  formContainer:{
    display: 'flex',
    flexDirection: 'column',
  },
  textField: {
    marginTop: theme.spacing.unit * 2,
  },
  button:{
    marginTop: theme.spacing.unit * 5,
    marginRight: theme.spacing.unit * 2,
    width: 100,
  },
  infoBox:{
    width: 400 + theme.spacing.unit * 6,
    padding: theme.spacing.unit*2,
    paddingRight: theme.spacing.unit * 4,
    display: 'flex',
    margin: 'auto',
    marginBottom: theme.spacing.unit * 3,
  },
  infoIcon: {
    marginRight: theme.spacing.unit,
  },
  flexContainer:{
    display: 'flex',
    alignItems: 'flex-end',
  },
  loginErrorMessage:{
    marginLeft: -theme.spacing.unit,
    marginTop: theme.spacing.unit * 3,
    width: theme.spacing.unit * 52,
  },
});

export default withStyles(styles)(Login);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Paper from "@material-ui/core/Paper/Paper";
import InfoButton from '@material-ui/icons/Info';
import CircularProgress from '@material-ui/core/CircularProgress';
import Typography from '@material-ui/core/Typography';
import NotificationContext from '../../notifications/NotificationContext';
import AuthorInput from "./searchcard/AuthorInput";
import GenreSelection from "./searchcard/GenreSelection";
import ContainsInput from "./searchcard/ContainsInput";
import TimeInput from "./searchcard/TimeInput";
import SearchCard from './searchcard/SearchCard';
import AddSearchCardButton from './buttons/AddSearchCardButton';
import SelectFormat from './SelectFormat';
import SearchButton from './buttons/SearchButton';
import Results from './results/Results';
import shortid from 'shortid';

// MUI variant of input fields inside browser
const inputVariant = 'standard';

// max. number of search cards that can be added by user
const maxCards = 4;

class Browser extends React.PureComponent {

  initialSearchCardObject = {
    id: shortid.generate(),
    authors: [],
    genres: [],
    keywords: '',
    timeFrom: '',
    timeTo: '',
  };

  state = {
    cardList: [JSON.parse(JSON.stringify(this.initialSearchCardObject))],
    selectedFormats: {checkedTXT: false, checkedJSON: false},
    loading: false,
    responseFiles: '',
    hits: 0,
    responseIn: false,
    error: false,
    errorMessage: 'no error',
    timeError: false,
    keywordError: false,
    formatError: true,
    searchID: 0,
    getAll: false,
  };

  getCardIndex = id => {
    return this.state.cardList.findIndex(card => card.id === id);
  };

  getLoading = () => {
    return this.state.loading;
  };

  handleChange = (name, value) => {
    this.setState({
      [name]: value,
    });
  };

  // add new search card
  onAddSearchCard = inputValues => {

    if(this.state && this.state.cardList.length >= maxCards ) {
      this.context.handleNotificationChange(true, `Ihre Anfrage enthält bereits ${maxCards} Teilsuchen.`, 'addCard', 'error');
      return;
    }

    let paramsPassed = (inputValues instanceof Object && 'authors' in inputValues);
    inputValues = paramsPassed?
      JSON.parse(JSON.stringify(inputValues)) : JSON.parse(JSON.stringify(this.initialSearchCardObject));

    inputValues.id = shortid.generate();

    this.setState(state => ({
      cardList: [...state.cardList, inputValues]
    }), () => {
      if(this.state.cardList.length === maxCards) {
        this.context.handleNotificationChange(true, `Sie können max. ${maxCards} Teilsuchen erstellen.`, 'addCard', 'warning');
      }
    });
  };

  // duplicate existing search card with content
  onDuplicateSearchCard = id => {
    this.onAddSearchCard(this.state.cardList.find(card => card.id === id));
  };

  updateSearchCardContent = (id, prop, value) => {
    if(this.state && this.state.cardList) {
      let newCardList = JSON.parse(JSON.stringify(this.state.cardList));

      newCardList.find(card => card.id === id)[prop] = JSON.parse(JSON.stringify(value));
      this.setState({cardList: newCardList });
    }
  };

  deleteSearchCard = id => {
    // last remaining card should not be deleted => warning
    if(this.state.cardList.length === 1) {
      this.context.handleNotificationChange(true, 'Ihre Anfrage muss min. 1 Teil-Suche enthalten.', 'deleteCard', 'warning');
      return;
    }
    this.setState(state => ({
      cardList: state.cardList.filter( card => (card.id !== id))
    }));
  };

  // update list of selected formats for results (txt, json)
  updateFormat = (formatProp, newValue) => {
    let newSelectedFormats = JSON.parse(JSON.stringify(this.state.selectedFormats));
    newSelectedFormats[formatProp] = JSON.parse(JSON.stringify(newValue));
    this.setState({selectedFormats: newSelectedFormats});
  };

  // see if user is still logged in
  requestStatus = func => {
    // check if session is still valid otherwise logout user
    fetch("/backend/lib/sessionManagement.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        loginStatus: true,
        loginID: this.props.sessionID
      })
    }).then(response => {
        if(response.ok) {
          response.json().then(data => {
            if(!data || data.status === 'error') {
              this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
              this.props.handleAppChange('loggedIn', false);
              this.props.handleAppChange('isAdmin', false);
            } else {
              func();
            }
          });
        } else {
          this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
          this.logout();
        }
      }
    ).catch(error => {
        this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error', 404);
        this.logout();
      }
    );
  };

  // submit search with or without criteria
  handleSubmit = (getAll) => {
    // check for errors before sending request
    if ((!getAll && (this.state.timeError || this.state.keywordError)) || this.state.formatError) {
      this.setState({
        error: true,
        errorMessage: 'Sie können keine Anfrage mit fehlenden/falschen Eingaben abschicken',
      });
      this.context.handleNotificationChange(true, 'Sie können keine Anfrage mit fehlenden/falschen Eingaben abschicken', 'login', 'error');
      return;
    }

    const payload = getAll ? {getAll: true} : {cards: this.state.cardList};

    // start loading animation, disable forms
    this.setState({
      loading: true,
      error: false,
      responseIn: false,
      responseFiles: [],
      getAll: false,
    });

    // request data + handle response
    fetch("/backend/lib/functions.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload)
    })
      .then(response => {
        if (response.ok) {
          response.json().then(data => {
            if (data && data.status && data.status === 'success') {
              // search succeeded (there are results)
              this.setState({
                responseFiles: data.filenames, // array of filenames separated by comma
                hits: data.hits, // number
                responseIn: true,
                searchID: shortid.generate(),
              }, () => {
                this.setState({loading: false});
              });
            } else {
              // server error / search not possible on server
              this.setState({
                errorMessage: 'Es ist ein Fehler auf dem Server aufgetreten.',
                error: true
              },() => {
                this.setState({loading: false});
              });
              this.context.handleNotificationChange(true, 'Es ist ein Fehler auf dem Server aufgetreten. Die Anfrage wurde abgebrochen.', 'search', 'error');
            }
          });
        } else {
          // other problem
          this.setState({
            errorMessage: response.statusText,
            error: true
          },() => {
            this.setState({loading: false});
          });
          this.context.handleNotificationChange(true, response.statusText, 'search', 'error', response.statusCode)
        }
      })
      .catch(error => {
        this.setState({
          errorMessage: error.message,
          error: true
        },() => {
          this.setState({loading: false});
        });
        this.context.handleNotificationChange(true, error.message, 'search', 'error', 404)
      });
    //this.setState({loading: false});
  };

  // do search with criteria entered by user
  handleSearchCriteria = () => {
    this.requestStatus(() => this.handleSubmit(false));
  };

  // do search not regarding criteria that might have been entered by user
  handleGetAll = () => {
    this.setState({responseIn: true, getAll: true, searchID: shortid.generate()});
  };

  render() {
    const { classes, authorsList, minYear, maxYear, genres, handleAppChange, sessionID } = this.props;
    const { cardList, selectedFormats, responseFiles, responseIn, loading, hits, searchID, getAll} = this.state;

    const renderResponseData = () => {
      let formats = selectedFormats.checkedJSON? ['json'] : [];
      if(selectedFormats.checkedTXT) formats.push('txt');
      return <Results
        filenames={responseFiles}
        number={hits}
        formats={formats}
        sessionID={sessionID}
        searchID={searchID}
        getAll={getAll}
        handleAppChange={handleAppChange}
      />;
    };

    return(
      <div className={classes.root}>
        <Paper className={classes.infoBox}>
          <InfoButton color={"primary"} className={classes.infoIcon}/>
          <Typography color={"primary"}>
            Schränken Sie die Suche ein, indem Sie eines oder mehrere Kriterien innerhalb einer Teil-Suche (=Kasten) eingeben.
            Teil-Suchen werden zu einer Such-Anfrage kombiniert, sodass Sie mehr Texte mit einer Suche erhalten können.
          </Typography>
        </Paper>
        <form onSubmit={this.handleSubmit.bind(this)}>
          <div className={classes.cardContainer}>
            {cardList.map((card, index, cardList) => (
              <SearchCard
                key={card.id} // not passed to component by react! => use id instead
                id={card.id}
                getIndex={this.getCardIndex}
                getDisabled={this.getLoading}
                onDuplicate={this.onDuplicateSearchCard}
                onDelete={this.deleteSearchCard}
              >
                <AuthorInput
                  key={card.id + '-authors'}
                  cardId={card.id}
                  variant={inputVariant}
                  initialValues={card.authors}
                  authorsList={authorsList}
                  autofocus={(index === cardList.length-1)}
                  disabled={this.getLoading()}
                  onInputChange={this.updateSearchCardContent}
                />
                <GenreSelection
                  key={card.id + '-genres'}
                  cardId={card.id}
                  genres={genres}
                  variant={inputVariant}
                  initialValues={card.genres}
                  disabled={this.getLoading()}
                  onInputChange={this.updateSearchCardContent}
                />
                <ContainsInput
                  key={card.id + '-keywords'}
                  cardId={card.id}
                  variant={inputVariant}
                  initialValue={card.keywords}
                  disabled={this.getLoading()}
                  onInputChange={this.updateSearchCardContent}
                  handleBrowserChange={this.handleChange}
                />
                <TimeInput
                  key={card.id + '-time'}
                  cardId={card.id}
                  variant={inputVariant}
                  initialTimeFrom={card.timeFrom}
                  initialTimeTo={card.timeTo}
                  disabled={this.getLoading()}
                  onInputChange={this.updateSearchCardContent}
                  minYear={minYear}
                  maxYear={maxYear}
                  handleBrowserChange={this.handleChange}
                />
              </SearchCard>
            ))}
          </div>
          <AddSearchCardButton action={this.onAddSearchCard} getDisabled={this.getLoading}/>
          <div className={classes.flexContainer}>
            <SelectFormat
              initialValues={selectedFormats}
              getDisabled={this.getLoading}
              onChange={this.updateFormat}
              handleBrowserChange={this.handleChange}
            />
          </div>
          <div className={classes.flexContainer}>
            <SearchButton
              type={'submit'} // form gets submitted when clicking on this button
              variant={'search'}
              getLoading={this.getLoading}
              handleSubmit={this.handleSearchCriteria}
            />
            <SearchButton
              getLoading={this.getLoading}
              handleSubmit={this.handleGetAll}
            />
          </div>
        </form>
        <div className={classes.flexContainer}>
          {loading && <CircularProgress className={classes.loadingAnimation} />}
          {responseIn && renderResponseData()}
        </div>
      </div>
    );
  }
}

Browser.propTypes = {
  classes: PropTypes.object.isRequired,
  authorsList: PropTypes.arrayOf(PropTypes.string).isRequired,
  minYear: PropTypes.string.isRequired,
  maxYear: PropTypes.string.isRequired,
  genres: PropTypes.arrayOf(PropTypes.string),
  handleAppChange: PropTypes.func.isRequired,
  sessionID: PropTypes.any.isRequired,
};

Browser.contextType = NotificationContext;

const styles = theme => ({
  root:{
    paddingLeft: theme.spacing.unit * 3,
    paddingBottom: theme.spacing.unit * 3,
    marginTop: theme.spacing.unit * 4,
  },
  infoBox:{
    minWidth: 400,
    maxWidth: 800 + theme.spacing.unit*7,
    padding: 20,
    display: 'flex',
    marginLeft: theme.spacing.unit,
    marginBottom: theme.spacing.unit * 3,
  },
  infoIcon: {
    marginRight: theme.spacing.unit,
  },
  cardContainer:{
    display: 'flex',
    flexFlow: 'row wrap',
  },
  flexContainer:{
    display: 'flex',
    marginTop: theme.spacing.unit * 2,
  },
  cardWarning:{
    margin: theme.spacing.unit,
  },
  loadingAnimation:{
    marginLeft: theme.spacing.unit,
  },
});

export default withStyles(styles)(Browser);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import {Switch, Route} from 'react-router-dom';
import AdminNav from './AdminNav';
import TextManagement from './tabs/TextManagement';
import ServerManagement from './tabs/ServerManagement';
import UserManagement from './tabs/UserManagement';
import MissingPage from '../MissingPage';
import NotificationContext from '../../notifications/NotificationContext';

// routing for Admin page
class Admin extends React.Component {
  state = {};

  handleChange = name => event => {
    this.setState({
      [name]: event.target.value,
    });
  };

  requestStatus = func => {
    // check if session is still valid otherwise logout user
    fetch("/backend/lib/sessionManagement.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        loginStatus: true,
        loginID: this.props.sessionID
      })
    }).then(response => {
        if(response.ok) {
          response.json().then(data => {
            if(!data || data.status === 'error') {
              this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
              this.props.handleAppChange('loggedIn', false);
              this.props.handleAppChange('isAdmin', false);
            } else {
              func();
            }
          });
        } else {
          this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
          this.logout();
        }
      }
    ).catch(error => {
        this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error', 404);
        this.logout();
      }
    );
  };

  render() {
    const {classes, requestNewAuthors, requestNewLog} = this.props;

    return (

      <div className={classes.root}>
        <AdminNav/>
        <div className={classes.pageContainer}>
          <Switch>
            <Route path='/admin/users' render={() => <UserManagement requestStatus={this.requestStatus} userStatus={this.requestStatus}/>} />
            <Route path='/admin/server' render={() => <ServerManagement requestStatus={this.requestStatus} userStatus={this.requestStatus}/>} />
            <Route path='/admin' render={() =>
              <TextManagement
                requestNewAuthors={requestNewAuthors}
                requestNewLog={requestNewLog}
                requestStatus={this.requestStatus}
              />}
            />
            <Route exact path='/admin/:other' render={() => <MissingPage/>} />
          </Switch>
        </div>
      </div>
    )
  }

}

Admin.propTypes = {
  classes: PropTypes.object.isRequired,
  requestNewAuthors: PropTypes.func.isRequired,
  requestNewLog: PropTypes.func.isRequired,
  handleAppChange: PropTypes.func.isRequired,
  sessionID: PropTypes.any.isRequired,
};

Admin.contextType = NotificationContext;

const styles = theme => ({
  root: {
    marginLeft: theme.spacing.unit * 4,
    marginTop: theme.spacing.unit * 4,
  },
  pageContainer:{
    paddingLeft: 200,
  }
});

export default withStyles(styles)(Admin);<file_sep>import React from "react";

// create React context to provide access to App Notification for children
const NotificationContext = React.createContext({});
export default NotificationContext;<file_sep>import { createMuiTheme } from '@material-ui/core/styles';
import indigo from '@material-ui/core/colors/indigo';
import red from '@material-ui/core/colors/red';
import grey from '@material-ui/core/colors/grey';

const theme = createMuiTheme({
  palette: {
    primary: {
      light: indigo[300],
      main: indigo[600],
      dark: indigo[900],
    },
    secondary: {
      light: red[300],
      main: red[600],
      dark: red[900],
    },
    background: grey,
  },
  typography: {
    useNextVariants: true,
  },
});

export default theme;<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import AddIcon from '@material-ui/icons/Add';

// button to add new empty search card in browser
class AddSearchCardButton extends React.Component {
  state = {

  };

  render() {
    const { classes, action, getDisabled } = this.props;
    return(
      <div className={classes.root}>
        <Button
          color="primary"
          variant="outlined"
          disabled={getDisabled()}
          onClick={action}>
          <AddIcon/>
          neue Teil-Suche hinzufügen
        </Button>
      </div>
    )
  }

}

AddSearchCardButton.propTypes = {
  classes: PropTypes.object.isRequired,
  action: PropTypes.func.isRequired,
  getDisabled: PropTypes.func.isRequired,
};

const styles = theme => ({
  root: {
    margin: theme.spacing.unit,
  },
});
export default withStyles(styles)(AddSearchCardButton);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';

// about the project, impressum
class About extends React.Component {
  state = {

  };

  render() {
    const { classes } = this.props;
    return(
      <div className={classes.root}>
        <Typography variant={'h5'} color={'primary'} className={classes.aboutText}>Über diese Seite</Typography><br/>
        <Typography className={classes.aboutText} paragraph={true}>
          Diese Webseite ist das Ergebnis des Projekts “Gutenberg German Poetry Corpus” im Rahmen des Moduls “Interdisziplinäres Medienprojekt” des Studiengangs B.Sc. Medieninformatik an der Freien Universität Berlin und Technischen Universität Berlin.
          Die Motivation dieses Projektes ist es, die Arbeit von beispielsweise Linguist*innen und Psycholog*innen zu erleichtern, um Erkenntnisse über die in Gedichten verwendete Sprache zu erlangen.
          Das Ergebnis ist diese Webanwendung, die es ermöglicht auf die Gedichte des  “Gutenberg-Korpus” (des größten deutschsprachigen Gedichts-Korpus) zuzugreifen, diese nach bestimmten Kriterien zu filtern und weitere Texte hinzuzufügen.
        </Typography>
        <Typography className={classes.aboutText} paragraph={true}>
          Projektleiterin war Frau Dr. <NAME>, wissenschaftliche Mitarbeiterin im Fachgebiet Allgemeine und neurokognitive Psychologie an der Freien Universität Berlin. Assistiert hat <NAME>, Studentin der Medieninformatik an der TU und FU Berlin.
          Am Projekt mitgewirkt haben <NAME>, <NAME>, <NAME>, <NAME> und <NAME>asiliou.
        </Typography>
        <Typography className={classes.aboutText}>
          Favicon made by {' '}<a href="https://www.freepik.com/" title="Freepik">Freepik</a>{' '}
          from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by{' '}
          <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank" rel="noopener noreferrer">CC 3.0 BY</a>
        </Typography>
      </div>
    );
  }
}

About.propTypes = {
  classes: PropTypes.object.isRequired,
};

const styles = theme => ({
  root:{
    margin: theme.spacing.unit * 4
  },
  aboutText:{
    minWidth: 500,
    maxWidth: 1000,
  }
});

export default withStyles(styles)(About);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import Snackbar from '@material-ui/core/Snackbar';
import SnackbarContent from '@material-ui/core/SnackbarContent';
import CloseIcon from '@material-ui/icons/Close';
import IconButton from '@material-ui/core/IconButton';
import green from '@material-ui/core/colors/green';
import amber from '@material-ui/core/colors/amber';
import NotificationContext from './NotificationContext';
import { getErrorMessage } from '../../utils/errorMessageHelper';

// show success/warning/error snackbar in right lower corner of viewport
class NotificationSnackbar extends React.Component {

  handleChange = name => event => {
    this.setState({
      [name]: event.target.value,
    });
  };

  // do not show Snackbar anymore
  handleClose = (event, reason) => {
    if(reason === 'clickaway') {
      return;
    }
    this.context.handleNotificationChange(false,'','','');
  };

  render() {
    const {classes} = this.props;
    const { error, message, statusCode, action, variant } = this.context;

    return (
      <div>
        <Snackbar
          anchorOrigin={{
            vertical: 'bottom',
            horizontal: 'right',
          }}
          open={error}
          autoHideDuration={8000}
          resumeHideDuration={0}
          disableWindowBlurListener={true}
          onClose={this.handleClose}
          transitionDuration={0}
        >
          <SnackbarContent
            classes={{root: classes[variant]}}
            aria-describedby="snackbar"
            message={<span id={'snackbar'}>{getErrorMessage(message, statusCode, action)}</span>}
            action={[
              <IconButton
                key="close"
                aria-label="Close"
                color="inherit"
                className={classes.close}
                onClick={this.handleClose}
              >
                <CloseIcon className={classes.icon} />
              </IconButton>,
            ]}
          />
        </Snackbar>
      </div>
    )
  }
}

NotificationSnackbar.propTypes = {
  classes: PropTypes.object.isRequired,
};

NotificationSnackbar.defaultProps = {
  message: 'Es ist ein Fehler aufgetreten.',
};

NotificationSnackbar.contextType = NotificationContext;

const styles = theme => ({
  root: {},
  error: {
    backgroundColor: theme.palette.error.dark,
  },
  success: {
    backgroundColor: green[600],
  },
  warning: {
    backgroundColor: amber[700],
  },
  icon: {
    fontSize: 20,
  },
  iconVariant: {
    opacity: 0.9,
    marginRight: theme.spacing.unit,
  },
});

export default withStyles(styles)(NotificationSnackbar);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import ListItemText from '@material-ui/core/ListItemText';
import Select from '@material-ui/core/Select';
import Checkbox from '@material-ui/core/Checkbox';

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
  PaperProps: {
    style: {
      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
      width: 250,
    },
  },
};

class GenreSelection extends React.PureComponent {
  state = {
    selectedGenre: this.props.initialValues,
    genres: this.props.genres,
  };

  // update state with new genres if prop changes
  componentWillReceiveProps = nextProps => {
    if(this.props.genres !== nextProps.genres) {
      this.setState({genres: nextProps.genres});
    }
  };

  handleChange = event => {
    this.setState({ selectedGenre: event.target.value }, () => {
      this.props.onInputChange(this.props.cardId, 'genres', this.state.selectedGenre); // update SearchCard state
    });
  };

  render() {
    const { classes, variant, disabled } = this.props;
    const { genres, selectedGenre } = this.state;
    return(
      <div className={classes.root}>
        <FormControl variant={variant} fullWidth={true} disabled={disabled}>
          <InputLabel htmlFor="selectGenre">Genre(s)</InputLabel>
          <Select
            multiple
            value={selectedGenre}
            onChange={this.handleChange}
            input={<Input id="selectGenre" />}
            renderValue={selected => selected.join(', ')}
            MenuProps={MenuProps}
          >
            {genres && Array.isArray(genres) && genres.map(name => (
              <MenuItem key={name} value={name}>
                <Checkbox
                  checked={selectedGenre.indexOf(name) > -1}
                  disableRipple={true}
                  color={"primary"}
                />
                <ListItemText primary={name} />
              </MenuItem>
            ))}
          </Select>
        </FormControl>
      </div>
    );
  }
}

GenreSelection.propTypes = {
  classes: PropTypes.object.isRequired,
  genres: PropTypes.arrayOf(PropTypes.string),
  cardId: PropTypes.string.isRequired,
  variant: PropTypes.string.isRequired,
  initialValues: PropTypes.array.isRequired,
  disabled: PropTypes.bool.isRequired,
  onInputChange: PropTypes.func.isRequired,
};

const styles = theme => ({
  root: {
    flexGrow: 1,
    paddingTop: 5,
    marginLeft: theme.spacing.unit,
    marginRight: theme.spacing.unit,
    position: 'relative',
  },
});

export default withStyles(styles)(GenreSelection);<file_sep>import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import theme from './theme/theme'
import { MuiThemeProvider } from '@material-ui/core/styles';
import {
  BrowserRouter as Router,
  Route,
  Redirect,
  Switch,
} from 'react-router-dom';
import NotificationContext from './components/notifications/NotificationContext';
import Header from './components/views/navigation/Header';
import Login from './components/views/login/Login';
import Browser from './components/views/browser/Browser';
import About from './components/views/About';
import Admin from './components/views/admin/Admin';
import MissingPage from './components/views/MissingPage';
import Notification from './components/notifications/NotificationSnackbar';
import Downloads from './components/views/Downloads';

// App component
class App extends React.Component {
  state = {
    loggedIn: false,
    sessionID: 0,
    isAdmin: false,
    timeRange: {
      minYear: '',
      maxYear: '',
    },
    authors: [],
    genres: ['poem', 'ballad', 'sonnet'],
    notification: {
      show: false,
      statusCode: 0,
      message: 'notification',
      action: '',
      variant: 'error',
    }
  };

  // set state of App component
  handleStateChange = (prop, value) => {
    this.setState({
      [prop]: value,
    }, () => {
      if(prop === 'sessionID' && this.state.loggedIn === true) {
        this.requestSuggestions();
      }
    });
  };

  // open/close notification snackbar, display message (depending on optional statusCode and variant)
  handleNotificationChange = (show, message, action, variant, statusCode, ) => {
    this.handleStateChange('notification', {
      show: show,
      statusCode: statusCode? statusCode : 0,
      message: message? message : '',
      action: action,
      variant: variant? variant : 'error',
    });
  };

  requestSuggestions = () => {
    // request initial data (authors, genres, time range, user status
    this.requestAuthors();
    this.requestLog();
  };

  // request authors list
  requestAuthors = () => {
    fetch("/backend/lib/functions.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        authors: true,
        id: this.state.sessionID,
      })
    }).then(response => {
        if(response.ok) {
          response.json().then(data => {
            if(data && data.status === 'success' && data.authors) {
              this.handleStateChange('authors', JSON.parse(data.authors));
            } else {
              // server error
              this.handleNotificationChange(true, 'Autoren/Genres/Zeitspanne konnten nicht vom Server geladen werden.', 'initialLoad', 'error');
            }
          });
        } else {
          this.handleNotificationChange(true, 'Autoren/Genres/Zeitspanne konnten nicht vom Server geladen werden.', 'initialLoad', 'error');
        }
      }
    ).catch(error => {
        this.handleNotificationChange(true, 'Autoren/Genres/Zeitspanne konnten nicht vom Server geladen werden.', 'initialLoad', 'error', 404);
      }
    );
  };

  // request genres + time range
  requestLog = () => {
    fetch("/backend/lib/functions.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        log: true,
        id: this.state.sessionID
      })
    }).then(response => {
        if(response.ok) {
          response.json().then(data => {
            if(data && data.status === 'success' && data.log) {
              let parsedLog = JSON.parse(data.log);
              let parsedGenre = parsedLog.genre? parsedLog.genre.replace('[','').replace(']','').split(',') : [];
              let parsedMinYear = parsedLog.minYear? parsedLog.minYear.slice(0,4) : '';
              let parsedMaxYear = parsedLog.maxYear? parsedLog.maxYear.slice(0,4) : '';
              this.handleStateChange('genres', parsedGenre);
              this.handleStateChange('timeRange', {minYear: parsedMinYear, maxYear: parsedMaxYear});
            } else {
              this.handleNotificationChange(true, 'Autoren/Genres/Zeitspanne konnten nicht vom Server geladen werden.', 'initialLoad', 'error');
            }
          });
        } else {
          this.handleNotificationChange(true, 'Autoren/Genres/Zeitspanne konnten nicht vom Server geladen werden.', 'initialLoad', 'error');
        }
      }
    ).catch(error => {
        this.handleNotificationChange(true, 'Autoren/Genres/Zeitspanne konnten nicht vom Server geladen werden.', 'initialLoad', 'error', 404);
      }
    );
  };

  // executed after component is inserted into the tree
  componentDidMount = () => {
    // request initial data (authors, genres, time range, user status
    this.requestAuthors();
    this.requestLog();
  };

  // logout user, request server to delete sessionID, display error if necessary
  logout = () => {
    fetch('/backend/lib/sessionManagement.php', {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        logout: true,
        loginID: this.state.sessionID
      })
    })
      .then(response => {
        if(response.ok && response.json().status === 'success') {
          this.handleNotificationChange(true, 'Logout erfolgreich', 'logout', 'success');
        }
        this.setState({
          loggedIn: false,
          isAdmin: false,
        });
      })
      .catch(error => {
        this.handleNotificationChange(true, error.message, 'logout', 'error', 404);
        this.setState({
          loggedIn: false,
          isAdmin: false,
        });
      });
  };

  render() {
    const { classes } = this.props;
    const { loggedIn, isAdmin, authors, timeRange, notification, genres, sessionID } = this.state;

    return (
      <MuiThemeProvider theme={theme}>
        <NotificationContext.Provider
          value={{
            error: notification.show,
            message: notification.message,
            statusCode: notification.statusCode,
            action: notification.action,
            variant: notification.variant,
            handleNotificationChange: this.handleNotificationChange,
          }}
        >
          <div className={classes.root}>
            <Router>
              <div>
                <Header loggedIn={loggedIn} isAdmin={isAdmin} logout={this.logout}/>
                <div className={classes.contentWrapper}>
                  <Switch>
                    <Route exact path='/' render={() => (
                      loggedIn? (<Browser sessionID={sessionID} handleAppChange={this.handleStateChange} authorsList={authors} minYear={timeRange.minYear} maxYear={timeRange.maxYear} genres={genres}/>) :
                        (<Redirect to='/login'/>)
                    )}/>
                    <Route path='/about' component={About}/>
                    <Route path='/downloads' render={() => (
                      loggedIn? (<Downloads sessionID={sessionID} handleAppChange={this.handleStateChange}/>) : (<Redirect to='/login'/>)
                    )}/>
                    <Route path='/admin' render={() => (
                      (loggedIn && isAdmin)?
                        (<Admin
                          requestNewAuthors={this.requestAuthors}
                          requestNewLog={this.requestLog}
                          sessionID={sessionID}
                          handleAppChange={this.handleStateChange}/>) :
                        (<Redirect to='/' />)
                    )}/>
                    <Route path='/login' render={() => (
                      loggedIn? (<Redirect to='/'/>) : (<Login handleAppStateChange={this.handleStateChange} />)
                    )}/>

                    <Route component={MissingPage}/>
                  </Switch>
                </div>
                <Notification />
              </div>
            </Router>
          </div>
        </NotificationContext.Provider>
      </MuiThemeProvider>
    );
  }
}

const styles = {
  root: {
    height: '100%',
  },
  contentWrapper: {
    height: '100%',
    paddingTop: 0,
  },
  routerWrapper: {
    height: '100%',
  },
};

export default withStyles(styles)(App);
<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import deburr from 'lodash/deburr';
import Downshift from 'downshift';
import keycode from 'keycode';
import { withStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Chip from '@material-ui/core/Chip';
import Paper from '@material-ui/core/Paper';
import MenuItem from '@material-ui/core/MenuItem';

// Quelle: https://material-ui.com/demos/autocomplete/ (Stand: Dezember 2018, mit eigenen Veränderungen)

// render input field for user
const InputField = inputProps => {
  const { InputProps, classes, ref, ...other } = inputProps;

  return (
    <TextField
      variant={inputProps.variant}
      InputLabelProps={{disableAnimation: false,}}
      InputProps={{
        inputRef: ref,
        classes: {
          root: classes.inputRoot,
          input: classes.inputInput,
        },
        ...InputProps,
      }}
      {...other}
    />
  );
};

// render author list to be displayed below input field
const Suggestion = ({ suggestion, index, itemProps, highlightedIndex, selectedItem }) => {
  const isHighlighted = highlightedIndex === index;
  const isSelected = (selectedItem || '').indexOf(suggestion) > -1;

  return (
    <MenuItem
      {...itemProps}
      key={suggestion}
      selected={isHighlighted}
      component="div"
      style={{
        fontWeight: isSelected ? 500 : 400,
      }}
    >
      {suggestion}
    </MenuItem>
  );
};

Suggestion.propTypes = {
  highlightedIndex: PropTypes.number,
  index: PropTypes.number,
  itemProps: PropTypes.object,
  selectedItems: PropTypes.string,
  suggestion: PropTypes.string.isRequired,
};

// render AuthorInput component
class AuthorInput extends React.PureComponent {
  state = {
    inputValue: '',
    selectedItems: this.props.initialValues, // shape: ['Goethe, <NAME>', 'Rilke, <NAME>']
  };

  // handle pressing of keys (used to detect user deleting with backspace key)
  handleKeyDown = event => {
    const { inputValue, selectedItems } = this.state;

    if (selectedItems.length && !inputValue.length && keycode(event) === 'backspace') {
      this.setState({
        selectedItems: selectedItems.slice(0, selectedItems.length - 1),
      });
    }
  };

  // extract suggestions from authorsList (max. 10 suggestions are being displayed)
  getSuggestions = value => {
    if(!value && value !== '') return ;
    const inputValue = deburr(value.trim()).toLowerCase();
    const inputLength = inputValue.length;
    let count = 0;
    return inputLength === 0 ? [] : this.props.authorsList.filter(author => {
      const keep = (count < 10 && author.toLowerCase().includes(inputValue)
        && this.state.selectedItems.indexOf(author) === -1);
      if (keep) {
        count += 1;
      }
      return keep;
    });
  };

  // set new state when input changes (not selected authors)
  handleInputChange = event => {
    this.setState({ inputValue: event.target.value });
  };

  // add new selected author to list that will be used for search
  handleChange = item => {
    let { selectedItems } = this.state;

    // allow a max. of 10 authors per search card
    if (selectedItems.length === 10) return;

    if (selectedItems.indexOf(item) === -1) {
      selectedItems = [...selectedItems, item];
    }

    this.setState({
      inputValue: '',
      selectedItems: selectedItems,
    }, () => {
      this.props.onInputChange(this.props.cardId, 'authors', this.state.selectedItems); // update SearchCard state
    });
  };

  // delete selected authors from state => not search for them
  handleDelete = item => () => {
    this.setState(state => {
      const selectedItem = [...state.selectedItems];
      selectedItem.splice(selectedItem.indexOf(item), 1);
      return { selectedItems: selectedItem };
    }, () => {
      this.props.onInputChange(this.props.cardId, 'authors', this.state.selectedItems); // update SearchCard state
    });
  };

  render() {
    const { classes, variant, disabled, autofocus } = this.props;
    const { inputValue, selectedItems } = this.state;

    return(
      <div className={classes.root}>
        <Downshift inputValue={inputValue} onChange={this.handleChange} selectedItem={selectedItems}>
          {({getInputProps, getItemProps, isOpen, inputValue: inputValue2, selectedItem: selectedItem2,
              highlightedIndex,}) => (
            <div className={classes.container}>
              {InputField({
                fullWidth: true,
                autoFocus: autofocus,
                classes,
                InputProps: getInputProps({
                  startAdornment: selectedItems.map(item => (
                    <Chip
                      key={item}
                      tabIndex={-1}
                      label={item}
                      className={classes.chip}
                      onDelete={this.handleDelete(item)}
                    />
                  )),
                  onChange: this.handleInputChange,
                  onKeyDown: this.handleKeyDown,
                  value: inputValue,
                  placeholder: 'Bsp.: Rilke, <NAME>',
                }),
                label: 'Autor*in',
                variant: variant,
                disabled: disabled,
              })}
              {isOpen ? (
                <Paper className={classes.paper} square>
                  {this.getSuggestions(inputValue2).map((suggestion, index) =>
                    Suggestion({
                      suggestion,
                      index,
                      itemProps: getItemProps({ item: suggestion }),
                      highlightedIndex,
                      selectedItem: selectedItem2,
                    }),
                  )}
                </Paper>
              ) : null}
            </div>
          )}
        </Downshift>
      </div>
    );

  }
}

AuthorInput.propTypes = {
  classes: PropTypes.object.isRequired,
  cardId: PropTypes.string.isRequired,
  variant: PropTypes.string,
  initialValues: PropTypes.arrayOf(PropTypes.string).isRequired,
  autofocus: PropTypes.bool.isRequired,
  disabled: PropTypes.bool.isRequired,
  onInputChange: PropTypes.func.isRequired,
  authorsList: PropTypes.arrayOf(PropTypes.string),
};

const styles = theme => ({
  root: {
    flexGrow: 1,
    paddingTop: 5,
  },
  container: {
    marginLeft: theme.spacing.unit,
    marginRight: theme.spacing.unit,
    flexGrow: 1,
    position: 'relative',
  },
  paper: {
    position: 'absolute',
      zIndex: 1,
      marginTop: theme.spacing.unit,
      left: 0,
      right: 0,
  },
  chip: {
    margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 4}px`,
  },
  inputRoot: {
    flexWrap: 'wrap',
  },
  inputInput: {
    width: 'auto',
    flexGrow: 1,
  },
});
export default withStyles(styles)(AuthorInput);
<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Typography from "@material-ui/core/Typography/Typography";
import Paper from "@material-ui/core/Paper/Paper";
import InfoButton from '@material-ui/icons/Info';

function InfoCard(props) {
  const { classes, message } = props;
  return(
    <Paper className={classes.infoBox}>
      <InfoButton color={"primary"} className={classes.infoIcon}/>
      <Typography color={"primary"}>
        {message}
      </Typography>
    </Paper>
  )
}

InfoCard.propTypes = {
  classes: PropTypes.object.isRequired,
  message: PropTypes.string,
};

const styles = theme => ({
  infoBox:{
    minWidth: 400,
    maxWidth: 800 + theme.spacing.unit*7,
    padding: 20,
    display: 'flex',
    marginLeft: theme.spacing.unit,
    marginBottom: theme.spacing.unit * 3,
  },
  infoIcon: {
    marginRight: theme.spacing.unit,
  },
});

export default withStyles(styles)(InfoCard);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import Button from "@material-ui/core/Button";
import NotificationContext from '../../../notifications/NotificationContext';
import InfoCard from '../../../InfoCard';

// "Server verwalten" tab in Admin View
class ServerManagement extends React.Component {
  state = {
    loading: false,
  };

  handleChange = (prop, value) => {
    this.setState({
      [prop]: value,
    });
  };

  // request server to empty its cache
  requestEmptyCache = () => {
    this.handleChange('loading', true);
    fetch(' /backend/lib/admin.php', {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        cache: true,
      })
    })
      .then(response => {
        if (response.ok) {
          response.json().then(data => {
            if (data && data.status === 'success') {
              // cache was emptied
              this.context.handleNotificationChange(true, 'Der Server-Cache wurde geleert.', 'emptyCache', 'success');
            } else {
              this.context.handleNotificationChange(true, 'Der Server-Cache konnte nicht geleert werden.', 'emptyCache', 'error');
            }
          })
        } else {
          this.context.handleNotificationChange(true, 'Der Server-Cache konnte nicht geleert werden.', 'emptyCache', 'error');
        }
        this.handleChange('loading', false);

      })
      .catch(error => {
        this.context.handleNotificationChange(true, 'Der Server-Cache konnte nicht geleert werden.', 'emptyCache', 'error');
        this.handleChange('loading', false);
      });
  };

  render() {
    const {classes, requestStatus} = this.props;
    const {loading} = this.state;

    return (
      <div>
        <InfoCard message={'Hier können Sie den Cache der Suchaufrufe auf dem Server leeren.'}/>
        <Button
          color={'primary'}
          variant={'contained'}
          disabled={loading}
          className={classes.cacheButton}
          onClick={() => requestStatus(this.requestEmptyCache)}
        >
          Cache Leeren
        </Button>
         </div>
    )
  }

}

ServerManagement.propTypes = {
  classes: PropTypes.object.isRequired,
  requestStatus: PropTypes.func.isRequired,
};

ServerManagement.contextType = NotificationContext;

const styles =  theme => ({
  serverPaper: {
    marginTop:'10px',
    marginRight: '20px',
  },
  serverBox: {
    padding: 5,
    display: 'flex',
  },
  cacheButton: {
    margin: theme.spacing.unit,
  }
});

export default withStyles(styles)(ServerManagement);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import InfoIcon from '@material-ui/icons/Info';
import Typography from '@material-ui/core/Typography';
import Dialog from '@material-ui/core/Dialog';

class ContainsInput extends React.PureComponent {
  state = {
    keywords: this.props.initialValue,
    dialogOpen: false,
    syntaxError: false,
    errorMessage: 'Fehler',
  };

  onInputChange = (event) => {
    this.setState({keywords: event.target.value,}, () => {
      this.props.onInputChange(this.props.cardId, 'keywords', this.state.keywords); // update SearchCard
    });
  };

  onFocus = () => {
    this.setState({syntaxError: false})
  };

  onOpenDialog = () => {
    this.setState({dialogOpen: true})
  };

  onCloseDialog = () => {
    this.setState({dialogOpen: false})
  };

  checkInput = () => {
    let error = false;
    let errorMessage = '';
    //let string = this.state.keywords;

    this.setState({
      syntaxError: error,
      errorMessage: errorMessage,
    }, () => {
      this.props.handleBrowserChange('keywordError', error);
    });
  };

  render() {
    const { classes, variant, disabled } = this.props;
    const { keywords, syntaxError } = this.state;

    return(
      <div className={classes.root}>
        <TextField
          id="textContains"
          label="Text enthält"
          placeholder="Geben Sie Stichwörter ein"
          multiline
          fullWidth={true}
          className={classes.textField}
          variant={variant}
          disabled={disabled}
          error={syntaxError}
          value={keywords}
          onChange={this.onInputChange}
          onFocus={this.onFocus}
          onBlur={this.checkInput}
          InputProps={{
            endAdornment: (
              <InputAdornment position="end">
                <IconButton onClick={this.onOpenDialog}>
                  <InfoIcon />
                </IconButton>
              </InputAdornment>)}}
        />

        {(this.state.syntaxError || this.state.timeFromError) &&
        <Typography color={'error'} className={classes.errorMessage}>{this.state.errorMessage}</Typography>}

        <Dialog
          open={this.state.dialogOpen}
          onClose={this.onCloseDialog}
          aria-labelledby="dialogTitle"
          className={classes.dialogWrapper}
        >
          <Typography variant={"h6"} color="primary" className={classes.listHeader}>Hinweise zum "Text enthält" Suchfeld</Typography>
          <div className={classes.listWrapper}>
            <ul>
              <Typography color={"error"}>
                <b>Achtung: Durch umfangreiche Eingaben wird die Suche auf dem Server sehr speicherintensiv und kann eventuell nicht ausgeführt werden.
                  Liefert eine Anfrage keine Ergebnisse, so kann dies auch an einer falschen Syntax in diesem Feld liegen.</b>
              </Typography>
              <li>
                <Typography>Geben Sie <b>einzelne Wörter oder Phrasen</b> an.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Der Frühling ist schön</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte, die die exakte Phrase enthalten. Groß- und Kleinschreibung wird nicht beachtet.</Typography>
              </li>
              <li>
                <Typography>Wenn Sie Phrasen suchen, die mit einem bestimmten Wort anfangen, kennzeichnen Sie dies mit einem <b>Stern</b> (*) am Ende der Phrase.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Blume*</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte mit dem Wort Blume und auch z.B. Blumenstrauß, aber nicht Ringelblume.</Typography>
              </li>
              <li>
                <Typography>Verbinden Sie Phrasen durch ein <b>"and"</b>.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Frühling "and" Sonne</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte, die die Worte "Frühling" und "Sonne" gleichzeitig enthalten.</Typography>
              </li>
              <li>
                <Typography>Verwenden Sie <b>"or"</b>, um nach alternativen Phrasen zu suchen. Sie können bis zu 3 Mal in einer Teil-Suche "or" verwenden.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Frühling "or" Sonne</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte, die mindestens eins der Worte "Frühling" und "Sonne" enthalten.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Frühling "or" Sonne "or" die Welt ist schön "or" die Blumen blühen</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte, die mindestens eine der 4 Wörter/Phrasen enthalten.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Frühling "or" Sonne "and" Liebe</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte, die mindestens eins der Worte "Frühling" oder "Sonne" und "Liebe" (die letzten beiden gleichzeitig, falls das erste nicht enthalten ist) enthalten.</Typography>
              </li>
              <li>
                <Typography>Schließen Sie Phrasen mit <b>"not"</b> von der Suche aus. Achtung: "not" soll immer am Ende der Eingabe (ggf. nach "or" oder "and") stehen.</Typography>
                <Typography color={"textSecondary"}>Bsp.: Frühling "or" Sonne "not" Liebe</Typography>
                <Typography color={"textSecondary"}>Ergebnis: Texte, die mindestens eins der Wörter "Frühling" oder "Sonne" und gleichzeitig nicht das Wort "Liebe" enthält</Typography>
              </li>
            </ul>
          </div>
        </Dialog>
      </div>
    );
  }
}

ContainsInput.propTypes = {
  classes: PropTypes.object.isRequired,
  cardId: PropTypes.string.isRequired,
  variant: PropTypes.string.isRequired,
  initialValue: PropTypes.string.isRequired,
  disabled: PropTypes.bool.isRequired,
  onInputChange: PropTypes.func.isRequired,
  handleBrowserChange: PropTypes.func.isRequired,
};

const styles = theme => ({
  root:{
    flexGrow: 1,
    paddingTop: 5,
    marginLeft: theme.spacing.unit,
    marginRight: theme.spacing.unit,
    position: 'relative',
  },
  textField:{
    //marginTop: theme.spacing.unit,
  },
  listWrapper:{
    margin: theme.spacing.unit * 2,
    marginRight: theme.spacing.unit * 4,
    marginTop: -3 * theme.spacing.unit,
    minWidth: 400,
  },
  dialogHeader:{
    marginLeft: theme.spacing.unit * 4,
    marginTop: theme.spacing.unit,
  },
  listHeader:{
    marginLeft: theme.spacing.unit * 4,
    marginTop: theme.spacing.unit * 4,
    marginBottom: theme.spacing.unit * 2,
  },
  dialogWrapper:{
    minWidth: 600,
  },
  errorMessage:{
    fontSize: '0.75rem',
  }
});

export default withStyles(styles)(ContainsInput);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import { Link } from "react-router-dom";
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';

class AdminNav extends React.Component {
  state = {
    selectedItem: -1,
  };

  handleChange = (prop, value) => {
    this.setState({
      [prop]: value,
    });
  };

  handleSelectChange = (item) => {
    this.handleChange('selectedItem', item);
  };

  render() {
    const {classes} = this.props;
    const {selectedItem} = this.state;

    return (
      <div className={classes.root}>
        <Drawer
          className={classes.drawer}
          variant="permanent"
          classes={{
            paper: classes.drawerPaper,
          }}
          anchor="left"
        >
          <List className={classes.navElements}>
            <ListItem button component={Link} to='/admin' color='primary' selected={selectedItem === 0}>
              <ListItemText primary={<Typography color={'primary'} variant={'h6'}>Texte verwalten</Typography>}/>
            </ListItem>
            <ListItem button component={Link} to='/admin/users' color='primary' selected={selectedItem === 1}>
              <ListItemText primary={<Typography color={'primary'} variant={'h6'}>User verwalten</Typography>} />
            </ListItem>
            <ListItem button component={Link} to='/admin/server' color='primary' selected={selectedItem === 2}>
              <ListItemText primary={<Typography color={'primary'} variant={'h6'}>Server verwalten</Typography>}/>
            </ListItem>
          </List>
        </Drawer>
      </div>
    )
  }

}

AdminNav.propTypes = {
  classes: PropTypes.object.isRequired,
};

const styles = ({
  root: {
    width: 'auto',
    height: '100%',
    //zIndex: 1,
  },
  navElements:{
    paddingTop: 100,
    height: 150,
  },
  navElement:{
    //display:'block',
  },
  drawer:{
    width: 210,
    flexShrink: 0,
  },
  drawerPaper:{
    width: 210,
    backgroundColor: '#e8e8e8',
  },
});

export default withStyles(styles)(AdminNav);<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import Button from "@material-ui/core/Button";
import TextField from '@material-ui/core/TextField';
import Paper from "@material-ui/core/Paper/Paper";
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import NotificationContext from '../../../notifications/NotificationContext';
import InfoCard from '../../../InfoCard';
import Typography from "@material-ui/core/Typography/Typography";
import IconButton from '@material-ui/core/IconButton';
import DeleteIcon from '@material-ui/icons/Delete';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';

class UserManagement extends React.Component {
  componentDidMount = () => {
    // load user list
    this.props.requestStatus(this.requestUsers);
  };

  state = {
    loading: false,
    users: [{user: 'exampleUser', isAdmin: true}],
    isOpen: false,
    newUser: {
      name: '',
      pw1: '',
      pw2: '',
      isAdmin: false,
    }
  };

  handleChange = (prop, value) => {
    this.setState({
      [prop]: value,
    });
  };

  handleUserChange = (event) => {
    let newUser = this.state.newUser;
    let name = event.target.name;
    newUser[name] = name === 'isAdmin'? event.target.checked : event.target.value;
    this.handleChange('newUser', newUser);
  };

  // check how many admins are in the users list
  getAdminNumber = users => users.filter(user => user.isAdmin).length;

  // check username min length of 3 characters and max length of 30 characters
  checkUsernameLength = username => username.length > 2 && username.length < 31;

  // compare passwords (before sending)
  comparePasswords = (pw1, pw2) => {
    return pw1 === pw2;
  };

  // Is password long enough?
  checkPasswordLength = password => password.length < 41 && password.length > 3;

  // request users list from server
  requestUsers = () => {
    this.handleChange('loading', true);
    fetch('/backend/lib/admin.php', {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        users: true,
      }),
    })
      .then(response => {
        if (response.ok) {
          response.json().then(data => {
            if (data && data.status === 'success') {
              // received users list from server
              this.handleChange('users', JSON.parse(data.users));
            } else {
              this.context.handleNotificationChange(true, 'Die Nutzerliste konnte nicht geladen werden.', 'loadUsers', 'error');
            }
          })
        } else {
          this.context.handleNotificationChange(true, 'Die Nutzerliste konnte nicht geladen werden.', 'loadUsers', 'error');
        }
        this.handleChange('loading', false);
      })
      .catch(error => {
        this.context.handleNotificationChange(true, 'Die Nutzerliste konnte nicht geladen werden.', 'loadUsers', 'error');
        this.handleChange('loading', false);
      });
  };

  // create new user on server
  requestNewUser = () => {
    // check inputs before sending request
    if(!this.checkUsernameLength(this.state.newUser.name)
      || !this.checkPasswordLength(this.state.newUser.pw1) || !this.checkPasswordLength(this.state.newUser.pw2)
      || !this.comparePasswords(this.state.newUser.pw1, this.state.newUser.pw2)
    ) {
      this.context.handleNotificationChange(true, 'Bitte geben Sie gültige Nutzerdaten ein.', 'newUser', 'error');
      return;
    }

    this.handleChange('loading', true);
    fetch('/backend/lib/admin.php', {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        username: this.state.newUser.name,
        password: <PASSWORD>,
        isadmin: this.state.newUser.isAdmin === false ? 0 : 1,
      }),
    })
      .then(response => {
        if (response.ok) {
          response.json().then(data => {
            if (data && data.status === 'success') {
              // created new user
              this.props.requestStatus(this.requestUsers);
              this.context.handleNotificationChange(true, 'Benutzer*in erfolgreich angelegt.', 'createUser', 'success');
            } else {
              this.context.handleNotificationChange(true, 'Benutzer*in konnte nicht angelegt werden.', 'createUser', 'error');
            }
          })
        } else {
          this.context.handleNotificationChange(true, 'Benutzer*in konnte nicht angelegt werden.', 'createUser', 'error');
        }
        this.handleChange('loading', false);
      })
      .catch(error => {
        this.context.handleNotificationChange(true, 'Benutzer*in konnte nicht angelegt werden.', 'createUser', 'error');
        this.handleChange('loading', false);
      });
  };

  // delete user from server
  requestDelete = username => {
    if(!username) return;
    if(this.state.users.filter(user => user.user === username)[0].isAdmin && this.getAdminNumber(this.state.users) === 1) {
      this.context.handleNotificationChange(true, 'Legen Sie einen neuen Admin-Account an bevor Sie den einzigen löschen.', 'deleteUser', 'error');
      return;
    }

    fetch('/backend/lib/admin.php', {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        delete: username,
      })
    })
      .then(response => {
        if (response.ok) {
          response.json().then(data => {
            if (data.status === 'success') {
              this.props.requestStatus(this.requestUsers);
              this.context.handleNotificationChange(true, 'Nutzer*in erfolgreich gelöscht.', 'deleteUser', 'success')
            } else {
              this.context.handleNotificationChange(true, 'Nutzer*in konnte nicht gelöscht werden.', 'deleteUser', 'error')
            }
          })
        } else {
          this.context.handleNotificationChange(true, 'Nutzer*in konnte nicht gelöscht werden.', 'deleteUser', 'error')
        }
      })
      .catch(error => {
          this.context.handleNotificationChange(true, 'Nutzer*in konnte nicht gelöscht werden.', 'deleteUser', 'error')
        }
      );
  };

  render() {
    const { classes, requestStatus } = this.props;
    const {  loading, newUser, users } = this.state;

    return (
        <div>
          <InfoCard message={'Hier können Sie Nutzer*innen hinzufügen und entfernen.'}/>
          <div className={classes.marginLeftContainer}>
            <Typography variant={'h6'} color={'primary'}>Registrierte Nutzer*innen</Typography>
            <Paper className={classes.tableWrapper}>
              <Table>
                <TableHead>
                  <TableRow>
                    <TableCell scope={'column'}>Nr.</TableCell>
                    <TableCell scope={'column'}>Username</TableCell>
                    <TableCell scope={'column'}>Admin</TableCell>
                    <TableCell scope={'column'}>Aktion</TableCell>
                  </TableRow>
                </TableHead>
                <TableBody>
                  {users.map((user, index) => (
                    <TableRow key={user.user}>
                      <TableCell scope={'row'}>{index + 1}</TableCell>
                      <TableCell>{user.user}</TableCell>
                      <TableCell>{user.isadmin? 'Ja' : 'Nein'}</TableCell>
                      <TableCell className={classes.flexContainer}>
                        <IconButton onClick={() => requestStatus(() => this.requestDelete(user.user))}>
                          <DeleteIcon color={'secondary'}/>
                        </IconButton>
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </Paper>
          </div>
          <br/>
          <div className={classes.marginLeftContainer}>
            <Typography variant={'h6'} color={'primary'}>Nutzer*in hinzufügen</Typography>
            <Paper className={classes.loginContainer}>
              <div className={classes.formContainer}>
                <Typography color={'primary'}>Bitte geben Sie Nutzerdaten ein:</Typography>
                <TextField
                  disabled={loading}
                  autoFocus={true}
                  className={classes.textField}
                  label={'Benutzername'}
                  type={'text'}
                  value={newUser.name}
                  error={!loading && newUser.name !== '' && !this.checkUsernameLength(newUser.name)}
                  name={'name'}
                  onChange={this.handleUserChange}
                />
                {!loading && newUser.name !== '' && !this.checkUsernameLength(newUser.name) && <Typography color={'error'} className={classes.errorMessage}>
                  Geben Sie mindestens 3 und höchstens 30 Zeichen ein.
                </Typography>}
                <TextField
                  className={classes.textField}
                  disabled={loading}
                  label={'Passwort'}
                  type={'password'}
                  name={'pw1'}
                  value={newUser.pw1}
                  error={!loading && newUser.pw1 !== '' && !this.checkPasswordLength(newUser.pw1)}
                  onChange={this.handleUserChange}
                />
                {!loading && newUser.pw1 !== '' && !this.checkPasswordLength(newUser.pw1) && <Typography color={'error'} className={classes.errorMessage}>
                  Das Passwort muss mindestens 4 und höchstens 40 Zeichen lang sein.
                </Typography>}
                <TextField
                  className={classes.textField}
                  disabled={loading}
                  label={'Passwort wiederholen'}
                  type={'password'}
                  name={'pw2'}
                  value={newUser.pw2}
                  error={!loading && newUser.pw1 !== '' && !this.comparePasswords(newUser.pw1, newUser.pw2)}
                  onChange={this.handleUserChange}
                />
                {!loading && !this.comparePasswords(newUser.pw1, newUser.pw2) && <Typography color={'error'} className={classes.errorMessage}>
                  Die Passwörter stimmen nicht überein.
                </Typography>}
                <FormControlLabel
                  control={
                  <Checkbox
                    checked={newUser.isAdmin}
                    onChange={this.handleUserChange}
                    name={'isAdmin'}
                  />
                  }
                  label={'Admin'}/>
                <div className={classes.flexContainer}>
                  <Button
                    disabled={loading}
                    size="small"
                    color="primary"
                    variant={"contained"}
                    className={classes.button}
                    onClick={() => requestStatus(this.requestNewUser)}
                  >
                    Hinzufügen
                  </Button>
                </div>
              </div>
            </Paper>
          </div>
          <br/>
        </div>
    );
  }
}

UserManagement.propTypes = {
  classes: PropTypes.object.isRequired,
  requestStatus: PropTypes.func.isRequired,
};

UserManagement.contextType = NotificationContext;

const styles = theme => ({

  root: {
    marginTop: theme.spacing.unit * 3,
    overflowX: 'auto',
    marginLeft: theme.spacing.unit,
    marginRight: '20px',
    width: 896,
  },
  marginLeftContainer: {
    marginLeft: theme.spacing.unit,
  },
  table: {
    minWidth: 400,
    maxWidth: 800,
    marginLeft: theme.spacing.unit,
  },
  newUser:{
    padding: '20px',
    marginTop: '20px'
  },
  nutzerpaper: {
    marginTop:'10px',
    marginRight: '20px',
  },
  nutzerBox: {
    padding: 5,
    display: 'flex',
  },
  tableWrapper: {
    marginTop: theme.spacing.unit * 3,
    overflowX: 'auto',
    marginRight: '20px',
    width: 896,
  },
  loginContainer:{
    width: 400,
    padding: theme.spacing.unit * 6,
    marginTop: theme.spacing.unit * 2
  },
  formContainer:{
    display: 'flex',
    flexDirection: 'column',
  },
  textField: {
    marginTop: theme.spacing.unit * 2,
  },
  button:{
    marginTop: theme.spacing.unit * 5,
    marginRight: theme.spacing.unit * 2,
    width: 110,
    padding: theme.spacing.unit,
  },
  flexContainer:{
    display: 'flex',
    alignItems: 'flex-end',
  },
  loginErrorMessage:{
    marginLeft: -theme.spacing.unit,
    marginTop: theme.spacing.unit * 3,
    width: theme.spacing.unit * 52,
  },
});

export default withStyles(styles)(UserManagement);

<file_sep>import React from 'react';
import PropTypes from 'prop-types';
import {withStyles} from '@material-ui/core/styles';
import Paper from "@material-ui/core/Paper/Paper";
import Typography from '@material-ui/core/Typography';
import Link from '@material-ui/core/Link';
import green from '@material-ui/core/colors/green';
import NotificationContext from '../../../notifications/NotificationContext';

// display search results + provide download link
class Results extends React.Component {
  state = {};

  shouldComponentUpdate = (nextProps) => {
    // prevent links from updating after search is complete
    return !(nextProps.formats !== this.props.formats
      && nextProps.searchID === this.props.searchID);
  };

  // see if user is still logged in
  requestStatus = func => {
    // check if session is still valid otherwise logout user
    fetch("/backend/lib/sessionManagement.php", {
      method: 'POST',
      credentials: 'same-origin', // allow cookies -> session management
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        loginStatus: true,
        loginID: this.props.sessionID
      })
    }).then(response => {
        if(response.ok) {
          response.json().then(data => {
            if(!data || data.status === 'error') {
              this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
              this.props.handleAppChange('loggedIn', false);
              this.props.handleAppChange('isAdmin', false);
            } else {
              func();
            }
          });
        } else {
          this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error');
          this.logout();
        }
      }
    ).catch(error => {
        this.context.handleNotificationChange(true, 'Ihre Sitzung ist abgelaufen.', 'sessionCheck', 'error', 404);
        this.logout();
      }
    );
  };

  downloadResults = (format,getAll) => {
    if(getAll) {
      let a = document.createElement('a');
      a.href = ('/backend/archive/') + (format==='json'? 'corpusJson.zip' : 'corpusTxt.zip');
      a.download = format==='json'? 'corpusJson' : 'corpusTxt';
      a.click();
    } else {
      // request data + handle response for each selected format separately
      fetch("/backend/lib/functions.php", {
        method: 'POST',
        credentials: 'same-origin', // allow cookies -> session management
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          download: this.props.filenames,
          format: format,
        }),
      })
        .then(response => {
          if(response.ok) {
            response.json().then(data => {
              if(data && data.status === 'success') {
                // create invisible anchor and click it
                let a = document.createElement('a');
                const filename = data.filename.substr(0, data.filename.length-4) + (format === 'json'? 'Json' : 'Txt');
                a.href = '/backend/database/_cache/' + data.filename;
                a.download = filename;
                a.click();
              } else {
                this.context.handleNotificationChange(true, 'Die Ergebnis-Dateien konnten nicht vom Server geladen werden.', 'download', 'error');
              }
            })
          } else {
            this.context.handleNotificationChange(true, 'Die Ergebnis-Dateien konnten nicht vom Server geladen werden.', 'download', 'error');
          }
        })
        .catch((error) => {
          this.context.handleNotificationChange(true, 'Die Ergebnis-Dateien konnten nicht vom Server geladen werden.', 'download', 'error');
        });
    }
  };

  render() {
    const {classes, number, formats, getAll} = this.props;

    return (
      <div className={classes.root}>
        <Paper className={classes.resultContainer} elevation={0} >
          <Typography>
            {formats.includes('json') &&
            <Link
              classes={{root: {cursor: 'pointer'}}}
              className={classes.resultLink}
              onClick={() => this.requestStatus(() => this.downloadResults('json', getAll))}
            >
               Suchergebnisse im JSON-Format herunterladen {!getAll && '(' + number + ' Treffer)'}
            </Link>}
            {formats.includes('txt') &&
            <Link
              classes={{root: {cursor: 'pointer'}}}
              className={classes.resultLink}
              onClick={() => this.requestStatus(() => this.downloadResults('txt', getAll))}
            >
              Suchergebnisse im TXT-Format herunterladen {!getAll && '(' + number + ' Treffer)'}
            </Link>}
          </Typography>
        </Paper>
      </div>
    )
  }
}

Results.propTypes = {
  classes: PropTypes.object.isRequired,
  filenames: PropTypes.string,
  number: PropTypes.number.isRequired,
  formats: PropTypes.arrayOf(PropTypes.string),
  searchID: PropTypes.any.isRequired,
  handleAppChange: PropTypes.func.isRequired,
  sessionID: PropTypes.any.isRequired,
  getAll: PropTypes.bool.isRequired,
};

Results.contextType = NotificationContext;

const styles = theme => ({
  root: {
    margin: theme.spacing.unit,
  },
  resultContainer:{
    padding: theme.spacing.unit,
    backgroundColor: green[600],
  },
  resultLink:{
    color: 'white',
    display: 'block',
    cursor: 'pointer',
  }
});

export default withStyles(styles)(Results);