Init
This commit is contained in:
commit
f0d178101a
545 changed files with 78540 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
*.pyc
|
||||||
|
.vscode/
|
||||||
|
.history
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
db.sqlite3
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
/staticfiles
|
||||||
|
!/assets/staticfiles
|
||||||
|
medias/
|
||||||
|
media/
|
||||||
|
*.mo
|
||||||
|
efundburo/production.py
|
||||||
|
*.ipynb
|
||||||
|
alpine
|
||||||
|
data.json
|
49
.gitlab-ci.yml
Normal file
49
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# This file is a template, and might need editing before it works on your project.
|
||||||
|
# Official framework image. Look for the different tagged releases at:
|
||||||
|
# https://hub.docker.com/r/library/python
|
||||||
|
image: python:latest
|
||||||
|
|
||||||
|
# Pick zero or more services to be used on all builds.
|
||||||
|
# Only needed when using a docker container to run your tests in.
|
||||||
|
# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
|
||||||
|
services:
|
||||||
|
- postgres:latest
|
||||||
|
|
||||||
|
variables:
|
||||||
|
POSTGRES_DB: database_name
|
||||||
|
|
||||||
|
# This folder is cached between builds
|
||||||
|
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- ~/.cache/pip/
|
||||||
|
|
||||||
|
# This is a basic example for a gem or script which doesn't use
|
||||||
|
# services such as redis or postgres
|
||||||
|
before_script:
|
||||||
|
- python -V # Print out python version for debugging
|
||||||
|
# Uncomment next line if your Django app needs a JS runtime:
|
||||||
|
# - apt-get update -q && apt-get install nodejs -yqq
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
|
||||||
|
# To get Django tests to work you may need to create a settings file using
|
||||||
|
# the following DATABASES:
|
||||||
|
#
|
||||||
|
# DATABASES = {
|
||||||
|
# 'default': {
|
||||||
|
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
# 'NAME': 'ci',
|
||||||
|
# 'USER': 'postgres',
|
||||||
|
# 'PASSWORD': 'postgres',
|
||||||
|
# 'HOST': 'postgres',
|
||||||
|
# 'PORT': '5432',
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# and then adding `--settings app.settings.ci` (or similar) to the test command
|
||||||
|
|
||||||
|
test:
|
||||||
|
variables:
|
||||||
|
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
|
||||||
|
script:
|
||||||
|
- python manage.py test
|
8
.prettierrc
Normal file
8
.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
printWidth: 100
|
||||||
|
tabWidth: 4
|
||||||
|
useTabs: true
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
jsxSingleQuote: true
|
||||||
|
trailingComma: "none"
|
||||||
|
arrowParens: "avoid"
|
63
README.md
Normal file
63
README.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# eFundBüro
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
|
||||||
|
- clone repository
|
||||||
|
- Tested with python version 3.6
|
||||||
|
- create virtualenv and `pip install -r requirements.txt`
|
||||||
|
- Run `python manage.py makemigrations`
|
||||||
|
- create db with `python manage.py migrate`
|
||||||
|
- create superuser with `python manage.py createsuperuser`
|
||||||
|
- import catalog from xlsx `python manage.py import_catalog ~/ownCloud/Project_eFundbuero/Datenbank-Zeugs/Fundbüro\ Muster\ Export\ aus\ MuseumPlus.xlsx`
|
||||||
|
|
||||||
|
## Sample uwsgi config file
|
||||||
|
|
||||||
|
```
|
||||||
|
[uwsgi]
|
||||||
|
socket = /home/app/app/uwsgi.sock
|
||||||
|
chdir = /home/app/app
|
||||||
|
venv = /home/app/pyvenv
|
||||||
|
wsgi-file = efundburo/wsgi.py
|
||||||
|
processes = 4
|
||||||
|
threads = 2
|
||||||
|
chmod-socket = 666
|
||||||
|
vacuum = true
|
||||||
|
plugins = python3
|
||||||
|
uid = app
|
||||||
|
gid = app
|
||||||
|
env = DJANGO_SETTINGS_MODULE=efundburo.production
|
||||||
|
env = DEBUG=True
|
||||||
|
wsgi-disable-file-wrapper = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production setup
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
#### API
|
||||||
|
|
||||||
|
* `GET /search/?query={keyword}` allow to retreive item matching the keyword in the
|
||||||
|
item title, description or inventory_number.
|
||||||
|
* `GET /item/{item_pk}/` return the details of an item
|
||||||
|
* `GET /item/{item_pk}/comments/` return the validated comments of an item.
|
||||||
|
* `POST /item/{item_pk}/comments/` allow you to create a new comment. The endpoint
|
||||||
|
expect a JSON dict with the following keys :
|
||||||
|
* `comment`: the content of the comment as a string
|
||||||
|
* `author`: the author of the comment as a string
|
||||||
|
* *optional* `field_specific`: the field to comment on. Can be any of the
|
||||||
|
field of the Item model
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
Application is running as `efundbuero` user in `/home/efundbuero/efundbuero`. To deploy:
|
||||||
|
|
||||||
|
```
|
||||||
|
# SSH to server
|
||||||
|
sudo su - efundbuero
|
||||||
|
workon efundbuero
|
||||||
|
cd efundbuero
|
||||||
|
git pull
|
||||||
|
# Run migrations, collect staticfiles etc if needed
|
||||||
|
exit
|
||||||
|
sudo systemctl restart apache2
|
||||||
|
```
|
54
assets/js/components/App.js
Normal file
54
assets/js/components/App.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||||
|
import AppContentContainer from './AppContent/AppContentContainer';
|
||||||
|
import AppMenuContainer from './AppMenu/AppMenuContainer';
|
||||||
|
import CIDetailViewContainer from './AppContent/CIDetailView/CIDetailViewContainer';
|
||||||
|
import reduxLang from '../middleware/lang';
|
||||||
|
import StartScreenContainer from './StartScreenContainer';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getLocaleFromURL, defineLocale, shouldDisplayDynamicComponent, getDjangoView, routePathForDynamicComponent } from '../utils';
|
||||||
|
import BurgerBtnContainer from './AppMenu/BurgerBtnContainer';
|
||||||
|
|
||||||
|
const App = ({ pending, menuOpen, setLocale, isPreLaunch }) => {
|
||||||
|
// set initial language
|
||||||
|
setLocale(defineLocale(isPreLaunch));
|
||||||
|
let localeFromURL = getLocaleFromURL(window.location.pathname);
|
||||||
|
if (localeFromURL === undefined) {
|
||||||
|
localeFromURL = ''
|
||||||
|
}
|
||||||
|
const basename = '/' + localeFromURL + useSelector(state => state.urls.site);
|
||||||
|
|
||||||
|
const activeSite = useSelector(state => state.urls.site);
|
||||||
|
const urls = useSelector(state => state.urls.allSites[activeSite].urls);
|
||||||
|
const DynamicComponent = React.memo(() => (
|
||||||
|
<div className="AppContent">
|
||||||
|
<BurgerBtnContainer />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: getDjangoView() }} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`App ${isTouchable() ? 'touch' : ''} ${pending ? 'loading' : ''} ${
|
||||||
|
menuOpen ? 'menuOpen' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Router basename={basename}>
|
||||||
|
<AppMenuContainer />
|
||||||
|
<Switch>
|
||||||
|
{ shouldDisplayDynamicComponent(urls) ? <Route path={routePathForDynamicComponent(basename)} component={DynamicComponent} /> : null }
|
||||||
|
<Route path='/' component={AppContentContainer} />
|
||||||
|
</Switch>
|
||||||
|
<Route path='/catalog/:itemId' component={CIDetailViewContainer} />
|
||||||
|
<Route path='/start' component={StartScreenContainer} />
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reduxLang()(App);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const isTouchable = () => {
|
||||||
|
return 'ontouchstart' in window || navigator.msMaxTouchPoints > 0;
|
||||||
|
};
|
14
assets/js/components/AppContainer.js
Normal file
14
assets/js/components/AppContainer.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
pending: state.ui.pending,
|
||||||
|
menuOpen: state.ui.menuOpen,
|
||||||
|
isPreLaunch: state.ui.isPreLaunch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppContainer = connect(mapStateToProps)(App);
|
||||||
|
|
||||||
|
export default AppContainer;
|
31
assets/js/components/AppContent/AppContent.js
Normal file
31
assets/js/components/AppContent/AppContent.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ContentHeader from './ContentHeader';
|
||||||
|
import ContentBodyContainer from './ContentBodyContainer';
|
||||||
|
import ContentOverlayContainer from './ContentOverlayContainer';
|
||||||
|
import PreLaunchPage from './PreLaunchPage';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
export default class AppContent extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { isPreLaunch, getMode } = this.props;
|
||||||
|
|
||||||
|
animateScrollTo(document.getElementById('root'));
|
||||||
|
|
||||||
|
if (isPreLaunch === undefined) {
|
||||||
|
getMode();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isPreLaunch } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={`AppContent${isPreLaunch ? ' is-pre-launch' : ''}`}>
|
||||||
|
<ContentHeader isPreLaunch={isPreLaunch} />
|
||||||
|
{!isPreLaunch && <ContentBodyContainer />}
|
||||||
|
{!isPreLaunch && <ContentOverlayContainer />}
|
||||||
|
{isPreLaunch && <PreLaunchPage />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
assets/js/components/AppContent/AppContentContainer.js
Normal file
14
assets/js/components/AppContent/AppContentContainer.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { getMode } from '../../redux/actions/mode';
|
||||||
|
import AppContent from './AppContent';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
catalogFetched: !!state.catalog.length,
|
||||||
|
isPreLaunch: state.ui.isPreLaunch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppContentContainer = connect(mapStateToProps, { getMode })(AppContent);
|
||||||
|
|
||||||
|
export default AppContentContainer;
|
89
assets/js/components/AppContent/CIDetailView/CIDetailView.js
Normal file
89
assets/js/components/AppContent/CIDetailView/CIDetailView.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import DVHeader from './DVHeader';
|
||||||
|
import DVBody from './DVBody';
|
||||||
|
import DVFooter from './DVFooter';
|
||||||
|
import Spinner from '../../Spinner';
|
||||||
|
import Div100vh from 'react-div-100vh';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
const CIDetailView = ({
|
||||||
|
activeItem,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
close,
|
||||||
|
catalog,
|
||||||
|
pending,
|
||||||
|
openDetail,
|
||||||
|
showNextItem,
|
||||||
|
showPrevItem,
|
||||||
|
match,
|
||||||
|
participated,
|
||||||
|
sendParticipation,
|
||||||
|
toggleParticipated,
|
||||||
|
color
|
||||||
|
}) => {
|
||||||
|
if (activeItem === null) {
|
||||||
|
// we have the catalog, but this view was not opened through an action
|
||||||
|
// but by it's URL. Now we need to dispatch an action that,
|
||||||
|
// depending on the current route, will tell which id the ID of the item to display.
|
||||||
|
if (catalog.length) {
|
||||||
|
catalog.forEach((item, i) => {
|
||||||
|
if (item.inventory_number === match.params.itemId.replace('__', ' ')) {
|
||||||
|
openDetail(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't know yet what data to display.
|
||||||
|
// Most certainly the catalog is being fetched.
|
||||||
|
return (
|
||||||
|
<div className='CIDetailView__loading AppContent'>
|
||||||
|
<DVHeader close={close} />
|
||||||
|
{pending && <Spinner />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (activeItem.inventory_number !== match.params.itemId.replace('__', ' ')) {
|
||||||
|
window.history.pushState(null, null, activeItem.inventory_number.replace(' ', '__'));
|
||||||
|
}
|
||||||
|
const doYouKnowMoreURL = (
|
||||||
|
useSelector(state => state.urls.site) + "do-you-know-more/?object_id=" + activeItem.inventory_number
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Div100vh>
|
||||||
|
<div className='CIDetailView AppContent' id='CIDetailView'>
|
||||||
|
<div id='CIDetailView-top-element'></div>
|
||||||
|
<DVHeader close={close} />
|
||||||
|
<DVBody
|
||||||
|
item={activeItem}
|
||||||
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
|
showNextItem={showNextItem}
|
||||||
|
showPrevItem={showPrevItem}
|
||||||
|
/>
|
||||||
|
<DVFooter
|
||||||
|
item={activeItem}
|
||||||
|
participated={participated}
|
||||||
|
sendParticipation={sendParticipation}
|
||||||
|
toggleParticipated={toggleParticipated}
|
||||||
|
doYouKnowMoreURL={doYouKnowMoreURL}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Div100vh>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CIDetailView.propTypes = {
|
||||||
|
activeItem: PropTypes.object,
|
||||||
|
isFirst: PropTypes.bool,
|
||||||
|
isLast: PropTypes.bool,
|
||||||
|
close: PropTypes.func.isRequired,
|
||||||
|
catalog: PropTypes.array,
|
||||||
|
openDetail: PropTypes.func.isRequired,
|
||||||
|
showNextItem: PropTypes.func.isRequired,
|
||||||
|
showPrevItem: PropTypes.func.isRequired,
|
||||||
|
match: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
export default CIDetailView;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
openDetail,
|
||||||
|
showNextItem,
|
||||||
|
showPrevItem,
|
||||||
|
toggleParticipated,
|
||||||
|
closeDetail
|
||||||
|
} from '../../../redux/actions/ui';
|
||||||
|
import CIDetailView from './CIDetailView';
|
||||||
|
import { sendParticipation } from '../../../redux/actions/participate';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
if (state.ui.activeItem === null) {
|
||||||
|
return {
|
||||||
|
catalog: state.catalog,
|
||||||
|
pending: state.ui.pending,
|
||||||
|
activeItem: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const activeItemData = state.catalog[state.ui.activeItem];
|
||||||
|
const isFirst = state.ui.activeItem === 0;
|
||||||
|
const isLast = state.ui.activeItem === state.catalog.length - 1;
|
||||||
|
console.log("????????????????????????????");
|
||||||
|
return {
|
||||||
|
activeItem: activeItemData,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
participated: state.ui.participated,
|
||||||
|
color: state.urls.color
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
close: () => dispatch(closeDetail()),
|
||||||
|
openDetail: item => dispatch(openDetail(item)),
|
||||||
|
showNextItem: () => dispatch(showNextItem()),
|
||||||
|
showPrevItem: () => dispatch(showPrevItem()),
|
||||||
|
sendParticipation: data => dispatch(sendParticipation(data)),
|
||||||
|
toggleParticipated: () => dispatch(toggleParticipated())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const CIDetailViewContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CIDetailView);
|
||||||
|
|
||||||
|
export default CIDetailViewContainer;
|
183
assets/js/components/AppContent/CIDetailView/DVBody.js
Normal file
183
assets/js/components/AppContent/CIDetailView/DVBody.js
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import reduxLang from '../../../middleware/lang';
|
||||||
|
import DVLabel from './DVLabel';
|
||||||
|
import DetailViewShare from './DetailViewShare';
|
||||||
|
import { DefaultPlayer as Video } from 'react-html5video';
|
||||||
|
import { useSwipeable } from 'react-swipeable';
|
||||||
|
import Div100vh from 'react-div-100vh';
|
||||||
|
|
||||||
|
const DVBody = ({ item, isFirst, isLast, showNextItem, showPrevItem, t }) => (
|
||||||
|
<div className='DVBody'>
|
||||||
|
<DetailMediaContainer
|
||||||
|
item={item}
|
||||||
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
|
showNextItem={showNextItem}
|
||||||
|
showPrevItem={showPrevItem}
|
||||||
|
/>
|
||||||
|
<DetailViewShare title={item.title} />
|
||||||
|
<DetailViewTags tags={item.tags} label={t('category')} />
|
||||||
|
<DetailAttributeContainer
|
||||||
|
name={t('inventory_number')}
|
||||||
|
content={item['inventory_number']}
|
||||||
|
phone={true}
|
||||||
|
/>
|
||||||
|
<DetailAttributeContainer name={t('title')} content={item.title} />
|
||||||
|
<DetailAttributeContainer name={t('date')} content={item.date} />
|
||||||
|
<DetailAttributeContainer name={t('owner')} content={item.participant} />
|
||||||
|
<DetailAttributeContainer name={t('special')} content={item.description} />
|
||||||
|
{item.history && <DetailAttributeContainer name={t('story')} content={item.history} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DVBody.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
isFirst: PropTypes.bool.isRequired,
|
||||||
|
isLast: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
export default reduxLang('DVBody')(DVBody);
|
||||||
|
|
||||||
|
const DetailMediaContainer = ({ item, isFirst, isLast, showNextItem, showPrevItem }) => {
|
||||||
|
const handlers = useSwipeable({
|
||||||
|
onSwipedLeft: !isLast ? showNextItem : undefined,
|
||||||
|
onSwipedRight: !isFirst ? showPrevItem : undefined
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={`DetailMediaContainer${item.youtube ? ' youtube' : ''}`} {...handlers}>
|
||||||
|
{!isFirst && <LeftNavButton handler={showPrevItem} />}
|
||||||
|
<DVLabel text={item.inventory_number} />
|
||||||
|
{item.video || item.youtube ? (
|
||||||
|
<DIVideo video={item.video} youtube={item.youtube} alt={item.title} />
|
||||||
|
) : (
|
||||||
|
<DIImage item={item} alt={item.title} />
|
||||||
|
)}
|
||||||
|
{!isLast && <RightNavButton handler={showNextItem} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DetailMediaContainer.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
isFirst: PropTypes.bool.isRequired,
|
||||||
|
isLast: PropTypes.bool.isRequired,
|
||||||
|
showNextItem: PropTypes.func.isRequired,
|
||||||
|
showPrevItem: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const LeftNavButton = ({ handler }) => (
|
||||||
|
<button className='LeftNavButton' onClick={handler}>
|
||||||
|
<img src='/static/gfx/alps_links.svg' alt='left' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
LeftNavButton.propTypes = {
|
||||||
|
handler: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
const RightNavButton = ({ handler }) => (
|
||||||
|
<button className='RightNavButton' onClick={handler}>
|
||||||
|
<img src='/static/gfx/alps_rechts.svg' alt='right' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
RightNavButton.propTypes = {
|
||||||
|
handler: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function youtube_parser(url) {
|
||||||
|
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
|
||||||
|
var match = url.match(regExp);
|
||||||
|
return match && match[7].length == 11 ? match[7] : false;
|
||||||
|
}
|
||||||
|
const DIVideo = ({ video, youtube }) => (
|
||||||
|
<div className='DIVideo'>
|
||||||
|
{!youtube ? (
|
||||||
|
<Video controls={['PlayPause', 'Seek', 'Time', 'Volume', 'Fullscreen']}>
|
||||||
|
<source src={video} type='video/mp4' />
|
||||||
|
</Video>
|
||||||
|
) : (
|
||||||
|
<iframe
|
||||||
|
width='100%'
|
||||||
|
src={`https://www.youtube.com/embed/${youtube_parser(youtube)}`}
|
||||||
|
frameBorder='0'
|
||||||
|
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DIVideo.propTypes = {
|
||||||
|
video: PropTypes.string,
|
||||||
|
youtube: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIImage = ({ item, alt = '' }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const openFullScreen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
|
||||||
|
// fix cut off part on Safari
|
||||||
|
document.getElementById('CIDetailView').style.overflow = 'visible';
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFullScreen = () => {
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
// revert Safari fix don when opening
|
||||||
|
document.getElementById('CIDetailView').style.overflow = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
return <FullScreenImage image={item.image} close={closeFullScreen} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='DIImage' onClick={openFullScreen}>
|
||||||
|
<img src={item.image} alt={alt} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DIImage.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const FullScreenImage = ({ image, close }) => (
|
||||||
|
<Div100vh>
|
||||||
|
<div
|
||||||
|
className='FullScreenImage'
|
||||||
|
style={{ backgroundImage: `url('${image}')` }}
|
||||||
|
onClick={close}
|
||||||
|
></div>
|
||||||
|
</Div100vh>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DetailViewTags = ({ tags, label }) => (
|
||||||
|
<div className='DetailViewTags'>
|
||||||
|
<DVLabel text={label} />
|
||||||
|
<div>
|
||||||
|
{tags.map((tag, i) => (
|
||||||
|
<span className='Tag' key={i}>
|
||||||
|
<button style={{border: '0px solid black'}}>{tag.name}</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DetailAttributeContainer = ({ name, content, phone = false }) => (
|
||||||
|
<div className={`DetailAttributeContainer${phone ? ' phone-invetory-number' : ''}`}>
|
||||||
|
<DVLabel text={name} />
|
||||||
|
<DAContent content={content} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DetailAttributeContainer.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.any.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const DAContent = ({ content }) => (
|
||||||
|
<div className='DAContent'>
|
||||||
|
<p className='medium-text'>{content}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DAContent.propTypes = {
|
||||||
|
content: PropTypes.any.isRequired
|
||||||
|
};
|
180
assets/js/components/AppContent/CIDetailView/DVFooter.js
Normal file
180
assets/js/components/AppContent/CIDetailView/DVFooter.js
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import reduxLang from '../../../middleware/lang';
|
||||||
|
import CVDescInput from './../../ContribView/CVDescInput';
|
||||||
|
import CVUploadInput from './../../ContribView/CVUploadInput';
|
||||||
|
import CVPersonalInfosInputs from '../../ContribView/CVPersonalInfosInputs';
|
||||||
|
import CVSubmit from './../../ContribView/CVSubmit';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import { Notification } from 'react-notification';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
@reduxLang('ContribView')
|
||||||
|
@autobind
|
||||||
|
export default class DVFooter extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isToggleOn: false,
|
||||||
|
desc: '',
|
||||||
|
file: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
birth_year: '',
|
||||||
|
address: '',
|
||||||
|
post_code: '',
|
||||||
|
city: '',
|
||||||
|
phone: '',
|
||||||
|
mail: '',
|
||||||
|
agb: false,
|
||||||
|
notification: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick() {
|
||||||
|
const { isToggleOn } = this.state;
|
||||||
|
if (isToggleOn) {
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
animateScrollTo(document.getElementById('CIDetailView-top-element'), {
|
||||||
|
elementToScroll: document.getElementById('CIDetailView')
|
||||||
|
}).then(hasScrolledToPosition => {
|
||||||
|
// scroll animation is finished
|
||||||
|
|
||||||
|
// "hasScrolledToPosition" indicates if page/element
|
||||||
|
// was scrolled to a desired position
|
||||||
|
// or if animation got interrupted
|
||||||
|
if (hasScrolledToPosition) {
|
||||||
|
// page is scrolled to a desired position
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isToggleOn: !isToggleOn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
isToggleOn: !isToggleOn
|
||||||
|
});
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
|
||||||
|
// if not executed 2 time, the scroll would not be complete
|
||||||
|
setImmediate(() =>
|
||||||
|
animateScrollTo(document.getElementById('detail-view-footer-button'), {
|
||||||
|
elementToScroll: document.getElementById('CIDetailView')
|
||||||
|
}).then(() =>
|
||||||
|
animateScrollTo(document.getElementById('detail-view-footer-button'), {
|
||||||
|
elementToScroll: document.getElementById('CIDetailView')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectTypeChange(event) {
|
||||||
|
this.setState({ object_type: event.target.value });
|
||||||
|
}
|
||||||
|
onDescChange(event) {
|
||||||
|
this.setState({ desc: event.target.value });
|
||||||
|
}
|
||||||
|
onFileChange(value) {
|
||||||
|
this.setState({ file: value });
|
||||||
|
}
|
||||||
|
onPersonalInfosChange(event) {
|
||||||
|
this.setState({ [event.target.name]: event.target.value });
|
||||||
|
}
|
||||||
|
onKeepChange(event) {
|
||||||
|
this.setState({ keep: event.target.value });
|
||||||
|
}
|
||||||
|
onAGBChange(event) {
|
||||||
|
this.setState({ agb: event.target.checked });
|
||||||
|
}
|
||||||
|
onSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.validForm()) {
|
||||||
|
const data = { ...this.state, object_id: this.props.item.inventory_number };
|
||||||
|
delete data.abg;
|
||||||
|
delete data.notification;
|
||||||
|
delete data.isToggleOn;
|
||||||
|
|
||||||
|
this.props.sendParticipation(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validForm() {
|
||||||
|
const { desc, first_name, last_name, mail } = this.state;
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
if (!desc) {
|
||||||
|
this.setState({ notification: t('noti_desc') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last_name) {
|
||||||
|
this.setState({ notification: t('noti_last_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first_name) {
|
||||||
|
this.setState({ notification: t('noti_first_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mail) {
|
||||||
|
this.setState({ notification: t('noti_mail') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotificationDismiss() {
|
||||||
|
this.setState({ notification: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isToggleOn, notification } = this.state;
|
||||||
|
const { t, participated, doYouKnowMoreURL, color } = this.props;
|
||||||
|
return (
|
||||||
|
<div className='DVFooter' style={{backgroundColor: color}}>
|
||||||
|
<div>
|
||||||
|
<div className='DVFooterTop'>
|
||||||
|
<a id='detail-view-footer-button' target="_blank" href={doYouKnowMoreURL}>
|
||||||
|
<p className='medium-text'>{t('know_more')}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{isToggleOn && (
|
||||||
|
<div className='DVDropdown'>
|
||||||
|
<div className='DVText'>
|
||||||
|
<p className='medium-text'>{/* TODO */}</p>
|
||||||
|
</div>
|
||||||
|
{participated && (
|
||||||
|
<div className='CVParticipatedMessage medium-text'>
|
||||||
|
{t('thank_you')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Notification
|
||||||
|
isActive={!!notification}
|
||||||
|
message={notification}
|
||||||
|
action={'schliessen'}
|
||||||
|
onClick={this.onNotificationDismiss}
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={this.onSubmit}
|
||||||
|
style={participated ? { opacity: 0, pointerEvents: 'none' } : {}}
|
||||||
|
>
|
||||||
|
<CVDescInput onDescChange={this.onDescChange} />
|
||||||
|
<CVUploadInput onFileChange={this.onFileChange} />
|
||||||
|
<CVPersonalInfosInputs
|
||||||
|
onPersonalInfosChange={this.onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
<CVSubmit />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
17
assets/js/components/AppContent/CIDetailView/DVHeader.js
Normal file
17
assets/js/components/AppContent/CIDetailView/DVHeader.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const DVHeader = ({ close }) => (
|
||||||
|
<div className='DVHeader'>
|
||||||
|
<Link to='/' onClick={close}>
|
||||||
|
<img src='/static/gfx/alps_x.svg' alt='close' />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
DVHeader.propTypes = {
|
||||||
|
close: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DVHeader;
|
9
assets/js/components/AppContent/CIDetailView/DVLabel.js
Normal file
9
assets/js/components/AppContent/CIDetailView/DVLabel.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const DVLabel = ({ text }) => <div className='DVLabel small-text'>{text}</div>;
|
||||||
|
DVLabel.propTypes = {
|
||||||
|
text: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DVLabel;
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import DVLabel from './DVLabel';
|
||||||
|
import reduxLang from '../../../middleware/lang';
|
||||||
|
|
||||||
|
const DetailViewShare = ({ title, t }) => (
|
||||||
|
<div className='DetailViewShare'>
|
||||||
|
<DVLabel text={t('share')} />
|
||||||
|
<div className='DetailViewShare--items'>
|
||||||
|
<ShareItem
|
||||||
|
icon={<img src='/static/gfx/alps_fb.svg' alt='fb' />}
|
||||||
|
href={`http://www.facebook.com/sharer.php?u=${window.location.href}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareItem
|
||||||
|
icon={<img src='/static/gfx/alps_twitter.svg' alt='tw' />}
|
||||||
|
href={`https://twitter.com/share?url=${window.location.href}&text=${encodeURIComponent(
|
||||||
|
title
|
||||||
|
)}&hashtags=alpinesmueum`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareItem
|
||||||
|
icon={<img src='/static/gfx/alps_mail.svg' alt='email' />}
|
||||||
|
href={`mailto:?subject=${title}&body=${window.location.href}`}
|
||||||
|
newTab={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ShareItem
|
||||||
|
icon={<img src='/static/gfx/whatsapp.svg' alt='whatsapp' />}
|
||||||
|
href={`whatsapp://send?text=${window.location.href}`}
|
||||||
|
newTab={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('DVBody')(DetailViewShare);
|
||||||
|
|
||||||
|
const ShareItem = ({ icon, href, newTab = true}) => (
|
||||||
|
<a className='ShareItem' href={href} target={newTab ? '_blank' : ''}>
|
||||||
|
{icon}
|
||||||
|
</a>
|
||||||
|
);
|
80
assets/js/components/AppContent/CatalogItem.js
Normal file
80
assets/js/components/AppContent/CatalogItem.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import GridImg from './GridImg';
|
||||||
|
|
||||||
|
@reduxLang('AppContent')
|
||||||
|
export default class CatalogItem extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { locale: props.locale };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMitmachenClick (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
let elem = ReactDOM.findDOMNode(event.target).parentNode.parentNode;
|
||||||
|
window.location.href = elem.getAttribute("href");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { item, openDetail, index, locale, isInternal } = this.props;
|
||||||
|
|
||||||
|
if (isInternal && (item.youtube || item.video)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.youtube && !item.image_width && !item.image_height) {
|
||||||
|
item.image_width = 480;
|
||||||
|
item.image_height = 360;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='CatalogItem grid-item'>
|
||||||
|
<div className='grid-gap'>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
item.inventory_number
|
||||||
|
? `/catalog/${item.inventory_number.replace(' ', '__')}`
|
||||||
|
: '/mitmachen' // this case is when it is an ad
|
||||||
|
}
|
||||||
|
onClick={item.inventory_number ? () => openDetail(index) : this.handleMitmachenClick} // only when not an add
|
||||||
|
>
|
||||||
|
<div className='CatalogItem__body'>
|
||||||
|
<GridImg
|
||||||
|
src={
|
||||||
|
item.youtube // case of youtube media
|
||||||
|
? `https://img.youtube.com/vi/${parse_video_id(
|
||||||
|
item.youtube
|
||||||
|
)}/0.jpg`
|
||||||
|
: item.inventory_number // other cases
|
||||||
|
? item.thumbnail // case of image or video
|
||||||
|
: item[locale] // case of bubble
|
||||||
|
}
|
||||||
|
item={item}
|
||||||
|
/>
|
||||||
|
{(item.video || item.youtube) && (
|
||||||
|
<img
|
||||||
|
className='CatalogItem__videoIcon'
|
||||||
|
src='/static/gfx/alps_play.svg'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CatalogItem.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
openDetail: PropTypes.func.isRequired,
|
||||||
|
index: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function parse_video_id(url) {
|
||||||
|
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
|
||||||
|
var match = url.match(regExp);
|
||||||
|
return match && match[7].length == 11 ? match[7] : false;
|
||||||
|
}
|
103
assets/js/components/AppContent/CatalogList.js
Normal file
103
assets/js/components/AppContent/CatalogList.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import React, { Component, createRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import CatalogItem from './CatalogItem';
|
||||||
|
import shuffleSeed from 'shuffle-seed';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import 'intersection-observer';
|
||||||
|
|
||||||
|
const rand = () => Math.floor(Math.random() * 1000000) + 1;
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
class _CatalogList extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.bottomElement = createRef();
|
||||||
|
this.observer = undefined;
|
||||||
|
this.state = {
|
||||||
|
pageNumber: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.rebuildLayout();
|
||||||
|
if (this.props.items.length) {
|
||||||
|
// Add an observer to the intersection with the bottom element
|
||||||
|
// (to know when the user scrolled to the end)
|
||||||
|
setImmediate(this.observeIntersection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observeIntersection() {
|
||||||
|
// if there is an old obeserver
|
||||||
|
if (this.observer) {
|
||||||
|
// we don't need it anymore so we disconnect itfl
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
if (!this.bottomElement.current) {
|
||||||
|
// we don't need the observer if there is no "load more" button
|
||||||
|
// (due to the fact that there are not a lot of elements)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.observer = new IntersectionObserver(entries => {
|
||||||
|
let isIntersecting = false;
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
isIntersecting = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isIntersecting) {
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.observer.observe(this.bottomElement.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore() {
|
||||||
|
this.setState({ pageNumber: this.state.pageNumber + 1 });
|
||||||
|
setImmediate(this.props.rebuildLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { items, ads, openDetail, showingSearchResults, t, isInternal } = this.props;
|
||||||
|
const ITEMS_PER_PAGE = 20;
|
||||||
|
let displayed_items = [];
|
||||||
|
|
||||||
|
displayed_items = items.slice(0, this.state.pageNumber * ITEMS_PER_PAGE);
|
||||||
|
|
||||||
|
let hasMore = true;
|
||||||
|
if (displayed_items.length === items.length) {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showingSearchResults && !displayed_items.length) {
|
||||||
|
return <div className='CatalogList--noResults medium-text'>{t('nothing_found')}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='grid-container'>
|
||||||
|
{displayed_items.map((item, i) => (
|
||||||
|
<CatalogItem item={item} key={i} index={i} openDetail={openDetail} isInternal={isInternal} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{hasMore && displayed_items.length && (
|
||||||
|
<button
|
||||||
|
className='load-more-btn medium-text'
|
||||||
|
ref={this.bottomElement}
|
||||||
|
onClick={this.loadMore}
|
||||||
|
>
|
||||||
|
{t('load_more')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_CatalogList.propTypes = {
|
||||||
|
items: PropTypes.array.isRequired,
|
||||||
|
openDetail: PropTypes.func.isRequired,
|
||||||
|
showingSearchResults: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
const CatalogList = reduxLang('AppContent')(_CatalogList);
|
||||||
|
export default CatalogList;
|
24
assets/js/components/AppContent/CatalogListContainer.js
Normal file
24
assets/js/components/AppContent/CatalogListContainer.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { openDetail, rebuildLayout } from '../../redux/actions/ui';
|
||||||
|
import CatalogList from './CatalogList';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
items: state.catalog,
|
||||||
|
ads: state.ads,
|
||||||
|
showingSearchResults: state.ui.showingSearchResults,
|
||||||
|
magnetInstance: state.ui.magnetInstance,
|
||||||
|
isInternal: state.ui.isInternal
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
openDetail: item => dispatch(openDetail(item)),
|
||||||
|
rebuildLayout: shuffle => dispatch(rebuildLayout(shuffle))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const CatalogListContainer = connect(mapStateToProps, mapDispatchToProps)(CatalogList);
|
||||||
|
|
||||||
|
export default CatalogListContainer;
|
27
assets/js/components/AppContent/ContentBody.js
Normal file
27
assets/js/components/AppContent/ContentBody.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import CatalogListContainer from './CatalogListContainer';
|
||||||
|
import Spinner from '../Spinner';
|
||||||
|
|
||||||
|
export default class ContentBody extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { catalogFetched, getCatalog } = this.props;
|
||||||
|
// this fetches the entire catalog, no matter if the list of a single item is opened
|
||||||
|
if (!catalogFetched) {
|
||||||
|
getCatalog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { pending } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={`ContentBody${pending ? ' loading' : ''}`}>
|
||||||
|
{pending ? <Spinner /> : <CatalogListContainer />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ContentBody.propTypes = {
|
||||||
|
pending: PropTypes.bool.isRequired,
|
||||||
|
catalogFetched: PropTypes.bool.isRequired,
|
||||||
|
getCatalog: PropTypes.func.isRequired
|
||||||
|
};
|
34
assets/js/components/AppContent/ContentBodyContainer.js
Normal file
34
assets/js/components/AppContent/ContentBodyContainer.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ContentBody from './ContentBody';
|
||||||
|
import { getCatalog } from '../../redux/actions/catalog';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
let pending = state.ui.pending;
|
||||||
|
|
||||||
|
// NOTE: do this in a middleware didn't work
|
||||||
|
if (!pending) {
|
||||||
|
// case where we have no items (but not because there are no search results)
|
||||||
|
if (!state.ui.showingSearchResults && state.catalog.length === 0) {
|
||||||
|
pending = true;
|
||||||
|
}
|
||||||
|
// case where we only have ads and are still pending catalog items
|
||||||
|
if (!state.ui.showingSearchResults && state.catalog.every(item => !item.inventory_number)) {
|
||||||
|
pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pending,
|
||||||
|
catalogFetched: !!state.catalog.length
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
getCatalog: () => dispatch(getCatalog())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContentBodyContainer = connect(mapStateToProps, mapDispatchToProps)(ContentBody);
|
||||||
|
|
||||||
|
export default ContentBodyContainer;
|
92
assets/js/components/AppContent/ContentHeader.js
Normal file
92
assets/js/components/AppContent/ContentHeader.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes, { func } from 'prop-types';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import HeaderSearchFieldContainer from './HeaderSearchFieldContainer';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import BurgerBtnContainer from '../AppMenu/BurgerBtnContainer';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
class _ContentHeader extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
searching: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSearching() {
|
||||||
|
this.setState({ searching: !this.state.searching });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { searching } = this.state;
|
||||||
|
const { isPreLaunch, color, scrollingText } = this.props;
|
||||||
|
return searching ? (
|
||||||
|
<div className='ContentHeader'>
|
||||||
|
<HeaderSearchFieldContainer closeFunc={this.toggleSearching} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="marquee" style={{backgroundColor: color}}>
|
||||||
|
<div className="marquee__inner" aria-hidden="true">
|
||||||
|
<span>{scrollingText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='ContentHeader'>
|
||||||
|
<HeaderIcon />
|
||||||
|
<BurgerBtnContainer />
|
||||||
|
<HeaderTitle />
|
||||||
|
{!isPreLaunch && <HeaderSearchBtn handler={this.toggleSearching} />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
color: state.urls.color,
|
||||||
|
scrollingText: state.urls.scrollingText
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const ContentHeader = connect(mapStateToProps)(_ContentHeader);
|
||||||
|
export default ContentHeader;
|
||||||
|
|
||||||
|
function HeaderIcon() {
|
||||||
|
const currentSite = useSelector(state => state.urls.site);
|
||||||
|
const allSites = Object.getOwnPropertyNames(useSelector(state => state.urls.allSites))
|
||||||
|
const siteID = allSites.findIndex((siteName) => siteName === currentSite) + 1;
|
||||||
|
return <h1 className='HeaderIcon big-text'>№ {siteID}</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getTitle = t => {
|
||||||
|
if ('ontouchstart' in window || navigator.msMaxTouchPoints > 0) {
|
||||||
|
return 'Fundbüro № 1';
|
||||||
|
} else {
|
||||||
|
return t('skiing');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function _HeaderTitle({ t }) {
|
||||||
|
const siteName = useSelector(state => state.urls.name);
|
||||||
|
return <h1 className='HeaderTitle big-text'>{siteName}</h1>;
|
||||||
|
}
|
||||||
|
_HeaderTitle.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
const HeaderTitle = reduxLang('HeaderTitle')(_HeaderTitle);
|
||||||
|
|
||||||
|
const HeaderSearchBtn = ({ handler }) => (
|
||||||
|
<div className='HeaderSearchBtn'>
|
||||||
|
<button onClick={handler}>
|
||||||
|
<img src='/static/gfx/alps_lupe.svg' alt='search' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
HeaderSearchBtn.propTypes = {
|
||||||
|
handler: PropTypes.func.isRequired
|
||||||
|
};
|
16
assets/js/components/AppContent/ContentOverlay.js
Normal file
16
assets/js/components/AppContent/ContentOverlay.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const ContentOverlay = ({ shuffle }) => (
|
||||||
|
<div className='ContentOverlay'>
|
||||||
|
<button onClick={shuffle}>
|
||||||
|
<img src='/static/gfx/alps_random.svg' alt='shuffle'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ContentOverlay.propTypes = {
|
||||||
|
shuffle: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentOverlay;
|
11
assets/js/components/AppContent/ContentOverlayContainer.js
Normal file
11
assets/js/components/AppContent/ContentOverlayContainer.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { shuffle } from '../../redux/actions/ui';
|
||||||
|
import ContentOverlay from './ContentOverlay';
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return { shuffle: () => dispatch(shuffle()) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContentOverlayContainer = connect(undefined, mapDispatchToProps)(ContentOverlay);
|
||||||
|
|
||||||
|
export default ContentOverlayContainer;
|
20
assets/js/components/AppContent/GridImg.js
Normal file
20
assets/js/components/AppContent/GridImg.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const GridImg = ({ src, item }) => {
|
||||||
|
let ratio = 1;
|
||||||
|
if (item.thumbnail_height && item.thumbnail_width) {
|
||||||
|
ratio = item.thumbnail_height / item.thumbnail_width;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url('${src}')`,
|
||||||
|
paddingBottom: `${ratio * 100}%`,
|
||||||
|
width: '100%',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundSize: 'cover'
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default GridImg;
|
7
assets/js/components/AppContent/GridImgContainer.js
Normal file
7
assets/js/components/AppContent/GridImgContainer.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { imageLoaded } from '../../redux/actions/ui';
|
||||||
|
import GridImg from './GridImg';
|
||||||
|
|
||||||
|
const GridImgContainer = connect(undefined, { imageLoaded })(GridImg);
|
||||||
|
|
||||||
|
export default GridImgContainer;
|
71
assets/js/components/AppContent/HeaderSearchField.js
Normal file
71
assets/js/components/AppContent/HeaderSearchField.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
class HeaderSearchField extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.searchInput = React.createRef();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
search: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.searchInput.current.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchInput(event) {
|
||||||
|
this.setState({ search: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.props.search(this.state.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
const { closeFunc, getCatalog, showingSearchResults } = this.props;
|
||||||
|
if (showingSearchResults) {
|
||||||
|
getCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
{closeFunc && closeFunc() };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
const { search } = this.state;
|
||||||
|
const { showClose, getCatalog, showingSearchResults } = this.props;
|
||||||
|
return (
|
||||||
|
<div className='HeaderSearchField'>
|
||||||
|
<form onSubmit={this.handleSearch}>
|
||||||
|
<input
|
||||||
|
className='medium-text'
|
||||||
|
type='text'
|
||||||
|
placeholder={t('search')}
|
||||||
|
ref={this.searchInput}
|
||||||
|
value={search}
|
||||||
|
onChange={this.handleSearchInput}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{showClose && <button className='HeaderSearchField_closeBtn' onClick={this.handleClose}>
|
||||||
|
<img src='/static/gfx/alps_x.svg' alt='close' />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderSearchField.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
search: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reduxLang('HeaderSearchField')(HeaderSearchField);
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { search, getCatalog } from '../../redux/actions/catalog';
|
||||||
|
import HeaderSearchField from './HeaderSearchField';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { showingSearchResults: state.ui.showingSearchResults };
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeaderSearchFieldContainer = connect(mapStateToProps, { search, getCatalog })(
|
||||||
|
HeaderSearchField
|
||||||
|
);
|
||||||
|
|
||||||
|
export default HeaderSearchFieldContainer;
|
182
assets/js/components/AppContent/PreLaunchForm.js
Normal file
182
assets/js/components/AppContent/PreLaunchForm.js
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import { Notification } from 'react-notification';
|
||||||
|
import Spinner from '../Spinner';
|
||||||
|
import CVDescInput from '../ContribView/CVDescInput';
|
||||||
|
import CVUploadInput from '../ContribView/CVUploadInput';
|
||||||
|
import CVPersonalInfosInputs from '../ContribView/CVPersonalInfosInputs';
|
||||||
|
import CVAGBInput from '../ContribView/CVAGBInput';
|
||||||
|
import CVSubmit from '../ContribView/CVSubmit';
|
||||||
|
import CVObjectTypeRadio from '../ContribView/CVObjectTypeRadio';
|
||||||
|
import CVKeepInput from '../ContribView/CVKeepInput';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
@reduxLang('ContribView')
|
||||||
|
@autobind
|
||||||
|
export default class PreLaunchForm extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
desc: '',
|
||||||
|
file: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
birth_year: '',
|
||||||
|
address: '',
|
||||||
|
post_code: '',
|
||||||
|
city: '',
|
||||||
|
phone: '',
|
||||||
|
mail: '',
|
||||||
|
agb: false,
|
||||||
|
notification: '',
|
||||||
|
uploadeBtn: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectTypeChange(event) {
|
||||||
|
this.setState({ object_type: event.target.value });
|
||||||
|
}
|
||||||
|
onDescChange(event) {
|
||||||
|
this.setState({ desc: event.target.value });
|
||||||
|
}
|
||||||
|
onFileChange(value) {
|
||||||
|
this.setState({ file: value });
|
||||||
|
}
|
||||||
|
onPersonalInfosChange(event) {
|
||||||
|
this.setState({ [event.target.name]: event.target.value });
|
||||||
|
}
|
||||||
|
onKeepChange(event) {
|
||||||
|
this.setState({ keep: event.target.value });
|
||||||
|
}
|
||||||
|
onAGBChange(event) {
|
||||||
|
this.setState({ agb: event.target.checked });
|
||||||
|
}
|
||||||
|
onSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.validForm()) {
|
||||||
|
const data = { ...this.state };
|
||||||
|
delete data.abg;
|
||||||
|
delete data.notification;
|
||||||
|
|
||||||
|
this.props.sendParticipation(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validForm() {
|
||||||
|
const {
|
||||||
|
object_type,
|
||||||
|
desc,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
mail,
|
||||||
|
keep,
|
||||||
|
agb
|
||||||
|
} = this.state;
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
if (!object_type) {
|
||||||
|
this.setState({ notification: t('noti_object_type') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!desc) {
|
||||||
|
this.setState({ notification: t('noti_desc') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first_name) {
|
||||||
|
this.setState({ notification: t('noti_first_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last_name) {
|
||||||
|
this.setState({ notification: t('noti_last_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mail) {
|
||||||
|
this.setState({ notification: t('noti_mail') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keep) {
|
||||||
|
this.setState({ notification: t('noti_keep') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agb) {
|
||||||
|
this.setState({ notification: t('noti_agb') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotificationDismiss() {
|
||||||
|
this.setState({ notification: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetUploadBtn() {
|
||||||
|
this.setState({ uploadeBtn: false });
|
||||||
|
setImmediate(() => this.setState({ uploadeBtn: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.participated) {
|
||||||
|
animateScrollTo(document.getElementById('pre-launch-contribute-form'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.locale !== this.props.locale) {
|
||||||
|
this.resetUploadBtn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { notification, uploadeBtn } = this.state;
|
||||||
|
const { pending, participated, t } = this.props;
|
||||||
|
if (pending) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`PreLaunchForm ContribView${participated ? ' participated' : ''}`}>
|
||||||
|
<h1 className='big-text'>{t('we_search')}</h1>
|
||||||
|
<div className='CVText'>
|
||||||
|
<p
|
||||||
|
className='medium-text'
|
||||||
|
dangerouslySetInnerHTML={{ __html: t('intro_text') }}
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<div className='CVBody'>
|
||||||
|
<Notification
|
||||||
|
isActive={!!notification}
|
||||||
|
message={notification}
|
||||||
|
action={t('noti_close')}
|
||||||
|
onClick={this.onNotificationDismiss}
|
||||||
|
/>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<CVObjectTypeRadio onObjectTypeChange={this.onObjectTypeChange} />
|
||||||
|
<CVDescInput onDescChange={this.onDescChange} />
|
||||||
|
{uploadeBtn && (
|
||||||
|
<CVUploadInput
|
||||||
|
onFileChange={this.onFileChange}
|
||||||
|
reset={this.resetUploadBtn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CVPersonalInfosInputs onPersonalInfosChange={this.onPersonalInfosChange} />
|
||||||
|
<CVKeepInput onKeepChange={this.onKeepChange} />
|
||||||
|
<CVAGBInput onAGBChange={this.onAGBChange} />
|
||||||
|
<CVSubmit />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{participated && (
|
||||||
|
<div className='CVParticipatedMessage medium-text'>{t('thank_you')}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
15
assets/js/components/AppContent/PreLaunchFormContainer.js
Normal file
15
assets/js/components/AppContent/PreLaunchFormContainer.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { sendParticipation } from '../../redux/actions/participate';
|
||||||
|
import { toggleParticipated } from '../../redux/actions/ui';
|
||||||
|
import PreLaunchForm from './PreLaunchForm';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { pending: state.ui.pending, participated: state.ui.participated };
|
||||||
|
};
|
||||||
|
|
||||||
|
const PreLaunchFormContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ sendParticipation, toggleParticipated }
|
||||||
|
)(PreLaunchForm);
|
||||||
|
|
||||||
|
export default PreLaunchFormContainer;
|
57
assets/js/components/AppContent/PreLaunchPage.js
Normal file
57
assets/js/components/AppContent/PreLaunchPage.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PreLaunchFormContainer from './PreLaunchFormContainer';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import Newsletter from '../InfoView/Newsletter';
|
||||||
|
import ImageWithLegend from '../InfoView/ImageWithLegend';
|
||||||
|
import Sponsors from '../InfoView/Sponsors';
|
||||||
|
import InfoViewFooter from '../InfoView/InfoViewFooter';
|
||||||
|
|
||||||
|
const PreLaunchPage = ({ t, locale }) => (
|
||||||
|
<div className='PreLaunchPage'>
|
||||||
|
<div
|
||||||
|
className='PreLaunchPage-hereo'
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(/static/gfx/2019-12-09-Banner-OnePage-${locale}.jpg)`
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div className='PreLaunchPage-video'>
|
||||||
|
<iframe
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
src='https://www.youtube.com/embed/kjpw4LunEEA'
|
||||||
|
frameBorder='0'
|
||||||
|
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
<div className='PreLaunchPage-form'>
|
||||||
|
<a id='pre-launch-contribute-form' />
|
||||||
|
<PreLaunchFormContainer />
|
||||||
|
</div>
|
||||||
|
<div className='PreLaunchPage-info'>
|
||||||
|
<a id='pre-launch-info' />
|
||||||
|
<div className='InfoView'>
|
||||||
|
<h1 style={{ paddingTop: 0 }} className='big-text'>
|
||||||
|
{t('welcome_title')}
|
||||||
|
</h1>
|
||||||
|
<div className='InfoContent'>
|
||||||
|
<p
|
||||||
|
className='medium-text'
|
||||||
|
dangerouslySetInnerHTML={{ __html: t('welcome_text') }}
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<div className='ImagesList'>
|
||||||
|
<ImageWithLegend
|
||||||
|
image='/static/gfx/alps_bruegger_A0080_NAm015_edited_06.12.19-optmimized.jpg'
|
||||||
|
legend={t('image_legend')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Newsletter />
|
||||||
|
<Sponsors />
|
||||||
|
<InfoViewFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default reduxLang('InfoView')(PreLaunchPage);
|
44
assets/js/components/AppMenu/AppMenu.js
Normal file
44
assets/js/components/AppMenu/AppMenu.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import MenuHeader from './MenuHeader';
|
||||||
|
import MenuBodyContainer from './MenuBodyContainer';
|
||||||
|
import MenuFooter from './MenuFooter';
|
||||||
|
import Redirecter from './Redirecter';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { getSiteDomain, mostStrictSiteMatch } from '../../utils';
|
||||||
|
|
||||||
|
|
||||||
|
const AppMenu = ({ menuOpen, toggleMenu, getCatalog, isPreLaunch, isInternal, deselectTags }) => {
|
||||||
|
const allSites = useSelector(state => state.urls.allSites);
|
||||||
|
const color = useSelector(state => state.urls.color);
|
||||||
|
return (
|
||||||
|
<div className={`AppMenu ${menuOpen ? 'open' : ''}`}>
|
||||||
|
{isInternal && <Redirecter />}
|
||||||
|
<div className='AppMenu__container'>
|
||||||
|
<div className="SitesTabGroup">
|
||||||
|
{
|
||||||
|
Object.getOwnPropertyNames(allSites).map((site, i) => {
|
||||||
|
const isActive = site === mostStrictSiteMatch(allSites, getSiteDomain());
|
||||||
|
if (isActive) {
|
||||||
|
return <a className="SitesTab active" key={i} style={{backgroundColor: color}} href={site}>№ {i + 1}</a>
|
||||||
|
} else {
|
||||||
|
return <a className="SitesTab" key={i} href={site}>№ {i + 1}</a>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MenuHeader
|
||||||
|
toggleMenu={toggleMenu}
|
||||||
|
getCatalog={getCatalog}
|
||||||
|
isPreLaunch={isPreLaunch}
|
||||||
|
deselectTags={deselectTags}
|
||||||
|
/>
|
||||||
|
<MenuBodyContainer />
|
||||||
|
<MenuFooter isPreLaunch={isPreLaunch} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppMenu;
|
19
assets/js/components/AppMenu/AppMenuContainer.js
Normal file
19
assets/js/components/AppMenu/AppMenuContainer.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import AppMenu from './AppMenu';
|
||||||
|
import { toggleMenu } from '../../redux/actions/ui';
|
||||||
|
import { getCatalog } from '../../redux/actions/catalog';
|
||||||
|
import { deselectTags } from '../../redux/actions/tags';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
menuOpen: state.ui.menuOpen,
|
||||||
|
isPreLaunch: state.ui.isPreLaunch,
|
||||||
|
isInternal: state.ui.isInternal
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppMenuContainer = connect(mapStateToProps, { toggleMenu, getCatalog, deselectTags })(
|
||||||
|
AppMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppMenuContainer;
|
25
assets/js/components/AppMenu/BurgerBtn.js
Normal file
25
assets/js/components/AppMenu/BurgerBtn.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faBars } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
const BurgerBtn = ({ menuOpen, toggleMenu }) => (
|
||||||
|
<button className='HeaderBurger' onClick={toggleMenu}>
|
||||||
|
{menuOpen && (
|
||||||
|
<div
|
||||||
|
className='openMenuOverlay'
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation(); // prevent the execution of the onClick event of the button
|
||||||
|
toggleMenu();
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
<FontAwesomeIcon icon={faBars} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
BurgerBtn.propTypes = {
|
||||||
|
menuOpen: PropTypes.bool.isRequired,
|
||||||
|
toggleMenu: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BurgerBtn;
|
14
assets/js/components/AppMenu/BurgerBtnContainer.js
Normal file
14
assets/js/components/AppMenu/BurgerBtnContainer.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { toggleMenu } from '../../redux/actions/ui';
|
||||||
|
import BurgerBtn from './BurgerBtn';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { menuOpen: state.ui.menuOpen };
|
||||||
|
};
|
||||||
|
|
||||||
|
const BurgerBtnContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ toggleMenu }
|
||||||
|
)(BurgerBtn);
|
||||||
|
|
||||||
|
export default BurgerBtnContainer;
|
30
assets/js/components/AppMenu/MenuBody.js
Normal file
30
assets/js/components/AppMenu/MenuBody.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import TagGroup from './TagGroup';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const MenuBody = ({ tagGroups, getTags, selectTag, isPreLaunch }) => {
|
||||||
|
const [isTagsRequested, setIsTagsRequested] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTagsRequested) {
|
||||||
|
getTags();
|
||||||
|
setIsTagsRequested(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (isPreLaunch || Object.keys(tagGroups).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div className='MenuBody'>
|
||||||
|
<TagGroup tags={tagGroups.misc} category={'misc'} selectTag={selectTag} />
|
||||||
|
<TagGroup tags={tagGroups.where} category={'where'} selectTag={selectTag} />
|
||||||
|
<TagGroup tags={tagGroups.when} category={'when'} selectTag={selectTag} />
|
||||||
|
<TagGroup tags={tagGroups.object_type} category={'object_type'} selectTag={selectTag} />
|
||||||
|
<TagGroup tags={tagGroups.who} category={'who'} selectTag={selectTag} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuBody;
|
11
assets/js/components/AppMenu/MenuBodyContainer.js
Normal file
11
assets/js/components/AppMenu/MenuBodyContainer.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { getTags, selectTag } from '../../redux/actions/tags';
|
||||||
|
import MenuBody from './MenuBody';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { tagGroups: state.tags, isPreLaunch: state.ui.isPreLaunch };
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuBodyContainer = connect(mapStateToProps, { getTags, selectTag })(MenuBody);
|
||||||
|
|
||||||
|
export default MenuBodyContainer;
|
64
assets/js/components/AppMenu/MenuFooter.js
Normal file
64
assets/js/components/AppMenu/MenuFooter.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { updateURLS, isDjangoContentPresent, urlWithoutLocale } from '../../utils';
|
||||||
|
|
||||||
|
const MenuFooter = ({ t, isPreLaunch }) => (
|
||||||
|
<div className='MenuFooter'>
|
||||||
|
<a
|
||||||
|
href='http://www.alpinesmuseum.ch'
|
||||||
|
target='_blank'
|
||||||
|
className='MenuFooter-museum-link small-text'
|
||||||
|
>
|
||||||
|
<img src='/static/gfx/alsp_logo.svg' alt='alpines museum logo' />
|
||||||
|
{t('footer_phrase')}
|
||||||
|
</a>
|
||||||
|
<hr />
|
||||||
|
<LangSelector isPreLaunch={isPreLaunch} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function onClickHandler(locale, _setLocale, dispatch) {
|
||||||
|
_setLocale(locale);
|
||||||
|
updateURLS(locale, dispatch);
|
||||||
|
window.location.replace("/" + locale + urlWithoutLocale(window.location.pathname));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduxLang('AppMenu')(MenuFooter);
|
||||||
|
|
||||||
|
const _LangSelector = ({ locale, setLocale, isPreLaunch }) => {
|
||||||
|
const _setLocale = locale => {
|
||||||
|
localStorage.setItem('userLang', locale);
|
||||||
|
setLocale(locale);
|
||||||
|
};
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='LangSelector small-text'>
|
||||||
|
<span onClick={() => onClickHandler('de', _setLocale, dispatch)} className={locale === 'de' ? 'activeLang' : ''}>
|
||||||
|
DE
|
||||||
|
</span>
|
||||||
|
<span onClick={() => onClickHandler('fr', _setLocale, dispatch)} className={locale === 'fr' ? 'activeLang' : ''}>
|
||||||
|
FR
|
||||||
|
</span>
|
||||||
|
{!isPreLaunch && (
|
||||||
|
<span
|
||||||
|
onClick={() => onClickHandler('it', _setLocale, dispatch)}
|
||||||
|
className={locale === 'it' ? 'activeLang' : ''}
|
||||||
|
>
|
||||||
|
IT
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!isPreLaunch && (
|
||||||
|
<span
|
||||||
|
onClick={() => onClickHandler('en', _setLocale, dispatch)}
|
||||||
|
className={locale === 'en' ? 'activeLang' : ''}
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LangSelector = reduxLang()(_LangSelector);
|
30
assets/js/components/AppMenu/MenuHeader.js
Normal file
30
assets/js/components/AppMenu/MenuHeader.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import MenuNav from './MenuNav';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
const MenuHeader = ({ toggleMenu, getCatalog, isPreLaunch, deselectTags }) => (
|
||||||
|
<div className='MenuHeader'>
|
||||||
|
<a href="/" onClick={() => {
|
||||||
|
toggleMenu();
|
||||||
|
deselectTags();
|
||||||
|
getCatalog(true);
|
||||||
|
animateScrollTo(document.getElementById('root'));
|
||||||
|
}}><MenuTitle /></a>
|
||||||
|
<MenuNav toggleMenu={toggleMenu} isPreLaunch={isPreLaunch} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MenuHeader;
|
||||||
|
|
||||||
|
const _MenuTitle = ({ t, locale }) => (
|
||||||
|
<div className={`MenuTitle${locale === 'de' || locale === 'en' ? ' reverted' : ''}`}>
|
||||||
|
<h2 className='MenuTitle--small medium-text'>{t('title_small')}</h2>
|
||||||
|
<div className='MenuTitle--big'>
|
||||||
|
<h1 className='big-text'>{t('title_big_line1')}</h1>
|
||||||
|
<h1 className='big-text'>{t('title_big_line2')}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const MenuTitle = reduxLang('AppMenu')(_MenuTitle);
|
30
assets/js/components/AppMenu/MenuNav.js
Normal file
30
assets/js/components/AppMenu/MenuNav.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import MenuNavItem from './MenuNavItem';
|
||||||
|
|
||||||
|
function MenuNav({ toggleMenu, isPreLaunch, navigationItems }) {
|
||||||
|
return (
|
||||||
|
<div className='MenNav'>
|
||||||
|
{Object.getOwnPropertyNames(navigationItems).map((navitem, i) => (
|
||||||
|
<MenuNavItem
|
||||||
|
key={i}
|
||||||
|
navitem={navitem}
|
||||||
|
toggleMenu={toggleMenu}
|
||||||
|
isPreLaunch={isPreLaunch}
|
||||||
|
to={navigationItems[navitem][0]}
|
||||||
|
name={navitem}
|
||||||
|
isEnabled={navigationItems[navitem][1]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
navigationItems: state.urls.allSites[state.urls.site].urls
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps)(MenuNav);
|
43
assets/js/components/AppMenu/MenuNavItem.js
Normal file
43
assets/js/components/AppMenu/MenuNavItem.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
|
||||||
|
const MenuNavItem = ({ to, name, isEnabled, navitem, toggleMenu, t, location, isPreLaunch }) => {
|
||||||
|
if (isPreLaunch) {
|
||||||
|
const navitems_urls = { contribute: '/mitmachen', about: '/info' };
|
||||||
|
|
||||||
|
navitems_urls.contribute = 'pre-launch-contribute-form';
|
||||||
|
navitems_urls.about = 'pre-launch-info';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='MenuNavItem medium-text'>
|
||||||
|
<a
|
||||||
|
href={`#${navitems_urls[navitem]}`}
|
||||||
|
className={location.pathname.includes(navitems_urls[navitem]) ? 'active' : ''}
|
||||||
|
onClick={() => {
|
||||||
|
animateScrollTo(document.getElementById(navitems_urls[navitem]));
|
||||||
|
toggleMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(navitem)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return isEnabled ? (
|
||||||
|
<div className='MenuNavItem medium-text'>
|
||||||
|
<a href={to} className={location.pathname.includes(to) ? 'active' : ''} onClick={toggleMenu}>{name}</a>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
};
|
||||||
|
MenuNavItem.propTypes = {
|
||||||
|
navitem: PropTypes.string.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
location: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reduxLang('AppMenu')(withRouter(MenuNavItem));
|
73
assets/js/components/AppMenu/Redirecter.js
Normal file
73
assets/js/components/AppMenu/Redirecter.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
|
||||||
|
const user_events = [
|
||||||
|
'mousemove',
|
||||||
|
'mousedown',
|
||||||
|
'keydown',
|
||||||
|
'scroll',
|
||||||
|
'touchstart',
|
||||||
|
'resize',
|
||||||
|
'visibilitychange'
|
||||||
|
];
|
||||||
|
const idle_timeout = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
const inactivityTime = (callback, idleTimeout) => {
|
||||||
|
var time;
|
||||||
|
|
||||||
|
user_events.forEach(function(name) {
|
||||||
|
document.addEventListener(name, resetTimer, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetTimer(destroy) {
|
||||||
|
clearTimeout(time);
|
||||||
|
if (destroy === true) {
|
||||||
|
user_events.forEach(function(name) {
|
||||||
|
document.removeEventListener(name, resetTimer, true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
time = setTimeout(callback, idleTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetTimer();
|
||||||
|
return resetTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
export default class Redirecter extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
goBackHome: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewTimer() {
|
||||||
|
if (this.resetTimer) {
|
||||||
|
// destroy the current timer
|
||||||
|
this.resetTimer(true);
|
||||||
|
}
|
||||||
|
this.resetTimer = inactivityTime(() => {
|
||||||
|
this.setState({ goBackHome: true });
|
||||||
|
this.setState({ goBackHome: false });
|
||||||
|
}, idle_timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setNewTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.setNewTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.goBackHome) {
|
||||||
|
animateScrollTo(document.getElementById('root'));
|
||||||
|
return <Redirect to='/' />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
31
assets/js/components/AppMenu/TagGroup.js
Normal file
31
assets/js/components/AppMenu/TagGroup.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
const TagGroup = ({ tags, category, selectTag }) => (
|
||||||
|
<div className='TagGroup'>
|
||||||
|
{tags.map((tag, i) => (
|
||||||
|
<Tag
|
||||||
|
key={i}
|
||||||
|
tag={tag}
|
||||||
|
selectTag={() => {
|
||||||
|
selectTag({ category, slug: tag.slug });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Tag = ({ tag, selectTag }) => {
|
||||||
|
const color = useSelector(state => state.urls.color);
|
||||||
|
return (
|
||||||
|
<div className='Tag small-text'>
|
||||||
|
<Link to='/'>
|
||||||
|
<button onClick={selectTag} onMouseOut={(e) => e.target.style.backgroundColor = 'transparent'} onMouseOver={(e) => e.target.style.backgroundColor = color} className={tag.selected ? ' selected' : ''}>
|
||||||
|
{tag.name}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default TagGroup;
|
49
assets/js/components/ContribView/CVAGBInput.js
Normal file
49
assets/js/components/ContribView/CVAGBInput.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { ModalContainer, Modal } from 'minimal-react-modal';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVAGBInput = ({ onAGBChange, t }) => (
|
||||||
|
<ModalContainer>
|
||||||
|
{(openModal, closeModal, isActive) => (
|
||||||
|
<div className='CVForm CVAGBInput'>
|
||||||
|
<div className='medium-text'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
name='AGB'
|
||||||
|
value='AGB'
|
||||||
|
className='checkboxAGB'
|
||||||
|
onChange={onAGBChange}
|
||||||
|
/>
|
||||||
|
{t('accept_rules_part1')}{' '}
|
||||||
|
<a onClick={openModal}>{t('accept_rules_part2')}</a>{t('accept_rules_part3')}
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isActive={isActive} // required
|
||||||
|
closeModal={closeModal} // required
|
||||||
|
>
|
||||||
|
<AGB closeModal={closeModal} />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVAGBInput);
|
||||||
|
|
||||||
|
const _AGB = ({ closeModal, t }) => (
|
||||||
|
<div className='Modal--container'>
|
||||||
|
<div className='Modal--body'>
|
||||||
|
<button onClick={closeModal}>
|
||||||
|
<img src='/static/gfx/alps_x.svg' alt='close' />
|
||||||
|
</button>
|
||||||
|
<h1 className='big-text'>{t('title')}</h1>
|
||||||
|
{[...Array(6).keys()].map(key => (
|
||||||
|
<Fragment key={key}>
|
||||||
|
<h4 className='medium-text'>{t(`rule${key + 1}_title`)}</h4>
|
||||||
|
<p className='small-text'>{t(`rule${key + 1}_text`)}</p>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const AGB = reduxLang('Rules')(_AGB);
|
158
assets/js/components/ContribView/CVBody.js
Normal file
158
assets/js/components/ContribView/CVBody.js
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import CVObjectTypeRadio from './CVObjectTypeRadio';
|
||||||
|
import CVDescInput from './CVDescInput';
|
||||||
|
import CVUploadInput from './CVUploadInput';
|
||||||
|
import CVPersonalInfosInputs from './CVPersonalInfosInputs';
|
||||||
|
import CVAGBInput from './CVAGBInput';
|
||||||
|
import CVKeepInput from './CVKeepInput';
|
||||||
|
import CVSubmit from './CVSubmit';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import { Notification } from 'react-notification';
|
||||||
|
import Spinner from '../Spinner';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
@reduxLang('ContribView')
|
||||||
|
@autobind
|
||||||
|
export default class CVBody extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
object_type: '',
|
||||||
|
desc: '',
|
||||||
|
file: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
birth_year: '',
|
||||||
|
address: '',
|
||||||
|
post_code: '',
|
||||||
|
city: '',
|
||||||
|
phone: '',
|
||||||
|
mail: '',
|
||||||
|
keep: '',
|
||||||
|
agb: false,
|
||||||
|
notification: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectTypeChange(event) {
|
||||||
|
this.setState({ object_type: event.target.value });
|
||||||
|
}
|
||||||
|
onDescChange(event) {
|
||||||
|
this.setState({ desc: event.target.value });
|
||||||
|
}
|
||||||
|
onFileChange(value) {
|
||||||
|
this.setState({ file: value });
|
||||||
|
}
|
||||||
|
onPersonalInfosChange(event) {
|
||||||
|
this.setState({ [event.target.name]: event.target.value });
|
||||||
|
}
|
||||||
|
onKeepChange(event) {
|
||||||
|
this.setState({ keep: event.target.value });
|
||||||
|
}
|
||||||
|
onAGBChange(event) {
|
||||||
|
this.setState({ agb: event.target.checked });
|
||||||
|
}
|
||||||
|
onSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.validForm()) {
|
||||||
|
const data = { ...this.state };
|
||||||
|
delete data.abg;
|
||||||
|
delete data.notification;
|
||||||
|
|
||||||
|
this.props.sendParticipation(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validForm() {
|
||||||
|
const {
|
||||||
|
object_type,
|
||||||
|
desc,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
mail,
|
||||||
|
keep,
|
||||||
|
agb
|
||||||
|
} = this.state;
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
if (!object_type) {
|
||||||
|
this.setState({ notification: t('noti_object_type') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!desc) {
|
||||||
|
this.setState({ notification: t('noti_desc') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last_name) {
|
||||||
|
this.setState({ notification: t('noti_last_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first_name) {
|
||||||
|
this.setState({ notification: t('noti_first_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mail) {
|
||||||
|
this.setState({ notification: t('noti_mail') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keep) {
|
||||||
|
this.setState({ notification: t('noti_keep') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agb) {
|
||||||
|
this.setState({ notification: t('noti_agb') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotificationDismiss() {
|
||||||
|
this.setState({ notification: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { notification } = this.state;
|
||||||
|
const { pending, participated, t } = this.props;
|
||||||
|
if (pending) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
if (participated) {
|
||||||
|
return (
|
||||||
|
<div className='CVBody participated'>
|
||||||
|
<div className='CVParticipatedMessage medium-text'>{t('thank_you')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='CVBody'>
|
||||||
|
<Notification
|
||||||
|
isActive={!!notification}
|
||||||
|
message={notification}
|
||||||
|
action={t('noti_close')}
|
||||||
|
onClick={this.onNotificationDismiss}
|
||||||
|
/>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<CVObjectTypeRadio onObjectTypeChange={this.onObjectTypeChange} />
|
||||||
|
<CVDescInput onDescChange={this.onDescChange} />
|
||||||
|
<CVUploadInput onFileChange={this.onFileChange} />
|
||||||
|
<CVPersonalInfosInputs onPersonalInfosChange={this.onPersonalInfosChange} />
|
||||||
|
<CVKeepInput onKeepChange={this.onKeepChange} />
|
||||||
|
<CVAGBInput onAGBChange={this.onAGBChange} />
|
||||||
|
<CVSubmit />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
15
assets/js/components/ContribView/CVBodyContainer.js
Normal file
15
assets/js/components/ContribView/CVBodyContainer.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { sendParticipation } from '../../redux/actions/participate';
|
||||||
|
import { toggleParticipated } from '../../redux/actions/ui';
|
||||||
|
import CVBody from './CVBody';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { pending: state.ui.pending, participated: state.ui.participated };
|
||||||
|
};
|
||||||
|
|
||||||
|
const CVBodyContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ sendParticipation, toggleParticipated }
|
||||||
|
)(CVBody);
|
||||||
|
|
||||||
|
export default CVBodyContainer;
|
16
assets/js/components/ContribView/CVDescInput.js
Normal file
16
assets/js/components/ContribView/CVDescInput.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVDescInput = ({ onDescChange, t }) => (
|
||||||
|
<div className='CVForm CVDescInput'>
|
||||||
|
<label className='small-text'>{t('description')}</label>
|
||||||
|
<textarea
|
||||||
|
name='Beschreibung'
|
||||||
|
rows='5'
|
||||||
|
className='textarea medium-text'
|
||||||
|
onChange={onDescChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVDescInput);
|
23
assets/js/components/ContribView/CVKeepInput.js
Normal file
23
assets/js/components/ContribView/CVKeepInput.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVKeepInput = ({ onKeepChange, t }) => (
|
||||||
|
<div className='CVForm CVKeepInput'>
|
||||||
|
<p />
|
||||||
|
<div className='medium-text'>
|
||||||
|
<div className='Auswahltext'>{t('keep_text')}</div>
|
||||||
|
<label>
|
||||||
|
<input type='radio' name='keep' value='forever' onChange={onKeepChange} />
|
||||||
|
<span> {t('give_forever')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
<input type='radio' name='keep' value='until_2021' onChange={onKeepChange} />
|
||||||
|
<span> {t('until_2012')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVKeepInput);
|
53
assets/js/components/ContribView/CVObjectTypeRadio.js
Normal file
53
assets/js/components/ContribView/CVObjectTypeRadio.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVObjectTypeRadio = ({ onObjectTypeChange, t }) => (
|
||||||
|
<div className='CVForm CVObjectTypeRadio'>
|
||||||
|
<label className='small-text'>{t('i_offer')}</label>
|
||||||
|
<div className='medium-text'>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='object_type'
|
||||||
|
value='objekt'
|
||||||
|
onChange={onObjectTypeChange}
|
||||||
|
/>
|
||||||
|
<span> {t('offer_object')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
<input type='radio' name='object_type' value='foto' onChange={onObjectTypeChange} />
|
||||||
|
<span> {t('offer_photo')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
<input type='radio' name='object_type' value='film' onChange={onObjectTypeChange} />
|
||||||
|
<span> {t('offer_film')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='object_type'
|
||||||
|
value='skigeschichte'
|
||||||
|
onChange={onObjectTypeChange}
|
||||||
|
/>
|
||||||
|
<span> {t('offer_story')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
name='object_type'
|
||||||
|
value='spezialwissen'
|
||||||
|
onChange={onObjectTypeChange}
|
||||||
|
/>
|
||||||
|
<span> {t('offer_knowledge')}</span>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVObjectTypeRadio);
|
81
assets/js/components/ContribView/CVPersonalInfosInputs.js
Normal file
81
assets/js/components/ContribView/CVPersonalInfosInputs.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVPersonalInfosInputs = ({ onPersonalInfosChange, t }) => (
|
||||||
|
<div className='CVPersonalInfosInputs'>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('first_name')}</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='last_name'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('last_name')}</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='first_name'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('birth_year')}</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='birth_year'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('address')}</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='address'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('post_code')}</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='post_code'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('city')}</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
name='city'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('phone')}</label>
|
||||||
|
<input
|
||||||
|
type='tel'
|
||||||
|
name='phone'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label className='small-text'>{t('mail')}</label>
|
||||||
|
<input
|
||||||
|
type='email'
|
||||||
|
name='mail'
|
||||||
|
className='medium-text'
|
||||||
|
onChange={onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVPersonalInfosInputs);
|
10
assets/js/components/ContribView/CVSubmit.js
Normal file
10
assets/js/components/ContribView/CVSubmit.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVSubmit = ({ t }) => (
|
||||||
|
<div className='CVForm CVSubmit'>
|
||||||
|
<input type='submit' value={t('send_now')} className='submitbtn big-text' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVSubmit);
|
21
assets/js/components/ContribView/CVUploadInput.js
Normal file
21
assets/js/components/ContribView/CVUploadInput.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const CVUploadInput = ({ onFileChange, t, locale, reset }) => {
|
||||||
|
|
||||||
|
const onImageChange = event => {
|
||||||
|
if (event.target.files && event.target.files[0]) {
|
||||||
|
let img = event.target.files[0];
|
||||||
|
onFileChange(img);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='CVForm CVUploadInput' id='upload-wetransfer-container'>
|
||||||
|
<label className='small-text'>{t('upload')}</label>
|
||||||
|
<input type="file" id="img" name="img" accept="image/*" onChange={onImageChange} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(CVUploadInput);
|
33
assets/js/components/ContribView/ContribView.js
Normal file
33
assets/js/components/ContribView/ContribView.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import CVBodyContainer from './CVBodyContainer';
|
||||||
|
import BurgerBtnContainer from '../AppMenu/BurgerBtnContainer';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
class ContribView extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
animateScrollTo(document.getElementById('root'), { speed: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
return (
|
||||||
|
<div className='ContribView AppContent'>
|
||||||
|
<BurgerBtnContainer />
|
||||||
|
<div className='ContribViewContent '>
|
||||||
|
<h1 className='big-text'>{t('we_search')}</h1>
|
||||||
|
<div className='CVText'>
|
||||||
|
<p
|
||||||
|
className='medium-text'
|
||||||
|
dangerouslySetInnerHTML={{ __html: t('intro_text') }}
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<CVBodyContainer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduxLang('ContribView')(ContribView);
|
100
assets/js/components/InfoView/DatenschutzerklarungBtn.js
Normal file
100
assets/js/components/InfoView/DatenschutzerklarungBtn.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ModalContainer, Modal } from 'minimal-react-modal';
|
||||||
|
|
||||||
|
const DatenschutzerklarungBtn = ({ label }) => (
|
||||||
|
<ModalContainer>
|
||||||
|
{(openModal, closeModal, isActive) => (
|
||||||
|
<>
|
||||||
|
<a onClick={openModal}>{label}</a>
|
||||||
|
|
||||||
|
<Modal isActive={isActive} closeModal={closeModal}>
|
||||||
|
<Datenschutzerklarung closeModal={closeModal} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContainer>
|
||||||
|
);
|
||||||
|
export default DatenschutzerklarungBtn;
|
||||||
|
|
||||||
|
const Datenschutzerklarung = ({ closeModal }) => (
|
||||||
|
<div className='Modal--container'>
|
||||||
|
<div className='Modal--body'>
|
||||||
|
<button onClick={closeModal}>
|
||||||
|
<img src='/static/gfx/alps_x.svg' alt='close' />
|
||||||
|
</button>
|
||||||
|
<h1 className='big-text'>Datenschutzerklärung</h1>
|
||||||
|
<p className='small-text'>
|
||||||
|
<br />
|
||||||
|
Beim Zugriff auf unsere Website werden keine persönlichen Daten gespeichert.
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Hosting</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Das Hosting der Daten erfolgt in der Schweiz und wird von der Firma 89grad GmbH
|
||||||
|
betrieben.
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Kontaktformular</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Wenn Sie sich per Kontaktformular bei uns melden, werden Ihre Angaben und
|
||||||
|
Kontaktdaten aus dem Formular zwecks Sicherstellung der Bearbeitung bei uns
|
||||||
|
gespeichert. Diese Daten werden nicht an Dritte weitergegeben.
|
||||||
|
</p>
|
||||||
|
<p className='small-text'>
|
||||||
|
<br />
|
||||||
|
Das Kontaktformular dient ausschliesslich dem Zweck, im Rahmen des Projekts
|
||||||
|
„Fundbüro für Erinnerungen“ mit Ihnen Kontakt aufnehmen zu können. Die von Ihnen
|
||||||
|
eingegebenen Daten verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre
|
||||||
|
Einwilligung zur Speicherung widerrufen oder der Zweck für die Datenspeicherung
|
||||||
|
entfällt (z.B. nach abgeschlossener Bearbeitung).
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Newsletter</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Der Versand der Newsletter erfolgt mittels „MailChimp“, einer
|
||||||
|
Newsletterversandplattform des US-Anbieters Rocket Science Group, LLC, 675 Ponce De
|
||||||
|
Leon Ave NE #5000, Atlanta, GA 30308, USA.
|
||||||
|
</p>
|
||||||
|
<p className='small-text'>
|
||||||
|
<br />
|
||||||
|
Die E-Mail-Adressen unserer Newsletterempfänger, als auch deren weitere, im Rahmen
|
||||||
|
dieser Hinweise beschriebenen Daten, werden auf den Servern von MailChimp in den USA
|
||||||
|
gespeichert. MailChimp verwendet diese Informationen zum Versand und zur Auswertung
|
||||||
|
der Newsletter in unserem Auftrag. Des Weiteren kann MailChimp nach eigenen
|
||||||
|
Informationen diese Daten zur Optimierung oder Verbesserung der eigenen Services
|
||||||
|
nutzen, z.B. zur technischen Optimierung des Versandes und der Darstellung der
|
||||||
|
Newsletter oder für wirtschaftliche Zwecke, um zu bestimmen aus welchen Ländern die
|
||||||
|
Empfänger kommen. MailChimp nutzt die Daten unserer Newsletterempfänger jedoch
|
||||||
|
nicht, um diese selbst anzuschreiben oder an Dritte weiterzugeben.
|
||||||
|
</p>
|
||||||
|
<p className='small-text'>
|
||||||
|
<br />
|
||||||
|
Wir vertrauen auf die Zuverlässigkeit und die IT- sowie Datensicherheit von
|
||||||
|
MailChimp. MailChimp ist unter dem US-EU-Datenschutzabkommen „Privacy Shield“
|
||||||
|
zertifiziert und verpflichtet sich damit die EU-Datenschutzvorgaben einzuhalten. Des
|
||||||
|
Weiteren haben wir mit MailChimp ein „Data-Processing-Agreement“ abgeschlossen.
|
||||||
|
Dabei handelt es sich um einen Vertrag, in dem sich MailChimp dazu verpflichtet, die
|
||||||
|
Daten unserer Nutzer zu schützen, entsprechend dessen Datenschutzbestimmungen in
|
||||||
|
unserem Auftrag zu verarbeiten und insbesondere nicht an Dritte weiter zu geben. Die
|
||||||
|
Datenschutzbestimmungen von MailChimp können Sie unter folgendem Link einsehen:{' '}
|
||||||
|
<a href='https://mailchimp.com/legal/privacy/' target='_blank'>
|
||||||
|
https://mailchimp.com/legal/privacy/
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Datenübertragung</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Auf unserer Internetseite besteht die Möglichkeit, uns über den Übertragungsdienst
|
||||||
|
WeTransfer gesichert Daten zuzusenden. Dabei erfolgt die Anmeldung und Übertragung
|
||||||
|
direkt über den Dienstleister WeTransfer B.V., Oostelijke Handelskade 751, 1019 BW
|
||||||
|
Amsterdam, the Netherlands.
|
||||||
|
<br />
|
||||||
|
Wir möchten Sie gerne ausdrücklich darauf hinweisen, dass wir als Betreiber der
|
||||||
|
Website keine Kenntnis davon haben, in welchem Umfang WeTransfer diese Daten nutzt.
|
||||||
|
WeTransfer stellt diesbezüglich selbst Informationen zur Verfügung. Diese können Sie
|
||||||
|
in der offiziellen Datenschutzerklärung abrufbar unter{' '}
|
||||||
|
<a href='https://wetransfer.com/legal/privacy' target='_blank'>
|
||||||
|
https://wetransfer.com/legal/privacy
|
||||||
|
</a>{' '}
|
||||||
|
einsehen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
9
assets/js/components/InfoView/ImageWithLegend.js
Normal file
9
assets/js/components/InfoView/ImageWithLegend.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ImageWithLegend = ({ image, legend }) => (
|
||||||
|
<div className='ImageWithLegend'>
|
||||||
|
<img src={image} />
|
||||||
|
{legend && <label>{legend}</label>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default ImageWithLegend;
|
89
assets/js/components/InfoView/ImpressumBtn.js
Normal file
89
assets/js/components/InfoView/ImpressumBtn.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ModalContainer, Modal } from 'minimal-react-modal';
|
||||||
|
|
||||||
|
const ImpressumBtn = ({ label }) => (
|
||||||
|
<ModalContainer>
|
||||||
|
{(openModal, closeModal, isActive) => (
|
||||||
|
<>
|
||||||
|
<a onClick={openModal}>{label}</a>
|
||||||
|
|
||||||
|
<Modal isActive={isActive} closeModal={closeModal}>
|
||||||
|
<Impressum closeModal={closeModal} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalContainer>
|
||||||
|
);
|
||||||
|
export default ImpressumBtn;
|
||||||
|
|
||||||
|
const Impressum = ({ closeModal }) => (
|
||||||
|
<div className='Modal--container'>
|
||||||
|
<div className='Modal--body'>
|
||||||
|
<button onClick={closeModal}>
|
||||||
|
<img src='/static/gfx/alps_x.svg' alt='close' />
|
||||||
|
</button>
|
||||||
|
<h1 className='big-text'>Impressum</h1>
|
||||||
|
<h4 className='medium-text'>Kontakt-Adresse</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Alpines Museum der Schweiz
|
||||||
|
<br />
|
||||||
|
Helvetiaplatz 4<br />
|
||||||
|
3005 Bern
|
||||||
|
<br />
|
||||||
|
Schweiz
|
||||||
|
</p>
|
||||||
|
<p className='small-text'>
|
||||||
|
E-Mail:
|
||||||
|
<br />
|
||||||
|
info@alpinesmuseum.ch
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Vertretungsberechtigte Person(en)</h4>
|
||||||
|
<p className='small-text'>Michael Fässler, Projektleiter</p>
|
||||||
|
<h4 className='medium-text'>Handelsregister-Eintrag</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Eingetragener Firmenname: Alpines Museum der Schweiz
|
||||||
|
<br />
|
||||||
|
Handelsregister Nr: CHE-107.817.066
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Mehrwertsteuer-Nummer</h4>
|
||||||
|
<p className='small-text'>CHE-107.817.066</p>
|
||||||
|
<h4 className='medium-text'>Haftungsausschluss</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Der Autor übernimmt keinerlei Gewähr hinsichtlich der inhaltlichen Richtigkeit,
|
||||||
|
Genauigkeit, Aktualität, Zuverlässigkeit und Vollständigkeit der Informationen.
|
||||||
|
<br />
|
||||||
|
Haftungsansprüche gegen den Autor wegen Schäden materieller oder immaterieller Art,
|
||||||
|
welche aus dem Zugriff oder der Nutzung bzw. Nichtnutzung der veröffentlichten
|
||||||
|
Informationen, durch Missbrauch der Verbindung oder durch technische Störungen
|
||||||
|
entstanden sind, werden ausgeschlossen.
|
||||||
|
</p>
|
||||||
|
<p className='small-text'>
|
||||||
|
<br />
|
||||||
|
Alle Angebote sind unverbindlich. Der Autor behält es sich ausdrücklich vor, Teile
|
||||||
|
der Seiten oder das gesamte Angebot ohne besondere Ankündigung zu verändern, zu
|
||||||
|
ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig
|
||||||
|
einzustellen.
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Haftungsausschluss für Links</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Verweise und Links auf Webseiten Dritter liegen ausserhalb unseres
|
||||||
|
Verantwortungsbereichs. Es wird jegliche Verantwortung für solche Webseiten
|
||||||
|
abgelehnt. Der Zugriff und die Nutzung solcher Webseiten erfolgen auf eigene Gefahr
|
||||||
|
des jeweiligen Nutzers.
|
||||||
|
</p>
|
||||||
|
<h4 className='medium-text'>Urheberrechte</h4>
|
||||||
|
<p className='small-text'>
|
||||||
|
Die Urheber- und alle anderen Rechte an Inhalten, Bildern, Fotos oder anderen
|
||||||
|
Dateien auf dieser Website, gehören ausschliesslich{' '}
|
||||||
|
<b>der Stiftung Alpines Museum der Schweiz</b> oder den speziell genannten
|
||||||
|
Rechteinhabern. Für die Reproduktion jeglicher Elemente ist die schriftliche
|
||||||
|
Zustimmung des Urheberrechtsträgers im Voraus einzuholen.
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
Quelle:{' '}
|
||||||
|
<a href='https://www.swissanwalt.ch/' target='_blank'>
|
||||||
|
SwissAnwalt
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
71
assets/js/components/InfoView/InfoView.js
Normal file
71
assets/js/components/InfoView/InfoView.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import BurgerBtnContainer from '../AppMenu/BurgerBtnContainer';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import ImageWithLegend from './ImageWithLegend';
|
||||||
|
import Newsletter from './Newsletter';
|
||||||
|
import Sponsors from './Sponsors';
|
||||||
|
import InfoViewFooter from './InfoViewFooter';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
class InfoView extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
animateScrollTo(document.getElementById('root'), { speed: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
return (
|
||||||
|
<div className='AppContent'>
|
||||||
|
<div className='InfoView'>
|
||||||
|
<BurgerBtnContainer />
|
||||||
|
<h1 style={{ paddingTop: 0 }} className='big-text'>
|
||||||
|
{t('welcome_title')}
|
||||||
|
</h1>
|
||||||
|
<div className='InfoContent'>
|
||||||
|
<p className='medium-text'>{t('exhibition_date')}</p>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<div className='embedded-youtube-container'>
|
||||||
|
<iframe
|
||||||
|
width='100%'
|
||||||
|
height='100%'
|
||||||
|
src='https://www.youtube.com/embed/kjpw4LunEEA'
|
||||||
|
frameBorder='0'
|
||||||
|
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className='medium-text'
|
||||||
|
dangerouslySetInnerHTML={{ __html: t('welcome_text') }}
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<div className='ImagesList'>
|
||||||
|
<ImageWithLegend image='/static/gfx/Fundbueuro_Sliderbild_1.jpg' />
|
||||||
|
<ImageWithLegend image='/static/gfx/Fundbueuro_Sliderbild_2.jpg' />
|
||||||
|
<ImageWithLegend image='/static/gfx/Fundbueuro_Sliderbild_3.jpg' />
|
||||||
|
<ImageWithLegend image='/static/gfx/Fundbueuro_Sliderbild_4.jpg' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className='big-text'>{t('about_title')}</h1>
|
||||||
|
<div className='InfoContent'>
|
||||||
|
<p
|
||||||
|
className='medium-text'
|
||||||
|
dangerouslySetInnerHTML={{ __html: t('about_text') }}
|
||||||
|
></p>
|
||||||
|
<a href='http://www.alpinesmuseum.ch' target='_blank'>
|
||||||
|
<img src='/static/gfx/alsp_logo.svg' alt='alpines museum logo' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Newsletter />
|
||||||
|
<Sponsors />
|
||||||
|
<InfoViewFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduxLang('InfoView')(InfoView);
|
12
assets/js/components/InfoView/InfoViewFooter.js
Normal file
12
assets/js/components/InfoView/InfoViewFooter.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ImpressumBtn from './ImpressumBtn';
|
||||||
|
import DatenschutzerklarungBtn from './DatenschutzerklarungBtn';
|
||||||
|
|
||||||
|
const InfoViewFooter = () => (
|
||||||
|
<div className='InfoView-footer'>
|
||||||
|
<ImpressumBtn label='Impressum' />
|
||||||
|
-
|
||||||
|
<DatenschutzerklarungBtn label='Datenschutzerklärung' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default InfoViewFooter;
|
17
assets/js/components/InfoView/Newsletter.js
Normal file
17
assets/js/components/InfoView/Newsletter.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const Newsletter = ({ t }) => {
|
||||||
|
return (
|
||||||
|
<div className='InfoView-newsletter'>
|
||||||
|
<a
|
||||||
|
className='medium-text'
|
||||||
|
href={`https://www.alpinesmuseum.ch${t('url_path')}`}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
{t('title')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default reduxLang('Newsletter')(Newsletter);
|
39
assets/js/components/InfoView/Sponsors.js
Normal file
39
assets/js/components/InfoView/Sponsors.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
|
||||||
|
const sponsors = [
|
||||||
|
'01_Lotteriefonds_BE.svg',
|
||||||
|
'02_Kulturstiftung.svg',
|
||||||
|
'03_Kanton_Bern.svg',
|
||||||
|
'04_Burgergemeinde_Bern.svg',
|
||||||
|
'05_SAC.svg',
|
||||||
|
'06_Binding_Stiftung.svg',
|
||||||
|
'07_Mobiliar.svg',
|
||||||
|
'08_Pro_Patria.svg',
|
||||||
|
'09_Swiss_Ski.svg'
|
||||||
|
];
|
||||||
|
|
||||||
|
const Sponsors = ({ t }) => (
|
||||||
|
<div className='InfoView-sponsors'>
|
||||||
|
<div className='CVForm innovationspartner'>
|
||||||
|
<label>{t('innovation_partner')}</label>
|
||||||
|
<div className='InfoView-sponsors--items'>
|
||||||
|
<div className='InfoView-sponsors--item'>
|
||||||
|
<img src='/static/gfx/sponsors/Engagement_Migros.svg' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className='small-text' dangerouslySetInnerHTML={{ __html: t('migros_text') }}></p>
|
||||||
|
</div>
|
||||||
|
<div className='CVForm'>
|
||||||
|
<label>{t('project_partner')}</label>
|
||||||
|
<div className='InfoView-sponsors--items'>
|
||||||
|
{sponsors.map((sponsor, i) => (
|
||||||
|
<div className='InfoView-sponsors--item' key={i}>
|
||||||
|
<img src={`/static/gfx/sponsors/${sponsor}`} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export default reduxLang('Sponsors')(Sponsors);
|
8
assets/js/components/Spinner.js
Normal file
8
assets/js/components/Spinner.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
const Spinner = () => {
|
||||||
|
const color = useSelector(state => state.urls.color);
|
||||||
|
return <div className='spinner' style={{borderTopColor: color}} />;
|
||||||
|
}
|
||||||
|
export default Spinner;
|
22
assets/js/components/SplashScreen.js
Normal file
22
assets/js/components/SplashScreen.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import AppContainer from './AppContainer';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { defineLocale, updateURLS } from "../utils";
|
||||||
|
|
||||||
|
export default function SplashScreen(props) {
|
||||||
|
const apiUrl = useSelector(state => state.urls.site);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const locale = defineLocale(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateURLS(locale, dispatch);
|
||||||
|
});
|
||||||
|
if (!apiUrl) {
|
||||||
|
return (
|
||||||
|
<div style={{fontSize: "48px", fontWeight: "600", margin: "auto"}}>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <AppContainer />
|
||||||
|
}
|
44
assets/js/components/StartScreen.js
Normal file
44
assets/js/components/StartScreen.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import reduxLang from '../middleware/lang';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
|
const _StartScreen = ({ t, search }) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [searched, setSearched] = useState(false);
|
||||||
|
|
||||||
|
const handleSearchInput = event => {
|
||||||
|
setSearchQuery(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
search(searchQuery);
|
||||||
|
setSearched(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (searched) {
|
||||||
|
return <Redirect to='/' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='StartScreen'>
|
||||||
|
<Link to='/'>
|
||||||
|
<img src='/static/gfx/alps_x.svg' alt='close' />
|
||||||
|
</Link>
|
||||||
|
<form onSubmit={handleSearch}>
|
||||||
|
<input
|
||||||
|
className='medium-text'
|
||||||
|
type='text'
|
||||||
|
id='start-screen-search-field'
|
||||||
|
placeholder={t('placeholder')}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={handleSearchInput}
|
||||||
|
/>
|
||||||
|
<img src='/static/gfx/alps_lupe.svg' alt='search' />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const StartScreen = reduxLang('StartScreen')(_StartScreen);
|
||||||
|
export default StartScreen;
|
10
assets/js/components/StartScreenContainer.js
Normal file
10
assets/js/components/StartScreenContainer.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { search } from '../redux/actions/catalog';
|
||||||
|
import StartScreen from './StartScreen';
|
||||||
|
|
||||||
|
const StartScreenContainer = connect(
|
||||||
|
undefined,
|
||||||
|
{ search }
|
||||||
|
)(StartScreen);
|
||||||
|
|
||||||
|
export default StartScreenContainer;
|
42
assets/js/components/Tablet/App.js
Normal file
42
assets/js/components/Tablet/App.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||||
|
import AppContentContainer from './AppContentContainer';
|
||||||
|
import CIDetailViewContainer from './CIDetailViewContainer';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import StartScreenContainer from './../StartScreenContainer';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getLocaleFromURL, defineLocale, shouldDisplayDynamicComponent, getDjangoView, routePathForDynamicComponent } from '../../utils';
|
||||||
|
|
||||||
|
const App = ({ pending, menuOpen, setLocale, isPreLaunch }) => {
|
||||||
|
// set initial language
|
||||||
|
setLocale(defineLocale(isPreLaunch));
|
||||||
|
let localeFromURL = getLocaleFromURL(window.location.pathname);
|
||||||
|
if (localeFromURL === undefined) {
|
||||||
|
localeFromURL = ''
|
||||||
|
}
|
||||||
|
const basename = '/' + localeFromURL + useSelector(state => state.urls.site);
|
||||||
|
|
||||||
|
const activeSite = useSelector(state => state.urls.site);
|
||||||
|
const urls = useSelector(state => state.urls.allSites[activeSite].urls);
|
||||||
|
const DynamicComponent = React.memo(() => (
|
||||||
|
<div className="AppContent">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: getDjangoView() }} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`App touch ${pending ? 'loading' : ''}`}
|
||||||
|
>
|
||||||
|
<Router basename={basename}>
|
||||||
|
<Switch>
|
||||||
|
{ shouldDisplayDynamicComponent(urls) ? <Route path={routePathForDynamicComponent(basename)} component={DynamicComponent} /> : null }
|
||||||
|
<Route path='/' component={AppContentContainer} />
|
||||||
|
</Switch>
|
||||||
|
<Route path='/catalog/:itemId' component={CIDetailViewContainer} />
|
||||||
|
<Route path='/start' component={StartScreenContainer} />
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reduxLang()(App);
|
13
assets/js/components/Tablet/AppContainer.js
Normal file
13
assets/js/components/Tablet/AppContainer.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
pending: state.ui.pending,
|
||||||
|
isPreLaunch: state.ui.isPreLaunch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppContainer = connect(mapStateToProps)(App);
|
||||||
|
|
||||||
|
export default AppContainer;
|
31
assets/js/components/Tablet/AppContent.js
Normal file
31
assets/js/components/Tablet/AppContent.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ContentHeader from './ContentHeader';
|
||||||
|
import ContentBodyContainer from './../AppContent/ContentBodyContainer';
|
||||||
|
import ContentOverlayContainer from './../AppContent/ContentOverlayContainer';
|
||||||
|
import PreLaunchPage from './../AppContent/PreLaunchPage';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
export default class AppContent extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const { isPreLaunch, getMode } = this.props;
|
||||||
|
|
||||||
|
animateScrollTo(document.getElementById('root'));
|
||||||
|
|
||||||
|
if (isPreLaunch === undefined) {
|
||||||
|
getMode();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isPreLaunch } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={`AppContent${isPreLaunch ? ' is-pre-launch' : ''}`}>
|
||||||
|
<ContentHeader isPreLaunch={isPreLaunch} />
|
||||||
|
{!isPreLaunch && <ContentBodyContainer />}
|
||||||
|
{!isPreLaunch && <ContentOverlayContainer />}
|
||||||
|
{isPreLaunch && <PreLaunchPage />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
assets/js/components/Tablet/AppContentContainer.js
Normal file
14
assets/js/components/Tablet/AppContentContainer.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { getMode } from '../../redux/actions/mode';
|
||||||
|
import AppContent from './AppContent';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
catalogFetched: !!state.catalog.length,
|
||||||
|
isPreLaunch: state.ui.isPreLaunch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppContentContainer = connect(mapStateToProps, { getMode })(AppContent);
|
||||||
|
|
||||||
|
export default AppContentContainer;
|
87
assets/js/components/Tablet/CIDetailView.js
Normal file
87
assets/js/components/Tablet/CIDetailView.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import DVHeader from './../AppContent/CIDetailView/DVHeader';
|
||||||
|
import DVBody from './DVBody';
|
||||||
|
import Spinner from './../Spinner';
|
||||||
|
import Div100vh from 'react-div-100vh';
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
const CIDetailView = ({
|
||||||
|
activeItem,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
close,
|
||||||
|
catalog,
|
||||||
|
pending,
|
||||||
|
openDetail,
|
||||||
|
showNextItem,
|
||||||
|
showPrevItem,
|
||||||
|
match,
|
||||||
|
participated,
|
||||||
|
sendParticipation,
|
||||||
|
toggleParticipated,
|
||||||
|
color
|
||||||
|
}) => {
|
||||||
|
if (activeItem === null) {
|
||||||
|
// we have the catalog, but this view was not opened through an action
|
||||||
|
// but by it's URL. Now we need to dispatch an action that,
|
||||||
|
// depending on the current route, will tell which id the ID of the item to display.
|
||||||
|
if (catalog.length) {
|
||||||
|
catalog.forEach((item, i) => {
|
||||||
|
if (item.inventory_number === match.params.itemId.replace('__', ' ')) {
|
||||||
|
openDetail(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't know yet what data to display.
|
||||||
|
// Most certainly the catalog is being fetched.
|
||||||
|
return (
|
||||||
|
<div className='CIDetailView__loading AppContent'>
|
||||||
|
<DVHeader close={close} />
|
||||||
|
{pending && <Spinner />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (activeItem.inventory_number !== match.params.itemId.replace('__', ' ')) {
|
||||||
|
window.history.pushState(null, null, activeItem.inventory_number.replace(' ', '__'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const doYouKnowMoreURL = (
|
||||||
|
useSelector(state => state.urls.site) + "do-you-know-more/?object_id=" + activeItem.inventory_number
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Div100vh>
|
||||||
|
<div className='CIDetailView AppContent' id='CIDetailView'>
|
||||||
|
<div id='CIDetailView-top-element'></div>
|
||||||
|
<DVHeader close={close} />
|
||||||
|
<DVBody
|
||||||
|
item={activeItem}
|
||||||
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
|
showNextItem={showNextItem}
|
||||||
|
showPrevItem={showPrevItem}
|
||||||
|
participated={participated}
|
||||||
|
sendParticipation={sendParticipation}
|
||||||
|
toggleParticipated={toggleParticipated}
|
||||||
|
doYouKnowMoreURL={doYouKnowMoreURL}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Div100vh>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CIDetailView.propTypes = {
|
||||||
|
activeItem: PropTypes.object,
|
||||||
|
isFirst: PropTypes.bool,
|
||||||
|
isLast: PropTypes.bool,
|
||||||
|
close: PropTypes.func.isRequired,
|
||||||
|
catalog: PropTypes.array,
|
||||||
|
openDetail: PropTypes.func.isRequired,
|
||||||
|
showNextItem: PropTypes.func.isRequired,
|
||||||
|
showPrevItem: PropTypes.func.isRequired,
|
||||||
|
match: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
export default CIDetailView;
|
54
assets/js/components/Tablet/CIDetailViewContainer.js
Normal file
54
assets/js/components/Tablet/CIDetailViewContainer.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
openDetail,
|
||||||
|
showNextItem,
|
||||||
|
showPrevItem,
|
||||||
|
toggleParticipated,
|
||||||
|
closeDetail
|
||||||
|
} from '../../redux/actions/ui';
|
||||||
|
import CIDetailView from './CIDetailView';
|
||||||
|
import { sendParticipation } from '../../redux/actions/participate';
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
if (state.ui.activeItem === null) {
|
||||||
|
return {
|
||||||
|
catalog: state.catalog,
|
||||||
|
pending: state.ui.pending,
|
||||||
|
activeItem: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeItemData = state.catalog[state.ui.activeItem];
|
||||||
|
if (activeItemData.inventory_number == undefined) {
|
||||||
|
activeItemData = state.catalog[state.ui.activeItem + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirst = state.ui.activeItem === 0;
|
||||||
|
const isLast = state.ui.activeItem === state.catalog.length - 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeItem: activeItemData,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
participated: state.ui.participated,
|
||||||
|
color: state.urls.color
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
close: () => dispatch(closeDetail()),
|
||||||
|
openDetail: item => dispatch(openDetail(item)),
|
||||||
|
showNextItem: () => dispatch(showNextItem()),
|
||||||
|
showPrevItem: () => dispatch(showPrevItem()),
|
||||||
|
sendParticipation: data => dispatch(sendParticipation(data)),
|
||||||
|
toggleParticipated: () => dispatch(toggleParticipated())
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const CIDetailViewContainer = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CIDetailView);
|
||||||
|
|
||||||
|
export default CIDetailViewContainer;
|
51
assets/js/components/Tablet/ContentHeader.js
Normal file
51
assets/js/components/Tablet/ContentHeader.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes, { func } from 'prop-types';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import HeaderSearchFieldContainer from './../AppContent/HeaderSearchFieldContainer';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import LangDropMenu from './LangDropMenu';
|
||||||
|
@autobind
|
||||||
|
class _ContentHeader extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
searching: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return <div className='ContentHeader'>
|
||||||
|
<HeaderSearchFieldContainer showClose={false} />
|
||||||
|
<LangDropMenu />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
color: state.urls.color,
|
||||||
|
scrollingText: state.urls.scrollingText
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const ContentHeader = connect(mapStateToProps)(_ContentHeader);
|
||||||
|
export default ContentHeader;
|
||||||
|
|
||||||
|
function _HeaderTitle({ t }) {
|
||||||
|
const siteName = useSelector(state => state.urls.name);
|
||||||
|
return <h1 className='HeaderTitle big-text'>{siteName}</h1>;
|
||||||
|
}
|
||||||
|
_HeaderTitle.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeaderSearchBtn = ({ handler }) => (
|
||||||
|
<div className='HeaderSearchBtn'>
|
||||||
|
<button onClick={handler}>
|
||||||
|
<img src='/static/gfx/alps_lupe.svg' alt='search' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
HeaderSearchBtn.propTypes = {
|
||||||
|
handler: PropTypes.func.isRequired
|
||||||
|
};
|
193
assets/js/components/Tablet/DVBody.js
Normal file
193
assets/js/components/Tablet/DVBody.js
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import DVKownMore from './DVKownMore';
|
||||||
|
import DVLabel from './../AppContent/CIDetailView/DVLabel';
|
||||||
|
import { DefaultPlayer as Video } from 'react-html5video';
|
||||||
|
import { useSwipeable } from 'react-swipeable';
|
||||||
|
import Div100vh from 'react-div-100vh';
|
||||||
|
|
||||||
|
|
||||||
|
const DVBody = ({ item, isFirst, isLast, showNextItem, showPrevItem, participated,
|
||||||
|
sendParticipation, toggleParticipated, doYouKnowMoreURL, color, t }) => {
|
||||||
|
return <div className='DVBody'>
|
||||||
|
<DetailMediaContainer
|
||||||
|
item={item}
|
||||||
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
|
showNextItem={showNextItem}
|
||||||
|
showPrevItem={showPrevItem}
|
||||||
|
/>
|
||||||
|
<DVKownMore
|
||||||
|
item={item}
|
||||||
|
participated={participated}
|
||||||
|
sendParticipation={sendParticipation}
|
||||||
|
toggleParticipated={toggleParticipated}
|
||||||
|
doYouKnowMoreURL={doYouKnowMoreURL}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
<DetailViewTags tags={item.tags} label={t('category')} />
|
||||||
|
<DetailAttributeContainer
|
||||||
|
name={t('inventory_number')}
|
||||||
|
content={item['inventory_number']}
|
||||||
|
phone={true}
|
||||||
|
/>
|
||||||
|
<DetailAttributeContainer name={t('title')} content={item.title} />
|
||||||
|
<DetailAttributeContainer name={t('date')} content={item.date} />
|
||||||
|
<DetailAttributeContainer name={t('owner')} content={item.participant} />
|
||||||
|
<DetailAttributeContainer name={t('special')} content={item.description} />
|
||||||
|
{item.history && <DetailAttributeContainer name={t('story')} content={item.history} />}
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
DVBody.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
isFirst: PropTypes.bool.isRequired,
|
||||||
|
isLast: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
export default reduxLang('DVBody')(DVBody);
|
||||||
|
|
||||||
|
const DetailMediaContainer = ({ item, isFirst, isLast, showNextItem, showPrevItem }) => {
|
||||||
|
const handlers = useSwipeable({
|
||||||
|
onSwipedLeft: !isLast ? showNextItem : undefined,
|
||||||
|
onSwipedRight: !isFirst ? showPrevItem : undefined
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={`DetailMediaContainer${item.youtube ? ' youtube' : ''}`} {...handlers}>
|
||||||
|
{!isFirst && <LeftNavButton handler={showPrevItem} />}
|
||||||
|
<DVLabel text={item.inventory_number} />
|
||||||
|
{item.video || item.youtube ? (
|
||||||
|
<DIVideo video={item.video} youtube={item.youtube} alt={item.title} />
|
||||||
|
) : (
|
||||||
|
<DIImage item={item} alt={item.title} />
|
||||||
|
)}
|
||||||
|
{!isLast && <RightNavButton handler={showNextItem} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DetailMediaContainer.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
isFirst: PropTypes.bool.isRequired,
|
||||||
|
isLast: PropTypes.bool.isRequired,
|
||||||
|
showNextItem: PropTypes.func.isRequired,
|
||||||
|
showPrevItem: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const LeftNavButton = ({ handler }) => (
|
||||||
|
<button className='LeftNavButton' onClick={handler}>
|
||||||
|
<img src='/static/gfx/alps_links.svg' alt='left' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
LeftNavButton.propTypes = {
|
||||||
|
handler: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
const RightNavButton = ({ handler }) => (
|
||||||
|
<button className='RightNavButton' onClick={handler}>
|
||||||
|
<img src='/static/gfx/alps_rechts.svg' alt='right' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
RightNavButton.propTypes = {
|
||||||
|
handler: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function youtube_parser(url) {
|
||||||
|
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
|
||||||
|
var match = url.match(regExp);
|
||||||
|
return match && match[7].length == 11 ? match[7] : false;
|
||||||
|
}
|
||||||
|
const DIVideo = ({ video, youtube }) => (
|
||||||
|
<div className='DIVideo'>
|
||||||
|
{!youtube ? (
|
||||||
|
<Video controls={['PlayPause', 'Seek', 'Time', 'Volume', 'Fullscreen']}>
|
||||||
|
<source src={video} type='video/mp4' />
|
||||||
|
</Video>
|
||||||
|
) : (
|
||||||
|
<iframe
|
||||||
|
width='100%'
|
||||||
|
src={`https://www.youtube.com/embed/${youtube_parser(youtube)}`}
|
||||||
|
frameBorder='0'
|
||||||
|
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DIVideo.propTypes = {
|
||||||
|
video: PropTypes.string,
|
||||||
|
youtube: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIImage = ({ item, alt = '' }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const openFullScreen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
|
||||||
|
// fix cut off part on Safari
|
||||||
|
document.getElementById('CIDetailView').style.overflow = 'visible';
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeFullScreen = () => {
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
// revert Safari fix don when opening
|
||||||
|
document.getElementById('CIDetailView').style.overflow = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
return <FullScreenImage image={item.image} close={closeFullScreen} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='DIImage' onClick={openFullScreen}>
|
||||||
|
<img src={item.image} alt={alt} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DIImage.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const FullScreenImage = ({ image, close }) => (
|
||||||
|
<Div100vh>
|
||||||
|
<div
|
||||||
|
className='FullScreenImage'
|
||||||
|
style={{ backgroundImage: `url('${image}')` }}
|
||||||
|
onClick={close}
|
||||||
|
></div>
|
||||||
|
</Div100vh>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DetailViewTags = ({ tags, label }) => (
|
||||||
|
<div className='DetailViewTags'>
|
||||||
|
<DVLabel text={label} />
|
||||||
|
<div>
|
||||||
|
{tags.map((tag, i) => (
|
||||||
|
<span className='Tag' key={i}>
|
||||||
|
<button style={{border: '0px solid black'}}>{tag.name}</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DetailAttributeContainer = ({ name, content, phone = false }) => (
|
||||||
|
<div className={`DetailAttributeContainer${phone ? ' phone-invetory-number' : ''}`}>
|
||||||
|
<DVLabel text={name} />
|
||||||
|
<DAContent content={content} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DetailAttributeContainer.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
content: PropTypes.any.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
const DAContent = ({ content }) => (
|
||||||
|
<div className='DAContent'>
|
||||||
|
<p className='medium-text'>{content}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
DAContent.propTypes = {
|
||||||
|
content: PropTypes.any.isRequired
|
||||||
|
};
|
174
assets/js/components/Tablet/DVKownMore.js
Normal file
174
assets/js/components/Tablet/DVKownMore.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import CVDescInput from './../ContribView/CVDescInput';
|
||||||
|
import CVPersonalInfosInputs from './../ContribView/CVPersonalInfosInputs';
|
||||||
|
import CVSubmit from './../ContribView/CVSubmit';
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
import { Notification } from 'react-notification';
|
||||||
|
import animateScrollTo from 'animated-scroll-to';
|
||||||
|
|
||||||
|
@reduxLang('ContribView')
|
||||||
|
@autobind
|
||||||
|
export default class DVKownMore extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isToggleOn: false,
|
||||||
|
desc: '',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
birth_year: '',
|
||||||
|
address: '',
|
||||||
|
post_code: '',
|
||||||
|
city: '',
|
||||||
|
phone: '',
|
||||||
|
mail: '',
|
||||||
|
agb: false,
|
||||||
|
notification: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick() {
|
||||||
|
const { isToggleOn } = this.state;
|
||||||
|
if (isToggleOn) {
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
animateScrollTo(document.getElementById('CIDetailView-top-element'), {
|
||||||
|
elementToScroll: document.getElementById('CIDetailView')
|
||||||
|
}).then(hasScrolledToPosition => {
|
||||||
|
// scroll animation is finished
|
||||||
|
|
||||||
|
// "hasScrolledToPosition" indicates if page/element
|
||||||
|
// was scrolled to a desired position
|
||||||
|
// or if animation got interrupted
|
||||||
|
if (hasScrolledToPosition) {
|
||||||
|
// page is scrolled to a desired position
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isToggleOn: !isToggleOn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
isToggleOn: !isToggleOn
|
||||||
|
});
|
||||||
|
this.props.toggleParticipated(false);
|
||||||
|
|
||||||
|
// if not executed 2 time, the scroll would not be complete
|
||||||
|
setImmediate(() =>
|
||||||
|
animateScrollTo(document.getElementById('detail-view-footer-button'), {
|
||||||
|
elementToScroll: document.getElementById('CIDetailView')
|
||||||
|
}).then(() =>
|
||||||
|
animateScrollTo(document.getElementById('detail-view-footer-button'), {
|
||||||
|
elementToScroll: document.getElementById('CIDetailView')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onObjectTypeChange(event) {
|
||||||
|
this.setState({ object_type: event.target.value });
|
||||||
|
}
|
||||||
|
onDescChange(event) {
|
||||||
|
this.setState({ desc: event.target.value });
|
||||||
|
}
|
||||||
|
onPersonalInfosChange(event) {
|
||||||
|
this.setState({ [event.target.name]: event.target.value });
|
||||||
|
}
|
||||||
|
onKeepChange(event) {
|
||||||
|
this.setState({ keep: event.target.value });
|
||||||
|
}
|
||||||
|
onAGBChange(event) {
|
||||||
|
this.setState({ agb: event.target.checked });
|
||||||
|
}
|
||||||
|
onSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.validForm()) {
|
||||||
|
const data = { ...this.state, object_id: this.props.item.inventory_number };
|
||||||
|
delete data.abg;
|
||||||
|
delete data.notification;
|
||||||
|
delete data.isToggleOn;
|
||||||
|
|
||||||
|
this.props.sendParticipation(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validForm() {
|
||||||
|
const { desc, first_name, last_name, mail } = this.state;
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
if (!desc) {
|
||||||
|
this.setState({ notification: t('noti_desc') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!last_name) {
|
||||||
|
this.setState({ notification: t('noti_last_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first_name) {
|
||||||
|
this.setState({ notification: t('noti_first_name') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mail) {
|
||||||
|
this.setState({ notification: t('noti_mail') });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNotificationDismiss() {
|
||||||
|
this.setState({ notification: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isToggleOn, notification } = this.state;
|
||||||
|
const { t, participated, doYouKnowMoreURL, color } = this.props;
|
||||||
|
return (
|
||||||
|
<div className='DVFooter' style={{backgroundColor: color}}>
|
||||||
|
<div>
|
||||||
|
<div className='DVFooterTop'>
|
||||||
|
<a id='detail-view-footer-button' href={doYouKnowMoreURL}>
|
||||||
|
<p className='medium-text'>{t('know_more')}</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{isToggleOn && (
|
||||||
|
<div className='DVDropdown'>
|
||||||
|
<div className='DVText'>
|
||||||
|
<p className='medium-text'>{/* TODO */}</p>
|
||||||
|
</div>
|
||||||
|
{participated && (
|
||||||
|
<div className='CVParticipatedMessage medium-text'>
|
||||||
|
{t('thank_you')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Notification
|
||||||
|
isActive={!!notification}
|
||||||
|
message={notification}
|
||||||
|
action={'schliessen'}
|
||||||
|
onClick={this.onNotificationDismiss}
|
||||||
|
/>
|
||||||
|
<form
|
||||||
|
onSubmit={this.onSubmit}
|
||||||
|
style={participated ? { opacity: 0, pointerEvents: 'none' } : {}}
|
||||||
|
>
|
||||||
|
<CVDescInput onDescChange={this.onDescChange} />
|
||||||
|
<CVPersonalInfosInputs
|
||||||
|
onPersonalInfosChange={this.onPersonalInfosChange}
|
||||||
|
/>
|
||||||
|
<CVSubmit />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
89
assets/js/components/Tablet/LangDropMenu.js
Normal file
89
assets/js/components/Tablet/LangDropMenu.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import reduxLang from '../../middleware/lang';
|
||||||
|
import { updateURLS, urlWithoutLocale } from '../../utils';
|
||||||
|
|
||||||
|
@reduxLang('AppContent')
|
||||||
|
class LangDropMenu extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.dropdownLinks = [
|
||||||
|
{ linkText: 'EN', val: 'en'},
|
||||||
|
{ linkText: 'DE', val: 'de'},
|
||||||
|
{ linkText: 'FR', val: 'fr'},
|
||||||
|
{ linkText: 'IT', val: 'it'}
|
||||||
|
];
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showMenu: false,
|
||||||
|
selectedLanguage: this.dropdownLinks.filter(lang => {
|
||||||
|
return lang.val == this.props.locale;
|
||||||
|
})[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
this.showMenu = this.showMenu.bind(this);
|
||||||
|
this.setLanguage = this.setLanguage.bind(this);
|
||||||
|
this.closeMenu = this.closeMenu.bind(this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
showMenu(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({ showMenu: true }, () => {
|
||||||
|
document.addEventListener('click', this.closeMenu);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(language) {
|
||||||
|
console.log(language);
|
||||||
|
this.setState({selectedLanguage: language});
|
||||||
|
localStorage.setItem('userLang', language.val);
|
||||||
|
|
||||||
|
this.props.setLocale(language.val);
|
||||||
|
updateURLS(language.val, this.props.dispatch);
|
||||||
|
window.location.replace("/" + language.val + urlWithoutLocale(window.location.pathname));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeMenu(event) {
|
||||||
|
if (!this.dropdownMenu.contains(event.target) || this.dropdownMenu.contains(event.target)) {
|
||||||
|
|
||||||
|
this.setState({ showMenu: false }, () => {
|
||||||
|
document.removeEventListener('click', this.closeMenu);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let dropdownMenuShow = null;
|
||||||
|
if (this.state.showMenu) {
|
||||||
|
dropdownMenuShow = <ul
|
||||||
|
className="dropdown-menu"
|
||||||
|
ref={(element) => {
|
||||||
|
this.dropdownMenu = element;
|
||||||
|
}}>
|
||||||
|
{this.dropdownLinks.map((dropdownLink, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
<NavLink to="#" onClick={() => this.setLanguage(dropdownLink)} className="dropdown-item small-text" exact>
|
||||||
|
<span>{dropdownLink.linkText}</span>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="dropdown HeaderBurger lang-dropdown">
|
||||||
|
<NavLink to='#' className='lang-links small-text' onClick={this.showMenu}>
|
||||||
|
<span>{this.state.selectedLanguage.linkText}</span>
|
||||||
|
<i className='fas fa-caret-down' />
|
||||||
|
</NavLink>
|
||||||
|
{dropdownMenuShow}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default LangDropMenu;
|
22
assets/js/components/Tablet/SplashScreen.js
Normal file
22
assets/js/components/Tablet/SplashScreen.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import AppContainer from './AppContainer';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { defineLocale, updateURLS } from "../../utils";
|
||||||
|
|
||||||
|
export default function SplashScreen(props) {
|
||||||
|
const apiUrl = useSelector(state => state.urls.site);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const locale = defineLocale(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateURLS(locale, dispatch);
|
||||||
|
});
|
||||||
|
if (!apiUrl) {
|
||||||
|
return (
|
||||||
|
<div style={{fontSize: "48px", fontWeight: "600", margin: "auto"}}>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <AppContainer />
|
||||||
|
}
|
15
assets/js/index.js
Normal file
15
assets/js/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import '@babel/polyfill';
|
||||||
|
import 'whatwg-fetch';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import store from './redux/store';
|
||||||
|
import '../less/index.less';
|
||||||
|
import SplashScreen from "./components/SplashScreen";
|
||||||
|
|
||||||
|
const app = (
|
||||||
|
<Provider store={store}>
|
||||||
|
<SplashScreen />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
ReactDOM.render(app, document.getElementById('root'));
|
15
assets/js/index_tablet.js
Normal file
15
assets/js/index_tablet.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import '@babel/polyfill';
|
||||||
|
import 'whatwg-fetch';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import store from './redux/store';
|
||||||
|
import '../less/index.less';
|
||||||
|
import SplashScreen from "./components/Tablet/SplashScreen";
|
||||||
|
|
||||||
|
const app = (
|
||||||
|
<Provider store={store}>
|
||||||
|
<SplashScreen />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
ReactDOM.render(app, document.getElementById('root'));
|
1226
assets/js/lib/magnet/README.md
Normal file
1226
assets/js/lib/magnet/README.md
Normal file
File diff suppressed because it is too large
Load diff
2198
assets/js/lib/magnet/magnet.js
Normal file
2198
assets/js/lib/magnet/magnet.js
Normal file
File diff suppressed because it is too large
Load diff
1
assets/js/lib/magnet/magnet.min.js
vendored
Normal file
1
assets/js/lib/magnet/magnet.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/js/lib/redux-lang/actionTypes.js
Normal file
1
assets/js/lib/redux-lang/actionTypes.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const REDUX_LANG_SET_LOCALE = 'REDUX_LANG_SET_LOCALE'
|
3
assets/js/lib/redux-lang/actions.js
Normal file
3
assets/js/lib/redux-lang/actions.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { REDUX_LANG_SET_LOCALE } from './actionTypes'
|
||||||
|
|
||||||
|
export const setLocale = value => ({ type: REDUX_LANG_SET_LOCALE, value })
|
19
assets/js/lib/redux-lang/createLang.js
Normal file
19
assets/js/lib/redux-lang/createLang.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
import * as actions from './actions'
|
||||||
|
import { getString } from './helpers'
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
reducerKey: 'locale'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLang = (dictionary, config = defaultConfig) => {
|
||||||
|
const getStringFromDictionary = getString(dictionary)
|
||||||
|
return screenKey => component => {
|
||||||
|
const { reducerKey } = config
|
||||||
|
const mstp = ({ [reducerKey]: locale }) =>
|
||||||
|
({ [reducerKey]: locale, t: getStringFromDictionary(locale)(screenKey) })
|
||||||
|
const mdtp = dispatch => bindActionCreators(actions, dispatch)
|
||||||
|
return connect(mstp, mdtp)(component)
|
||||||
|
}
|
||||||
|
}
|
17
assets/js/lib/redux-lang/helpers.js
Normal file
17
assets/js/lib/redux-lang/helpers.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { vsprintf } from 'sprintf-js'
|
||||||
|
|
||||||
|
export const applyReplacements = (obj, replacements = []) => {
|
||||||
|
return (typeof obj === 'string' && replacements.length > 0)
|
||||||
|
? vsprintf(obj, replacements)
|
||||||
|
: obj
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getString = dictionary => localeKey => screenKey => (stringKey, replacements = []) => {
|
||||||
|
return [localeKey, screenKey, stringKey].reduce((acc, key) => {
|
||||||
|
if (acc[key]) return applyReplacements(acc[key], replacements)
|
||||||
|
console.warn(`ReduxLang: ${localeKey} / ${screenKey} / ${stringKey} does not exist!`)
|
||||||
|
return key
|
||||||
|
}, dictionary)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const square = number => number * number
|
9
assets/js/lib/redux-lang/index.js
Normal file
9
assets/js/lib/redux-lang/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { createLang } from './createLang'
|
||||||
|
import { setLocale } from './actions'
|
||||||
|
import langReducer from './reducer'
|
||||||
|
|
||||||
|
export {
|
||||||
|
createLang,
|
||||||
|
setLocale,
|
||||||
|
langReducer
|
||||||
|
}
|
14
assets/js/lib/redux-lang/reducer.js
Normal file
14
assets/js/lib/redux-lang/reducer.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import * as types from './actionTypes'
|
||||||
|
|
||||||
|
export default (initialState) => {
|
||||||
|
return (state = initialState, action = {}) => {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case types.REDUX_LANG_SET_LOCALE:
|
||||||
|
return action.value ? action.value : state
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
assets/js/middleware/lang.js
Normal file
4
assets/js/middleware/lang.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import { createLang } from '../lib/redux-lang';
|
||||||
|
import dictionary from '../../lang';
|
||||||
|
const reduxLang = createLang(dictionary);
|
||||||
|
export default reduxLang;
|
8
assets/js/redux/actions/ads.js
Normal file
8
assets/js/redux/actions/ads.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const FETCH_ADS_SUCCESS = '[ads] GET';
|
||||||
|
export const FETCH_ADS_ERROR = '[ads] Fetch success';
|
||||||
|
export const UPDATE_ADS = '[ads] Update';
|
||||||
|
|
||||||
|
export const updateAds = data => ({
|
||||||
|
type: UPDATE_ADS,
|
||||||
|
payload: data
|
||||||
|
});
|
7
assets/js/redux/actions/api.js
Normal file
7
assets/js/redux/actions/api.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const API_REQUEST = '[app] API Request';
|
||||||
|
|
||||||
|
export const apiRequest = (method, url, body, onSuccess, onError) => ({
|
||||||
|
type: API_REQUEST,
|
||||||
|
payload: body,
|
||||||
|
meta: { method, url, onSuccess, onError }
|
||||||
|
});
|
33
assets/js/redux/actions/catalog.js
Normal file
33
assets/js/redux/actions/catalog.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
export const GET_CATALOG = '[catalog] GET';
|
||||||
|
export const FETCH_CATALOG_SUCCESS = '[catalog] Fetch success';
|
||||||
|
export const FETCH_CATALOG_ERROR = '[catalog] Fetch error';
|
||||||
|
export const UPDATE_CATALOG = '[catalog] Update';
|
||||||
|
export const SEARCH_CATALOG = '[catalog] Search for items';
|
||||||
|
export const SEARCH_CATALOG_SUCCESS = '[catalog] Search success';
|
||||||
|
export const FILTER_CATALOG = '[catalog] Filter items by tags';
|
||||||
|
export const FILTER_CATALOG_SUCCESS = '[catalog] Filter success';
|
||||||
|
|
||||||
|
export const getCatalog = (clear = false) => ({
|
||||||
|
type: GET_CATALOG,
|
||||||
|
payload: clear
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateCatalog = data => ({
|
||||||
|
type: UPDATE_CATALOG,
|
||||||
|
payload: data
|
||||||
|
});
|
||||||
|
|
||||||
|
export const search = query => {
|
||||||
|
return {
|
||||||
|
type: SEARCH_CATALOG,
|
||||||
|
payload: query
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filter = tags => {
|
||||||
|
return {
|
||||||
|
type: FILTER_CATALOG,
|
||||||
|
payload: tags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
7
assets/js/redux/actions/mode.js
Normal file
7
assets/js/redux/actions/mode.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const GET_MODE = '[mode] GET';
|
||||||
|
export const FETCH_MODE_SUCCESS = '[mode] Fetch mode SUCCESS';
|
||||||
|
export const FETCH_MODE_ERROR = '[mode] Fetch mode ERROR';
|
||||||
|
|
||||||
|
export const getMode = () => ({
|
||||||
|
type: GET_MODE
|
||||||
|
});
|
5
assets/js/redux/actions/participate.js
Normal file
5
assets/js/redux/actions/participate.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const SEND_PARTICIPATION = '[participate] Send participation';
|
||||||
|
export const SEND_PARTICIPATION_SUCCESS = '[participate] Send participation sucess';
|
||||||
|
export const SEND_PARTICIPATION_ERROR = '[participate] Send participation error';
|
||||||
|
|
||||||
|
export const sendParticipation = data => ({ type: SEND_PARTICIPATION, payload: data });
|
24
assets/js/redux/actions/tags.js
Normal file
24
assets/js/redux/actions/tags.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export const GET_TAGS = '[tags] GET';
|
||||||
|
export const FETCH_TAGS_SUCCESS = '[tags] Fetch Success';
|
||||||
|
export const FETCH_TAGS_ERROR = '[tags] Fetch Error';
|
||||||
|
export const UPDATE_TAGS = '[tags] Update';
|
||||||
|
export const SELECT_TAG = '[tags] Select a tag';
|
||||||
|
export const DESELECT_TAGS = '[tags] Deselect all tags';
|
||||||
|
|
||||||
|
export const getTags = () => ({
|
||||||
|
type: GET_TAGS
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateTags = data => ({
|
||||||
|
type: UPDATE_TAGS,
|
||||||
|
payload: data
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectTag = ({ category, slug }) => ({
|
||||||
|
type: SELECT_TAG,
|
||||||
|
payload: { category, slug }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deselectTags = () => ({
|
||||||
|
type: DESELECT_TAGS
|
||||||
|
});
|
92
assets/js/redux/actions/ui.js
Normal file
92
assets/js/redux/actions/ui.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/* SPINNER */
|
||||||
|
|
||||||
|
export const SHOW_SPINNER = '[ui] show spinner';
|
||||||
|
export const HIDE_SPINNER = '[ui] hide spinner';
|
||||||
|
|
||||||
|
export const showSpinner = () => ({
|
||||||
|
type: SHOW_SPINNER
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hideSpinner = () => ({
|
||||||
|
type: HIDE_SPINNER
|
||||||
|
});
|
||||||
|
|
||||||
|
/* DETAIL VIEW */
|
||||||
|
|
||||||
|
export const OPEN_DETAIL = '[ui] Open detail view';
|
||||||
|
export const CLOSE_DETAIL = '[ui] Close detail view';
|
||||||
|
export const SHOW_NEXT_ITEM = '[ui] Show the next item';
|
||||||
|
export const SHOW_PREV_ITEM = '[ui] Show the previous item';
|
||||||
|
export const SET_MAGNET_INSTANCE = '[ui] Set Magnet instance';
|
||||||
|
|
||||||
|
export const openDetail = item => {
|
||||||
|
return {
|
||||||
|
type: OPEN_DETAIL,
|
||||||
|
item
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showNextItem = () => ({
|
||||||
|
type: SHOW_NEXT_ITEM
|
||||||
|
});
|
||||||
|
|
||||||
|
export const showPrevItem = () => ({
|
||||||
|
type: SHOW_PREV_ITEM
|
||||||
|
});
|
||||||
|
|
||||||
|
export const closeDetail = () => ({
|
||||||
|
type: CLOSE_DETAIL
|
||||||
|
});
|
||||||
|
|
||||||
|
/* OTHER */
|
||||||
|
|
||||||
|
export const TOGGEL_MENU = '[ui] toggle app menu';
|
||||||
|
export const TOGGEL_SHOWING_SEARCH_RESULTS = '[ui] toggle showing search results';
|
||||||
|
export const TOGGLE_PARTICIPATED = '[ui] toogle participated';
|
||||||
|
export const REBUILD_LAYOUT = '[ui] Rebuild the grid layout';
|
||||||
|
export const SHUFFLE_CATALOG = '[ui] Shuffle items';
|
||||||
|
export const SET_PRE_LAUNCH = '[ui] Set the pre-launch flag';
|
||||||
|
export const SET_INTERNAL = '[ui] Set the internal flag';
|
||||||
|
|
||||||
|
export const toggleMenu = () => ({
|
||||||
|
type: TOGGEL_MENU
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toggleShowingSearchResults = show => ({
|
||||||
|
type: TOGGEL_SHOWING_SEARCH_RESULTS,
|
||||||
|
payload: show
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toggleParticipated = participated => ({
|
||||||
|
type: TOGGLE_PARTICIPATED,
|
||||||
|
payload: participated
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setMagnetInstance = instance => ({
|
||||||
|
type: SET_MAGNET_INSTANCE,
|
||||||
|
payload: instance
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rebuildLayout = () => ({
|
||||||
|
type: REBUILD_LAYOUT
|
||||||
|
});
|
||||||
|
|
||||||
|
export const shuffle = () => {
|
||||||
|
return {
|
||||||
|
type: SHUFFLE_CATALOG
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setPreLaunch = isPreLaunch => {
|
||||||
|
return {
|
||||||
|
type: SET_PRE_LAUNCH,
|
||||||
|
payload: isPreLaunch
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setInternal = isInternal => {
|
||||||
|
return {
|
||||||
|
type: SET_INTERNAL,
|
||||||
|
payload: isInternal
|
||||||
|
};
|
||||||
|
};
|
1
assets/js/redux/actions/urls.js
Normal file
1
assets/js/redux/actions/urls.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const UPDATE_URLS = 'update urls'
|
26
assets/js/redux/middleware/ads.js
Normal file
26
assets/js/redux/middleware/ads.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { GET_CATALOG } from '../actions/catalog';
|
||||||
|
import { FETCH_ADS_SUCCESS, FETCH_ADS_ERROR, updateAds } from '../actions/ads';
|
||||||
|
import { apiRequest } from '../actions/api';
|
||||||
|
import { showSpinner, hideSpinner } from '../actions/ui';
|
||||||
|
|
||||||
|
export const getAdsFlow = store => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
if (action.type === GET_CATALOG && !state.ui.isPreLaunch) {
|
||||||
|
store.dispatch(apiRequest('GET', state.urls.ads, null, FETCH_ADS_SUCCESS, FETCH_ADS_ERROR));
|
||||||
|
store.dispatch(showSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// on successful fetch, process the ads data
|
||||||
|
export const processAdsCollection = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === FETCH_ADS_SUCCESS) {
|
||||||
|
dispatch(updateAds(action.payload.results));
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adsMdl = [getAdsFlow, processAdsCollection];
|
20
assets/js/redux/middleware/api.js
Normal file
20
assets/js/redux/middleware/api.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { API_REQUEST } from '../actions/api';
|
||||||
|
|
||||||
|
// this middleware care only for API calls
|
||||||
|
export const api = ({ dispatch }) => next => action => {
|
||||||
|
if (action.type === API_REQUEST) {
|
||||||
|
const { method, url, onSuccess, onError } = action.meta;
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method,
|
||||||
|
...(method === 'POST' ? { body: JSON.stringify(action.payload) } : {}),
|
||||||
|
headers: {
|
||||||
|
...(method === 'POST' ? { 'Content-Type': 'application/json' } : {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => dispatch({ type: onSuccess, payload: data }))
|
||||||
|
.catch(error => dispatch({ type: onError, payload: error }));
|
||||||
|
}
|
||||||
|
return next(action);
|
||||||
|
};
|
176
assets/js/redux/middleware/catalog.js
Normal file
176
assets/js/redux/middleware/catalog.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import {
|
||||||
|
FETCH_CATALOG_SUCCESS,
|
||||||
|
FETCH_CATALOG_ERROR,
|
||||||
|
GET_CATALOG,
|
||||||
|
updateCatalog,
|
||||||
|
SEARCH_CATALOG,
|
||||||
|
SEARCH_CATALOG_SUCCESS,
|
||||||
|
FILTER_CATALOG,
|
||||||
|
FILTER_CATALOG_SUCCESS,
|
||||||
|
UPDATE_CATALOG
|
||||||
|
} from '../actions/catalog';
|
||||||
|
import {
|
||||||
|
showSpinner,
|
||||||
|
toggleShowingSearchResults,
|
||||||
|
hideSpinner,
|
||||||
|
setMagnetInstance
|
||||||
|
} from '../actions/ui';
|
||||||
|
import { apiRequest } from '../actions/api';
|
||||||
|
import shuffleSeed from 'shuffle-seed';
|
||||||
|
import { UPDATE_ADS } from '../actions/ads';
|
||||||
|
|
||||||
|
const rand = () => Math.floor(Math.random() * 1000000) + 1;
|
||||||
|
|
||||||
|
// this middleware only care about the getCatalog action
|
||||||
|
export const getCatalogFlow = store => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
if (action.type === GET_CATALOG && !state.ui.isPreLaunch) {
|
||||||
|
store.dispatch(showSpinner());
|
||||||
|
if (action.payload) {
|
||||||
|
// if we want to clear the list
|
||||||
|
store.dispatch(updateCatalog([]));
|
||||||
|
}
|
||||||
|
store.dispatch(apiRequest('GET', state.urls.items, null, FETCH_CATALOG_SUCCESS, FETCH_CATALOG_ERROR));
|
||||||
|
store.dispatch(setMagnetInstance());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// on successful fetch, process the catalog data
|
||||||
|
export const processCatalogCollection = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === FETCH_CATALOG_SUCCESS) {
|
||||||
|
dispatch(toggleShowingSearchResults(false));
|
||||||
|
dispatch(updateCatalog(action.payload.catalog));
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchCatalogFlow = (store) => next => action => {
|
||||||
|
next(action);
|
||||||
|
const state = store.getState();
|
||||||
|
const url = new URL(state.urls.items, window.location.origin);
|
||||||
|
url.searchParams.set("query", encodeURIComponent(action.payload));
|
||||||
|
|
||||||
|
if (action.type === SEARCH_CATALOG) {
|
||||||
|
store.dispatch(
|
||||||
|
apiRequest(
|
||||||
|
'GET',
|
||||||
|
url.pathname + url.search,
|
||||||
|
null,
|
||||||
|
SEARCH_CATALOG_SUCCESS,
|
||||||
|
FETCH_CATALOG_ERROR
|
||||||
|
)
|
||||||
|
);
|
||||||
|
store.dispatch(showSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateCatalogFlow = store => next => action => {
|
||||||
|
if (action.type === UPDATE_CATALOG || action.type === UPDATE_ADS) {
|
||||||
|
const state = store.getState();
|
||||||
|
if (action.type === UPDATE_ADS && state.catalog.length === 0) {
|
||||||
|
next(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ITEMS_PER_PAGE = 10;
|
||||||
|
let ads = state.ads;
|
||||||
|
if (action.type === UPDATE_ADS) {
|
||||||
|
ads = action.payload
|
||||||
|
}
|
||||||
|
let items = state.catalog;
|
||||||
|
if (action.type === UPDATE_CATALOG) {
|
||||||
|
items = action.payload;
|
||||||
|
}
|
||||||
|
if (!state.ui.showingSearchResults && state.ui.isInternal) {
|
||||||
|
items = items.concat(ads.filter(ad => {
|
||||||
|
return !ad.show_per_page;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (!state.ui.showingSearchResults && !state.ui.isInternal) {
|
||||||
|
items = items.concat(ads);
|
||||||
|
}
|
||||||
|
const shuffled = shuffleSeed.shuffle(items, rand());
|
||||||
|
const prio = shuffled.filter(item => {
|
||||||
|
return item.prio;
|
||||||
|
});
|
||||||
|
const not_prio = shuffled.filter(item => {
|
||||||
|
return !item.prio;
|
||||||
|
});
|
||||||
|
const all_items = prio.concat(not_prio);
|
||||||
|
if(!state.ui.showingSearchResults && state.ui.isInternal) {
|
||||||
|
const perPageAds = ads.filter(ad => {
|
||||||
|
return ad.show_per_page;
|
||||||
|
});
|
||||||
|
const pages_count = parseInt((all_items.length - 1) / ITEMS_PER_PAGE);
|
||||||
|
for (let step = 1; step < pages_count; step++) {
|
||||||
|
perPageAds.map((ad, i) => {
|
||||||
|
let indx = (step) * ITEMS_PER_PAGE;
|
||||||
|
all_items.splice(indx, 0, ad);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(all_items);
|
||||||
|
}
|
||||||
|
if (action.type === UPDATE_ADS) {
|
||||||
|
store.dispatch(updateCatalog(all_items));
|
||||||
|
} else {
|
||||||
|
action.payload = all_items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
// on successful search, process the catalog data
|
||||||
|
export const processCatalogSearchCollection = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === SEARCH_CATALOG_SUCCESS) {
|
||||||
|
dispatch(toggleShowingSearchResults(true));
|
||||||
|
dispatch(updateCatalog(action.payload.catalog));
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterCatalogFlow = (store) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
if (action.type === FILTER_CATALOG) {
|
||||||
|
const tagsStr = action.payload.join(',');
|
||||||
|
const url = new URL(state.urls.items, window.location.origin);
|
||||||
|
url.searchParams.set("tags", encodeURIComponent(tagsStr));
|
||||||
|
store.dispatch(
|
||||||
|
apiRequest(
|
||||||
|
'GET',
|
||||||
|
url.pathname + url.search,
|
||||||
|
null,
|
||||||
|
FILTER_CATALOG_SUCCESS,
|
||||||
|
FETCH_CATALOG_ERROR
|
||||||
|
)
|
||||||
|
);
|
||||||
|
store.dispatch(showSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// on successful filter, process the catalog data
|
||||||
|
export const processCatalogFilterCollection = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === FILTER_CATALOG_SUCCESS) {
|
||||||
|
dispatch(toggleShowingSearchResults(true));
|
||||||
|
dispatch(updateCatalog(action.payload.catalog));
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const catalogMdl = [
|
||||||
|
getCatalogFlow,
|
||||||
|
searchCatalogFlow,
|
||||||
|
processCatalogCollection,
|
||||||
|
updateCatalogFlow,
|
||||||
|
processCatalogSearchCollection,
|
||||||
|
filterCatalogFlow,
|
||||||
|
processCatalogFilterCollection
|
||||||
|
];
|
24
assets/js/redux/middleware/mode.js
Normal file
24
assets/js/redux/middleware/mode.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { GET_MODE, FETCH_MODE_SUCCESS, FETCH_MODE_ERROR } from '../actions/mode';
|
||||||
|
import { showSpinner, hideSpinner, setPreLaunch, setInternal } from '../actions/ui';
|
||||||
|
import { apiRequest } from '../actions/api';
|
||||||
|
|
||||||
|
export const getModeFlow = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === GET_MODE) {
|
||||||
|
dispatch(apiRequest('GET', '/mode/', null, FETCH_MODE_SUCCESS, FETCH_MODE_ERROR));
|
||||||
|
dispatch(showSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processModeCollection = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === FETCH_MODE_SUCCESS) {
|
||||||
|
dispatch(setPreLaunch(action.payload.mode === 'minimal'));
|
||||||
|
dispatch(setInternal(action.payload.mode === 'internal'));
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modeMdl = [getModeFlow, processModeCollection];
|
38
assets/js/redux/middleware/participate.js
Normal file
38
assets/js/redux/middleware/participate.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import {
|
||||||
|
SEND_PARTICIPATION,
|
||||||
|
SEND_PARTICIPATION_SUCCESS,
|
||||||
|
SEND_PARTICIPATION_ERROR
|
||||||
|
} from '../actions/participate';
|
||||||
|
import { showSpinner, hideSpinner, toggleParticipated } from '../actions/ui';
|
||||||
|
|
||||||
|
|
||||||
|
export const sendParticipationFlow = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === SEND_PARTICIPATION) {
|
||||||
|
const formData = new FormData();
|
||||||
|
for (let key of Object.getOwnPropertyNames(action.payload)) {
|
||||||
|
formData.append(key, action.payload[key])
|
||||||
|
}
|
||||||
|
fetch('/participate/',
|
||||||
|
{ method: "POST", body: formData }
|
||||||
|
)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => dispatch({ type: SEND_PARTICIPATION_SUCCESS, payload: data }))
|
||||||
|
.catch(error => dispatch({ type: SEND_PARTICIPATION_ERROR, payload: error }))
|
||||||
|
|
||||||
|
dispatch(showSpinner());
|
||||||
|
dispatch(toggleParticipated(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const successfulParticipationFlow = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === SEND_PARTICIPATION_SUCCESS) {
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
dispatch(toggleParticipated(true));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const participateMdl = [sendParticipationFlow, successfulParticipationFlow];
|
48
assets/js/redux/middleware/tags.js
Normal file
48
assets/js/redux/middleware/tags.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
FETCH_TAGS_SUCCESS,
|
||||||
|
FETCH_TAGS_ERROR,
|
||||||
|
GET_TAGS,
|
||||||
|
updateTags,
|
||||||
|
SELECT_TAG
|
||||||
|
} from '../actions/tags';
|
||||||
|
import { showSpinner, hideSpinner } from '../actions/ui';
|
||||||
|
import { apiRequest } from '../actions/api';
|
||||||
|
import { filter } from '../actions/catalog';
|
||||||
|
|
||||||
|
// this middleware only care about the getTags action
|
||||||
|
export const getTagsFlow = (store) => next => action => {
|
||||||
|
next(action);
|
||||||
|
const state = store.getState();
|
||||||
|
if (action.type === GET_TAGS) {
|
||||||
|
store.dispatch(apiRequest('GET', state.urls.tags, null, FETCH_TAGS_SUCCESS, FETCH_TAGS_ERROR));
|
||||||
|
store.dispatch(showSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// on successful fetch, process the tags data
|
||||||
|
export const processTagsCollection = ({ dispatch }) => next => action => {
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
if (action.type === FETCH_TAGS_SUCCESS) {
|
||||||
|
dispatch(updateTags(action.payload));
|
||||||
|
dispatch(hideSpinner());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectTagFlow = store => next => action => {
|
||||||
|
next(action);
|
||||||
|
if (action.type === SELECT_TAG) {
|
||||||
|
const tags = [];
|
||||||
|
const state = store.getState();
|
||||||
|
Object.keys(state.tags).forEach(key => {
|
||||||
|
state.tags[key].forEach(tag => {
|
||||||
|
if (tag.selected) {
|
||||||
|
tags.push(tag.slug);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
store.dispatch(filter(tags));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tagsMdl = [getTagsFlow, processTagsCollection, selectTagFlow];
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue