This commit is contained in:
PCoder 2022-09-23 07:38:37 +05:30
commit f0d178101a
545 changed files with 78540 additions and 0 deletions

17
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
```

View 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;
};

View 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;

View 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>
);
}
}

View 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;

View 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;

View File

@ -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;

View 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
};

View 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>
);
}
}

View 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;

View 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;

View File

@ -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}`}
/>
&nbsp; &nbsp;
<ShareItem
icon={<img src='/static/gfx/alps_twitter.svg' alt='tw' />}
href={`https://twitter.com/share?url=${window.location.href}&amp;text=${encodeURIComponent(
title
)}&amp;hashtags=alpinesmueum`}
/>
&nbsp; &nbsp;
<ShareItem
icon={<img src='/static/gfx/alps_mail.svg' alt='email' />}
href={`mailto:?subject=${title}&body=${window.location.href}`}
newTab={false}
/>
&nbsp; &nbsp;
<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>
);

View 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;
}

View 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;

View 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;

View 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
};

View 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;

View 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'>&#x2116; {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
};

View 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;

View 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;

View 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;

View 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;

View 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);

View File

@ -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;

View 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>
);
}
}

View 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;

View 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);

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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);

View 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);

View 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);

View 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));

View 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;
}
}

View 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;

View 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}
/>
&nbsp; {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);

View 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>
);
}
}

View 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;

View 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);

View 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>&nbsp; {t('give_forever')}</span>
</label>
<br />
<label>
<input type='radio' name='keep' value='until_2021' onChange={onKeepChange} />
<span>&nbsp; {t('until_2012')}</span>
</label>
<br />
</div>
</div>
);
export default reduxLang('ContribView')(CVKeepInput);

View 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>&nbsp; {t('offer_object')}</span>
</label>
<br />
<label>
<input type='radio' name='object_type' value='foto' onChange={onObjectTypeChange} />
<span>&nbsp; {t('offer_photo')}</span>
</label>
<br />
<label>
<input type='radio' name='object_type' value='film' onChange={onObjectTypeChange} />
<span>&nbsp; {t('offer_film')}</span>
</label>
<br />
<label>
<input
type='radio'
name='object_type'
value='skigeschichte'
onChange={onObjectTypeChange}
/>
<span>&nbsp; {t('offer_story')}</span>
</label>
<br />
<label>
<input
type='radio'
name='object_type'
value='spezialwissen'
onChange={onObjectTypeChange}
/>
<span>&nbsp; {t('offer_knowledge')}</span>
</label>
<br />
</div>
</div>
);
export default reduxLang('ContribView')(CVObjectTypeRadio);

View 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);

View 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);

View 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);

View 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);

View 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>
);

View 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;

View 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>
);

View 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);

View 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' />
&nbsp;&nbsp;-&nbsp;&nbsp;
<DatenschutzerklarungBtn label='Datenschutzerklärung' />
</div>
);
export default InfoViewFooter;

View 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);

View 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);

View 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;

View 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 />
}

View 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;

View 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;

View 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);

View 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;

View 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>
);
}
}

View 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;

View 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;

View 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;

View 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
};

View 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
};

View 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>
);
}
}

View 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;

View 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
View 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
View 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'));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
assets/js/lib/magnet/magnet.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
export const REDUX_LANG_SET_LOCALE = 'REDUX_LANG_SET_LOCALE'

View File

@ -0,0 +1,3 @@
import { REDUX_LANG_SET_LOCALE } from './actionTypes'
export const setLocale = value => ({ type: REDUX_LANG_SET_LOCALE, value })

View 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)
}
}

View 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

View File

@ -0,0 +1,9 @@
import { createLang } from './createLang'
import { setLocale } from './actions'
import langReducer from './reducer'
export {
createLang,
setLocale,
langReducer
}

View 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
}
}
}

View File

@ -0,0 +1,4 @@
import { createLang } from '../lib/redux-lang';
import dictionary from '../../lang';
const reduxLang = createLang(dictionary);
export default reduxLang;

View 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
});

View 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 }
});

View 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
};
};

View 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
});

View 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 });

View 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
});

View 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
};
};

View File

@ -0,0 +1 @@
export const UPDATE_URLS = 'update urls'

View 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];

View 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);
};

View 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
];

View 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];

View 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];

View 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