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