first commit
@@ -0,0 +1,19 @@
|
||||
SERVER_URL=http://localhost:80
|
||||
MONGO_URI=mongodb://declic:declic42@ds115653.mlab.com:15653/next-auth
|
||||
FACEBOOK_ID=2332071840142634
|
||||
FACEBOOK_SECRET=afb8bbca72bd8c517c64f81e998e4f12
|
||||
LINKEDIN_ID=77k1m8qx7w6ygj
|
||||
LINKEDIN_SECRET=mbcnVjyy2vqAZz1B
|
||||
GOOGLE_ID=
|
||||
GOOGLE_SECRET=
|
||||
TWITTER_KEY=
|
||||
TWITTER_SECRET=
|
||||
EMAIL_FROM=contact@application.bzh
|
||||
EMAIL_SERVER=mail.gandi.net
|
||||
EMAIL_PORT=587
|
||||
EMAIL_SECURE=true
|
||||
EMAIL_USERNAME=noreply@application.bzh
|
||||
EMAIL_PASSWORD=PWApwaPWApwa56!!!
|
||||
FIREBASE_API_KEY=AAAAHlF5rsI:APA91bHXvjGDJRLzVvqmBDPM1Et96Bw0Iuvc7EGe9wt4a_lSJijDlbZ3RT96seLDK-QM6Mf8qQ4dH6MI9iWt1_d3uzKG2bxr56zltglZ3Z9xU8gz2iJeRGmlOd0YdV43IumIVe6dIOr-
|
||||
RECAPTCHA_VERIFY_URL=https://www.google.com/recaptcha/api/siteverify
|
||||
RECAPTCHA_SECRET=6LcCvHcUAAAAAIG-_iR5AXqkwxINfgqxKZ_kG244
|
||||
@@ -0,0 +1,6 @@
|
||||
/node_modules/
|
||||
/.next/
|
||||
/out/
|
||||
TODO
|
||||
/cypress/*
|
||||
!/cypress/integration
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"name": "Attach to Chrome",
|
||||
"port": 3000,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import React, { Component } from "react";
|
||||
import classnames from "classnames";
|
||||
import _ from "lodash";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
import Griddle from "griddle-react";
|
||||
import MoreVertIcon from "@material-ui/icons/MoreVert";
|
||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import red from "@material-ui/core/colors/red";
|
||||
import Collapse from "@material-ui/core/Collapse";
|
||||
// import { FormattedDate, FormattedTime } from "react-intl";
|
||||
import { withStyles, grey400 } from "@material-ui/core/styles";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardContent,
|
||||
CardActions,
|
||||
IconButton,
|
||||
MenuItem
|
||||
} from "@material-ui/core";
|
||||
import GriddleCustomTableComponent from "../custom/GriddleCustomTableComponent";
|
||||
|
||||
const styles = theme => ({
|
||||
card: {
|
||||
maxWidth: 400
|
||||
},
|
||||
media: {
|
||||
height: 0,
|
||||
paddingTop: "56.25%" // 16:9
|
||||
},
|
||||
actions: {
|
||||
display: "flex"
|
||||
},
|
||||
expand: {
|
||||
transform: "rotate(0deg)",
|
||||
transition: theme.transitions.create("transform", {
|
||||
duration: theme.transitions.duration.shortest
|
||||
}),
|
||||
marginLeft: "auto"
|
||||
},
|
||||
expandOpen: {
|
||||
transform: "rotate(180deg)"
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: red[500]
|
||||
}
|
||||
});
|
||||
|
||||
const iconButtonElement = (
|
||||
<IconButton touch tooltip="more" tooltipPosition="bottom-left">
|
||||
<MoreVertIcon color={grey400} />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
const rightIconMenu = (
|
||||
<MenuIcon iconButtonElement={iconButtonElement}>
|
||||
<MenuItem>Call</MenuItem>
|
||||
</MenuIcon>
|
||||
);
|
||||
|
||||
class CustomRowComponent extends React.Component {
|
||||
state = {
|
||||
expanded: false
|
||||
};
|
||||
|
||||
handleExpandClick = () => {
|
||||
this.setState({ expanded: !this.state.expanded });
|
||||
};
|
||||
|
||||
// SchemaName SchemaType Caption
|
||||
// Id_Indice int (Nothing)
|
||||
// Id_Itv nvarchar Code
|
||||
// Id_Res_Aff uniqueidentifier Affaire
|
||||
// Id_Res_Elt uniqueidentifier Elément
|
||||
// Dt_Crt datetime Date création
|
||||
// Dt_Itv datetime Date
|
||||
// Id_Opr_Sas nvarchar Demandeur
|
||||
// Id_Opr_Itv nvarchar Intervenant
|
||||
// Id_Itv_Typ nvarchar Type
|
||||
// Id_Cli nvarchar Code client
|
||||
// Id_Adr_Key nvarchar Adresse
|
||||
// Id_Cct_Key nvarchar Contact
|
||||
// Id_Res_Elt_Fab uniqueidentifier Elément
|
||||
// Lbl_Itv nvarchar Résumé
|
||||
// Lbm_Itv_Dmd nvarchar Demande
|
||||
// Lbm_Itv_Sol nvarchar Description intervention
|
||||
// Ldv_Gar int Garantie
|
||||
// Ldv_Sts int Statut
|
||||
// Dt_Clt datetime Date clôture
|
||||
// ClassObjectId nvarchar (Nothing)
|
||||
|
||||
|
||||
render() {
|
||||
const { rowData } = this.props;
|
||||
|
||||
let interventionDate = new Date(rowData.Dt_Itv);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title={rowData.Lbl_Itv} subheader={(interventionDate).toLocaleDateString("fr-FR") + ' ' + interventionDate.toLocaleTimeString("fr-FR", {hour: '2-digit', minute:'2-digit'})}/>
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<IconButton
|
||||
className={classnames(styles.expand, {
|
||||
[styles.expandOpen]: this.state.expanded
|
||||
})}
|
||||
onClick={this.handleExpandClick}
|
||||
aria-expanded={this.state.expanded}
|
||||
aria-label="Show more"
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
<EditIcon onClick={() => alert('future edit')} />
|
||||
</CardActions>
|
||||
<Collapse in={this.state.expanded} timeout="auto" unmountOnExit>
|
||||
<CardContent>
|
||||
<h3>Details</h3>
|
||||
<p>
|
||||
{rowData.Id_Adr_Key}
|
||||
</p>
|
||||
<p>
|
||||
{rowData.Id_Cct_Key}
|
||||
</p>
|
||||
<p>
|
||||
{rowData.Lbm_Itv_Dmd}
|
||||
</p>
|
||||
<p>
|
||||
{rowData.Id_Opr_Itv}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Collapse>
|
||||
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomRowComponent.propTypes = {
|
||||
rowData: PropTypes.func.isRequired,
|
||||
editQuest: PropTypes.func.isRequired,
|
||||
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
class Intervention extends Component {
|
||||
state = {
|
||||
data: []
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
console.log("componentWillMount Intervention props : ", this.props.data);
|
||||
this.setState({
|
||||
|
||||
data: this.props.data
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() { }
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
console.log(
|
||||
"componentWillReceiveProps Intervention nextProps",
|
||||
nextProps.data
|
||||
);
|
||||
this.setState({
|
||||
data: nextProps.data
|
||||
});
|
||||
}
|
||||
|
||||
// rowDataSelector = (state, props) => {
|
||||
// let o = state
|
||||
// .get("data")
|
||||
// .find(rowMap => rowMap.get("griddleKey") === props.griddleKey);
|
||||
// o = o.concat({ gameId: this.props.gameId });
|
||||
// return o.toJSON();
|
||||
// };
|
||||
|
||||
// enhancedWithRowData = connect((state, props) => ({
|
||||
// editQuest: this.props.edit,
|
||||
// // rowData will be available into MyCustomComponent
|
||||
// rowData: this.rowDataSelector(state, props),
|
||||
// value: this.props.gameId
|
||||
// }));
|
||||
|
||||
NewLayout = ({ Table, Pagination, Filter }) => (
|
||||
<div>
|
||||
<Filter />
|
||||
<Pagination />
|
||||
<Table />
|
||||
</div>
|
||||
);
|
||||
|
||||
CustomTableBody = ({ rowIds, Row, style, className }) => (
|
||||
<div style={style} className={className}>
|
||||
{rowIds && rowIds.map(r => <Row key={r} griddleKey={r} />)}
|
||||
</div>
|
||||
);
|
||||
|
||||
_onNext = () => {
|
||||
// const { currentPage, pageSize, filterText } = this.state;
|
||||
// this.props.fetchUserGamesRequest(currentPage + 1, pageSize, filterText);
|
||||
};
|
||||
|
||||
_onPrevious = () => {
|
||||
// const { currentPage, pageSize, filterText } = this.state;
|
||||
// this.props.fetchUserGamesRequest(currentPage - 1, pageSize, filterText);
|
||||
};
|
||||
|
||||
_onGetPage = pageNumber => {
|
||||
const { pageSize, filterText } = this.state;
|
||||
|
||||
this.props._onGetPage(pageNumber, pageSize, filterText)
|
||||
.then( (data) => this.setState(data))
|
||||
// this.props.fetchUserGamesRequest(pageNumber, pageSize, filterText);
|
||||
};
|
||||
|
||||
_onFilter = filterText => {
|
||||
// this.setState({ currentPage: 1 });
|
||||
// const { currentPage, pageSize } = this.state;
|
||||
// this.setState({ filterText });
|
||||
// this.props.fetchUserGamesRequest(currentPage, pageSize, filterText);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, currentPage, pageSize, recordCount } = this.state;
|
||||
return (
|
||||
<div>
|
||||
{!this.props.data && (
|
||||
<div className="text-center">
|
||||
<div className="alert alert-info">
|
||||
Aucune intervention disponnible.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
<Griddle
|
||||
data={this.props.data}
|
||||
pageProperties={{
|
||||
currentPage,
|
||||
pageSize,
|
||||
recordCount
|
||||
}}
|
||||
components={{
|
||||
Layout: this.NewLayout,
|
||||
Row: CustomRowComponent,
|
||||
TableContainer: GriddleCustomTableComponent,
|
||||
TableBody: this.CustomTableBody
|
||||
}}
|
||||
events={{
|
||||
onNext: this._onNext,
|
||||
onPrevious: this._onPrevious,
|
||||
onGetPage: this._onGetPage,
|
||||
onFilter: this._onFilter
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Intervention;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Snackbar } from '@material-ui/core';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardActionArea from '@material-ui/core/CardActionArea';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import cookie from 'react-cookies'
|
||||
|
||||
const styles = theme => ({
|
||||
cardSnack: {
|
||||
maxWidth: '100%'
|
||||
}
|
||||
});
|
||||
class CookiesRGPD extends Component {
|
||||
state = {
|
||||
openSnack: false
|
||||
}
|
||||
handleClose = () => {
|
||||
this.setState({ openSnack: false });
|
||||
cookie.save('RGPD', true, {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (window !== undefined) {
|
||||
// don't show if we are in App
|
||||
if (window.navigator.standalone ||
|
||||
window.matchMedia('(display-mode: standalone)').matches) {
|
||||
this.setState({ openSnack: false })
|
||||
} else {
|
||||
|
||||
this.setState({ openSnack: !cookie.load('RGPD') })
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
autoHideDuration={null}
|
||||
open={this.state.openSnack}
|
||||
onClose={this.handleClose}
|
||||
>
|
||||
<Card className={classes.cardSnack}>
|
||||
<CardActionArea>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="h2">
|
||||
Utilisation des cookies
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
En poursuivant votre navigation, vous acceptez l'utilisation de cookies ou technologies similaires, y compris de partenaires tiers pour la diffusion de publicité ciblée et de contenus pertinents au regard de vos centres d'intérêts. En savoir plus.
|
||||
Afin de continuer à améliorer la protection de vos données personnelles, nous avons mis à jour notre politique de confidentialité.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<CardActions>
|
||||
<IconButton
|
||||
key="close"
|
||||
aria-label="Close"
|
||||
color="inherit"
|
||||
className={classes.close}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(CookiesRGPD)
|
||||
@@ -0,0 +1,245 @@
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Hidden from '@material-ui/core/Hidden';
|
||||
import CardMedia from '@material-ui/core/CardMedia';
|
||||
import LocalPhone from '@material-ui/icons/LocalPhone';
|
||||
import LocationOn from '@material-ui/icons/LocationOn';
|
||||
import Security from '@material-ui/icons/Security';
|
||||
import Mail from '@material-ui/icons/Mail';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Link from 'next/link'
|
||||
import { truncate } from 'lodash'
|
||||
import Social from './social.js'
|
||||
import WhatshotIcon from '@material-ui/icons/Whatshot';
|
||||
import dynamic from 'next/dynamic'
|
||||
import ButtonBase from "@material-ui/core/ButtonBase";
|
||||
import Router from 'next/router'
|
||||
import { Fragment } from 'react';
|
||||
|
||||
const PWAInstallSnack = dynamic(() => import('./PWAInstallSnack.js'), {
|
||||
ssr: false
|
||||
});
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.secondary.main,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
width: 'calc( 100% - theme.spacing.unit*2)'
|
||||
},
|
||||
grid: {
|
||||
display: 'flex',
|
||||
width: '99%',
|
||||
margin: 0
|
||||
},
|
||||
|
||||
paper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing.unit * 2,
|
||||
textAlign: 'center'
|
||||
},
|
||||
pointer: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
item: {
|
||||
|
||||
margin: theme.spacing.unit * 2,
|
||||
minWidth: '200px',
|
||||
maxHeight: '300px',
|
||||
|
||||
},
|
||||
icon: {
|
||||
fontSize: '64px',
|
||||
margin: 'auto',
|
||||
textAlign: 'center'
|
||||
|
||||
},
|
||||
card: {
|
||||
display: 'flex',
|
||||
},
|
||||
cardDetails: {
|
||||
flex: 1,
|
||||
|
||||
},
|
||||
cardMedia: {
|
||||
objectFit: 'cover',
|
||||
width: 150,
|
||||
height: 150
|
||||
},
|
||||
link: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
image: {
|
||||
position: 'relative',
|
||||
height: 200,
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
width: '100% !important', // Overrides inline-style
|
||||
height: 100,
|
||||
},
|
||||
'&:hover, &$focusVisible': {
|
||||
zIndex: 1,
|
||||
'& $imageBackdrop': {
|
||||
opacity: 0.15,
|
||||
},
|
||||
'& $imageMarked': {
|
||||
opacity: 0,
|
||||
},
|
||||
'& $imageTitle': {
|
||||
border: '4px solid currentColor',
|
||||
},
|
||||
},
|
||||
},
|
||||
focusVisible: {},
|
||||
imageButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
imageSrc: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center 40%',
|
||||
},
|
||||
imageBackdrop: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: theme.palette.common.black,
|
||||
opacity: 0.4,
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
imageTitle: {
|
||||
position: 'relative',
|
||||
padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px ${theme.spacing.unit + 6}px`,
|
||||
},
|
||||
imageMarked: {
|
||||
height: 3,
|
||||
width: 18,
|
||||
backgroundColor: theme.palette.common.white,
|
||||
position: 'absolute',
|
||||
bottom: -2,
|
||||
left: 'calc(50% - 9px)',
|
||||
transition: theme.transitions.create('opacity'),
|
||||
},
|
||||
social: {
|
||||
margin: 'auto'
|
||||
}
|
||||
});
|
||||
|
||||
class Footer extends React.Component {
|
||||
|
||||
render() {
|
||||
const { classes, show } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
|
||||
<Grid container className={classes.grid} spacing={16}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Grid container className={classes.demo} justify="center" spacing={16}>
|
||||
<Grid item className={classes.item}>
|
||||
<PWAInstallSnack >
|
||||
{({ initInstall }) =>
|
||||
|
||||
<Button
|
||||
aria-label='install' variant="contained" color="primary" onClick={initInstall}>
|
||||
<WhatshotIcon ></WhatshotIcon>
|
||||
<Typography variant="h6" color="inherit">
|
||||
Instaler l'app
|
||||
</Typography>
|
||||
</Button>
|
||||
|
||||
}
|
||||
</PWAInstallSnack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Grid container className={classes.demo} justify="center" spacing={16}>
|
||||
<Grid item className={classes.item}>
|
||||
<Paper className={classes.paper} >
|
||||
<a href="https://goo.gl/maps/mE4X4Xm4i5L2" target="_blank" rel="noopener">
|
||||
<LocationOn className={classes.icon} />
|
||||
</a>
|
||||
<Typography variant="h5" gutterBottom>e-declic Auray</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item className={classes.item}>
|
||||
|
||||
<ButtonBase
|
||||
name='contact'
|
||||
focusRipple
|
||||
key={'image.title'}
|
||||
className={classes.image}
|
||||
focusVisibleClassName={classes.focusVisible}
|
||||
style={{
|
||||
width: 200, height: 140
|
||||
}}
|
||||
onClick={() => Router.push('/contact')}
|
||||
>
|
||||
<span
|
||||
className={classes.imageSrc}
|
||||
|
||||
/>
|
||||
<span className={classes.imageBackdrop} />
|
||||
<span className={classes.imageButton}>
|
||||
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h5"
|
||||
color="inherit"
|
||||
className={classes.imageTitle}
|
||||
>
|
||||
<Mail className={classes.icon + ' ' + classes.link} />
|
||||
<span className={classes.imageMarked} />
|
||||
</Typography>
|
||||
</span>
|
||||
</ButtonBase>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Grid item className={classes.item}>
|
||||
<Paper className={classes.paper} >
|
||||
<LocalPhone className={classes.icon} />
|
||||
<Link href={"tel:+33297290060"} ><a>
|
||||
<Typography variant="h5" gutterBottom>+33.(0)297 290 060</Typography>
|
||||
</a></Link>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item className={classes.item}>
|
||||
<Paper className={classes.paper} >
|
||||
<Social className={classes.icon} delay='1000' svgWidth='64'></Social>
|
||||
<Typography variant="h5" gutterBottom>Nous Suivre</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Footer)
|
||||
@@ -0,0 +1,180 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { withStyles, getMuiTheme } from '@material-ui/core/styles';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import Link from 'next/link'
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import SendIcon from '@material-ui/icons/Send';
|
||||
import CodeIcon from '@material-ui/icons/Code';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import FingerprintIcon from '@material-ui/icons/Fingerprint';
|
||||
import LocalBarIcon from '@material-ui/icons/LocalBar';
|
||||
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import { ListItemText } from '@material-ui/core';
|
||||
|
||||
import { Spring, animated } from 'react-spring'
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
'& a':{
|
||||
textDecoration:'none'
|
||||
}
|
||||
},
|
||||
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
bar: {
|
||||
},
|
||||
menuButton: {
|
||||
marginLeft: -12,
|
||||
marginRight: 20,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'none',
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
menuLogo: {
|
||||
marginLeft: 20,
|
||||
marginRight: 40,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
title: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'block',
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
marginRight: 15,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
sectionDesktop: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
sectionMobile: {
|
||||
display: 'flex',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
fullList: {
|
||||
width: 'auto',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
});
|
||||
|
||||
class Header extends React.Component {
|
||||
state = {
|
||||
top: false,
|
||||
left: false,
|
||||
bottom: false,
|
||||
right: false,
|
||||
toggle: true
|
||||
};
|
||||
|
||||
toggleDrawer = (side, open) => () => {
|
||||
this.setState({
|
||||
[side]: open,
|
||||
});
|
||||
};
|
||||
|
||||
toggle = () => this.setState(state => ({ toggle: !state.toggle }))
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Drawer anchor="top" open={this.state.top} onClose={this.toggleDrawer('top', false)}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={this.toggleDrawer('top', false)}
|
||||
onKeyDown={this.toggleDrawer('top', false)}
|
||||
>
|
||||
<div className={classes.fullList}>
|
||||
<List>
|
||||
<Link href="/" prefetch >
|
||||
<a><ListItem>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='Accueil'>
|
||||
</ListItemText>
|
||||
</ListItem></a>
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link href="/contact" prefetch >
|
||||
<a><ListItem>
|
||||
<ListItemIcon>
|
||||
<SendIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary='contact'>
|
||||
</ListItemText>
|
||||
</ListItem></a>
|
||||
</Link>
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
<AppBar position="static" className={classes.appBar}>
|
||||
<Toolbar>
|
||||
<IconButton className={classes.menuButton} onClick={this.toggleDrawer('top', true)}
|
||||
color="inherit" aria-label="Open drawer">
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Link href="/" prefetch>
|
||||
<a><div onClick={this.toggle}>
|
||||
<Spring native from={{ x: 0 }} to={{ x: this.state.toggle ? 1 : 0 }} config={{ duration: 1000 }}>
|
||||
{({ x }) => (
|
||||
<animated.div
|
||||
style={{
|
||||
opacity: x.interpolate({ output: [1, 1] }),
|
||||
transform: x
|
||||
.interpolate({
|
||||
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
|
||||
output: [1, 0.97, 0.9, 1.1, 0.9, 1.1, 1.03, 1]
|
||||
})
|
||||
.interpolate(x => `scale(${x})`)
|
||||
}}>
|
||||
<img alt="AG2L" src='/static/images/icons/AG2L-Logo.png' className={classes.menuLogo} />
|
||||
</animated.div>
|
||||
)}
|
||||
</Spring>
|
||||
</div></a>
|
||||
</Link>
|
||||
|
||||
<Link href="/contact" prefetch >
|
||||
<a><Typography className={classes.title} variant="h5" color="inherit" noWrap>
|
||||
Contact
|
||||
</Typography></a>
|
||||
</Link>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default withStyles(styles)(Header)
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import { Snackbar } from '@material-ui/core';
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
const styles = theme => ({
|
||||
close: {
|
||||
padding: theme.spacing.unit / 2,
|
||||
},
|
||||
});
|
||||
var EdeclicLib = null
|
||||
class Messager extends Component {
|
||||
state = {
|
||||
openSnack: false,
|
||||
title: '',
|
||||
body: '',
|
||||
link: '',
|
||||
tokenStopListener: null,
|
||||
messageStopListener: null,
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
EdeclicLib = (await import('./firebase-client.js')).default
|
||||
if (!EdeclicLib.messaging)
|
||||
return
|
||||
this.setState({ tokenStopListener: EdeclicLib.messaging.onTokenRefresh(this.RefreshToken) })
|
||||
this.setState({ messageStopListener: EdeclicLib.messaging.onMessage(this.MessageReceiver) })
|
||||
|
||||
}
|
||||
componentWillUnmount() {
|
||||
|
||||
if (process.browser) {
|
||||
if (!EdeclicLib.messaging)
|
||||
return
|
||||
if (!!this.state.tokenStopListener)
|
||||
this.state.tokenStopListener();
|
||||
if (!!this.state.messageStopListener)
|
||||
this.state.messageStopListener();
|
||||
}
|
||||
}
|
||||
|
||||
RefreshToken = () => {
|
||||
EdeclicLib.messaging.getToken().then(function (refreshedToken) {
|
||||
console.log('Token refreshed.');
|
||||
// Indicate that the new Instance ID token has not yet been sent to the
|
||||
// app server.
|
||||
EdeclicLib.setTokenSentToServer(false);
|
||||
// Send Instance ID token to app server.
|
||||
EdeclicLib.sendTokenToServer(refreshedToken);
|
||||
|
||||
}).catch(function (err) {
|
||||
console.log('Unable to retrieve refreshed token ', err);
|
||||
|
||||
});
|
||||
}
|
||||
// Handle incoming messages. Called when:
|
||||
// - a message is received while the app has focus
|
||||
// - the user clicks on an app notification created by a service worker
|
||||
// `messaging.setBackgroundMessageHandler` handler.
|
||||
MessageReceiver = (payload) => {
|
||||
console.log('Message received in Messager. ', payload);
|
||||
|
||||
this.setState({
|
||||
openSnack: true,
|
||||
title: payload.notification.title,
|
||||
body: payload.notification.body,
|
||||
link: payload.notification.click_action
|
||||
});
|
||||
|
||||
}
|
||||
handleClose = () => {
|
||||
this.setState({ openSnack: false });
|
||||
|
||||
}
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={this.state.openSnack}
|
||||
autoHideDuration={6000}
|
||||
onClose={this.handleClose}
|
||||
ContentProps={{
|
||||
'aria-describedby': 'message-id',
|
||||
}}
|
||||
message={<span id="message-id">{this.state.title + " : " + this.state.body}</span>}
|
||||
action={[
|
||||
<Link href={this.state.link}><a>Voir</a></Link>,
|
||||
<IconButton
|
||||
key="close"
|
||||
aria-label="Close"
|
||||
color="inherit"
|
||||
className={classes.close}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>,
|
||||
]}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default withStyles(styles)(Messager);
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Header from './Header'
|
||||
import Messager from './Messager'
|
||||
|
||||
const layoutStyle = {
|
||||
root:{
|
||||
'& a':{
|
||||
textDecoration:'none!important',
|
||||
color:'#111'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Layout = (props) => (
|
||||
<div className={props.classes.root} >
|
||||
<Messager/>
|
||||
<Header menu={props.menu} />
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default withStyles(layoutStyle)(Layout);
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardActionArea from '@material-ui/core/CardActionArea';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardMedia from '@material-ui/core/CardMedia';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
|
||||
const styles = theme => ({
|
||||
cardSnack: {
|
||||
maxWidth: 345,
|
||||
},
|
||||
media: {
|
||||
height: 140,
|
||||
}
|
||||
});
|
||||
|
||||
class PWAInstallSnack extends Component {
|
||||
state = {
|
||||
openSnack: false,
|
||||
buttonDisplay: true
|
||||
}
|
||||
|
||||
isAvailable = () =>{
|
||||
console.log(this.buttonDisplay)
|
||||
return this.state.buttonDisplay;
|
||||
}
|
||||
|
||||
initInstall = () =>
|
||||
{
|
||||
this.setState({openSnack: true})
|
||||
}
|
||||
getStateAndHelpers() {
|
||||
return {
|
||||
on: this.state.openSnack,
|
||||
initInstall: this.initInstall,
|
||||
}
|
||||
}
|
||||
iOS() {
|
||||
|
||||
var iDevices = [
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
'iPad',
|
||||
'iPhone',
|
||||
'iPod'
|
||||
];
|
||||
|
||||
if (!!navigator.platform) {
|
||||
while (iDevices.length) {
|
||||
if (navigator.platform === iDevices.pop()){ return true; }
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (window !== undefined) {
|
||||
// don't show if we are in App
|
||||
if (window.navigator.standalone ||
|
||||
window.matchMedia('(display-mode: standalone)').matches)
|
||||
{
|
||||
console.log('standalone')
|
||||
this.setState({ openSnack: false, buttonDisplay: false })
|
||||
}
|
||||
else if(this.IsSafari())
|
||||
{
|
||||
console.log('safari')
|
||||
this.setState({ openSnack: false, buttonDisplay: false })
|
||||
|
||||
if(this.iOS())
|
||||
this.setState({ openSnack: false, buttonDisplay: true })
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//A2HS 'Add 2 Home Screen'
|
||||
PromptA2HS = (event, reason) => {
|
||||
|
||||
this.setState({ openSnack: false })
|
||||
console.log('prompted')
|
||||
if (window !== undefined) {
|
||||
window.promptInstall();
|
||||
}
|
||||
}
|
||||
IsSafari = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
var isSafari = /Safari/.test(window.navigator.userAgent) && /Apple Computer/.test(window.navigator.vendor);
|
||||
return isSafari;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
handleClose = () => {
|
||||
this.setState({ openSnack: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
var safariSnack = (<Card className={classes.cardSnack}>
|
||||
<CardActionArea>
|
||||
<CardMedia
|
||||
className={classes.media}
|
||||
image="/static/images/help/addToHomeFull.png"
|
||||
title="homeScreen"
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="h2">
|
||||
-> les PWA doivent encore s'installer manuellement sous IOS
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button name="compris" size="small" color="secondary" onClick={this.handleClose}>
|
||||
j'ai compris
|
||||
</Button>
|
||||
</CardActions>
|
||||
</CardActionArea>
|
||||
|
||||
</Card>);
|
||||
|
||||
var usualSnack = (<Card className={classes.cardSnack}>
|
||||
<CardActionArea>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="h2">
|
||||
Voulez-vous installer notre application ?
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<CardActions>
|
||||
<Button name='oui' size="small" color="secondary" onClick={this.PromptA2HS}>
|
||||
oui
|
||||
</Button>
|
||||
<Button name='non' size="small" color="secondary" onClick={this.handleClose}>
|
||||
non
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>);
|
||||
|
||||
|
||||
|
||||
return <Fragment>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={this.state.openSnack}
|
||||
onClose={this.handleClose}
|
||||
ContentProps={{
|
||||
'aria-describedby': 'message-id',
|
||||
}}>
|
||||
{this.IsSafari() ? safariSnack : usualSnack}
|
||||
</Snackbar>
|
||||
{this.state.buttonDisplay && this.props.children(this.getStateAndHelpers())}
|
||||
</Fragment>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default withStyles(styles)(PWAInstallSnack)
|
||||
@@ -0,0 +1,80 @@
|
||||
/* global window */
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Transition, animated } from 'react-spring';
|
||||
|
||||
export default class extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.children = React.Children.toArray(props.children);
|
||||
this.state = {
|
||||
count: React.Children.count(props.children),
|
||||
currentIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.animation);
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
this.animation = setInterval(() => {
|
||||
if (window.document.visibilityState === 'hidden') {
|
||||
// tab invisible; pause for one round to avoid flickering
|
||||
this.pauseAnimation = true;
|
||||
return;
|
||||
}
|
||||
if (!this.pauseAnimation) {
|
||||
this.setState({
|
||||
currentIndex: (this.state.currentIndex + 1) % this.state.count
|
||||
});
|
||||
} else {
|
||||
this.pauseAnimation = false;
|
||||
}
|
||||
}, this.props.duration || 1500);
|
||||
}
|
||||
|
||||
render() {
|
||||
const currentIndex = this.state.currentIndex;
|
||||
return (
|
||||
<div>
|
||||
<Transition
|
||||
native
|
||||
keys={currentIndex}
|
||||
initial={null}
|
||||
from={{ opacity: 0, y: -50 }}
|
||||
enter={{ opacity: 1, y: 0 }}
|
||||
leave={{ opacity: 0, y: 60 }}
|
||||
>
|
||||
{(item) => ({opacity, y}) => (
|
||||
<animated.div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: y.interpolate(y => `translate3d(0, ${y}%, 0)`),
|
||||
opacity
|
||||
}}
|
||||
>
|
||||
{this.props.children[currentIndex]}
|
||||
</animated.div>
|
||||
)}
|
||||
</Transition>
|
||||
<style jsx>
|
||||
{`
|
||||
{
|
||||
width: 100%;
|
||||
height: 3.4em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// eslint-disable-next-line prefer-stateless-function
|
||||
const GriddleCustomTableComponent = OriginalComponent => class CustomTableComponent extends React.Component {
|
||||
static contextTypes = {
|
||||
components: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
return <this.context.components.TableBody />
|
||||
}
|
||||
}
|
||||
|
||||
export default GriddleCustomTableComponent
|
||||
@@ -0,0 +1,137 @@
|
||||
//var firebase = require("firebase");
|
||||
import firebase from "firebase/app"
|
||||
import 'firebase/database'
|
||||
import 'firebase/messaging'
|
||||
//based on
|
||||
//https://stackoverflow.com/questions/1479319/simplest-cleanest-way-to-implement-singleton-in-javascript
|
||||
class Singleton {
|
||||
// Properties & Methods
|
||||
messaging;
|
||||
fcmToken;
|
||||
|
||||
constructor() {
|
||||
console.log('firebase-client constructor')
|
||||
if (!Singleton.instance) {
|
||||
Singleton.instance = this
|
||||
}
|
||||
// Initialize object
|
||||
if (!firebase.messaging.isSupported()) {
|
||||
console.log('firebase.messaging not supported')
|
||||
return;
|
||||
}
|
||||
|
||||
let config = {
|
||||
apiKey: "AIzaSyArZab_4hUDgksQ4Dqdwv970JyRSlbGaKY",
|
||||
authDomain: "edeclicpwa.firebaseapp.com",
|
||||
databaseURL: "https://edeclicpwa.firebaseio.com",
|
||||
projectId: "edeclicpwa",
|
||||
storageBucket: "edeclicpwa.appspot.com",
|
||||
messagingSenderId: "130215947970"
|
||||
}
|
||||
firebase.initializeApp(config);
|
||||
|
||||
navigator.serviceWorker
|
||||
.register('/service-worker.js')
|
||||
.then((registration) => {
|
||||
firebase.messaging().useServiceWorker(registration);
|
||||
});
|
||||
this.messaging = firebase.messaging();
|
||||
this.messaging.usePublicVapidKey('BEiMSVYvblP5oKcIY7b90r-5xLk1QYDra1sSdZHdaynihcb3Hx9wdZdI9IUHmbqureB_-IpOFKpm7arBB93J3rk');
|
||||
console.log('FireBase Singleton Initialized')
|
||||
return Singleton.instance
|
||||
}
|
||||
|
||||
isSupported()
|
||||
{
|
||||
this.messaging.app.firebase_.messaging.isSupported()
|
||||
}
|
||||
|
||||
async requestPermission() {
|
||||
console.log('Requesting permission...');
|
||||
|
||||
this.messaging.requestPermission().then(function () {
|
||||
console.log('Notification permission granted.');
|
||||
// TODO(developer): Retrieve an Instance ID token for use with FCM.
|
||||
// [START_EXCLUDE]
|
||||
// In many cases once an app has been granted notification permission, it
|
||||
// should update its UI reflecting this.
|
||||
// resetUI();
|
||||
// [END_EXCLUDE]
|
||||
}).catch(function (err) {
|
||||
console.log('Unable to get permission to notify.', err);
|
||||
});
|
||||
}
|
||||
|
||||
async askForToken() {
|
||||
var res = await this.messaging.getToken().then(async (currentToken) => {
|
||||
if (currentToken) {
|
||||
// console.log(currentToken)
|
||||
// this.fcmToken = currentToken;
|
||||
await this.sendTokenToServer(currentToken);
|
||||
return currentToken;
|
||||
}
|
||||
else {
|
||||
// Show permission request.
|
||||
console.log('No Instance ID token available. Request permission to generate one.');
|
||||
this.requestPermission()
|
||||
// Show permission UI.
|
||||
// updateUIForPushPermissionRequired();
|
||||
await this.setTokenSentToServer(false);
|
||||
}
|
||||
}).catch(async (err) => {
|
||||
console.log('An error occurred while retrieving token. ', err);
|
||||
// showToken('Error retrieving Instance ID token. ', err);
|
||||
await this.setTokenSentToServer(false);
|
||||
})
|
||||
console.log("Token : ")
|
||||
console.log(res);
|
||||
return res
|
||||
}
|
||||
async deleteToken() {
|
||||
// Delete Instance ID token.
|
||||
// [START delete_token]
|
||||
this.messaging.getToken().then(async (currentToken) => {
|
||||
this.messaging.deleteToken(currentToken).then(async () => {
|
||||
console.log('Token deleted.');
|
||||
await this.setTokenSentToServer(false);
|
||||
// [START_EXCLUDE]
|
||||
// Once token is deleted update UI.
|
||||
//resetUI();
|
||||
// [END_EXCLUDE]
|
||||
}).catch((err) => {
|
||||
console.log('Unable to delete token. ', err);
|
||||
});
|
||||
// [END delete_token]
|
||||
}).catch((err) => {
|
||||
console.log('Error retrieving Instance ID token. ', err);
|
||||
//showToken('Error retrieving Instance ID token. ', err);
|
||||
});
|
||||
}
|
||||
async sendTokenToServer(currentToken) {
|
||||
var tokenSent = await this.isTokenSentToServer()
|
||||
if (!tokenSent) {
|
||||
console.log('Sending token to server...');
|
||||
firebase.database().ref('subscription/' + currentToken).set({
|
||||
general: true,
|
||||
pwa: true,
|
||||
boats: false
|
||||
});
|
||||
|
||||
this.setTokenSentToServer(true);
|
||||
} else {
|
||||
console.log('Token already sent to server so won\'t send it again ' +
|
||||
'unless it changes');
|
||||
}
|
||||
}
|
||||
async isTokenSentToServer() {
|
||||
return window.localStorage.getItem('sentToServer') === '1';
|
||||
}
|
||||
async setTokenSentToServer(sent) {
|
||||
window.localStorage.setItem('sentToServer', sent ? '1' : '0');
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new Singleton()
|
||||
Object.freeze(instance)
|
||||
|
||||
export default instance
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import PropTypes from 'prop-types';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
|
||||
import TextRotationNone from '@material-ui/icons/TextRotationNone';
|
||||
import Subtitles from '@material-ui/icons/Subtitles';
|
||||
import LinkIcon from '@material-ui/icons/Link';
|
||||
import ImageIcon from '@material-ui/icons/Image';
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
input: {
|
||||
position: 'relative',
|
||||
},
|
||||
button: {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
class PushParamForm extends Component {
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Fragment>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Créer votre message
|
||||
</Typography>
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
value={this.props.title} InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<TextRotationNone />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
required id="title" label="Titre" fullWidth onChange={(event) => this.props.onSelectedTitle(event.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<TextField value={this.props.body}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Subtitles />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
required id="text" label="Body" fullWidth onChange={(event) => this.props.onSelectedBody(event.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<TextField
|
||||
placeholder="https://"
|
||||
value={this.props.action} InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<LinkIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}} required id="link" label="Action link" fullWidth onChange={(event) => this.props.onSelectedAction(event.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<TextField
|
||||
placeholder="https://"
|
||||
value={this.props.iconLink} InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<ImageIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}} required id="IconLink" label="Icon link" fullWidth onChange={(event) => this.props.onSelectedIconLink(event.target.value)} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<TextField
|
||||
placeholder="https://"
|
||||
value={this.props.imageLink} InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<ImageIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}} required id="ImageLink" label="Image link" fullWidth onChange={(event) => this.props.onSelectedImageLink(event.target.value)} />
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} md={6}>
|
||||
<input
|
||||
accept="image/*"
|
||||
className={classes.input}
|
||||
id="contained-button-file"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
accept="image/x-png,image/gif,image/jpeg"
|
||||
/>
|
||||
<label htmlFor="contained-button-file">
|
||||
<Button variant="contained" component="span" className={classes.button}>
|
||||
Upload picture
|
||||
</Button>
|
||||
</label>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
</Fragment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PushParamForm.propTypes = {
|
||||
onSelectedTitle: PropTypes.func.isRequired,
|
||||
onSelectedBody: PropTypes.func.isRequired,
|
||||
onSelectedAction: PropTypes.func.isRequired,
|
||||
onSelectedImageLink: PropTypes.func.isRequired,
|
||||
onSelectedIconLink: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default withStyles(styles)(PushParamForm);
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const styles = theme => ({
|
||||
title: {
|
||||
position: 'relative',
|
||||
},
|
||||
maxImageSize:
|
||||
{
|
||||
maxWidth:'540px',
|
||||
maxHeight:'180px'
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
class PushReview extends Component {
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<Grid container spacing={16}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Typography variant="h6" gutterBottom className={classes.title}>
|
||||
Prêt à envoyer
|
||||
</Typography> <Typography variant="h6" gutterBottom className={classes.title}>
|
||||
{this.props.target === 'a' ? "A moi" : "A tous les inscrits"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item container direction="column" xs={12} sm={12}>
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="h6" gutterBottom className={classes.title}>
|
||||
{this.props.title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Typography variant="h6" gutterBottom className={classes.title}>
|
||||
{this.props.body}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<Typography variant="h6" gutterBottom className={classes.title}>
|
||||
{this.props.action}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12}>
|
||||
<img src={this.props.iconLink} alt="icon" className={classes.maxImageSize}></img>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<img src={this.props.imageLink} alt="image" className={classes.maxImageSize}></img>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(PushReview);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import { Text } from '@vx/text';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import PropTypes from 'prop-types';
|
||||
import getConfig from 'next/config'
|
||||
|
||||
const {publicRuntimeConfig} = getConfig()
|
||||
const styles = theme => ({
|
||||
title: {
|
||||
position: 'relative',
|
||||
}, formControl: {
|
||||
margin: theme.spacing.unit,
|
||||
minWidth: 120,
|
||||
},
|
||||
selectEmpty: {
|
||||
marginTop: theme.spacing.unit * 2,
|
||||
},
|
||||
})
|
||||
|
||||
class PushTargetForm extends Component {
|
||||
state = {
|
||||
open: false
|
||||
}
|
||||
|
||||
handleChangeSelect = event => {
|
||||
this.props.onSelectedTopic(event.target.value);
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.setState({ open: true });
|
||||
};
|
||||
|
||||
handleChange = event => {
|
||||
|
||||
this.setState({ open: event.target.value === 'c' && this.props.topic === '' });
|
||||
|
||||
this.props.onSelectedTarget(event.target.value);
|
||||
};
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const topics = publicRuntimeConfig.push.topics
|
||||
return (
|
||||
<Fragment>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<p>Choisir une cible</p>
|
||||
</Typography>
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={12} md={12}>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Cible</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="position"
|
||||
name="position"
|
||||
|
||||
onChange={this.handleChange}
|
||||
row
|
||||
>
|
||||
<FormControlLabel
|
||||
value="top"
|
||||
control={<Radio
|
||||
checked={this.props.target === 'a'}
|
||||
onChange={this.handleChange}
|
||||
value="a"
|
||||
name="radio-button-demo"
|
||||
aria-label="A"
|
||||
|
||||
/>}
|
||||
label="A moi"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="start"
|
||||
control={<Radio
|
||||
checked={this.props.target === 'b'}
|
||||
onChange={this.handleChange}
|
||||
value="b"
|
||||
name="radio-button-demo"
|
||||
aria-label="B"
|
||||
|
||||
/>}
|
||||
label="A tous les inscrits"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="start"
|
||||
control={<Radio
|
||||
checked={this.props.target === 'c'}
|
||||
onChange={this.handleChange}
|
||||
value="c"
|
||||
name="radio-button-demo"
|
||||
aria-label="C"
|
||||
|
||||
/>}
|
||||
label="Par sujet"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
<FormControl className={classes.formControl} disabled={!(this.props.target === 'c')}>
|
||||
<InputLabel htmlFor="demo-controlled-open-select">Sujet</InputLabel>
|
||||
<Select
|
||||
open={this.state.open}
|
||||
onClose={this.handleClose}
|
||||
onOpen={this.handleOpen}
|
||||
value={this.props.topic}
|
||||
onChange={this.handleChangeSelect}
|
||||
|
||||
>
|
||||
{topics.map((item)=> {return <MenuItem value={item.key}>{item.key}</MenuItem>})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
PushTargetForm.propTypes = {
|
||||
onSelectedTarget: PropTypes.func.isRequired,
|
||||
onSelectedTopic: PropTypes.func.isRequired,
|
||||
target: PropTypes.string.isRequired,
|
||||
topic: PropTypes.string.isRequired
|
||||
|
||||
}
|
||||
export default withStyles(styles)(PushTargetForm);
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { Row, Col, Form, Input, Label, Button } from 'reactstrap'
|
||||
import Cookies from 'universal-cookie'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
email: '',
|
||||
session: this.props.session,
|
||||
providers: this.props.providers,
|
||||
submitting: false
|
||||
}
|
||||
this.handleSubmit = this.handleSubmit.bind(this)
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this)
|
||||
|
||||
}
|
||||
|
||||
handleEmailChange(event) {
|
||||
this.setState({
|
||||
email: event.target.value.trim()
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
if (!this.state.email) return
|
||||
|
||||
this.setState({
|
||||
submitting: true
|
||||
})
|
||||
|
||||
// Save current URL so user is redirected back here after signing in
|
||||
const cookies = new Cookies()
|
||||
cookies.set('redirect_url', window.location.pathname, { path: '/' })
|
||||
|
||||
NextAuth.signin(this.state.email)
|
||||
.then(() => {
|
||||
Router.push(`/auth/check-email?email=${this.state.email}`)
|
||||
})
|
||||
.catch(err => {
|
||||
Router.push(`/auth/error?action=signin&type=email&email=${this.state.email}`)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return (<div />)
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p className="text-center" style={{ marginTop: 10, marginBottom: 30 }}>{`If you don't have an account, one will be created when you sign in.`}</p>
|
||||
<Row>
|
||||
<Col xs={12} md={6}>
|
||||
<SignInButtons providers={this.props.providers} />
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<Form id="signin" method="post" action="/auth/email/signin" onSubmit={this.handleSubmit}>
|
||||
<Input name="_csrf" type="hidden" value={this.state.session.csrfToken} />
|
||||
<p>
|
||||
<Label htmlFor="email">Email address</Label><br />
|
||||
<Input name="email" disabled={this.state.submitting} type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange} />
|
||||
</p>
|
||||
<p className="text-right">
|
||||
<Button id="submitButton" disabled={this.state.submitting} outline color="dark" type="submit">
|
||||
{this.state.submitting === true && <span className="icon icon-spin ion-md-refresh mr-2" />}
|
||||
Sign in with email
|
||||
</Button>
|
||||
</p>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
|
||||
<Col xs={12} md={6}>
|
||||
<p className="text-center small">
|
||||
<Link href="/auth/credentials"><a>Sign in with credentials</a></Link>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment >
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SignInButtons extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
Object.keys(this.props.providers).map((provider, i) => {
|
||||
if (!this.props.providers[provider].signin) return null
|
||||
|
||||
return (
|
||||
<p key={i}>
|
||||
<a className="btn btn-block btn-outline-secondary" href={this.props.providers[provider].signin}>
|
||||
Sign in with {provider}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
})
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
|
||||
import Layout from '../components/MyLayout.js'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { Spring, animated } from 'react-spring'
|
||||
import { interpolate, interpolateAll, separate } from 'flubber'
|
||||
import { GradientPinkRed as Gradient } from '@vx/gradient'
|
||||
import { Text } from '@vx/text';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
},
|
||||
});
|
||||
|
||||
// 'bin2': 'M192 1024h640l64-704h-768zM640 128v-128h-256v128h-320v192l64-64h768l64 64v-192h-320zM576 128h-128v-64h128v64z',
|
||||
// 'bold': 'M707.88 484.652c37.498-44.542 60.12-102.008 60.12-164.652 0-141.16-114.842-256-256-256h-320v896h384c141.158 0 256-114.842 256-256 0-92.956-49.798-174.496-124.12-219.348zM384 192h101.5c55.968 0 101.5 57.42 101.5 128s-45.532 128-101.5 128h-101.5v-256zM543 832h-159v-256h159c58.45 0 106 57.42 106 128s-47.55 128-106 128z',
|
||||
// 'underline': 'M704 64h128v416c0 159.058-143.268 288-320 288-176.73 0-320-128.942-320-288v-416h128v416c0 40.166 18.238 78.704 51.354 108.506 36.896 33.204 86.846 51.494 140.646 51.494s103.75-18.29 140.646-51.494c33.116-29.802 51.354-68.34 51.354-108.506v-416zM192 832h640v128h-640z',
|
||||
// 'italic': 'M896 64v64h-128l-320 768h128v64h-448v-64h128l320-768h-128v-64z',
|
||||
// 'paragraph-left': 'M0 64h1024v128h-1024zM0 256h640v128h-640zM0 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z',
|
||||
// 'paragraph-center': 'M0 64h1024v128h-1024zM192 256h640v128h-640zM192 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z',
|
||||
// 'paragraph-right': 'M0 64h1024v128h-1024zM384 256h640v128h-640zM384 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z',
|
||||
// 'google': 'M522.2 438.8v175.6h290.4c-11.8 75.4-87.8 220.8-290.4 220.8-174.8 0-317.4-144.8-317.4-323.2s142.6-323.2 317.4-323.2c99.4 0 166 42.4 204 79l139-133.8c-89.2-83.6-204.8-134-343-134-283 0-512 229-512 512s229 512 512 512c295.4 0 491.6-207.8 491.6-500.2 0-33.6-3.6-59.2-8-84.8l-483.6-0.2z',
|
||||
// 'facebook': 'M608 192h160v-192h-160c-123.514 0-224 100.486-224 224v96h-128v192h128v512h192v-512h160l32-192h-192v-96c0-17.346 14.654-32 32-32z',
|
||||
// 'twitter': 'M1024 226.4c-37.6 16.8-78.2 28-120.6 33 43.4-26 76.6-67.2 92.4-116.2-40.6 24-85.6 41.6-133.4 51-38.4-40.8-93-66.2-153.4-66.2-116 0-210 94-210 210 0 16.4 1.8 32.4 5.4 47.8-174.6-8.8-329.4-92.4-433-219.6-18 31-28.4 67.2-28.4 105.6 0 72.8 37 137.2 93.4 174.8-34.4-1-66.8-10.6-95.2-26.2 0 0.8 0 1.8 0 2.6 0 101.8 72.4 186.8 168.6 206-17.6 4.8-36.2 7.4-55.4 7.4-13.6 0-26.6-1.4-39.6-3.8 26.8 83.4 104.4 144.2 196.2 146-72 56.4-162.4 90-261 90-17 0-33.6-1-50.2-3 93.2 59.8 203.6 94.4 322.2 94.4 386.4 0 597.8-320.2 597.8-597.8 0-9.2-0.2-18.2-0.6-27.2 41-29.4 76.6-66.4 104.8-108.6z',
|
||||
// 'linkedin2': 'M384 384h177.106v90.782h2.532c24.64-44.194 84.958-90.782 174.842-90.782 186.946 0 221.52 116.376 221.52 267.734v308.266h-184.61v-273.278c0-65.184-1.334-149.026-96.028-149.026-96.148 0-110.82 70.986-110.82 144.292v278.012h-184.542v-576z M64 384h192v576h-192v-576z M256 224c0 53.019-42.981 96-96 96s-96-42.981-96-96c0-53.019 42.981-96 96-96s96 42.981 96 96z',
|
||||
|
||||
|
||||
class Image extends React.Component {
|
||||
|
||||
//TODO:https://github.com/veltman/flubber
|
||||
// for multi shape interpolation
|
||||
state = {
|
||||
paths: [
|
||||
['M 4.1011306,18.138058 C 2.331559,17.122773 2.6024383,14.859921 4.5707888,14.214339 c 1.1026388,-0.361698 1.9711507,-0.199331 2.8110534,0.525296 0.8690356,0.74987 1.0374015,1.48815 0.5534758,2.427702 -0.6579674,1.277374 -2.4908068,1.741422 -3.8341874,0.970721 z '
|
||||
, 'M 10.631851,15.182299 C 9.1656813,14.751768 8.7340471,13.059324 9.8467661,12.103776 10.527925,11.51888 11.447639,11.385214 12.326604,11.743318 c 0.788932,0.321555 1.190552,0.91555 1.190552,1.76115 0,0.689726 -0.633869,1.48443 -1.318177,1.653143 -0.820425,0.201948 -0.955486,0.204293 -1.567128,0.0247 z '
|
||||
, 'M 4.7521177,13.02856 C 2.9082762,12.471124 2.3313006,10.653967 3.603096,9.4101092 4.695148,8.3419987 6.4288405,8.3355162 7.5124752,9.3942534 8.285595,10.150333 8.3999412,10.893721 7.8808416,11.788709 7.2663072,12.848273 5.9207259,13.381011 4.7521177,13.027878 Z '
|
||||
, 'M 15.388892,12.283788 c -0.58348,-0.202084 -0.968125,-0.775943 -0.968125,-1.444286 0,-1.5622262 2.510542,-2.1126367 3.341497,-0.732767 0.838771,1.392834 -0.662497,2.769942 -2.373372,2.177053 z '
|
||||
, 'M 10.24832,9.9111326 C 9.9120272,9.7166264 9.5352364,9.3847324 9.4110101,9.1740924 8.5785356,7.7613854 10.263526,6.053759 12.003506,6.5467794 c 1.831736,0.5192301 2.068755,2.7702231 0.364795,3.4646396 -0.846384,0.344859 -1.395814,0.318794 -2.119981,-0.1002864 z '
|
||||
, 'M 4.7709634,7.6206842 C 4.0603393,7.4381829 3.2673775,6.7563237 3.0215248,6.116808 2.3948596,4.4861553 4.2885421,2.8190798 6.2516733,3.2729259 7.6353145,3.5929599 8.506468,4.9099156 8.0846798,6.0438273 7.8009787,6.8068061 7.0880867,7.4384634 6.2985231,7.6270307 5.5598051,7.8031859 5.4791529,7.8027691 4.7709634,7.6201555 Z'],
|
||||
['M11.344,5.71c0-0.73,0.074-1.122,1.199-1.122h1.502V1.871h-2.404c-2.886,0-3.903,1.36-3.903,3.646v1.765h-1.8V10h1.8v8.128h3.601V10h2.403l0.32-2.718h-2.724L11.344,5.71z'
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"],
|
||||
['M18.258,3.266c-0.693,0.405-1.46,0.698-2.277,0.857c-0.653-0.686-1.586-1.115-2.618-1.115c-1.98,0-3.586,1.581-3.586,3.53c0,0.276,0.031,0.545,0.092,0.805C6.888,7.195,4.245,5.79,2.476,3.654C2.167,4.176,1.99,4.781,1.99,5.429c0,1.224,0.633,2.305,1.596,2.938C2.999,8.349,2.445,8.19,1.961,7.925C1.96,7.94,1.96,7.954,1.96,7.97c0,1.71,1.237,3.138,2.877,3.462c-0.301,0.08-0.617,0.123-0.945,0.123c-0.23,0-0.456-0.021-0.674-0.062c0.456,1.402,1.781,2.422,3.35,2.451c-1.228,0.947-2.773,1.512-4.454,1.512c-0.291,0-0.575-0.016-0.855-0.049c1.588,1,3.473,1.586,5.498,1.586c6.598,0,10.205-5.379,10.205-10.045c0-0.153-0.003-0.305-0.01-0.456c0.7-0.499,1.308-1.12,1.789-1.827c-0.644,0.28-1.334,0.469-2.06,0.555C17.422,4.782,17.99,4.091,18.258,3.266'
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"],
|
||||
['m 7.9524801,6.8649712 h 4.4013439 v 2.4344997 h 0.06282 c 0.612359,-1.1856702 2.111339,-2.4344997 4.345083,-2.4344997 4.645877,0 5.505101,3.1207251 5.505101,7.1797098 v 8.266623 h -4.587857 v -7.328365 c 0,-1.747966 -0.03217,-3.996275 -2.386436,-3.996275 -2.389421,0 -2.754033,1.903479 -2.754033,3.869364 v 7.455276 H 7.9523576 Z'
|
||||
, 'M 0,6.8649712 H 4.7714878 V 22.311304 H 0 Z '
|
||||
, 'M 4.7714878,2.5743644 c 0,1.4210168 -1.0681544,2.5743638 -2.3857592,2.5743638 C 1.0681544,5.1487282 0,3.9958278 0,2.5743644 0,1.1533476 1.0680012,0 2.3857286,0 3.7033029,0 4.7714878,1.1529004 4.7714878,2.5743644 Z'
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
, "M0,0 L0.01,0 L0.01,0.01Z"
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
links: [
|
||||
'https://www.talentstube.com/entreprise/5-e-declic/',
|
||||
'https://www.facebook.com/edeclic/',
|
||||
'https://twitter.com/edeclic',
|
||||
'https://www.linkedin.com/company/edeclic/',
|
||||
//
|
||||
],
|
||||
|
||||
index: 0
|
||||
}
|
||||
goNext = () => this.setState(state => ({ index: state.index + 1 >= state.paths.length ? 0 : state.index + 1 }))
|
||||
render() {
|
||||
const { paths, links, index } = this.state
|
||||
|
||||
const interpolator = interpolateAll(paths[index], paths[index + 1] || paths[0], { maxSegmentLength: 0.1, single: true })
|
||||
// const interpolatorR = separate(paths[0], paths[2] || paths[0], { single: true })
|
||||
return (
|
||||
<svg width={this.props.svgWidth} viewBox="0 0 22 22" style={{margin: 'auto'}}>
|
||||
<Gradient id="gradient" />
|
||||
<g fill="url(#gradient)">
|
||||
<a href={links[index]} target='_blank'>
|
||||
<Spring reset native from={{ t: 0 }} to={{ t: 1 }} onRest={this.goNext} delay={this.props.delay}>
|
||||
{({ t }) => <animated.path d={t.interpolate(interpolator)} ></animated.path>}
|
||||
|
||||
</Spring>
|
||||
</a>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Image.defaultProps = {
|
||||
delay: '500',
|
||||
svgWidth: '100'
|
||||
}
|
||||
|
||||
Image.propTypes = {
|
||||
delay: PropTypes.string.isRequired,
|
||||
svgWidth: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Image)
|
||||
@@ -0,0 +1,29 @@
|
||||
curl -X POST -H "Authorization: Bearer ya29.c.ElpvBqGmaItXdsYUwpgjzQoZBGzAdmkD1S-iUZBmZ0Yxt_hiCw32TdudGjaXu-nPn3-crDtj0tw54UDk0zgxSa3rSKA82HC-bc9BZDsS-4xh7W6m4BfYsFvI07I" -H "Content-Type: application/json" -d '{
|
||||
"message":{
|
||||
"webpush": {
|
||||
"headers": {
|
||||
"Urgency": "high"
|
||||
},
|
||||
"notification": {
|
||||
"body": "notification web",
|
||||
"requireInteraction": "true",
|
||||
"icon":"http://localhost:8000/static/images/icons/e-declicIco128x128.png",
|
||||
"image": "http://localhost:8000/static/images/icons/e-declicIco128x128.png"
|
||||
},
|
||||
},
|
||||
"token": "f7gCuQGRQ6I:APA91bEGGUKCUBbVlxSFQbL4zL6ishb6MJzmqLEh1Tiwl-gewgl8wDzjBeUv5AJb9kAm8McbWj8EhkBTbIobJkeeUS4weLIQtSxb44NFNYdwDP8ifKaq6AXp-Bx-PZeOvJ5uE5bntkKU"
|
||||
}
|
||||
}' https://fcm.googleapis.com/v1/projects/edeclicpwa/messages:send
|
||||
|
||||
curl -X POST -H "Authorization:key=AAAAHlF5rsI:APA91bHXvjGDJRLzVvqmBDPM1Et96Bw0Iuvc7EGe9wt4a_lSJijDlbZ3RT96seLDK-QM6Mf8qQ4dH6MI9iWt1_d3uzKG2bxr56zltglZ3Z9xU8gz2iJeRGmlOd0YdV43IumIVe6dIOr-" https://iid.googleapis.com/iid/v1/efrEbYvXcR0:APA91bFvWQS57rXVUF68Uz-rGljwYD2WnKCRbOWFSsNyazG6MM08iztjtQZK4BZJPd8XPo0CNecYGcetBjOn6pjl1LSepzCeZucSS4TEpO2PNpl3RNJTVSLIi9J9eU-Dm-HeJbd_Quye/rel/topics/testse -H "Content-Type: application/json"
|
||||
|
||||
|
||||
curl -X POST -H "Authorization:key=AAAAHlF5rsI:APA91bHXvjGDJRLzVvqmBDPM1Et96Bw0Iuvc7EGe9wt4a_lSJijDlbZ3RT96seLDK-QM6Mf8qQ4dH6MI9iWt1_d3uzKG2bxr56zltglZ3Z9xU8gz2iJeRGmlOd0YdV43IumIVe6dIOr-" https://iid.googleapis.com/iid/v1/dWt_RrUJSos:APA91bHLFxW4s2yXpLYeR3mz_WJGQDIdrbO0XkFelSFbZI_146s46iYPQUeAKut_VThfB79P8xmR0ezGR6JOTzRQmi_SBl7q_c-9LUx1gj840Of7N4aaNf_HbyZhjiG3UJTBx8Klftk9/rel/topics/testse -H "Content-Type: application/json"
|
||||
|
||||
|
||||
{"payload":{"notification":{"title":"FCM Message","body":"This is an FCM Message"},"webpush":{"headers":{"Urgency":"high"},"notification":{"body":"This is a message from FCM to web","icon":"http://localhost:8000/static/images/icons/e-declicIco128x128.png","image":"http://localhost:8000/static/images/icons/e-declicIco128x128.png"}}}}
|
||||
|
||||
|
||||
https://us-central1-edeclicpwa.cloudfunctions.net/pushFixedMessage
|
||||
|
||||
curl -X POST -H "Authorization:key=AAAAHlF5rsI:APA91bHXvjGDJRLzVvqmBDPM1Et96Bw0Iuvc7EGe9wt4a_lSJijDlbZ3RT96seLDK-QM6Mf8qQ4dH6MI9iWt1_d3uzKG2bxr56zltglZ3Z9xU8gz2iJeRGmlOd0YdV43IumIVe6dIOr-" https://iid.googleapis.com/iid/info/f7gCuQGRQ6I:APA91bEGGUKCUBbVlxSFQbL4zL6ishb6MJzmqLEh1Tiwl-gewgl8wDzjBeUv5AJb9kAm8McbWj8EhkBTbIobJkeeUS4weLIQtSxb44NFNYdwDP8ifKaq6AXp-Bx-PZeOvJ5uE5bntkKU?details=true -H "Content-Type: application/json"
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,34 @@
|
||||
describe('home', () => {
|
||||
it('should assert that root / is up', () => {
|
||||
cy.visit('http://localhost:8000')
|
||||
// cy.get('#Title')
|
||||
// .should('have.text', 'Nous sommes une agence web');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Conseil', () => {
|
||||
it('should assert that /qui-sommes-nous is up', () => {
|
||||
cy.visit('http://localhost:8000/qui-sommes-nous')
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('solutions', () => {
|
||||
it('should assert that /solutions is up', () => {
|
||||
cy.visit('http://localhost:8000/solutions')
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('blog', () => {
|
||||
it('should assert that /blog is up', () => {
|
||||
cy.visit('http://localhost:8000/blog')
|
||||
});
|
||||
});
|
||||
|
||||
describe('contact', () => {
|
||||
it('should assert that /contact is up', () => {
|
||||
cy.visit('http://localhost:8000/contact')
|
||||
cy.get('a').should('have.attr', 'href', 'http://www.e-declic.com')
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
/**
|
||||
* Welcome to your Workbox-powered service worker!
|
||||
*
|
||||
* You'll need to register this file in your web app and you should
|
||||
* disable HTTP caching for this file too.
|
||||
* See https://goo.gl/nhQhGp
|
||||
*
|
||||
* The rest of the code is auto-generated. Please don't update this file
|
||||
* directly; instead, make changes to your Workbox build configuration
|
||||
* and re-run your build process.
|
||||
* See https://goo.gl/2aRDsh
|
||||
*/
|
||||
|
||||
importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");
|
||||
|
||||
importScripts(
|
||||
"/static/js/firebase-messaging-sw.js",
|
||||
"/static/js/backgroundSync-sw.js"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'pwa',
|
||||
port: 8000,
|
||||
script: 'yarn start',
|
||||
cwd: '/home/app',
|
||||
env: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* next-auth.config.js Example
|
||||
*
|
||||
* Environment variables for this example:
|
||||
*
|
||||
* PORT=3000
|
||||
* SERVER_URL=http://localhost:3000
|
||||
* MONGO_URI=mongodb://localhost:27017/my-database
|
||||
*
|
||||
* If you wish, you can put these in a `.env` to seperate your environment
|
||||
* specific configuration from your code.
|
||||
**/
|
||||
|
||||
// Load environment variables from a .env file if one exists
|
||||
require('dotenv').load()
|
||||
|
||||
const nextAuthProviders = require('./next-auth.providers')
|
||||
const nextAuthFunctions = require('./next-auth.functions')
|
||||
|
||||
// If we want to pass a custom session store then we also need to pass an
|
||||
// instance of Express Session along with it.
|
||||
const expressSession = require('express-session')
|
||||
const MongoStore = require('connect-mongo')(expressSession)
|
||||
|
||||
// If no store set, NextAuth defaults to using Express Sessions in-memory
|
||||
// session store (the fallback is intended as fallback for testing only).
|
||||
let sessionStore
|
||||
if (process.env.MONGO_URI) {
|
||||
sessionStore = new MongoStore({
|
||||
url: process.env.MONGO_URI,
|
||||
autoRemove: 'interval',
|
||||
autoRemoveInterval: 10, // Removes expired sessions every 10 minutes
|
||||
collection: 'sessions',
|
||||
stringify: false
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
// We connect to the User DB before we define our functions.
|
||||
// next-auth.functions.js returns an async method that does that and returns
|
||||
// an object with the functions needed for authentication.
|
||||
return nextAuthFunctions()
|
||||
.then(functions => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// This is the config block we return, ready to be passed to NextAuth
|
||||
resolve({
|
||||
// Define a port (if none passed, will not start Express)
|
||||
// Note: This project omits a port for NextAuth as it uses Express to
|
||||
// add additional routes for the examples, so it takes control of
|
||||
// starting Express, rather than leaving it to NextAuth.
|
||||
// port: process.env.PORT || 3000,
|
||||
// Secret used to encrypt session data on the server.
|
||||
sessionSecret: 'pwacrypretenonsecrettedsecsessi',
|
||||
// Maximum Session Age in ms (optional, default is 7 days).
|
||||
// The expiry time for a session is reset every time a user revisits
|
||||
// the site or revalidates their session token. This is the maximum
|
||||
// idle time value.
|
||||
sessionMaxAge: 60000 * 60 * 24 * 7,
|
||||
// Session Revalidation in X ms (optional, default is 60 seconds).
|
||||
// Specifies how often a Single Page App should revalidate a session.
|
||||
// Does not impact the session life on the server, but causes clients
|
||||
// to refetch session info (even if it is in a local cache) after N
|
||||
// seconds has elapsed since it was last checked so they always display
|
||||
// state correctly.
|
||||
// If set to 0 will revalidate a session before rendering every page.
|
||||
sessionRevalidateAge: 60000,
|
||||
// Canonical URL of the server (optiona, but recommended).
|
||||
// e.g. 'http://localhost:3000' or 'https://www.example.com'
|
||||
// Used in callbak URLs and email sign in links. It will be auto
|
||||
// generated if not specified, which may cause problems if your site
|
||||
// uses multiple aliases (e.g. 'example.com and 'www.examples.com').
|
||||
serverUrl: process.env.SERVER_URL || null,
|
||||
// Add an Express Session store.
|
||||
expressSession: expressSession,
|
||||
sessionStore: sessionStore,
|
||||
// Define oAuth Providers
|
||||
providers: nextAuthProviders(),
|
||||
// Define functions for manging users and sending email.
|
||||
functions: functions,
|
||||
|
||||
csrf:{whitelist:['/account/user']}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* next-auth.functions.js Example
|
||||
*
|
||||
* This file defines functions NextAuth to look up, add and update users.
|
||||
*
|
||||
* It returns a Promise with the functions matching these signatures:
|
||||
*
|
||||
* {
|
||||
* find: ({
|
||||
* id,
|
||||
* email,
|
||||
* emailToken,
|
||||
* provider,
|
||||
* poviderToken
|
||||
* } = {}) => {},
|
||||
* update: (user) => {},
|
||||
* insert: (user) => {},
|
||||
* remove: (id) => {},
|
||||
* serialize: (user) => {},
|
||||
* deserialize: (id) => {}
|
||||
* }
|
||||
*
|
||||
* Each function returns Promise.resolve() - or Promise.reject() on error.
|
||||
*
|
||||
* This specific example supports both MongoDB and NeDB, but can be refactored
|
||||
* to work with any database.
|
||||
*
|
||||
* Environment variables for this example:
|
||||
*
|
||||
* MONGO_URI=mongodb://localhost:27017/my-database
|
||||
* EMAIL_FROM=username@gmail.com
|
||||
* EMAIL_SERVER=smtp.gmail.com
|
||||
* EMAIL_PORT=465
|
||||
* EMAIL_USERNAME=username@gmail.com
|
||||
* EMAIL_PASSWORD=p4ssw0rd
|
||||
*
|
||||
* If you wish, you can put these in a `.env` to seperate your environment
|
||||
* specific configuration from your code.
|
||||
**/
|
||||
|
||||
// Load environment variables from a .env file if one exists
|
||||
require('dotenv').load()
|
||||
|
||||
// This config file uses MongoDB for User accounts, as well as session storage.
|
||||
// This config includes options for NeDB, which it defaults to if no DB URI
|
||||
// is specified. NeDB is an in-memory only database intended here for testing.
|
||||
const MongoClient = require('mongodb').MongoClient
|
||||
const NeDB = require('nedb')
|
||||
const MongoObjectId = (process.env.MONGO_URI) ? require('mongodb').ObjectId : (id) => { return id }
|
||||
|
||||
// Use Node Mailer for email sign in
|
||||
const nodemailer = require('nodemailer')
|
||||
const nodemailerSmtpTransport = require('nodemailer-smtp-transport')
|
||||
const nodemailerDirectTransport = require('nodemailer-direct-transport')
|
||||
|
||||
// Send email direct from localhost if no mail server configured
|
||||
let nodemailerTransport = nodemailerDirectTransport()
|
||||
if (process.env.EMAIL_SERVER && process.env.EMAIL_USERNAME && process.env.EMAIL_PASSWORD) {
|
||||
nodemailerTransport = nodemailerSmtpTransport({
|
||||
host: process.env.EMAIL_SERVER,
|
||||
port: process.env.EMAIL_PORT || 25,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (process.env.MONGO_URI) {
|
||||
// Connect to MongoDB Database and return user connection
|
||||
MongoClient.connect(process.env.MONGO_URI, (err, mongoClient) => {
|
||||
if (err) return reject(err)
|
||||
const dbName = process.env.MONGO_URI.split('/').pop().split('?').shift()
|
||||
const db = mongoClient.db(dbName)
|
||||
return resolve(db.collection('users'))
|
||||
})
|
||||
} else {
|
||||
// If no MongoDB URI string specified, use NeDB, an in-memory work-a-like.
|
||||
// NeDB is not persistant and is intended for testing only.
|
||||
let collection = new NeDB({ autoload: true })
|
||||
collection.loadDatabase(err => {
|
||||
if (err) return reject(err)
|
||||
resolve(collection)
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(usersCollection => {
|
||||
return Promise.resolve({
|
||||
// If a user is not found find() should return null (with no error).
|
||||
find: ({id, email, emailToken, provider} = {}) => {
|
||||
let query = {}
|
||||
|
||||
// Find needs to support looking up a user by ID, Email, Email Token,
|
||||
// and Provider Name + Users ID for that Provider
|
||||
if (id) {
|
||||
query = { _id: MongoObjectId(id) }
|
||||
} else if (email) {
|
||||
query = { email: email }
|
||||
} else if (emailToken) {
|
||||
query = { emailToken: emailToken }
|
||||
} else if (provider) {
|
||||
query = { [`${provider.name}.id`]: provider.id }
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.findOne(query, (err, user) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(user)
|
||||
})
|
||||
})
|
||||
},
|
||||
// The user parameter contains a basic user object to be added to the DB.
|
||||
// The oAuthProfile parameter is passed when signing in via oAuth.
|
||||
//
|
||||
// The optional oAuthProfile parameter contains all properties associated
|
||||
// with the users account on the oAuth service they are signing in with.
|
||||
//
|
||||
// You can use this to capture profile.avatar, profile.location, etc.
|
||||
insert: (user, oAuthProfile) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.insert(user, (err, response) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
// Mongo Client automatically adds an id to an inserted object, but
|
||||
// if using a work-a-like we may need to add it from the response.
|
||||
if (!user._id && response._id) user._id = response._id
|
||||
|
||||
return resolve(user)
|
||||
})
|
||||
})
|
||||
},
|
||||
// The user parameter contains a basic user object to be added to the DB.
|
||||
// The oAuthProfile parameter is passed when signing in via oAuth.
|
||||
//
|
||||
// The optional oAuthProfile parameter contains all properties associated
|
||||
// with the users account on the oAuth service they are signing in with.
|
||||
//
|
||||
// You can use this to capture profile.avatar, profile.location, etc.
|
||||
update: (user, profile) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.update({_id: MongoObjectId(user._id)}, user, {}, (err) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(user)
|
||||
})
|
||||
})
|
||||
},
|
||||
// The remove parameter is passed the ID of a user account to delete.
|
||||
//
|
||||
// This method is not used in the current version of next-auth but will
|
||||
// be in a future release, to provide an endpoint for account deletion.
|
||||
remove: (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.remove({_id: MongoObjectId(id)}, (err) => {
|
||||
if (err) return reject(err)
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
},
|
||||
// Seralize turns the value of the ID key from a User object
|
||||
serialize: (user) => {
|
||||
// Supports serialization from Mongo Object *and* deserialize() object
|
||||
if (user.id) {
|
||||
// Handle responses from deserialize()
|
||||
return Promise.resolve(user.id)
|
||||
} else if (user._id) {
|
||||
// Handle responses from find(), insert(), update()
|
||||
return Promise.resolve(user._id)
|
||||
} else {
|
||||
return Promise.reject(new Error("Unable to serialise user"))
|
||||
}
|
||||
},
|
||||
// Deseralize turns a User ID into a normalized User object that is
|
||||
// exported to clients. It should not return private/sensitive fields,
|
||||
// only fields you want to expose via the user interface.
|
||||
deserialize: (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
usersCollection.findOne({ _id: MongoObjectId(id) }, (err, user) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
// If user not found (e.g. account deleted) return null object
|
||||
if (!user) return resolve(null)
|
||||
|
||||
return resolve({
|
||||
id: user._id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
admin: user.admin || false
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
// Define method for sending links for signing in over email.
|
||||
sendSignInEmail: ({
|
||||
email = null,
|
||||
url = null
|
||||
} = {}) => {
|
||||
nodemailer
|
||||
.createTransport(nodemailerTransport)
|
||||
.sendMail({
|
||||
to: email,
|
||||
from: process.env.EMAIL_FROM,
|
||||
subject: 'Sign in link',
|
||||
text: `Use the link below to sign in:\n\n${url}\n\n`,
|
||||
html: `<p>Use the link below to sign in:</p><p>${url}</p>`
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending email to ' + email, err)
|
||||
}
|
||||
})
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Generated sign in link ' + url + ' for ' + email)
|
||||
}
|
||||
},
|
||||
|
||||
// Credentials Sign In
|
||||
//
|
||||
// If you use this you will need to define your own way to validate
|
||||
// credentials. Unlike with oAuth or Email Sign In, accounts are not
|
||||
// created automatically so you will need to provide a way to create them.
|
||||
//
|
||||
// This feature is intended for strategies like Two Factor Authentication.
|
||||
//
|
||||
// To disable this option, do not set signin (or set it to null).
|
||||
/*
|
||||
*/
|
||||
signIn: ({form, req}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Should validate credentials (e.g. hash password, compare 2FA token
|
||||
// etc) and return a valid user object from a database.
|
||||
return usersCollection.findOne({
|
||||
email: form.email
|
||||
}, (err, user) => {
|
||||
if (err) return reject(err)
|
||||
if (!user) return resolve(null)
|
||||
|
||||
// Check credentials - e.g. compare bcrypt password hashes
|
||||
if (form.password === "test1234") {
|
||||
// If valid, return user object - e.g. { id, name, email }
|
||||
return resolve(user)
|
||||
} else {
|
||||
// If invalid, return null
|
||||
return resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
// Session Object (optional)
|
||||
//
|
||||
// The session object that gets returned to the client. You don't need to
|
||||
// specify this function here unless you want to override or extend the
|
||||
// default (e.g. with any other properties you have added to req.session)
|
||||
//
|
||||
// Note: The object returned will be stored in localStorage and visible
|
||||
// client side so do not return data you would not want the user to see.
|
||||
/*
|
||||
session: (session, req) => {
|
||||
if (req.session && req.session.someCustomProperty)
|
||||
session.someCustomProperty = req.session.someCustomProperty
|
||||
|
||||
session.someOtherCustomProperty = "Example custom property"
|
||||
|
||||
return session
|
||||
}
|
||||
*/
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* next-auth.providers.js Example
|
||||
*
|
||||
* This file returns a simple array of oAuth Provider objects for NextAuth.
|
||||
*
|
||||
* This example returns an array based on what environment variables are set,
|
||||
* with explicit support for Facebook, Google and Twitter, but it can be used
|
||||
* to add strategies for other oAuth providers.
|
||||
*
|
||||
* Environment variables for this example:
|
||||
*
|
||||
* FACEBOOK_ID=
|
||||
* FACEBOOK_SECRET=
|
||||
* GOOGLE_ID=
|
||||
* GOOGLE_SECRET=
|
||||
* TWITTER_KEY=
|
||||
* TWITTER_SECRET=
|
||||
*
|
||||
* If you wish, you can put these in a `.env` to seperate your environment
|
||||
* specific configuration from your code.
|
||||
**/
|
||||
|
||||
// Load environment variables from a .env file if one exists
|
||||
require('dotenv').load()
|
||||
|
||||
module.exports = () => {
|
||||
let providers = []
|
||||
|
||||
if (process.env.FACEBOOK_ID && process.env.FACEBOOK_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Facebook',
|
||||
providerOptions: {
|
||||
scope: ['email', 'public_profile']
|
||||
},
|
||||
Strategy: require('passport-facebook').Strategy,
|
||||
strategyOptions: {
|
||||
clientID: process.env.FACEBOOK_ID,
|
||||
clientSecret: process.env.FACEBOOK_SECRET,
|
||||
profileFields: ['id', 'displayName', 'email', 'link']
|
||||
},
|
||||
getProfile(profile) {
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile._json.email
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (process.env.LINKEDIN_ID && process.env.LINKEDIN_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Linkedin',
|
||||
providerOptions: {
|
||||
scope: ['r_emailaddress', 'r_basicprofile']
|
||||
},
|
||||
Strategy: require('passport-linkedin-oauth2').Strategy,
|
||||
strategyOptions: {
|
||||
clientID: process.env.LINKEDIN_ID,
|
||||
clientSecret: process.env.LINKEDIN_SECRET,
|
||||
profileFields: ['r_emailaddress', 'r_basicprofile']
|
||||
},
|
||||
getProfile(profile) {
|
||||
console.log(profile);
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
// name: profile.displayName,
|
||||
// email: profile._json.email
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (process.env.GOOGLE_ID && process.env.GOOGLE_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Google',
|
||||
providerOptions: {
|
||||
scope: ['profile', 'email']
|
||||
},
|
||||
Strategy: require('passport-google-oauth').OAuth2Strategy,
|
||||
strategyOptions: {
|
||||
clientID: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET
|
||||
},
|
||||
getProfile(profile) {
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.emails[0].value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Twitter doesn't expose emails by default.
|
||||
* If we don't get one NextAuth will create a placeholder in the form
|
||||
* `{provider}-{account-id}@localhost.localdomain`
|
||||
*
|
||||
* To have your Twitter oAuth return emails go to apps.twitter.com and add
|
||||
* links to your Terms and Conditions and Privacy Policy under the "Settings"
|
||||
* tab, then check the "Request email addresses" from users box under the
|
||||
* "Permissions" tab.
|
||||
**/
|
||||
if (process.env.TWITTER_KEY && process.env.TWITTER_SECRET) {
|
||||
providers.push({
|
||||
providerName: 'Twitter',
|
||||
providerOptions: {
|
||||
scope: []
|
||||
},
|
||||
Strategy: require('passport-twitter').Strategy,
|
||||
strategyOptions: {
|
||||
consumerKey: process.env.TWITTER_KEY,
|
||||
consumerSecret: process.env.TWITTER_SECRET,
|
||||
userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true'
|
||||
},
|
||||
getProfile(profile) {
|
||||
// Normalize profile into one with {id, name, email} keys
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: (profile.emails && profile.emails[0].value) ? profile.emails[0].value : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// next.config.js
|
||||
const fetch = require('isomorphic-unfetch')
|
||||
const withPlugins = require('next-compose-plugins');
|
||||
|
||||
const withOffline = require('next-offline')
|
||||
|
||||
const optimizedImages = require('next-optimized-images')
|
||||
|
||||
const { ANALYZE, DEV } = process.env
|
||||
|
||||
module.exports = withPlugins(
|
||||
[
|
||||
[optimizedImages, {
|
||||
/* config for next-optimized-images */
|
||||
}],
|
||||
|
||||
[withOffline, {
|
||||
dontAutoRegisterSw: DEV ? true : false,
|
||||
workboxOpts: {
|
||||
globPatterns: ['static/**/*'],
|
||||
globDirectory: '.',
|
||||
runtimeCaching: [
|
||||
{ urlPattern: /.*auth.*/, handler: 'networkOnly' },
|
||||
{ urlPattern: /.*account.*/, handler: 'networkOnly' },
|
||||
{ urlPattern: /^https?.*/, handler: 'networkFirst' },
|
||||
{ urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
|
||||
handler: "cacheFirst",
|
||||
options: {
|
||||
cacheName: "images",
|
||||
expiration: {
|
||||
maxEntries: 100
|
||||
}
|
||||
}}
|
||||
],
|
||||
importScripts:
|
||||
['/static/js/firebase-messaging-sw.js',
|
||||
'/static/js/backgroundSync-sw.js'
|
||||
]
|
||||
},
|
||||
|
||||
}],
|
||||
[{
|
||||
// serverRuntimeConfig: { // Will only be available on the server side
|
||||
// // mySecret: 'secret',
|
||||
// // secondSecret: process.env.SECOND_SECRET // Pass through env variables
|
||||
// },
|
||||
publicRuntimeConfig: { // Will be available on both server and client
|
||||
staticFolder: '/static',
|
||||
push: {
|
||||
'defaultAction': 'https://application.bzh/' ,
|
||||
'defaultIconLink': 'https://application.bzh/static/images/icons/e-declicIco152x152.png' ,
|
||||
'defaultImageLink': 'https://application.bzh/static/images/banner/atc-site-web-responsive.jpg',
|
||||
'topics':[
|
||||
{ key: 'e-declic', value: false },
|
||||
{ key: 'PWA', value: false }
|
||||
]
|
||||
},
|
||||
apiUrl:{
|
||||
'token':'https://poc-api.ag2l.fr:50000/token/',
|
||||
'Qtbl_Itv_Sav': 'https://poc-api.ag2l.fr:50000/rest/server.Qtbl_Itv_Sav'
|
||||
},
|
||||
ReCAPTCHA:{
|
||||
"siteKey":""
|
||||
},
|
||||
eDeclicUrl:'http://www.e-declic.com',
|
||||
}}]
|
||||
|
||||
],
|
||||
{
|
||||
webpack: (config, { isServer }) => {
|
||||
if (ANALYZE) {
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
|
||||
|
||||
config.plugins.push(new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'server',
|
||||
analyzerPort: isServer ? 8888 : 8889,
|
||||
openAnalyzer: true
|
||||
}))
|
||||
}
|
||||
|
||||
config.module.rules.push(
|
||||
{
|
||||
test: /\.md$/,
|
||||
use: 'raw-loader'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return config
|
||||
}
|
||||
},
|
||||
{
|
||||
generateBuildId: async () => {
|
||||
// For example get the latest git commit hash here
|
||||
|
||||
return 'v0.1.0'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
);
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "WP-next",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^3.2.0",
|
||||
"@material-ui/icons": "^3.0.1",
|
||||
"@material-ui/styles": "^3.0.0-alpha.8",
|
||||
"@material/animation": "^0.40.1",
|
||||
"@vx/gradient": "^0.0.165",
|
||||
"@vx/text": "^0.0.179",
|
||||
"axios": "^0.18.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"connect-mongo": "^2.0.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"cypress": "^3.1.0",
|
||||
"data-prefetch-link": "^1.1.3",
|
||||
"dotenv": "^6.1.0",
|
||||
"enzyme": "^3.8.0",
|
||||
"express": "^4.16.4",
|
||||
"firebase": "^5.7.0",
|
||||
"flubber": "^0.4.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"googleapis": "^35.0.0",
|
||||
"griddle": "^0.1.2",
|
||||
"griddle-react": "^1.13.1",
|
||||
"he": "^1.2.0",
|
||||
"isomorphic-unfetch": "^3.0.0",
|
||||
"jss": "^9.8.7",
|
||||
"jss-nested": "^6.0.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lscache": "^1.3.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mongodb": "^3.1.10",
|
||||
"nedb": "^1.8.0",
|
||||
"next": "^7.0.2",
|
||||
"next-auth": "^1.12.1",
|
||||
"next-compose-plugins": "^2.1.1",
|
||||
"next-offline": "^3.2.2",
|
||||
"next-optimized-images": "^1.4.1",
|
||||
"nodemailer": "4.1.0",
|
||||
"nodemailer-direct-transport": "^3.3.2",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"now": "^11.4.6",
|
||||
"passport": "^0.4.0",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-linkedin-oauth2": "^1.5.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.8.0-alpha.1",
|
||||
"react-async-component": "^2.0.0",
|
||||
"react-cookies": "^0.1.0",
|
||||
"react-dom": "^16.8.0-alpha.1",
|
||||
"react-ga": "^2.5.6",
|
||||
"react-google-recaptcha": "^1.0.4",
|
||||
"react-intl": "^2.8.0",
|
||||
"react-jss": "^8.6.1",
|
||||
"react-markdown": "^4.0.3",
|
||||
"react-measure": "^2.2.2",
|
||||
"react-redux": "^6.0.0",
|
||||
"react-spring": "^6.1.7",
|
||||
"reactstrap": "^7.0.2",
|
||||
"start-server-and-test": "^1.7.5",
|
||||
"universal-cookie": "^3.0.7"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env PORT=3000 DEV=1 node server.js",
|
||||
"bs": "yarn build && yarn start",
|
||||
"pm2-prod": "yarn build && pm2 start --name 'next' -- start",
|
||||
"build": "next build && yarn copysw",
|
||||
"start": "cross-env PORT=8000 NODE_ENV=production node server.js",
|
||||
"test:cypress": "start-server-and-test http://localhost:8000 cypress",
|
||||
"cypress": "cypress run",
|
||||
"analyze": "cross-env ANALYZE=1 next build",
|
||||
"copysw": "cp -R static/js .next/static/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-bundle-analyzer": "^3.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import App, { Container } from 'next/app';
|
||||
import { MuiThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import JssProvider from 'react-jss/lib/JssProvider';
|
||||
import getPageContext from '../src/getPageContext';
|
||||
|
||||
import Router from 'next/router'
|
||||
import getConfig from 'next/config'
|
||||
import Head from 'next/head'
|
||||
|
||||
import { initGA, logPageView } from '../utils/analytics'
|
||||
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
|
||||
function tokenIsValid(tokenExpireString) {
|
||||
if (!!tokenExpireString)
|
||||
return true
|
||||
let tokenExpireDate = new Date(tokenExpireString)
|
||||
|
||||
return (tokenExpireDate.getTime() > Date().now.getTime())
|
||||
}
|
||||
|
||||
class MyApp extends App {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.pageContext = getPageContext();
|
||||
}
|
||||
|
||||
pageContext = null;
|
||||
|
||||
static async getInitialProps({ Component, ctx }) {
|
||||
let err = false;
|
||||
|
||||
let pageProps = {}
|
||||
|
||||
if (Component.getInitialProps) {
|
||||
pageProps = await Component.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
return { pageProps: { ...pageProps } }
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Remove the server-side injected CSS.
|
||||
const jssStyles = document.querySelector('#jss-server-side');
|
||||
if (jssStyles && jssStyles.parentNode) {
|
||||
jssStyles.parentNode.removeChild(jssStyles);
|
||||
}
|
||||
if (process.env.DEV !== 1) {
|
||||
initGA()
|
||||
logPageView()
|
||||
Router.router.events.on('routeChangeComplete', logPageView)
|
||||
}
|
||||
|
||||
|
||||
if (!!localStorage.AG2LToken
|
||||
|| (!!localStorage.AG2LTokenExpireDateTime && tokenIsValid(localStorage.AG2LTokenExpireDateTime))) {
|
||||
const res = await fetch(publicRuntimeConfig.apiUrl.token,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: 'username=AG2L&grant_type=password&password=AG2L'
|
||||
})
|
||||
|
||||
if(res.ok == true)
|
||||
{
|
||||
const data = await res.json()
|
||||
|
||||
localStorage.AG2LToken = data.access_token
|
||||
localStorage.AG2LTokenExpireDateTime = new Date(Date.now() + data.expires_in).getTime()
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: + trycatch
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (process.env.DEV !== 1) {
|
||||
Router.router.events.off('routeChangeComplete', logPageView);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { Component, pageProps, menu } = this.props;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Head>
|
||||
<title>e-declic</title>
|
||||
</Head>
|
||||
{/* Wrap every page in Jss and Theme providers */}
|
||||
<JssProvider
|
||||
registry={this.pageContext.sheetsRegistry}
|
||||
generateClassName={this.pageContext.generateClassName}
|
||||
>
|
||||
{/* MuiThemeProvider makes the theme available down the React
|
||||
tree thanks to React context. */}
|
||||
<MuiThemeProvider
|
||||
theme={this.pageContext.theme}
|
||||
sheetsManager={this.pageContext.sheetsManager}
|
||||
>
|
||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||
<CssBaseline />
|
||||
{/* Pass pageContext to the _document though the renderPage enhancer
|
||||
to render collected styles on server side.
|
||||
messagingSingleton={messagingSingleton} */}
|
||||
<Component pageContext={this.pageContext} menu={menu} {...pageProps} />
|
||||
</MuiThemeProvider>
|
||||
</JssProvider>
|
||||
<noscript>This is a modern pwa site. You must allow javascript !</noscript>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Document, { Head, Main, NextScript } from 'next/document';
|
||||
import flush from 'styled-jsx/server';
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
const { pageContext } = this.props;
|
||||
|
||||
return (
|
||||
<html lang="fr" dir="ltr">
|
||||
<Head>
|
||||
<meta charSet="utf-8" />
|
||||
{/* TODO: A enlever avant MEP
|
||||
<meta name="robots" content="noindex, nofollow"></meta>
|
||||
*/}
|
||||
<meta name="google-site-verification" content="aR4mfPRusAD1JYt9Fu59tm-XWl3IAwHI-UKlwSTBdv8" />
|
||||
{/* Use minimum-scale=1 to enable GPU rasterization */}
|
||||
<meta name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
||||
/>
|
||||
{/* PWA primary color */}
|
||||
<meta name="theme-color" content={pageContext.theme.palette.primary.main} />
|
||||
{/* <link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"
|
||||
/> */}
|
||||
<link rel="manifest" href="/static/manifest.json" />
|
||||
<link rel='shortcut icon' type='image/x-icon' href='/static/favicon.ico' />
|
||||
|
||||
<script type="text/javascript" src="/static/js/pwaInstaller.js" ></script>
|
||||
|
||||
<link rel="mask-icon" href="/static/images/icons/e-declicIco.svg" color="#5bbad5" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/icons/e-declicIco32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/icons/e-declicIco16x16.png" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="apple-touch-icon" href="/static/images/icons/e-declicIco57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/icons/e-declicIco152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/static/images/icons/e-declicIco167x167.png"/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/icons/e-declicIco180x180.png" />
|
||||
|
||||
<link href="/static/images/splashscreen/2048x2732.png" sizes="2048x2732" rel="apple-touch-startup-image" />
|
||||
<link href="/static/images/splashscreen/1668x2224.png" sizes="1668x2224" rel="apple-touch-startup-image" />
|
||||
<link href="/static/images/splashscreen/1536x2048.png" sizes="1536x2048" rel="apple-touch-startup-image" />
|
||||
<link href="/static/images/splashscreen/1125x2436.png" sizes="1125x2436" rel="apple-touch-startup-image" />
|
||||
<link href="/static/images/splashscreen/1242x2208.png" sizes="1242x2208" rel="apple-touch-startup-image" />
|
||||
<link href="/static/images/splashscreen/750x1334.png" sizes="750x1334" rel="apple-touch-startup-image" />
|
||||
<link href="/static/images/splashscreen/640x1136.png" sizes="640x1136" rel="apple-touch-startup-image" />
|
||||
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
|
||||
{/* <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"/> */}
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyDocument.getInitialProps = ctx => {
|
||||
|
||||
|
||||
|
||||
// Resolution order
|
||||
//
|
||||
// On the server:
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. document.getInitialProps
|
||||
// 4. app.render
|
||||
// 5. page.render
|
||||
// 6. document.render
|
||||
//
|
||||
// On the server with error:
|
||||
// 1. document.getInitialProps
|
||||
// 2. app.render
|
||||
// 3. page.render
|
||||
// 4. document.render
|
||||
//
|
||||
// On the client
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. app.render
|
||||
// 4. page.render
|
||||
|
||||
// Render app and page and get the context of the page with collected side effects.
|
||||
let pageContext;
|
||||
const page = ctx.renderPage(Component => {
|
||||
const WrappedComponent = props => {
|
||||
pageContext = props.pageContext;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
WrappedComponent.propTypes = {
|
||||
pageContext: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
});
|
||||
|
||||
return {
|
||||
...page,
|
||||
pageContext,
|
||||
// Styles fragment is rendered after the app and page rendering finish.
|
||||
styles: (
|
||||
<React.Fragment>
|
||||
<style
|
||||
id="jss-server-side"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: pageContext.sheetsRegistry.toString() }}
|
||||
/>
|
||||
{flush() || null}
|
||||
</React.Fragment>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export default MyDocument;
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Creating a page named _error.js lets you override HTTP error messages
|
||||
*/
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { withRouter } from 'next/router'
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
},
|
||||
});
|
||||
class ErrorPage extends React.Component {
|
||||
|
||||
static propTypes() {
|
||||
return {
|
||||
errorCode: React.PropTypes.number.isRequired,
|
||||
url: React.PropTypes.string.isRequired
|
||||
}
|
||||
}
|
||||
|
||||
static getInitialProps({res, xhr}) {
|
||||
const errorCode = res ? res.statusCode : (xhr ? xhr.status : null)
|
||||
return {errorCode}
|
||||
}
|
||||
|
||||
render() {
|
||||
var response
|
||||
switch (this.props.errorCode) {
|
||||
case 200: // Also display a 404 if someone requests /_error explicitly
|
||||
case 404:
|
||||
response = (
|
||||
<div>
|
||||
<div>
|
||||
<h1 >Page Not Found</h1>
|
||||
<p>La page <strong>{ this.props.url }</strong> n'existe pas.</p>
|
||||
<p><Link href="/"><a>Retour au site</a></Link></p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
case 500:
|
||||
response = (
|
||||
<div>
|
||||
<div >
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>An internal server error occurred.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
default:
|
||||
response = (
|
||||
<div>
|
||||
<div >
|
||||
<h1 >HTTP { this.props.errorCode } Error</h1>
|
||||
<p>
|
||||
An <strong>HTTP { this.props.errorCode }</strong> error occurred while
|
||||
trying to access <strong>{ this.props.router.pathname }</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(withStyles(styles)(ErrorPage))
|
||||
@@ -0,0 +1,269 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import fetch from 'isomorphic-fetch'
|
||||
import { Row, Col, Form, FormGroup, Label, Input, Button } from 'reactstrap'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
import Layout from '../components/MyLayout'
|
||||
import Cookies from 'universal-cookie'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
state={
|
||||
session: {},
|
||||
isSignedIn: false,
|
||||
name: '',
|
||||
email: '',
|
||||
emailVerified: false,
|
||||
alertText: null,
|
||||
alertStyle: null
|
||||
}
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
let props = {};
|
||||
props.session = await NextAuth.init({req})
|
||||
props.linkedAccounts = await NextAuth.linked({req})
|
||||
return props
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
session: props.session,
|
||||
isSignedIn: (props.session.user) ? true : false,
|
||||
name: '',
|
||||
email: '',
|
||||
emailVerified: false,
|
||||
alertText: null,
|
||||
alertStyle: null
|
||||
}
|
||||
if (props.session.user) {
|
||||
this.state.name = props.session.user.name
|
||||
this.state.email = props.session.user.email
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this)
|
||||
this.onSubmit = this.onSubmit.bind(this)
|
||||
this.handleSignoutSubmit = this.handleSignoutSubmit.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const session = await NextAuth.init({force: true})
|
||||
this.setState({
|
||||
session: session,
|
||||
isSignedIn: (session.user) ? true : false
|
||||
})
|
||||
|
||||
// If the user bounces off to link/unlink their account we want them to
|
||||
// land back here after signing in with the other service / unlinking.
|
||||
const cookies = new Cookies()
|
||||
cookies.set('redirect_url', window.location.pathname, { path: '/' })
|
||||
|
||||
this.getProfile()
|
||||
}
|
||||
|
||||
getProfile() {
|
||||
fetch('/account/user', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(user => {
|
||||
if (!user.name || !user.email) return
|
||||
this.setState({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
[event.target.name]: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
async onSubmit(e) {
|
||||
// Submits the URL encoded form without causing a page reload
|
||||
e.preventDefault()
|
||||
|
||||
this.setState({
|
||||
alertText: null,
|
||||
alertStyle: null
|
||||
})
|
||||
|
||||
const formData = {
|
||||
_csrf: await NextAuth.csrfToken(),
|
||||
//_csrf:this.state.session.csrfToken,
|
||||
name: this.state.name || '',
|
||||
email: this.state.email || ''
|
||||
}
|
||||
|
||||
// URL encode form
|
||||
// Note: This uses a x-www-form-urlencoded rather than sending JSON so that
|
||||
// the form also in browsers without JavaScript
|
||||
const encodedForm = Object.keys(formData).map((key) => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
|
||||
fetch('/account/user', {
|
||||
credentials: 'include',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: encodedForm
|
||||
})
|
||||
.then(async res => {
|
||||
if (res.status === 200) {
|
||||
this.getProfile()
|
||||
this.setState({
|
||||
alertText: 'Changes to your profile have been saved',
|
||||
alertStyle: 'alert-success',
|
||||
})
|
||||
// Force update session so that changes to name or email are reflected
|
||||
// immediately in the navbar (as we pass our session to it).
|
||||
this.setState({
|
||||
session: await NextAuth.init({force: true}), // Update session data
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
session: await NextAuth.init({force: true}), // Update session data
|
||||
alertText: 'Failed to save changes to your profile',
|
||||
alertStyle: 'alert-danger',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async handleSignoutSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
// Save current URL so user is redirected back here after signing out
|
||||
const cookies = new Cookies()
|
||||
cookies.set('redirect_url', window.location.pathname, { path: '/' })
|
||||
|
||||
await NextAuth.signout()
|
||||
Router.push('/')
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.isSignedIn === true) {
|
||||
const alert = (this.state.alertText === null) ? <div/> : <div className={`alert ${this.state.alertStyle}`} role="alert">{this.state.alertText}</div>
|
||||
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
<Row className="mb-1">
|
||||
<Col xs="12">
|
||||
<h1 className="display-2">Your Account</h1>
|
||||
<p className="lead text-muted">
|
||||
Edit your profile and link accounts
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{alert}
|
||||
<Row className="mt-4">
|
||||
<Col xs="12" md="8" lg="9">
|
||||
<Form method="post" action="/account/user" onSubmit={this.onSubmit}>
|
||||
<Input name="_csrf" type="hidden" value={this.state.session.csrfToken} onChange={()=>{}}/>
|
||||
<FormGroup row>
|
||||
<Label sm={2}>Name:</Label>
|
||||
<Col sm={10} md={8}>
|
||||
<Input name="name" value={this.state.name} onChange={this.handleChange}/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label sm={2}>Email:</Label>
|
||||
<Col sm={10} md={8}>
|
||||
<Input name="email" value={(this.state.email.match(/.*@localhost\.localdomain$/)) ? '' : this.state.email} onChange={this.handleChange}/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Col sm={12} md={10}>
|
||||
<p className="text-right">
|
||||
<Button color="primary" type="submit">Save Changes</Button>
|
||||
</p>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
<Form id="signout" method="post" action="/auth/signout" onSubmit={this.handleSignoutSubmit}>
|
||||
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
|
||||
<Button type="submit" block className="pl-4 rounded-0 text-left dropdown-item"><span className="icon ion-md-log-out mr-1"></span> Sign out</Button>
|
||||
</Form>
|
||||
</Col>
|
||||
<Col xs="12" md="4" lg="3">
|
||||
<LinkAccounts
|
||||
session={this.props.session}
|
||||
linkedAccounts={this.props.linkedAccounts}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<h2>Delete your account</h2>
|
||||
<p>
|
||||
If you delete your account it will be erased immediately.
|
||||
You can sign up again at any time.
|
||||
</p>
|
||||
<Form id="signout" method="post" action="/account/delete">
|
||||
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
|
||||
<Button type="submit" color="outline-danger"><span className="icon ion-md-trash mr-1"></span> Delete Account</Button>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Layout {...this.props} navmenu={false}>
|
||||
<Row>
|
||||
<Col xs="12" className="text-center pt-5 pb-5">
|
||||
<p className="lead m-0">
|
||||
<Link href="/auth"><a>Sign in to manage your profile</a></Link>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkAccounts extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{
|
||||
Object.keys(this.props.linkedAccounts).map((provider, i) => {
|
||||
return <LinkAccount key={i} provider={provider} session={this.props.session} linked={this.props.linkedAccounts[provider]}/>
|
||||
})
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkAccount extends React.Component {
|
||||
render() {
|
||||
if (this.props.linked === true) {
|
||||
return (
|
||||
<form method="post" action={`/auth/oauth/${this.props.provider.toLowerCase()}/unlink`}>
|
||||
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
|
||||
<p>
|
||||
<button className="btn btn-block btn-outline-danger" type="submit">
|
||||
Unlink from {this.props.provider}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<p>
|
||||
<a className="btn btn-block btn-outline-primary" href={`/auth/oauth/${this.props.provider.toLowerCase()}`}>
|
||||
Link with {this.props.provider}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import Router from 'next/router'
|
||||
import Cookies from 'universal-cookie'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
const session = await NextAuth.init({force: true, req: req})
|
||||
|
||||
const cookies = new Cookies((req && req.headers.cookie) ? req.headers.cookie : null)
|
||||
|
||||
// If the user is signed in, we look for a redirect URL cookie and send
|
||||
// them to that page, so that people signing in end up back on the page they
|
||||
// were on before signing in. Defaults to '/'.
|
||||
let redirectTo = '/'
|
||||
if (session.user) {
|
||||
// Read redirect URL to redirect to from cookies
|
||||
redirectTo = cookies.get('redirect_url') || redirectTo
|
||||
|
||||
// Allow relative paths only - strip protocol/host/port if they exist.
|
||||
redirectTo = redirectTo.replace( /^[a-zA-Z]{3,5}\:\/{2}[a-zA-Z0-9_.:-]+\//, '')
|
||||
}
|
||||
|
||||
return {
|
||||
session: session,
|
||||
redirectTo: redirectTo
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Get latest session data after rendering on client *then* redirect.
|
||||
// The ensures client state is always updated after signing in or out.
|
||||
// (That's why we use a callback page)
|
||||
const session = await NextAuth.init({force: true})
|
||||
Router.push(this.props.redirectTo || '/')
|
||||
}
|
||||
|
||||
render() {
|
||||
// Provide a link for clients without JavaScript as a fallback.
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"/>
|
||||
</Head>
|
||||
<a href={this.props.redirectTo}>
|
||||
|
||||
</a>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
|
||||
import Layout from '../../components/MyLayout'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req, res, query}) {
|
||||
let props = {};
|
||||
props.session = await NextAuth.init({force: true, req: req})
|
||||
|
||||
// If signed in already, instead of displaying message send to callback page
|
||||
// which should redirect them to whatever page it normally sends clients to
|
||||
if (props.session.user) {
|
||||
if (req) {
|
||||
res.redirect('/auth/callback')
|
||||
} else {
|
||||
Router.push('/auth/callback')
|
||||
}
|
||||
}
|
||||
|
||||
props.email = query.email
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
<div className="text-center pt-5 pb-5">
|
||||
<h1 className="display-4">Check your email</h1>
|
||||
<p className="lead">
|
||||
A sign in link has been sent to { (this.props.email) ? <span className="font-weight-bold">{this.props.email}</span> : <span>your inbox</span> }.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req}) {
|
||||
return {
|
||||
session: await NextAuth.init({req}),
|
||||
linkedAccounts: await NextAuth.linked({req}),
|
||||
providers: await NextAuth.providers({req})
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
email: '',
|
||||
password: '',
|
||||
session: this.props.session
|
||||
}
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this)
|
||||
this.handlePasswordChange = this.handlePasswordChange.bind(this)
|
||||
this.handleSignInSubmit = this.handleSignInSubmit.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.session.user) {
|
||||
Router.push(`/auth/`)
|
||||
}
|
||||
}
|
||||
|
||||
handleEmailChange(event) {
|
||||
this.setState({
|
||||
email: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handlePasswordChange(event) {
|
||||
this.setState({
|
||||
password: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
handleSignInSubmit(event) {
|
||||
event.preventDefault()
|
||||
|
||||
// An object passed NextAuth.signin will be passed to your signin() function
|
||||
NextAuth.signin({
|
||||
email: this.state.email,
|
||||
password: this.state.password
|
||||
})
|
||||
.then(authenticated => {
|
||||
Router.push(`/auth/callback`)
|
||||
})
|
||||
.catch(() => {
|
||||
alert("Authentication failed.")
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return null
|
||||
} else {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="text-center">
|
||||
<h1 className="display-5 mt-4 mb-2">PWA - Custom Sign In</h1>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-12 col-md-10 col-lg-8 col-xl-7 mr-auto ml-auto">
|
||||
|
||||
<div className="card mt-3 mb-3">
|
||||
<h4 className="card-header">Sign In</h4>
|
||||
<div className="card-body pb-0">
|
||||
<form id="signin" method="post" action="/auth/signin" onSubmit={this.handleSignInSubmit}>
|
||||
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
|
||||
<p>
|
||||
<label htmlFor="email">Email address</label><br/>
|
||||
<input name="email" type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange}/>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="password">Password</label><br/>
|
||||
<input name="password" type="password" placeholder="" id="password" className="form-control" value={this.state.password} onChange={this.handlePasswordChange}/>
|
||||
</p>
|
||||
<p className="text-right">
|
||||
<button id="submitButton" type="submit" className="btn btn-outline-primary">Sign in</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center">
|
||||
<Link href="/auth"><a>Back</a></Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import Layout from '../../components/MyLayout'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req, query}) {
|
||||
let props = {};
|
||||
props.action = query.action || null
|
||||
props.type = query.type || null
|
||||
props.service = query.service || null
|
||||
return props
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.action == 'signin' && this.props.type == 'oauth') {
|
||||
return(
|
||||
<Layout menu={this.props.menu}>
|
||||
<div className="text-center mb-5">
|
||||
<h1 className="display-4 mt-5 mb-3">Unable to sign in</h1>
|
||||
<p className="lead">An account associated with your email address already exists.</p>
|
||||
<p className="lead"><Link href="/auth"><a>Sign in with email or another service</a></Link></p>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-8 mr-auto ml-auto mb-5">
|
||||
<div className="text-muted">
|
||||
<h4 className="mb-2">Why am I seeing this?</h4>
|
||||
<p className="mb-2">
|
||||
It looks like you might have already signed up using another service.
|
||||
</p>
|
||||
<p className="mb-3">
|
||||
To protect your account, if you have perviously signed up
|
||||
using another service you must link accounts before you
|
||||
can use a different service to sign in.
|
||||
</p>
|
||||
<h4 className="mb-2">How do I fix this?</h4>
|
||||
<p className="mb-0">
|
||||
To sign in using another service, first sign in using your email address then link accounts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
} else if (this.props.action == 'signin' && this.props.type == 'token-invalid') {
|
||||
return(
|
||||
<Layout menu={this.props.menu}>
|
||||
<div className="text-center mb-5">
|
||||
<h1 className="display-4 mt-5 mb-2">Link not valid</h1>
|
||||
<p className="lead">This sign in link is no longer valid.</p>
|
||||
<p className="lead"><Link href="/auth"><a>Get a new sign in link</a></Link></p>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
} else {
|
||||
return(
|
||||
<Layout menu={this.props.menu}>
|
||||
<div className="text-center mb-5">
|
||||
<h1 className="display-4 mt-5">Error signing in</h1>
|
||||
<p className="lead">An error occured while trying to sign in.</p>
|
||||
<p className="lead"><Link href="/auth"><a>Sign in with email or another service</a></Link></p>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react'
|
||||
import Head from 'next/head'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { Row, Col } from 'reactstrap'
|
||||
import Cookies from 'universal-cookie'
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
|
||||
import Layout from '../../components/MyLayout'
|
||||
import SignIn from '../../components/signin'
|
||||
|
||||
export default class extends React.Component {
|
||||
|
||||
static async getInitialProps({req, res, query}) {
|
||||
let props = {};
|
||||
props.session = await NextAuth.init({force: true, req: req})
|
||||
props.providers = await NextAuth.providers({req})
|
||||
|
||||
// If signed in already, redirect to account management page.
|
||||
if (props.session.user) {
|
||||
if (req) {
|
||||
res.redirect('/account')
|
||||
} else {
|
||||
Router.push('/account')
|
||||
}
|
||||
}
|
||||
|
||||
// If passed a redirect parameter, save it as a cookie
|
||||
if (query.redirect) {
|
||||
const cookies = new Cookies((req && req.headers.cookie) ? req.headers.cookie : null)
|
||||
cookies.set('redirect_url', query.redirect, { path: '/' })
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.session.user) {
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
<p className="lead text-center mt-5 mb-5">
|
||||
<Link href="/auth"><a>Manage your profile</a></Link>
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
<h1 className="text-center display-4 mt-5">Sign up / Sign in</h1>
|
||||
<Row className="mb-5">
|
||||
<Col lg="8" className="mr-auto ml-auto" style={{marginBottom: 20}}>
|
||||
<SignIn session={this.props.session} providers={this.props.providers}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import Layout from '../components/MyLayout.js'
|
||||
import axios from 'axios'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import green from '@material-ui/core/colors/green';
|
||||
import orange from '@material-ui/core/colors/orange';
|
||||
import SnackbarContent from '@material-ui/core/SnackbarContent';
|
||||
import { Transition, config } from 'react-spring'
|
||||
import getConfig from 'next/config'
|
||||
import Head from 'next/head'
|
||||
|
||||
const {publicRuntimeConfig} = getConfig()
|
||||
|
||||
//variable for client side only import
|
||||
let back = ''
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
margin: theme.spacing.unit,
|
||||
maxWidth: '400px', [theme.breakpoints.down('md')]: {
|
||||
maxWidth: `${100 - (theme.spacing.unit)}vw`
|
||||
},
|
||||
|
||||
},
|
||||
wrapper: {
|
||||
maxWidth: '400px',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
maxWidth: `${100 - (theme.spacing.unit)}vw`
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
'& form':{
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: `${100 - (theme.spacing.unit)}vw`
|
||||
},
|
||||
padding: theme.spacing.unit * 2,
|
||||
|
||||
display:'flex',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
website: {
|
||||
width: 400,
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: `${100 - (theme.spacing.unit)}vw`
|
||||
},
|
||||
},
|
||||
textField: {
|
||||
|
||||
width: '100%'
|
||||
},
|
||||
button: {
|
||||
|
||||
margin: theme.spacing.unit,
|
||||
alignSelf:'flex-end'
|
||||
},
|
||||
link: {
|
||||
|
||||
cursor: 'pointer'
|
||||
},
|
||||
success: {
|
||||
backgroundColor: green[600],
|
||||
},
|
||||
error: {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: orange[600],
|
||||
},
|
||||
|
||||
spock: {
|
||||
display: 'inline'
|
||||
},
|
||||
errorCaptcha: {
|
||||
border: '2px',
|
||||
borderColor: 'red',
|
||||
borderStyle: 'solid'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let spring = { ...config.default, precision: 0.1 }
|
||||
const recaptchaRef = React.createRef();
|
||||
class Contact extends React.Component {
|
||||
state = {
|
||||
nom:"",
|
||||
prenom:"",
|
||||
mail:"",
|
||||
tel:"",
|
||||
message: "",
|
||||
checked: false,
|
||||
openSnack: false,
|
||||
loading: false,
|
||||
error: false,
|
||||
warning: false,
|
||||
success: false,
|
||||
snackMessage: ""
|
||||
};
|
||||
|
||||
handleMessage = event => {
|
||||
this.setState({ success: false, message: event.target.value })
|
||||
}
|
||||
handleField = e => {
|
||||
this.setState({ success: false, [e.target.name]: e.target.value })
|
||||
}
|
||||
sendMessage = (recaptchaValue) => {
|
||||
|
||||
axios.post("/feedback", {
|
||||
nom:this.state.nom,
|
||||
prenom:this.state.prenom,
|
||||
tel:this.state.tel,
|
||||
mail:this.state.mail,
|
||||
message: this.state.message,
|
||||
recaptchaValue: recaptchaValue
|
||||
}).then(response => {
|
||||
this.setState({ success: true, error: false, warning: false, message: '', snackMessage: "Votre message à bien été envoyé.", loading: false, openSnack: true })
|
||||
}).catch(error => {
|
||||
if (error.status != 500) {
|
||||
if (process.browser) {
|
||||
// client-side-only code
|
||||
back.saveEventDataLocally([{ message: this.state.message, recaptchaValue }]);
|
||||
}
|
||||
this.setState({ success: false, error: false, warning: true,
|
||||
snackMessage: "📴 Votre message n'a pas pu aboutir, mais a été sauvegardé. Nous l'enverrons lorsque nous aurons une meilleure connectivité.",
|
||||
loading: false, openSnack: true })
|
||||
}
|
||||
else
|
||||
this.setState({ success: false, error: true, warning: false,
|
||||
snackMessage: "Une erreur système est survenue.",
|
||||
loading: false, openSnack: true })
|
||||
});
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ openSnack: false });
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
const recaptchaValue = recaptchaRef.current.getValue();
|
||||
if (this.state.mail === '' || this.state.message === '' || recaptchaValue === '')
|
||||
this.setState({ success: false, error: true, warning: false,
|
||||
snackMessage: "Veuillez entrer les champs necessaires et valider le Captcha.",
|
||||
loading: false, openSnack: true })
|
||||
else
|
||||
this.setState({ success: false, error: false, warning: false, loading: true }, this.sendMessage(recaptchaValue))
|
||||
}
|
||||
config = (item, state) => (state === 'leave' ? [{ duration: 1000 }, spring] : [{ duration: 5000 }, spring])
|
||||
|
||||
async componentDidMount() {
|
||||
//loading lib for client side only
|
||||
let result = await import("../src/backSync.js")
|
||||
back = result.default
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
<Head>
|
||||
<title>Devis Développement PWA</title>
|
||||
<meta name="description" content="Vous souhaitez créer une PWA, développer des fonctionnalités à votre site web et améliorer l'expérience utilisateur sur votre site internet? Contactez-nous pour parler de votre projet." />
|
||||
<link rel="canonical" href="https://www.application.bzh/contact"/>
|
||||
</Head>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.wrapper}>
|
||||
<Slide direction="right" in={true} mountOnEnter unmountOnExit timeout={800}>
|
||||
<Paper elevation={4} className={classes.paper}>
|
||||
{this.state.loading && (
|
||||
<div className="alert alert-info">
|
||||
<CircularProgress color="secondary" />Validating your inputs...
|
||||
</div>
|
||||
)}
|
||||
{!this.state.loading &&
|
||||
(<form onSubmit={this.handleSubmit}>
|
||||
<TextField
|
||||
id="prenom"
|
||||
name="prenom"
|
||||
label="Prénom"
|
||||
value={this.state.prenom}
|
||||
onChange={this.handleField}
|
||||
className={classes.textField}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
id="nom"
|
||||
name="nom"
|
||||
label="Nom"
|
||||
value={this.state.nom}
|
||||
onChange={this.handleField}
|
||||
className={classes.textField}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
id="mail"
|
||||
name="mail"
|
||||
label="Email*"
|
||||
value={this.state.mail}
|
||||
onChange={this.handleField}
|
||||
className={classes.textField}
|
||||
margin="normal"
|
||||
error={this.state.error}
|
||||
/>
|
||||
<TextField
|
||||
id="tel"
|
||||
name="tel"
|
||||
label="Tel"
|
||||
value={this.state.tel}
|
||||
onChange={this.handleField}
|
||||
className={classes.textField}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
id="standard-multiline-flexible"
|
||||
label="Message*"
|
||||
multiline
|
||||
rowsMax={10}
|
||||
rows={4}
|
||||
value={this.state.message}
|
||||
onChange={this.handleMessage}
|
||||
className={classes.textField}
|
||||
margin="normal"
|
||||
error={this.state.error}
|
||||
/>
|
||||
<div className={this.state.error ? classes.errorCaptcha : ''}>
|
||||
<ReCAPTCHA
|
||||
id="component-outlined"
|
||||
ref={recaptchaRef}
|
||||
sitekey={publicRuntimeConfig.ReCAPTCHA.siteKey}
|
||||
size="normal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<Button type="submit" variant="outlined" color="secondary" className={classes.button}>
|
||||
Send
|
||||
</Button>
|
||||
<Transition
|
||||
config={this.config}
|
||||
items={this.state.success}
|
||||
from={{ opacity: 0 }}
|
||||
enter={{ opacity: 1 }}
|
||||
leave={{ opacity: 0 }}>
|
||||
{show =>
|
||||
show && (props => <div style={props} className={classes.spock}>🖖</div>)
|
||||
}
|
||||
</Transition>
|
||||
</form>)
|
||||
}
|
||||
</Paper>
|
||||
</Slide>
|
||||
</div>
|
||||
<br></br>
|
||||
<div>
|
||||
<Paper elevation={4} className={classes.website}>
|
||||
<a href={publicRuntimeConfig.eDeclicUrl} >
|
||||
<Typography variant="subtitle1" color="secondary" className={classes.link} >
|
||||
www.e-declic.com
|
||||
</Typography>
|
||||
</a>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={this.state.openSnack}
|
||||
onClose={this.handleClose}
|
||||
autoHideDuration={6000}
|
||||
TransitionComponent={(props)=><Slide {...props} direction="left" />}
|
||||
ContentProps={{
|
||||
'aria-describedby': 'message-id',
|
||||
}}
|
||||
>
|
||||
<SnackbarContent
|
||||
aria-describedby="message-id"
|
||||
className={classes[this.state.error ? 'error' : this.state.warning ? 'warning' : 'success']}
|
||||
message={<span id="message-id">{this.state.snackMessage}</span>}
|
||||
/>
|
||||
</Snackbar>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Contact)
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import Layout from '../components/MyLayout.js'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Head from 'next/head'
|
||||
import _ from 'lodash'
|
||||
import getConfig from 'next/config'
|
||||
import FormatListNumbered from '@material-ui/icons/FormatListNumbered'
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardActionArea from '@material-ui/core/CardActionArea';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
margin: theme.spacing.unit,
|
||||
height: 250,
|
||||
width: 400
|
||||
},
|
||||
|
||||
icon: {
|
||||
fontSize: '64px',
|
||||
margin: 'auto',
|
||||
textAlign: 'center'
|
||||
|
||||
},
|
||||
innerCard:{
|
||||
display:'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
text: {
|
||||
justifyContent: 'center',
|
||||
textAlign:'center'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class Index extends React.Component {
|
||||
state = {
|
||||
};
|
||||
|
||||
|
||||
async componentDidMount() {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, menu } = this.props;
|
||||
return (
|
||||
<Layout menu={menu}>
|
||||
<Head>
|
||||
<title>AG2L SAV</title>
|
||||
<meta name="description" content="" />
|
||||
<link rel="canonical" href="https://pwa-boiler.bzh" />
|
||||
</Head>
|
||||
|
||||
<div className={classes.root}>
|
||||
|
||||
|
||||
<Card className={classes.card}>
|
||||
<Link href={"listeSAV"} prefetch><a>
|
||||
<CardActionArea >
|
||||
<CardContent>
|
||||
<div className={classes.innerCard + ' '+classes.icon}>
|
||||
|
||||
<FormatListNumbered className={classes.icon} />
|
||||
<Typography variant='h3' className={classes.text}>Interventions</Typography>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardActionArea></a>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
export default withStyles(styles)(Index)
|
||||
@@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
import Layout from '../components/MyLayout.js'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Head from 'next/head'
|
||||
import _ from 'lodash'
|
||||
import getConfig from 'next/config'
|
||||
import Intervention from '../components/AG2L/Intervention'
|
||||
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
margin: theme.spacing.unit,
|
||||
height: 250,
|
||||
width: 400
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
|
||||
class ListeSAV extends React.Component {
|
||||
state = {
|
||||
data: []
|
||||
};
|
||||
|
||||
|
||||
async componentWillMount() {
|
||||
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
||||
if (!!localStorage.AG2LToken) {
|
||||
const res = await fetch(publicRuntimeConfig.apiUrl.Qtbl_Itv_Sav + '?range=0-1',
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': 'Bearer ' + localStorage.AG2LToken
|
||||
}
|
||||
})
|
||||
|
||||
if (res.ok == true) {
|
||||
res.headers.forEach(function (val, key) { console.log(key + ' -> ' + val); });
|
||||
const data = await res.json()
|
||||
console.log(data)
|
||||
this.setState({ data: data["server.Qtbl_Itv_Sav"] })
|
||||
}
|
||||
else {
|
||||
//TODO: + trycatch
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onGetPage = (pageNumber, pageSize) => {
|
||||
let from = pageNumber * pageSize;
|
||||
let to = from + pageSize
|
||||
let requestSuffix = "?range=" + from + '-' + to
|
||||
return fetch(publicRuntimeConfig.apiUrl.Qtbl_Itv_Sav + requestSuffix,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': 'Bearer ' + localStorage.AG2LToken
|
||||
}
|
||||
}).then(
|
||||
(res) => {
|
||||
if (res.ok == true) {
|
||||
res.headers.forEach(function (val, key) { console.log(key + ' -> ' + val); });
|
||||
res.json().then(
|
||||
(j) => {return j}
|
||||
)
|
||||
|
||||
}
|
||||
else {
|
||||
//TODO: + trycatch
|
||||
return null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, menu } = this.props;
|
||||
const { data } = this.state;
|
||||
return (
|
||||
<Layout menu={menu}>
|
||||
<Head>
|
||||
<title>AG2L SAV</title>
|
||||
<meta name="description" content="" />
|
||||
<link rel="canonical" href="https://AG2L-pwa.bzh" />
|
||||
</Head>
|
||||
|
||||
<div className={classes.root}>
|
||||
<Intervention data={data}
|
||||
_onGetPage={this.onGetPage}></Intervention>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
export default withStyles(styles)(ListeSAV)
|
||||
@@ -0,0 +1,279 @@
|
||||
import React, { Component } from 'react'
|
||||
import { withStyles, getMuiTheme } from '@material-ui/core/styles';
|
||||
import Layout from '../components/MyLayout.js'
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import axios from 'axios'
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
import SnackbarContent from '@material-ui/core/SnackbarContent';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import getConfig from 'next/config'
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
|
||||
var EdeclicLib=null
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
//variable for client side only import
|
||||
let back = ''
|
||||
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
margin: theme.spacing.unit,
|
||||
|
||||
},
|
||||
paper: {
|
||||
width: 400,
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: `${100 - (theme.spacing.unit)}vw`
|
||||
},
|
||||
padding: theme.spacing.unit * 2
|
||||
},
|
||||
colorSwitchBase: {
|
||||
color: theme.palette.primary,
|
||||
'&$colorChecked': {
|
||||
color: theme.palette.primary,
|
||||
'& + $colorBar': {
|
||||
backgroundColor: theme.palette.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
colorBar: {},
|
||||
colorChecked: {},
|
||||
bandeau: {
|
||||
maxHeight: '5vh',
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.secondary,
|
||||
margin: theme.spacing.unit * 2
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
class parametres extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
|
||||
}
|
||||
state = {
|
||||
general: false,
|
||||
topics: publicRuntimeConfig.push.topics,
|
||||
loading: false,
|
||||
openSnack: false,
|
||||
error: false,
|
||||
warning: false,
|
||||
success: false,
|
||||
snackMessage: "",
|
||||
isSafari: false
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
|
||||
EdeclicLib = (await import('../components/firebase-client.js')).default
|
||||
if (!EdeclicLib.messaging)
|
||||
return
|
||||
if (EdeclicLib.isSupported() === false) {
|
||||
this.setState({ isSafari: true });
|
||||
return;
|
||||
}
|
||||
|
||||
EdeclicLib.askForToken().then((token) => {
|
||||
console.log("componentDidMount token received :");
|
||||
console.log(token);
|
||||
const url = "/push/info/" + token
|
||||
axios.get(url).then(response => {
|
||||
this.refreshCheckboxes(!!token, response.data.topics)
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
if (error.status != 500) {
|
||||
if (process.browser) {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log("error out" + error)
|
||||
if (error.status != 500) {
|
||||
if (process.browser) {
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
refreshCheckboxes(hasToken, topics) {
|
||||
//general
|
||||
this.setState({ general: hasToken })
|
||||
|
||||
//all topics
|
||||
if (!!topics) {
|
||||
const topicsTemp = cloneDeep(this.state.topics)
|
||||
topicsTemp.map(temp =>
|
||||
temp.value = !!topics[temp.key]
|
||||
|
||||
)
|
||||
this.setState({ topics: topicsTemp })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async sendPushTopicSubscription(topic) {
|
||||
var token = await EdeclicLib.askForToken();
|
||||
|
||||
const url = "/push/subscribe"
|
||||
axios.post(url, {
|
||||
token, topic
|
||||
}).then(response => {
|
||||
this.setState({ success: true, error: false, warning: false, message: '', snackMessage: "Le topic a bien été ajouté.", loading: false, openSnack: true })
|
||||
}).catch(error => {
|
||||
if (error.status != 500) {
|
||||
if (process.browser) {
|
||||
// client-side-only code
|
||||
// back.saveEventDataLocally([{ message: this.state.message }]);
|
||||
}
|
||||
}
|
||||
this.setState({ success: false, error: false, warning: true, snackMessage: "📴 Votre souscription au topic n'a pas pu aboutir, mais a été sauvegardé. Nous l'enverrons lorsque nous aurons une meilleure connectivité.", loading: false, openSnack: true })
|
||||
});
|
||||
}
|
||||
async sendPushTopicUnSubscription(topic) {
|
||||
var token = await EdeclicLib.askForToken();
|
||||
|
||||
const url = "/push/unsubscribe"
|
||||
axios.post(url, {
|
||||
token, topic
|
||||
}).then(response => {
|
||||
this.setState({ success: true, error: false, warning: false, message: '', snackMessage: "Le topic a bien été retiré.", loading: false, openSnack: true })
|
||||
}).catch(error => {
|
||||
if (error.status != 500) {
|
||||
if (process.browser) {
|
||||
// client-side-only code
|
||||
//back.saveEventDataLocally([{ message: this.state.message }]);
|
||||
}
|
||||
}
|
||||
this.setState({ success: false, error: false, warning: true, snackMessage: "📴 Votre désengagement au topic n'a pas pu aboutir, mais a été notifié. Nous l'enverrons lorsque nous aurons une meilleure connectivité.", loading: false, openSnack: true })
|
||||
});
|
||||
}
|
||||
handleChange = name => event => {
|
||||
|
||||
if (name === 'general') {
|
||||
console.log('general')
|
||||
if (event.target.checked === true) {
|
||||
console.log('event.target.checked === true')
|
||||
|
||||
if (!process.browser) return
|
||||
EdeclicLib.askForToken();
|
||||
console.log('token asked')
|
||||
this.setState({ general: true })
|
||||
}
|
||||
else {
|
||||
console.log('')
|
||||
|
||||
if (!process.browser) return
|
||||
EdeclicLib.deleteToken();
|
||||
let topicsTemp = cloneDeep(this.state.topics)
|
||||
topicsTemp.map(a => a.value = false)
|
||||
this.setState({ topics: topicsTemp, general: false })
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (event.target.checked === true) {
|
||||
this.sendPushTopicSubscription(name)
|
||||
}
|
||||
else {
|
||||
this.sendPushTopicUnSubscription(name)
|
||||
}
|
||||
|
||||
let topicsTemp = cloneDeep(this.state.topics)
|
||||
topicsTemp.map(temp => {
|
||||
if (temp.key === name)
|
||||
temp.value = event.target.checked
|
||||
})
|
||||
|
||||
this.setState({ topics: topicsTemp, general: true })
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ openSnack: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.bandeau} hidden={!this.state.isSafari}>
|
||||
<Typography variant="h6">🚧 Votre navigateur ne permet pas la reception de notification. 🚧</Typography>
|
||||
</div>
|
||||
<Paper elevation={4} className={classes.paper}>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Je souhaites recevoir des notifications : </FormLabel>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={this.state.isSafari}
|
||||
checked={this.state.general}
|
||||
onChange={this.handleChange('general')}
|
||||
value="general"
|
||||
classes={{
|
||||
switchBase: classes.colorSwitchBase,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Générales"
|
||||
/>
|
||||
|
||||
{this.state.topics.map(ar =>
|
||||
|
||||
<FormControlLabel key={ar.key}
|
||||
control={
|
||||
<Switch
|
||||
disabled={this.state.isSafari}
|
||||
checked={ar.value}
|
||||
onChange={this.handleChange(ar.key)}
|
||||
value={ar.key}
|
||||
/>
|
||||
}
|
||||
label={`Sur ${ar.key}`}
|
||||
/>, this)}
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
</Paper>
|
||||
</div>
|
||||
|
||||
<Snackbar
|
||||
autoHideDuration={4000}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={this.state.openSnack}
|
||||
onClose={this.handleClose}
|
||||
ContentProps={{
|
||||
'aria-describedby': 'message-id',
|
||||
}}
|
||||
>
|
||||
<SnackbarContent
|
||||
|
||||
aria-describedby="message-id"
|
||||
className={classes[this.state.error ? 'error' : this.state.warning ? 'warning' : 'success']}
|
||||
message={<span id="message-id">{this.state.snackMessage}</span>}
|
||||
/></Snackbar>
|
||||
</Layout >
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(parametres)
|
||||
@@ -0,0 +1,362 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { NextAuth } from 'next-auth/client'
|
||||
import Layout from '../components/MyLayout.js'
|
||||
import withStyles from '@material-ui/core/styles/withStyles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Stepper from '@material-ui/core/Stepper';
|
||||
import Step from '@material-ui/core/Step';
|
||||
import StepLabel from '@material-ui/core/StepLabel';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import PushParamForm from '../components/push/PushParamForm';
|
||||
import PushReview from '../components/push/PushReview';
|
||||
import PushTargetForm from '../components/push/PushTargetForm';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import axios from 'axios'
|
||||
import getConfig from 'next/config'
|
||||
import green from '@material-ui/core/colors/green';
|
||||
import orange from '@material-ui/core/colors/orange';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
import SnackbarContent from '@material-ui/core/SnackbarContent';
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import Cookies from 'universal-cookie'
|
||||
import Router from 'next/router'
|
||||
|
||||
const styles = theme => ({
|
||||
appBar: {
|
||||
position: 'relative',
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing.unit * 2,
|
||||
marginRight: theme.spacing.unit * 2,
|
||||
[theme.breakpoints.up(600 + theme.spacing.unit * 2 * 2)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
padding: theme.spacing.unit * 2,
|
||||
[theme.breakpoints.up(600 + theme.spacing.unit * 3 * 2)]: {
|
||||
marginTop: theme.spacing.unit * 6,
|
||||
marginBottom: theme.spacing.unit * 6,
|
||||
padding: theme.spacing.unit * 3,
|
||||
},
|
||||
},
|
||||
stepper: {
|
||||
padding: `${theme.spacing.unit * 3}px 0 ${theme.spacing.unit * 5}px`,
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
button: {
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
marginLeft: theme.spacing.unit,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
let back = ''
|
||||
const steps = ['Parameters', 'Target', 'Review'];
|
||||
|
||||
class PushCreator extends React.Component {
|
||||
state = {
|
||||
session: this.props.session,
|
||||
isSignedIn: (this.props.session.user) ? true : false,
|
||||
activeStep: 0,
|
||||
title: '',
|
||||
body: '',
|
||||
action: publicRuntimeConfig.push.defaultAction,
|
||||
iconLink: publicRuntimeConfig.push.defaultIconLink,
|
||||
imageLink: publicRuntimeConfig.push.defaultImageLink,
|
||||
target: 'a',
|
||||
topic: '',
|
||||
loading: false,
|
||||
openSnack: false,
|
||||
error: false,
|
||||
warning: false,
|
||||
success: false,
|
||||
snackMessage: ""
|
||||
};
|
||||
|
||||
static async getInitialProps({ req }) {
|
||||
let props = {};
|
||||
|
||||
props.session = await NextAuth.init({ req })
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
async componentDidMount() {
|
||||
|
||||
}
|
||||
|
||||
getStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return <PushParamForm
|
||||
onSelectedTitle={this.setTitle}
|
||||
onSelectedBody={this.setBody}
|
||||
onSelectedAction={this.setAction}
|
||||
onSelectedImageLink={this.setImageLink}
|
||||
onSelectedIconLink={this.setIconLink}
|
||||
title={this.state.title}
|
||||
body={this.state.body}
|
||||
action={this.state.action}
|
||||
imageLink={this.state.imageLink}
|
||||
iconLink={this.state.iconLink}
|
||||
/>;
|
||||
case 1:
|
||||
return <PushTargetForm
|
||||
onSelectedTarget={this.setTarget}
|
||||
onSelectedTopic={this.setTopic}
|
||||
target={this.state.target}
|
||||
topic={this.state.topic} />;
|
||||
case 2:
|
||||
return <PushReview
|
||||
title={this.state.title}
|
||||
body={this.state.body}
|
||||
action={this.state.action}
|
||||
imageLink={this.state.imageLink}
|
||||
iconLink={this.state.iconLink}
|
||||
target={this.state.target} />;
|
||||
default:
|
||||
throw new Error('Unknown step');
|
||||
}
|
||||
}
|
||||
|
||||
setTitle = (title) => {
|
||||
this.setState(state => ({
|
||||
title
|
||||
}));
|
||||
}
|
||||
|
||||
setBody = (body) => {
|
||||
this.setState(state => ({
|
||||
body
|
||||
}));
|
||||
}
|
||||
|
||||
setAction = (action) => {
|
||||
this.setState(state => ({
|
||||
action
|
||||
}));
|
||||
}
|
||||
setImageLink = (imageLink) => {
|
||||
this.setState(state => ({
|
||||
imageLink
|
||||
}));
|
||||
}
|
||||
setIconLink = (iconLink) => {
|
||||
this.setState(state => ({
|
||||
iconLink
|
||||
}));
|
||||
}
|
||||
setTarget = (target) => {
|
||||
this.setState(state => ({
|
||||
target
|
||||
}));
|
||||
}
|
||||
setTopic = (topic) => {
|
||||
this.setState(state => ({
|
||||
topic
|
||||
}));
|
||||
}
|
||||
async componentDidMount() {
|
||||
//loading lib for client side only
|
||||
let result = await import("../src/backSync.js")
|
||||
back = result.default
|
||||
|
||||
const cookies = new Cookies()
|
||||
cookies.set('redirect_url', window.location.pathname, { path: '/' })
|
||||
}
|
||||
|
||||
sendPush(url, payload) {
|
||||
axios.post(url, {
|
||||
payload
|
||||
}).then(response => {
|
||||
this.setState({ success: true, error: false, warning: false, message: '', snackMessage: "Votre message à bien été envoyé.", loading: false, openSnack: false })
|
||||
}).catch(error => {
|
||||
if (error.status != 500) {
|
||||
if (process.browser) {
|
||||
// client-side-only code
|
||||
back.saveEventDataLocally([{ message: this.state.message }]);
|
||||
}
|
||||
}
|
||||
this.setState({ success: false, error: false, warning: true, snackMessage: "📴 Votre message n'a pas pu aboutir, mais a été sauvegardé. Nous l'enverrons lorsque nous aurons une meilleure connectivité.", loading: false, openSnack: true })
|
||||
});
|
||||
}
|
||||
|
||||
async sendSingleMessage(payload) {
|
||||
let EdeclicLib = (await import('../components/firebase-client.js')).default
|
||||
if (!EdeclicLib.messaging)
|
||||
return
|
||||
payload.message.token = await EdeclicLib.askForToken();
|
||||
|
||||
this.sendPush("/push/message", payload)
|
||||
}
|
||||
|
||||
sendToAll(payload) {
|
||||
this.sendPush("/push/allsubscribers", payload)
|
||||
}
|
||||
|
||||
sendToTopic(payload) {
|
||||
payload.message.topic = this.state.topic
|
||||
this.sendPush("/push/message", payload)
|
||||
}
|
||||
|
||||
handleNext = async () => {
|
||||
"use strict";
|
||||
if (this.state.activeStep == 2) {
|
||||
//TODO: verify infos
|
||||
let payload = {
|
||||
message: {
|
||||
webpush: {
|
||||
headers: {
|
||||
Urgency: "high"
|
||||
},
|
||||
notification: {
|
||||
title: this.state.title,
|
||||
body: this.state.body,
|
||||
icon: this.state.iconLink,
|
||||
image: this.state.imageLink,
|
||||
click_action: this.state.action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//single
|
||||
if (this.state.target === 'a') {
|
||||
this.sendSingleMessage(payload)
|
||||
}//to All
|
||||
else if (this.state.target === 'b') {
|
||||
this.sendToAll(payload.message)
|
||||
}//to topics
|
||||
else if (this.state.target === 'c') {
|
||||
this.sendToTopic(payload)
|
||||
}
|
||||
|
||||
}
|
||||
this.setState(state => ({
|
||||
activeStep: state.activeStep + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
handleBack = () => {
|
||||
this.setState(state => ({
|
||||
activeStep: state.activeStep - 1,
|
||||
}));
|
||||
};
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({
|
||||
activeStep: 0,
|
||||
});
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ openSnack: false })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { activeStep } = this.state;
|
||||
|
||||
return (
|
||||
<Layout menu={this.props.menu}>
|
||||
{(this.state.isSignedIn) &&
|
||||
(<Fragment>
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
{this.state.loading && (
|
||||
<div className="alert alert-info">
|
||||
<CircularProgress color="secondary" />Verification en cours...
|
||||
</div>
|
||||
)}
|
||||
<Typography component="h1" variant="h4" align="center">
|
||||
Push Notification Launcher
|
||||
</Typography>
|
||||
<Stepper activeStep={activeStep} className={classes.stepper}>
|
||||
{steps.map(label => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<Fragment>
|
||||
{activeStep === steps.length ? (
|
||||
<Fragment>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Votre push a été envoyé sur firebase.
|
||||
</Typography>
|
||||
<Typography variant="subtitle1">
|
||||
Il devrait être soumis dans quelques instants.
|
||||
</Typography>
|
||||
<div className={classes.buttons}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={this.handleReset} className={classes.button}>
|
||||
Nouveau 🏭
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
{this.getStepContent(activeStep)}
|
||||
<div className={classes.buttons}>
|
||||
{activeStep !== 0 && (
|
||||
<Button onClick={this.handleBack} className={classes.button}>
|
||||
Retour
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={this.handleNext}
|
||||
className={classes.button}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? 'Launch 🚀' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</Paper>
|
||||
</main>
|
||||
</Fragment>
|
||||
)}
|
||||
{(!this.state.isSignedIn) &&
|
||||
(<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.button}
|
||||
onClick={() => Router.push('/auth/credentials')}>Sign in to use that feature</Button>)
|
||||
}
|
||||
<Snackbar
|
||||
autoHideDuration={6000}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={this.state.openSnack}
|
||||
onClose={this.handleClose}
|
||||
>
|
||||
<SnackbarContent
|
||||
className={classes[this.state.error ? 'error' : this.state.warning ? 'warning' : 'success']}
|
||||
message={<span id="message-id">{this.state.snackMessage}</span>}
|
||||
/></Snackbar>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default withStyles(styles)(PushCreator);
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Example account management routes
|
||||
**/
|
||||
'use strict'
|
||||
|
||||
module.exports = (expressApp, functions) => {
|
||||
|
||||
if (expressApp === null) {
|
||||
throw new Error('expressApp option must be an express server instance')
|
||||
}
|
||||
|
||||
// Expose a route to return user profile if logged in with a session
|
||||
expressApp.get('/account/user', (req, res) => {
|
||||
if (req.user) {
|
||||
functions.find({id: req.user.id})
|
||||
.then(user => {
|
||||
if (!user) return res.status(500).json({error: 'Unable to fetch profile'})
|
||||
res.json({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
emailVerified: (user.emailVerified && user.emailVerified === true) ? true : false
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
return res.status(500).json({error: 'Unable to fetch profile'})
|
||||
})
|
||||
} else {
|
||||
return res.status(403).json({error: 'Must be signed in to get profile'})
|
||||
}
|
||||
})
|
||||
|
||||
// Expose a route to allow users to update their profiles (name, email)
|
||||
expressApp.post('/account/user', (req, res) => {
|
||||
if (req.user) {
|
||||
functions.find({id: req.user.id})
|
||||
.then(user => {
|
||||
if (!user) return res.status(500).json({error: 'Unable to fetch profile'})
|
||||
|
||||
if (req.body.name)
|
||||
user.name = req.body.name
|
||||
|
||||
if (req.body.email) {
|
||||
// Reset email verification field if email address has changed
|
||||
if (req.body.email && req.body.email !== user.email)
|
||||
user.emailVerified = false
|
||||
|
||||
user.email = req.body.email
|
||||
}
|
||||
return functions.update(user)
|
||||
})
|
||||
.then(user => {
|
||||
return res.status(204).redirect('/account')
|
||||
})
|
||||
.catch(err => {
|
||||
return res.status(500).json({error: 'Unable to fetch profile'})
|
||||
})
|
||||
} else {
|
||||
return res.status(403).json({error: 'Must be signed in to update profile'})
|
||||
}
|
||||
})
|
||||
|
||||
// Expose a route to allow users to delete their profile.
|
||||
expressApp.post('/account/delete', (req, res) => {
|
||||
if (req.user) {
|
||||
functions.remove(req.user.id)
|
||||
.then(() => {
|
||||
// Destroy local session after deleting account
|
||||
req.logout()
|
||||
req.session.destroy(() => {
|
||||
// When the account has been deleted, redirect client to
|
||||
// /auth/callback to ensure the client has it's local session state
|
||||
// updated to reflect that the user is no longer logged in.
|
||||
return res.redirect(`/auth/callback?action=signout`)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
return res.status(500).json({error: 'Unable to delete profile'})
|
||||
})
|
||||
} else {
|
||||
return res.status(403).json({error: 'Must be signed in to delete profile'})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Defines an endpoint that returns a list of users. You must be signed in and
|
||||
* have "admin": true set in your profile to be able to call the /admin/users
|
||||
* end point (you will need to configure persistant Mongo database to do that).
|
||||
*
|
||||
* Note: These routes only work if you have actually configured a MONGO_URI!
|
||||
* They do not work if you are using the fallback in-memory database.
|
||||
**/
|
||||
'use strict'
|
||||
|
||||
const MongoClient = require('mongodb').MongoClient
|
||||
|
||||
let usersCollection
|
||||
if (process.env.MONGO_URI) {
|
||||
// Connect to MongoDB Database and return user connection
|
||||
MongoClient.connect(process.env.MONGO_URI, (err, mongoClient) => {
|
||||
if (err) throw new Error(err)
|
||||
const dbName = process.env.MONGO_URI.split('/').pop().split('?').shift()
|
||||
const db = mongoClient.db(dbName)
|
||||
usersCollection = db.collection('users')
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (expressApp) => {
|
||||
|
||||
if (expressApp === null) {
|
||||
throw new Error('expressApp option must be an express server instance')
|
||||
}
|
||||
|
||||
expressApp.get('/admin/users', (req, res) => {
|
||||
// Check user is logged in and has admin access
|
||||
if (!req.user || !req.user.admin || req.user.admin !== true)
|
||||
return res.status('403').end()
|
||||
|
||||
const page = (req.query.page && parseInt(req.query.page) > 0) ? parseInt(req.query.page) : 1
|
||||
const sort = (req.query.sort) ? { [req.query.sort]: 1 } : {}
|
||||
|
||||
let size = 10
|
||||
if (req.query.size
|
||||
&& parseInt(req.query.size) > 0
|
||||
&& parseInt(req.query.size) < 500) {
|
||||
size = parseInt(req.query.size)
|
||||
}
|
||||
|
||||
const skip = (size*(page-1) > 0) ? size*(page-1) : 0
|
||||
|
||||
let response = {
|
||||
users: [],
|
||||
page: page,
|
||||
size: size,
|
||||
sort: req.params.sort,
|
||||
total: 0
|
||||
}
|
||||
|
||||
if (req.params.sort) response.sort = req.params.sort
|
||||
|
||||
let result
|
||||
return new Promise(function(resolve, reject) {
|
||||
result = usersCollection
|
||||
.find()
|
||||
.skip(skip)
|
||||
.sort(sort)
|
||||
.limit(size)
|
||||
|
||||
result.toArray((err, users) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(users)
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(users => {
|
||||
response.users = users
|
||||
return result.count()
|
||||
})
|
||||
.then(count => {
|
||||
response.total = count
|
||||
return res.json(response)
|
||||
})
|
||||
.catch(err => {
|
||||
return res.status(500).json(err)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
|
||||
|
||||
require('dotenv').load()
|
||||
const utils = require('./src/utils');
|
||||
const mailer = require("./src/mailer.js")
|
||||
const { google } = require('googleapis');
|
||||
//const express = require('express')
|
||||
const axios = require('axios')
|
||||
const next = require('next')
|
||||
const nextAuth = require('next-auth')
|
||||
const nextAuthConfig = require('./next-auth.config')
|
||||
const { join } = require('path');
|
||||
const { parse } = require('url');
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
// const passport = require('passport')
|
||||
// const FacebookStrategy = require('passport-facebook').Strategy
|
||||
|
||||
const routes = {
|
||||
admin: require('./routes/admin'),
|
||||
account: require('./routes/account')
|
||||
}
|
||||
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
function getAccessToken() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var key = require('./service-account.json');
|
||||
var jwtClient = new google.auth.JWT(
|
||||
key.client_email,
|
||||
null,
|
||||
key.private_key,
|
||||
'https://www.googleapis.com/auth/firebase.messaging',
|
||||
null
|
||||
);
|
||||
jwtClient.authorize(function (err, tokens) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(tokens.access_token);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// passport.use(new FacebookStrategy({
|
||||
// clientID: process.env.FACEBOOK_ID,
|
||||
// clientSecret: process.env.FACEBOOK_SECRET,
|
||||
// callbackURL: "http://localhost:3000/auth/facebook/callback",
|
||||
// enableProof: true
|
||||
// },
|
||||
// function (accessToken, refreshToken, profile, cb) {
|
||||
// User.findOrCreate({ facebookId: profile.id }, function (err, user) {
|
||||
// return cb(err, user);
|
||||
// });
|
||||
// console.log(profile)
|
||||
|
||||
// console.log("accessToken : " + accessToken);
|
||||
// console.log("refreshToken" + refreshToken);//****not needed because we only use accesstoken to connect to our api*/
|
||||
|
||||
// return cb(null, profile);
|
||||
// }
|
||||
// ));
|
||||
|
||||
app.prepare()
|
||||
.then(() => {
|
||||
// Load configuration and return config object
|
||||
return nextAuthConfig()
|
||||
})
|
||||
.then(nextAuthOptions => {
|
||||
// Pass Next.js App instance and NextAuth options to NextAuth
|
||||
// Note We do not pass a port in nextAuthOptions, because we want to add some
|
||||
// additional routes before Express starts (if you do pass a port, NextAuth
|
||||
// tells NextApp to handle default routing and starts Express automatically).
|
||||
return nextAuth(app, nextAuthOptions)
|
||||
})
|
||||
.then(nextAuthOptions => {
|
||||
|
||||
const express = nextAuthOptions.express
|
||||
const expressApp = nextAuthOptions.expressApp
|
||||
|
||||
// Add admin routes
|
||||
routes.admin(expressApp)
|
||||
|
||||
// Add account management route - reuses functions defined for NextAuth
|
||||
routes.account(expressApp, nextAuthOptions.functions)
|
||||
|
||||
expressApp.use('/', express.static(__dirname + '/.next/'))
|
||||
|
||||
var jsonParser = bodyParser.json()
|
||||
|
||||
expressApp.post('/feedback', jsonParser, (req, res) => {
|
||||
|
||||
let message = req.body.message;
|
||||
let nom = req.body.nom;
|
||||
let prenom = req.body.prenom;
|
||||
let tel = req.body.tel;
|
||||
let mail = req.body.mail;
|
||||
let recaptchaValue = req.body.recaptchaValue;
|
||||
|
||||
console.log(message);
|
||||
if (message === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message is empty." }
|
||||
}); return;
|
||||
}
|
||||
if (mail === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "email is empty." }
|
||||
}); return;
|
||||
}
|
||||
utils.verifyCaptcha(recaptchaValue)
|
||||
.then(
|
||||
respo => {
|
||||
if (respo.data.success == true) {
|
||||
try {
|
||||
mailer.sendEmail(message, nom, prenom, tel, mail);
|
||||
res.status(200).send({})
|
||||
} catch (err) {
|
||||
res.status(500).send({ message: "Send mail fail." })
|
||||
}
|
||||
}
|
||||
else
|
||||
res.status(500).send({ message: "Captcha invalid." })
|
||||
})
|
||||
.catch(() =>
|
||||
res.status(403).send({ message: "Captcha verification impossible." })
|
||||
)
|
||||
|
||||
});
|
||||
|
||||
expressApp.post('/push/message', jsonParser, (req, res) => {
|
||||
let message = req.body.payload;
|
||||
|
||||
if (message === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message is empty." }
|
||||
}); return;
|
||||
}
|
||||
else {
|
||||
getAccessToken().then((googleAccessToken) => {
|
||||
utils.setAuthorizationHeader(googleAccessToken);
|
||||
// console.log(message);
|
||||
axios.post('https://fcm.googleapis.com/v1/projects/edeclicpwa/messages:send',
|
||||
message
|
||||
).then(r => {
|
||||
console.log("message : "); console.log(r)
|
||||
res.status(200).send({ message: "message sent to firebase" })
|
||||
}
|
||||
)
|
||||
.catch(err => console.log("first catch" + err)
|
||||
//res.status(500).send({ message: "Push error" })
|
||||
)
|
||||
}).catch(err => console.log("second catch" + err)
|
||||
//err => res.status(500).send({ message: "Push error" + err })
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
expressApp.get('/push/info/:token', (req, res) => {
|
||||
|
||||
var token = req.params.token
|
||||
if (token === '') {
|
||||
res.status(498).json({
|
||||
errors: { global: "No token specified." }
|
||||
}); return;
|
||||
}
|
||||
|
||||
var url = 'https://iid.googleapis.com/iid/info/' + token + '?details=true'
|
||||
|
||||
utils.setAuthorizationHeaderForIID(process.env.FIREBASE_API_KEY)
|
||||
axios.get(url).then(r => {
|
||||
if (!!r.data.rel && !!r.data.rel.topics) {
|
||||
res.status(200).send({ topics: r.data.rel.topics })
|
||||
}
|
||||
else
|
||||
res.status(200).send({ topics: null })
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
res.status(500).send({ message: "Push error" })
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
expressApp.post('/push/subscribe', jsonParser, (req, res) => {
|
||||
let message = req.body;
|
||||
|
||||
if (message === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message is empty." }
|
||||
}); return;
|
||||
}
|
||||
else {
|
||||
if (message.topic === '' || message.token === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message isnot well formed." }
|
||||
}); return;
|
||||
}
|
||||
|
||||
var url = 'https://iid.googleapis.com/iid/v1/' + message.token + '/rel/topics/' + message.topic
|
||||
|
||||
utils.setAuthorizationHeaderForIID(process.env.FIREBASE_API_KEY)
|
||||
|
||||
axios.post(url)
|
||||
.then(r => {
|
||||
res.status(200).send({ message: "message sent to firebase" })
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
expressApp.post('/push/unsubscribe', jsonParser, (req, res) => {
|
||||
let message = req.body;
|
||||
|
||||
if (message === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message is empty." }
|
||||
}); return;
|
||||
}
|
||||
else {
|
||||
if (message.topic === '' || message.token === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message is not well formed." }
|
||||
}); return;
|
||||
}
|
||||
message.topic = '/topics/' + message.topic
|
||||
var payload = { to: message.topic, registration_tokens: [message.token] }
|
||||
var url = 'https://iid.googleapis.com/iid/v1:batchRemove'
|
||||
|
||||
utils.setAuthorizationHeaderForIID(process.env.FIREBASE_API_KEY)
|
||||
axios.post(url, payload
|
||||
).then(r => {
|
||||
res.status(200).send({ message: "message sent to firebase" })
|
||||
}
|
||||
)
|
||||
.catch(err => console.log(err)
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
//https://us-central1-edeclicpwa.cloudfunctions.net/pushMessageToAllSubscribers
|
||||
expressApp.post('/push/allsubscribers', jsonParser, (req, res) => {
|
||||
let message = req.body.payload;
|
||||
|
||||
if (message === '') {
|
||||
res.status(400).json({
|
||||
errors: { global: "Message is empty." }
|
||||
}); return;
|
||||
}
|
||||
else {
|
||||
|
||||
utils.setAuthorizationHeader(process.env.FIREBASE_API_KEY)
|
||||
// console.log(message);
|
||||
axios.post('https://us-central1-edeclicpwa.cloudfunctions.net/pushMessageToAllSubscribers',
|
||||
{ payload: message }
|
||||
).then(r => {
|
||||
res.status(200).send({ message: "message sent to firebase" })
|
||||
}
|
||||
)
|
||||
.catch(err => console.log(err))
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
expressApp.get('/static/js/*.js', (req, res) => {
|
||||
|
||||
const parsedUrl = parse(req.url, true);
|
||||
const { pathname } = parsedUrl;
|
||||
const filePath = join(__dirname, '.next', pathname);
|
||||
app.serveStatic(req, res, filePath);
|
||||
})
|
||||
|
||||
expressApp.get('/manifest.json', (req, res) => {
|
||||
|
||||
const parsedUrl = parse(req.url, true);
|
||||
const { pathname } = parsedUrl;
|
||||
const filePath = join(__dirname, '/static/', pathname);
|
||||
app.serveStatic(req, res, filePath);
|
||||
})
|
||||
|
||||
|
||||
expressApp.get('*', (req, res) => {
|
||||
return handle(req, res)
|
||||
})
|
||||
|
||||
expressApp.listen(process.env.PORT, (err) => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on http://localhost:' + process.env.PORT)
|
||||
console.log('> access Token googleapi >' + getAccessToken())
|
||||
})
|
||||
})
|
||||
.catch((ex) => {
|
||||
console.error(ex.stack)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "edeclicpwa",
|
||||
"private_key_id": "aef80b8a8d6f9f10aa0ef0cffd46992399fac45d",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZuQVADJXtA7Hb\n2R36sZxD3fw5QOmLwvAGnIXtexf/hppz6J1mUDsbNDLF3FxlgzdKQguGE0+q6QI+\nIWEMYPVQPot8Tbs8rcL06EKkhLR3KHRI6hWRyFNnWKnig7xVD5LUNVjgtHNIx9TC\ndf+/F6kSsz7w7cjHZEFZ5k7eAeD3M15EhtGCCyJ9DM3MO/pKzW4lah1jTGeV1ZYS\nimuBSbSX9Z6k6l3dUayMYaARYjZkBu1syhUGs/KoeqS+o+bR1OJX/JiaYe0lWWAg\nuL4315c2SkuXeHX2y9BvxbY2TGfhAPuNFPSsJgt0hXE6QeSIjFZXePmWIq28tNhH\n6OR/EJFzAgMBAAECggEAZ23/YgF4mct3C19V4BnPB+ilYReGuzfkqediskIXUPMD\nXcvkNk4n/hDqi9dW53yR4AuHCO8UmjcuMxDNV0GaWEAWKHuO1tEfPBQ4UIqgZrkH\noPnfPE2j3YUf03VMm0YWNQyQx9LBr5IK70R6NbAKSFFxtafoiVyFtSz1S38t/ZB/\nqigCvK48kG6vZCyAozuKVbkLKorY8THZRbK24Mwp4rnAxRHUxG9pXCGe1/j1FvSP\nPRGPptvRSLP2kRPZXELKga7/yCPIOqT2jEoWiXAdfU2gyED4jEhby+ryBHuTsxpo\nJiaGtbUWMKjOEUOWekgTMvtgQD4T5WP5zx2AAZ5K4QKBgQDz7075SYWTy98MZSC2\nMVF6Ei2mGDMxLDJqpTnc3Oi7+eOXI8uTo5SqgaBHfM/uTywcipqSUQTseDN7C1wu\nWOb6r0shK6FfDP1GQc9l0mmNuIZ8JLkc6nB8Es5wwd4vLRhuSsy6pSz/wDovC3Q2\n5se/B7ir+kYeFlhIMAjN91rukwKBgQDkfdDpAociVjFpcjly5Zf60uL/BzIZOdpn\n1Iq/kAxFr2kuZzGDpwUgXvSU4nELivrsf452x3+R78C2SWwil9IRrD3DAYJzT3qP\nb6l4EuKuHRPtVJBqIPWaMiT6Ov6aAQpGYh5qZjFeHSIDBzXHd/OBbSzisE3zTOqm\nED3/eQq9oQKBgAD/XYdPcahlEQhv8W5NTVP+dwlS2AK/d4VQH6hzjtAV+YRItTBp\nXtZDqXAhZohG8ps7Rd6LTkXZR/yc00etPWSRCvGbyBEncHG1GzADaEMYGhSv4cHo\ng4U+XnG/mTUALjVlQOkSe9if5J0EovkGgJKbaXnqkBbXaI0DBUYyWMDZAoGBAMND\nGfLmbCFV02gvaxTbTCPXcJFMzu1r2U99/Qxzx2kN3C8BlPjTFLhzLUTGtqCMpp7Q\n6yhqmIRYhTHCURzG7YiYzzcE5TwxoaVOYV7xlLICu3LIH5nyjLC3RY5qOAXX+bXo\nR+HZbzrkXpqD4NuTkI78g609yX+wLZ64pqLaB+nBAoGATMflJNxgyWtY6b2yFYgm\nCOhMecJKMS2Rgmdl3FQGxAMPMIy18URW/t99LmbUhqQGsdFPDTcxR6V4ajWqltrj\nLkbrEtSHp7xFAQD2anUeEwxtW8PpICwhgGXJUSjXU8+2A20CaWcTwH5USnNp8kp/\n9402JWBkoe+kodILFHZ363M=\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-hffwl@edeclicpwa.iam.gserviceaccount.com",
|
||||
"client_id": "100436348289421185617",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-hffwl%40edeclicpwa.iam.gserviceaccount.com"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
import idb from "./idb-promised.js"
|
||||
|
||||
|
||||
const dbPromise = createIndexedDB();
|
||||
|
||||
function createIndexedDB() {
|
||||
if (!('indexedDB' in window)) { return null; }
|
||||
return idb.open('dashboardr', 1, function (upgradeDb) {
|
||||
if (!upgradeDb.objectStoreNames.contains('events')) {
|
||||
const eventsOS = upgradeDb.createObjectStore('events', { keyPath: 'id' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const backSync = {
|
||||
saveEventDataLocally: (events) => {
|
||||
if (!('indexedDB' in window)) {
|
||||
return null;
|
||||
}
|
||||
return dbPromise.then(db => {
|
||||
const tx = db.transaction('events', 'readwrite');
|
||||
const store = tx.objectStore('events');
|
||||
return Promise.all(events.map(event => store.put(event)))
|
||||
.catch(() => {
|
||||
tx.abort();
|
||||
throw Error('Events were not added to the store');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default backSync;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import lscache from 'lscache';
|
||||
import fetch from 'isomorphic-unfetch';
|
||||
|
||||
const TTL_MINUTES = 5;
|
||||
|
||||
export default async function(url, options) {
|
||||
// We don't cache anything when server-side rendering.
|
||||
// That way if users refresh the page they always get fresh data.
|
||||
if (typeof window === 'undefined') {
|
||||
return fetch(url, options).then(response => response.json());
|
||||
}
|
||||
|
||||
let cachedResponse = lscache.get(url);
|
||||
|
||||
// If there is no cached response,
|
||||
// do the actual call and store the response
|
||||
if (cachedResponse === null) {
|
||||
cachedResponse = await fetch(url, options)
|
||||
.then(response => response.json());
|
||||
lscache.set(url, cachedResponse, TTL_MINUTES);
|
||||
}
|
||||
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
export function overrideCache(key, val) {
|
||||
|
||||
lscache.set(key, val, TTL_MINUTES);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import { SheetsRegistry } from 'jss';
|
||||
import { createMuiTheme, createGenerateClassName } from '@material-ui/core/styles';
|
||||
import blue from '@material-ui/core/colors/blue';
|
||||
import grey from '@material-ui/core/colors/grey';
|
||||
import purple from '@material-ui/core/colors/purple';
|
||||
import green from '@material-ui/core/colors/green';
|
||||
// A theme with custom primary and secondary color.
|
||||
// It's optional.
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#ffffff',
|
||||
},
|
||||
secondary: {
|
||||
main: '#333',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
useNextVariants: true,
|
||||
// Use the system font instead of the default Roboto font.
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(','),
|
||||
},
|
||||
overrides: {
|
||||
MuiButton: {
|
||||
root:{
|
||||
color: 'black', // Some CSS
|
||||
}
|
||||
|
||||
},
|
||||
MuiStepIcon:{ completed:{
|
||||
color:'#43a047!important'
|
||||
},
|
||||
active:{
|
||||
color:'#43a047!important'
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
function createPageContext() {
|
||||
return {
|
||||
theme,
|
||||
// This is needed in order to deduplicate the injection of CSS in the page.
|
||||
sheetsManager: new Map(),
|
||||
// This is needed in order to inject the critical CSS.
|
||||
sheetsRegistry: new SheetsRegistry(),
|
||||
// The standard class name generator.
|
||||
generateClassName: createGenerateClassName(),
|
||||
};
|
||||
}
|
||||
|
||||
export default function getPageContext() {
|
||||
// Make sure to create a new context for every server-side request so that data
|
||||
// isn't shared between connections (which would be bad).
|
||||
if (!process.browser) {
|
||||
return createPageContext();
|
||||
}
|
||||
|
||||
// Reuse context on the client-side.
|
||||
if (!global.__INIT_MATERIAL_UI__) {
|
||||
global.__INIT_MATERIAL_UI__ = createPageContext();
|
||||
}
|
||||
|
||||
return global.__INIT_MATERIAL_UI__;
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
Copyright 2018 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
(function() {
|
||||
function toArray(arr) {
|
||||
return Array.prototype.slice.call(arr);
|
||||
}
|
||||
|
||||
function promisifyRequest(request) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
request.onsuccess = function() {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function promisifyRequestCall(obj, method, args) {
|
||||
var request;
|
||||
var p = new Promise(function(resolve, reject) {
|
||||
request = obj[method].apply(obj, args);
|
||||
promisifyRequest(request).then(resolve, reject);
|
||||
});
|
||||
|
||||
p.request = request;
|
||||
return p;
|
||||
}
|
||||
|
||||
function promisifyCursorRequestCall(obj, method, args) {
|
||||
var p = promisifyRequestCall(obj, method, args);
|
||||
return p.then(function(value) {
|
||||
if (!value) return;
|
||||
return new Cursor(value, p.request);
|
||||
});
|
||||
}
|
||||
|
||||
function proxyProperties(ProxyClass, targetProp, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
Object.defineProperty(ProxyClass.prototype, prop, {
|
||||
get: function() {
|
||||
return this[targetProp][prop];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function proxyRequestMethods(ProxyClass, targetProp, Constructor, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
if (!(prop in Constructor.prototype)) return;
|
||||
ProxyClass.prototype[prop] = function() {
|
||||
return promisifyRequestCall(this[targetProp], prop, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function proxyMethods(ProxyClass, targetProp, Constructor, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
if (!(prop in Constructor.prototype)) return;
|
||||
ProxyClass.prototype[prop] = function() {
|
||||
return this[targetProp][prop].apply(this[targetProp], arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function proxyCursorRequestMethods(ProxyClass, targetProp, Constructor, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
if (!(prop in Constructor.prototype)) return;
|
||||
ProxyClass.prototype[prop] = function() {
|
||||
return promisifyCursorRequestCall(this[targetProp], prop, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function Index(index) {
|
||||
this._index = index;
|
||||
}
|
||||
|
||||
proxyProperties(Index, '_index', [
|
||||
'name',
|
||||
'keyPath',
|
||||
'multiEntry',
|
||||
'unique'
|
||||
]);
|
||||
|
||||
proxyRequestMethods(Index, '_index', IDBIndex, [
|
||||
'get',
|
||||
'getKey',
|
||||
'getAll',
|
||||
'getAllKeys',
|
||||
'count'
|
||||
]);
|
||||
|
||||
proxyCursorRequestMethods(Index, '_index', IDBIndex, [
|
||||
'openCursor',
|
||||
'openKeyCursor'
|
||||
]);
|
||||
|
||||
function Cursor(cursor, request) {
|
||||
this._cursor = cursor;
|
||||
this._request = request;
|
||||
}
|
||||
|
||||
proxyProperties(Cursor, '_cursor', [
|
||||
'direction',
|
||||
'key',
|
||||
'primaryKey',
|
||||
'value'
|
||||
]);
|
||||
|
||||
proxyRequestMethods(Cursor, '_cursor', IDBCursor, [
|
||||
'update',
|
||||
'delete'
|
||||
]);
|
||||
|
||||
// proxy 'next' methods
|
||||
['advance', 'continue', 'continuePrimaryKey'].forEach(function(methodName) {
|
||||
if (!(methodName in IDBCursor.prototype)) return;
|
||||
Cursor.prototype[methodName] = function() {
|
||||
var cursor = this;
|
||||
var args = arguments;
|
||||
return Promise.resolve().then(function() {
|
||||
cursor._cursor[methodName].apply(cursor._cursor, args);
|
||||
return promisifyRequest(cursor._request).then(function(value) {
|
||||
if (!value) return;
|
||||
return new Cursor(value, cursor._request);
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function ObjectStore(store) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
ObjectStore.prototype.createIndex = function() {
|
||||
return new Index(this._store.createIndex.apply(this._store, arguments));
|
||||
};
|
||||
|
||||
ObjectStore.prototype.index = function() {
|
||||
return new Index(this._store.index.apply(this._store, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(ObjectStore, '_store', [
|
||||
'name',
|
||||
'keyPath',
|
||||
'indexNames',
|
||||
'autoIncrement'
|
||||
]);
|
||||
|
||||
proxyRequestMethods(ObjectStore, '_store', IDBObjectStore, [
|
||||
'put',
|
||||
'add',
|
||||
'delete',
|
||||
'clear',
|
||||
'get',
|
||||
'getAll',
|
||||
'getAllKeys',
|
||||
'count'
|
||||
]);
|
||||
|
||||
proxyCursorRequestMethods(ObjectStore, '_store', IDBObjectStore, [
|
||||
'openCursor',
|
||||
'openKeyCursor'
|
||||
]);
|
||||
|
||||
proxyMethods(ObjectStore, '_store', IDBObjectStore, [
|
||||
'deleteIndex'
|
||||
]);
|
||||
|
||||
function Transaction(idbTransaction) {
|
||||
this._tx = idbTransaction;
|
||||
this.complete = new Promise(function(resolve, reject) {
|
||||
idbTransaction.oncomplete = function() {
|
||||
resolve();
|
||||
};
|
||||
idbTransaction.onerror = function() {
|
||||
reject(idbTransaction.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Transaction.prototype.objectStore = function() {
|
||||
return new ObjectStore(this._tx.objectStore.apply(this._tx, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(Transaction, '_tx', [
|
||||
'objectStoreNames',
|
||||
'mode'
|
||||
]);
|
||||
|
||||
proxyMethods(Transaction, '_tx', IDBTransaction, [
|
||||
'abort'
|
||||
]);
|
||||
|
||||
function UpgradeDB(db, oldVersion, transaction) {
|
||||
this._db = db;
|
||||
this.oldVersion = oldVersion;
|
||||
this.transaction = new Transaction(transaction);
|
||||
}
|
||||
|
||||
UpgradeDB.prototype.createObjectStore = function() {
|
||||
return new ObjectStore(this._db.createObjectStore.apply(this._db, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(UpgradeDB, '_db', [
|
||||
'name',
|
||||
'version',
|
||||
'objectStoreNames'
|
||||
]);
|
||||
|
||||
proxyMethods(UpgradeDB, '_db', IDBDatabase, [
|
||||
'deleteObjectStore',
|
||||
'close'
|
||||
]);
|
||||
|
||||
function DB(db) {
|
||||
this._db = db;
|
||||
}
|
||||
|
||||
DB.prototype.transaction = function() {
|
||||
return new Transaction(this._db.transaction.apply(this._db, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(DB, '_db', [
|
||||
'name',
|
||||
'version',
|
||||
'objectStoreNames'
|
||||
]);
|
||||
|
||||
proxyMethods(DB, '_db', IDBDatabase, [
|
||||
'close'
|
||||
]);
|
||||
|
||||
// Add cursor iterators
|
||||
// TODO: remove this once browsers do the right thing with promises
|
||||
['openCursor', 'openKeyCursor'].forEach(function(funcName) {
|
||||
[ObjectStore, Index].forEach(function(Constructor) {
|
||||
Constructor.prototype[funcName.replace('open', 'iterate')] = function() {
|
||||
var args = toArray(arguments);
|
||||
var callback = args[args.length - 1];
|
||||
var request = (this._store || this._index)[funcName].apply(this._store, args.slice(0, -1));
|
||||
request.onsuccess = function() {
|
||||
callback(request.result);
|
||||
};
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// polyfill getAll
|
||||
[Index, ObjectStore].forEach(function(Constructor) {
|
||||
if (Constructor.prototype.getAll) return;
|
||||
Constructor.prototype.getAll = function(query, count) {
|
||||
var instance = this;
|
||||
var items = [];
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
instance.iterateCursor(query, function(cursor) {
|
||||
if (!cursor) {
|
||||
resolve(items);
|
||||
return;
|
||||
}
|
||||
items.push(cursor.value);
|
||||
|
||||
if (count !== undefined && items.length == count) {
|
||||
resolve(items);
|
||||
return;
|
||||
}
|
||||
cursor.continue();
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
var exp = {
|
||||
open: function(name, version, upgradeCallback) {
|
||||
var p = promisifyRequestCall(indexedDB, 'open', [name, version]);
|
||||
var request = p.request;
|
||||
|
||||
request.onupgradeneeded = function(event) {
|
||||
if (upgradeCallback) {
|
||||
upgradeCallback(new UpgradeDB(request.result, event.oldVersion, request.transaction));
|
||||
}
|
||||
};
|
||||
|
||||
return p.then(function(db) {
|
||||
return new DB(db);
|
||||
});
|
||||
},
|
||||
delete: function(name) {
|
||||
return promisifyRequestCall(indexedDB, 'deleteDatabase', [name]);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = exp;
|
||||
}
|
||||
else {
|
||||
self.idb = exp;
|
||||
}
|
||||
}());
|
||||
@@ -0,0 +1,40 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
|
||||
function setup() {
|
||||
return nodemailer.createTransport({
|
||||
host: process.env.EMAIL_SERVER,
|
||||
port: process.env.EMAIL_PORT,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.sendEmail = function(text,nom,prenom,tel,mail) {
|
||||
const tranport = setup();
|
||||
const email = {
|
||||
from: process.env.EMAIL_FROM,
|
||||
to: "thomas.berrod@e-declic.com",
|
||||
subject: "PWA feedback 💌⚡️",
|
||||
text: `
|
||||
Hello Team,
|
||||
A new user sent a feedback 👌:
|
||||
--->
|
||||
${text}
|
||||
|
||||
nom: ${nom}
|
||||
prenom:${prenom}
|
||||
mail:${mail}
|
||||
tel:${tel}
|
||||
|
||||
<---
|
||||
Have a nice day !
|
||||
- e-declic Team PWA
|
||||
|
||||
`
|
||||
};
|
||||
|
||||
tranport.sendMail(email);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const axios = require('axios')
|
||||
exports.verifyCaptcha = (recaptchaValue) =>
|
||||
{
|
||||
const verifyUrl = process.env.RECAPTCHA_VERIFY_URL;
|
||||
const ReCAPTCHASecret = process.env.RECAPTCHA_SECRET
|
||||
return axios.get(verifyUrl, {
|
||||
params: { secret: ReCAPTCHASecret, response: recaptchaValue }
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
exports.setAuthorizationHeader = (token = null) => {
|
||||
if (token) {
|
||||
axios.defaults.headers.common.authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
delete axios.defaults.headers.common.authorization;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.setAuthorizationHeaderForIID = (token = null) => {
|
||||
if (token) {
|
||||
axios.defaults.headers.common.authorization = `key=${token}`;
|
||||
} else {
|
||||
delete axios.defaults.headers.common.authorization;
|
||||
}
|
||||
};
|
||||
|
||||
//https://www.google.com/recaptcha/api/siteverify?secret=6LeNK3QUAAAAACR2Y6Rx98hBKTtomtjETRqffKPM&response=03AMGVjXjw6EQMFg9ZIqwT8OEXgupun-bYxZRZ6iC6SNpO2YRw1cfFBi6eMKelpTgE8l91__TCCowcUq4vhIr890XSs2WSTTx_hFNAW2YFFY6kxjIT9U2EIWp47F7GqQGwcJBtogIok7gDPS4qp1qqk37fUiIQnvtCJUCwCDnGkyr0_ecxixq4tR0pl9wu56zwCRi-WXFiZhvN7rCzZbbpL7TQfJvh5Mz3JqnV2WbtR4MP-YIJl226Ec7BZewqbSFYYl6alkR2JuXN5i8lWhRYxa6VQcDb_vPEPw
|
||||
@@ -0,0 +1,76 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu,
|
||||
roboto, noto, segoe ui, arial, sans-serif;
|
||||
background: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#root {
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.card1,
|
||||
.card2,
|
||||
.card3,
|
||||
.card4 {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.card1 {
|
||||
min-width: 60ch;
|
||||
min-height: 60ch;
|
||||
width: 45vw;
|
||||
height: 45vw;
|
||||
max-width: 100ch;
|
||||
max-height: 100ch;
|
||||
background-image: url(https://image.flaticon.com/icons/svg/119/119596.svg);
|
||||
}
|
||||
|
||||
.card2 {
|
||||
width: 25ch;
|
||||
height: 25ch;
|
||||
background-image: url(https://image.flaticon.com/icons/svg/789/789395.svg);
|
||||
}
|
||||
|
||||
.card3 {
|
||||
opacity: 0.9;
|
||||
width: 25ch;
|
||||
height: 25ch;
|
||||
background-image: url(https://image.flaticon.com/icons/svg/414/414927.svg);
|
||||
}
|
||||
|
||||
.card4 {
|
||||
width: 25ch;
|
||||
height: 25ch;
|
||||
background-image: url(https://image.flaticon.com/icons/svg/789/789392.svg);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
.cell {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: #777777;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0px 10px 60px -10px rgba(0, 0, 0, 0.2);
|
||||
transition: box-shadow 0.5s;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.default {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
box-shadow: 0px 20px 60px -10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.shuffle {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
padding: 0px 20px 0px 20px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background: white;
|
||||
border: solid 2px #ffaad4;
|
||||
border-radius: 7px;
|
||||
color: #ffaad4;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 100;
|
||||
line-height: 45px;
|
||||
}
|
||||
|
||||
.shuffle:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.details {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #ffffffa0;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
font-weight: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.details h1 {
|
||||
color: #ca6a9a;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.details p {
|
||||
color: #777777;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
margin: 0;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
top: calc(50% - 50px);
|
||||
left: calc(50% - 50px);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 20px 60px -10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
font-size: 20px;
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
.main {
|
||||
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
|
||||
helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 30px 70px 30px 70px;
|
||||
height: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.grid {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
background-color: #ca6a9a !important;
|
||||
border-color: #ca6a9a !important;
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="57px" height="57px" viewBox="0 0 57 57" enable-background="new 0 0 57 57" xml:space="preserve">
|
||||
<image overflow="visible" width="512" height="512" xlink:href="../../Downloads/e-declicIco512x512.png" transform="matrix(0.1113 0 0 0.1113 0 -4.882812e-04)">
|
||||
</image>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 644 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="330px" height="373px" viewBox="0 0 330 373" enable-background="new 0 0 330 373" xml:space="preserve">
|
||||
<g id="XMLID_2_">
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M297.26,213.2V260.1H286.98V213.2c-0.01-10.091-4.24-19.721-11.66-26.551L235.54,150.2v-13.94
|
||||
l46.711,42.81C291.81,187.851,297.25,200.221,297.26,213.2z"/>
|
||||
<path fill="#FFFFFF" d="M286.979,260.101h10.279c0,68.75-59.221,79.02-92.579,79.02v-9.21c0-0.38,0-0.76,0.01-1.14
|
||||
C219.6,328.771,286.979,325.421,286.979,260.101z"/>
|
||||
<path fill="#383D4C" d="M286.979,213.2V260.1c0,65.32-67.38,68.67-82.29,68.67c0.189-22.77,7.029-45,19.7-63.96
|
||||
c1.359-2.04,1.09-4.75-0.641-6.49l-36.011-36c-3.33-3.329-5.09-7.92-4.859-12.619c0.229-4.699,2.439-9.091,6.08-12.069
|
||||
c7.01-5.36,16.931-4.602,23.029,1.77l50.771,50.78l7.279-7.28l-34.5-34.51v-58.189l39.78,36.449
|
||||
C282.739,193.48,286.97,203.11,286.979,213.2z"/>
|
||||
<path fill="#FFFFFF" d="M227.789,143.101l7.75,7.101v58.189l34.5,34.51l-7.279,7.28l-50.771-50.78
|
||||
c-6.1-6.37-16.02-7.13-23.029-1.77c-3.641,2.979-5.851,7.37-6.08,12.069s1.529,9.289,4.859,12.619l36.011,36
|
||||
c1.729,1.74,2,4.45,0.641,6.49c-12.67,18.96-19.511,41.19-19.7,63.96c-0.01,0.38-0.01,0.76-0.01,1.14v9.21H76.08
|
||||
c-14.189-0.02-25.7-11.528-25.71-25.72V56.21c0.01-14.2,11.521-25.7,25.71-25.721h133.74c14.2,0.021,25.7,11.521,25.72,25.721
|
||||
v80.05l-0.81-0.74L227.789,143.101z M225.25,198.11V71.641H60.649v216.04H189.25v10.29H60.649v15.43
|
||||
c0,8.521,6.91,15.431,15.43,15.431h118.351c0.159-23.391,6.779-46.289,19.108-66.159l-33.069-33.08
|
||||
c-5.38-5.38-8.239-12.79-7.869-20.391c0.369-7.601,3.938-14.689,9.819-19.521c11.109-8.69,26.979-7.64,36.851,2.439L225.25,198.11
|
||||
z M225.25,61.351v-5.14c0-8.521-6.91-15.431-15.431-15.431H76.08c-8.52,0-15.43,6.91-15.43,15.431v5.14H225.25z"/>
|
||||
<polygon fill="#FFFFFF" points="235.539,136.261 235.539,150.201 227.789,143.101 234.729,135.521 "/>
|
||||
<path fill="#383D4C" d="M225.25,71.641v126.47l-5.979-5.989c-9.87-10.08-25.74-11.129-36.851-2.439
|
||||
c-5.881,4.83-9.449,11.92-9.819,19.521s2.489,15.011,7.87,20.391l33.068,33.08c-12.329,19.87-18.949,42.77-19.108,66.159H76.08
|
||||
c-8.52,0-15.43-6.909-15.43-15.431v-15.43h128.6v-10.29H60.649V71.641H225.25z M214.96,164.23v-20.58
|
||||
c0-5.68-4.601-10.279-10.279-10.279H184.1c-5.681,0-10.28,4.6-10.28,10.279v20.58c0,5.681,4.601,10.29,10.28,10.29h20.58
|
||||
C210.359,174.521,214.96,169.911,214.96,164.23z M214.96,112.791v-20.57c0-5.69-4.601-10.29-10.279-10.29H184.1
|
||||
c-5.681,0-10.28,4.6-10.28,10.29v20.57c0,5.689,4.601,10.29,10.28,10.29h20.58C210.359,123.081,214.96,118.48,214.96,112.791z
|
||||
M163.53,267.101v-20.57c0-5.68-4.61-10.289-10.29-10.289h-20.57c-5.689,0-10.29,4.609-10.29,10.289v20.57
|
||||
c0,5.69,4.601,10.29,10.29,10.29h20.57C158.919,277.391,163.53,272.791,163.53,267.101z M163.53,215.671v-20.58
|
||||
c0-5.68-4.61-10.29-10.29-10.29h-20.57c-5.689,0-10.29,4.61-10.29,10.29v20.58c0,5.68,4.601,10.279,10.29,10.279h20.57
|
||||
C158.919,225.95,163.53,221.351,163.53,215.671z M163.53,164.23v-20.58c0-5.68-4.61-10.279-10.29-10.279h-20.57
|
||||
c-5.689,0-10.29,4.6-10.29,10.279v20.58c0,5.681,4.601,10.29,10.29,10.29h20.57C158.919,174.521,163.53,169.911,163.53,164.23z
|
||||
M163.53,112.791v-20.57c0-5.69-4.61-10.29-10.29-10.29h-20.57c-5.689,0-10.29,4.6-10.29,10.29v20.57
|
||||
c0,5.689,4.601,10.29,10.29,10.29h20.57C158.919,123.081,163.53,118.48,163.53,112.791z M148.1,318.54v-10.29h-10.29v10.29H148.1z
|
||||
M112.089,267.101v-20.57c0-5.68-4.6-10.289-10.29-10.289H81.229c-5.681,0-10.29,4.609-10.29,10.289v20.57
|
||||
c0,5.69,4.609,10.29,10.29,10.29h20.569C107.49,277.391,112.089,272.791,112.089,267.101z M112.089,215.671v-20.58
|
||||
c0-5.68-4.6-10.29-10.29-10.29H81.229c-5.681,0-10.29,4.61-10.29,10.29v20.58c0,5.68,4.609,10.279,10.29,10.279h20.569
|
||||
C107.49,225.95,112.089,221.351,112.089,215.671z M112.089,164.23v-20.58c0-5.68-4.6-10.279-10.29-10.279H81.229
|
||||
c-5.681,0-10.29,4.6-10.29,10.279v20.58c0,5.681,4.609,10.29,10.29,10.29h20.569C107.49,174.521,112.089,169.911,112.089,164.23z
|
||||
M112.089,112.791v-20.57c0-5.69-4.6-10.29-10.29-10.29H81.229c-5.681,0-10.29,4.6-10.29,10.29v20.57
|
||||
c0,5.689,4.609,10.29,10.29,10.29h20.569C107.49,123.081,112.089,118.48,112.089,112.791z"/>
|
||||
<path fill="#383D4C" d="M225.25,56.21v5.14H60.649v-5.14c0-8.521,6.91-15.431,15.43-15.431h133.74
|
||||
C218.34,40.781,225.25,47.69,225.25,56.21z"/>
|
||||
<path fill="#FFFFFF" d="M214.96,143.65v20.58c0,5.681-4.601,10.29-10.279,10.29H184.1c-5.681,0-10.28-4.609-10.28-10.29v-20.58
|
||||
c0-5.68,4.601-10.279,10.28-10.279h20.58C210.359,133.371,214.96,137.971,214.96,143.65z"/>
|
||||
<path fill="#FFFFFF" d="M214.96,92.221v20.57c0,5.689-4.601,10.29-10.279,10.29H184.1c-5.681,0-10.28-4.601-10.28-10.29v-20.57
|
||||
c0-5.69,4.601-10.29,10.28-10.29h20.58C210.359,81.931,214.96,86.531,214.96,92.221z"/>
|
||||
<path fill="#FFFFFF" d="M163.53,246.53v20.57c0,5.69-4.61,10.29-10.29,10.29h-20.57c-5.689,0-10.29-4.6-10.29-10.29v-20.57
|
||||
c0-5.68,4.601-10.29,10.29-10.29h20.57C158.919,236.24,163.53,240.851,163.53,246.53z"/>
|
||||
<path fill="#FFFFFF" d="M163.53,195.091v20.58c0,5.68-4.61,10.279-10.29,10.279h-20.57c-5.689,0-10.29-4.601-10.29-10.279v-20.58
|
||||
c0-5.68,4.601-10.29,10.29-10.29h20.57C158.919,184.801,163.53,189.411,163.53,195.091z"/>
|
||||
<path fill="#FFFFFF" d="M163.53,143.65v20.58c0,5.681-4.61,10.29-10.29,10.29h-20.57c-5.689,0-10.29-4.609-10.29-10.29v-20.58
|
||||
c0-5.68,4.601-10.279,10.29-10.279h20.57C158.919,133.371,163.53,137.971,163.53,143.65z"/>
|
||||
<path fill="#FFFFFF" d="M163.53,92.221v20.57c0,5.689-4.61,10.29-10.29,10.29h-20.57c-5.689,0-10.29-4.601-10.29-10.29v-20.57
|
||||
c0-5.69,4.601-10.29,10.29-10.29h20.57C158.919,81.931,163.53,86.531,163.53,92.221z"/>
|
||||
<rect x="137.81" y="308.251" fill="#FFFFFF" width="10.29" height="10.29"/>
|
||||
<path fill="#FFFFFF" d="M112.089,246.53v20.57c0,5.69-4.6,10.29-10.29,10.29H81.229c-5.681,0-10.29-4.6-10.29-10.29v-20.57
|
||||
c0-5.68,4.609-10.29,10.29-10.29h20.569C107.49,236.24,112.089,240.851,112.089,246.53z"/>
|
||||
<path fill="#FFFFFF" d="M112.089,195.091v20.58c0,5.68-4.6,10.279-10.29,10.279H81.229c-5.681,0-10.29-4.601-10.29-10.279v-20.58
|
||||
c0-5.68,4.609-10.29,10.29-10.29h20.569C107.49,184.801,112.089,189.411,112.089,195.091z"/>
|
||||
<path fill="#FFFFFF" d="M112.089,143.65v20.58c0,5.681-4.6,10.29-10.29,10.29H81.229c-5.681,0-10.29-4.609-10.29-10.29v-20.58
|
||||
c0-5.68,4.609-10.279,10.29-10.279h20.569C107.49,133.371,112.089,137.971,112.089,143.65z"/>
|
||||
<path fill="#FFFFFF" d="M112.089,92.221v20.57c0,5.689-4.6,10.29-10.29,10.29H81.229c-5.681,0-10.29-4.601-10.29-10.29v-20.57
|
||||
c0-5.69,4.609-10.29,10.29-10.29h20.569C107.49,81.931,112.089,86.531,112.089,92.221z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="XMLID_1_">
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M202.769,155.37H191.58c-0.01-0.26-0.01-0.47-0.01-0.59c0-0.88,0.399-1.33,1.04-1.49l5.189-0.74
|
||||
l0.771-0.12l-0.24-0.33c-0.86-1.21-2.271-1.93-3.75-1.93c-2.54,0-4.61,2.07-4.61,4.61c0,2.54,2.07,4.61,4.61,4.61
|
||||
c1.72,0,3.31-0.98,4.1-2.5h3.891c-0.44,1.68-1.431,3.2-2.79,4.31c-1.46,1.19-3.311,1.84-5.2,1.84c-4.55,0-8.26-3.7-8.26-8.26
|
||||
c0-4.55,3.71-8.26,8.26-8.26c4.56,0,8.26,3.71,8.26,8.26c0,0.19,0,0.38-0.02,0.58L202.769,155.37z"/>
|
||||
<path fill="#383D4C" d="M194.58,159.39c-2.54,0-4.61-2.07-4.61-4.61c0-2.54,2.07-4.61,4.61-4.61c1.479,0,2.89,0.72,3.75,1.93
|
||||
l0.24,0.33l-0.771,0.12l-5.189,0.74c-0.641,0.16-1.04,0.61-1.04,1.49c0,0.12,0,0.33,0.01,0.59h11.189l0.051-0.01
|
||||
c0.02-0.2,0.02-0.39,0.02-0.58c0-4.55-3.7-8.26-8.26-8.26c-4.55,0-8.26,3.71-8.26,8.26c0,4.56,3.71,8.26,8.26,8.26
|
||||
c1.89,0,3.74-0.65,5.2-1.84c1.359-1.11,2.35-2.63,2.79-4.31h-3.891C197.889,158.41,196.3,159.39,194.58,159.39z M206.71,142.86
|
||||
v21.87l-24.38,3.5h-0.26v-25.37H206.71z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="13.54419mm"
|
||||
height="13.950245mm"
|
||||
viewBox="0 0 13.544191 13.950245"
|
||||
version="1.1"
|
||||
id="svg1050"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="e-declicIcon.svg">
|
||||
<defs
|
||||
id="defs1044" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9195959"
|
||||
inkscape:cx="60.399667"
|
||||
inkscape:cy="7.6386097"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata1047">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-0.82788564,-0.72169744)">
|
||||
<path
|
||||
d="m 105.0772,92.130958 c -2.50437,0 -4.5413,2.037292 -4.5413,4.542367 0,2.504369 2.03693,4.540955 4.5413,4.540955 1.03858,0 2.05458,-0.36019 2.85927,-1.01353 0.74859,-0.608189 1.28764,-1.443567 1.53141,-2.369256 h -2.13784 c -0.4325,0.839258 -1.30704,1.377244 -2.25284,1.377244 -1.3977,0 -2.53576,-1.137355 -2.53576,-2.535413 0,-1.399117 1.13806,-2.53612 2.53576,-2.53612 0.81421,0 1.58503,0.397228 2.06199,1.062214 l 0.13194,0.183444 -0.42263,0.06138 -2.85503,0.41028 c -0.35525,0.08784 -0.57397,0.334434 -0.57397,0.818798 0,0.06421 0.002,0.18168 0.005,0.322791 h 6.15209 l 0.0307,-0.0053 c 0.008,-0.108303 0.0117,-0.215547 0.0117,-0.3175 -1e-5,-2.505076 -2.03765,-4.542368 -4.54167,-4.542368"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.35277775"
|
||||
id="path989"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="M 0.82788564,0.72169744 V 14.671942 h 0.13793 L 14.372076,12.746128 V 0.72169744 Z"
|
||||
style="fill:#110b09;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.35277775"
|
||||
id="path965"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 7.4833696,2.203632 c -2.5043695,0 -4.5413083,2.0372916 -4.5413083,4.5423667 0,2.5043693 2.0369388,4.5409553 4.5413083,4.5409553 1.0385777,0 2.0545777,-0.360186 2.8592634,-1.013531 0.748595,-0.6081883 1.287639,-1.4435663 1.531409,-2.369255 H 9.7362083 C 9.3037027,8.7434264 8.4291667,9.2814124 7.4833696,9.2814124 c -1.3977056,0 -2.5357667,-1.1373557 -2.5357667,-2.5354137 0,-1.3991167 1.1380611,-2.5361195 2.5357667,-2.5361195 0.8142111,0 1.5850304,0.3972277 2.0619861,1.0622138 L 9.6772944,5.4555374 9.2546667,5.5169207 6.3996363,5.9272014 C 6.044389,6.0150434 5.8256667,6.2616347 5.8256667,6.7459987 c 0,0.064205 0.00177,0.1816803 0.00494,0.3227916 h 6.1520913 l 0.03069,-0.00529 c 0.0078,-0.108303 0.01164,-0.2155473 0.01164,-0.3175 C 12.025026,4.2409236 9.987382,2.203632 7.4833656,2.203632"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.35277775"
|
||||
id="path989-8"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2018 Google Inc.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
// importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js');
|
||||
|
||||
if (workbox) {
|
||||
console.log(`Yay! Workbox from backgroundSync-sw.js is loaded 🎉`);
|
||||
workbox.precaching.precacheAndRoute([]);
|
||||
|
||||
// const showNotification = (z) => {
|
||||
// console.log(z);
|
||||
// self.registration.showNotification('🔄 Votre message a été envoyé !', {
|
||||
// body: '🎉`🎉`🎉`'
|
||||
// });
|
||||
// };
|
||||
|
||||
const bgSyncPlugin = new workbox.backgroundSync.Plugin(
|
||||
'dashboardr-queue'
|
||||
);
|
||||
|
||||
const networkWithBackgroundSync = new workbox.strategies.NetworkOnly({
|
||||
plugins: [bgSyncPlugin],
|
||||
});
|
||||
|
||||
workbox.routing.registerRoute(
|
||||
/\/feedback/,
|
||||
networkWithBackgroundSync,
|
||||
'POST'
|
||||
);
|
||||
|
||||
} else {
|
||||
console.log(`Boo! Workbox from backgroundSync-sw.js didn't load 😬`);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
importScripts("https://www.gstatic.com/firebasejs/5.5.8/firebase-app.js")
|
||||
importScripts("https://www.gstatic.com/firebasejs/5.5.8/firebase-messaging.js")
|
||||
|
||||
if (firebase.messaging.isSupported()) {
|
||||
// Initialize Firebase
|
||||
var config = {
|
||||
apiKey: "AIzaSyArZab_4hUDgksQ4Dqdwv970JyRSlbGaKY",
|
||||
authDomain: "edeclicpwa.firebaseapp.com",
|
||||
databaseURL: "https://edeclicpwa.firebaseio.com",
|
||||
projectId: "edeclicpwa",
|
||||
storageBucket: "edeclicpwa.appspot.com",
|
||||
messagingSenderId: "130215947970"
|
||||
};
|
||||
firebase.initializeApp(config);
|
||||
|
||||
var messaging = firebase.messaging();
|
||||
|
||||
/**
|
||||
* Here is is the code snippet to initialize Firebase Messaging in the Service
|
||||
* Worker when your app is not hosted on Firebase Hosting.
|
||||
// [START initialize_firebase_in_sw]
|
||||
// Give the service worker access to Firebase Messaging.
|
||||
// Note that you can only use Firebase Messaging here, other Firebase libraries
|
||||
// are not available in the service worker.
|
||||
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js');
|
||||
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');
|
||||
// Initialize the Firebase app in the service worker by passing in the
|
||||
// messagingSenderId.
|
||||
firebase.initializeApp({
|
||||
'messagingSenderId': 'YOUR-SENDER-ID'
|
||||
});
|
||||
// Retrieve an instance of Firebase Messaging so that it can handle background
|
||||
// messages.
|
||||
const messaging = firebase.messaging();
|
||||
// [END initialize_firebase_in_sw]
|
||||
**/
|
||||
|
||||
|
||||
// If you would like to customize notifications that are received in the
|
||||
// background (Web app is closed or not in browser focus) then you should
|
||||
// implement this optional method.
|
||||
// [START background_handler]
|
||||
messaging.setBackgroundMessageHandler(function (payload) {
|
||||
console.log('[firebase-messaging-sw.js] Received background message ', payload);
|
||||
// Customize notification here
|
||||
var notificationTitle = 'Background Message Title';
|
||||
var notificationOptions = {
|
||||
body: 'Background Message body.',
|
||||
icon: '/firebase-logo.png'
|
||||
};
|
||||
|
||||
return self.registration.showNotification(notificationTitle,
|
||||
notificationOptions);
|
||||
});
|
||||
// [END background_handler]
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
Copyright 2018 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
(function() {
|
||||
function toArray(arr) {
|
||||
return Array.prototype.slice.call(arr);
|
||||
}
|
||||
|
||||
function promisifyRequest(request) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
request.onsuccess = function() {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function promisifyRequestCall(obj, method, args) {
|
||||
var request;
|
||||
var p = new Promise(function(resolve, reject) {
|
||||
request = obj[method].apply(obj, args);
|
||||
promisifyRequest(request).then(resolve, reject);
|
||||
});
|
||||
|
||||
p.request = request;
|
||||
return p;
|
||||
}
|
||||
|
||||
function promisifyCursorRequestCall(obj, method, args) {
|
||||
var p = promisifyRequestCall(obj, method, args);
|
||||
return p.then(function(value) {
|
||||
if (!value) return;
|
||||
return new Cursor(value, p.request);
|
||||
});
|
||||
}
|
||||
|
||||
function proxyProperties(ProxyClass, targetProp, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
Object.defineProperty(ProxyClass.prototype, prop, {
|
||||
get: function() {
|
||||
return this[targetProp][prop];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function proxyRequestMethods(ProxyClass, targetProp, Constructor, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
if (!(prop in Constructor.prototype)) return;
|
||||
ProxyClass.prototype[prop] = function() {
|
||||
return promisifyRequestCall(this[targetProp], prop, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function proxyMethods(ProxyClass, targetProp, Constructor, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
if (!(prop in Constructor.prototype)) return;
|
||||
ProxyClass.prototype[prop] = function() {
|
||||
return this[targetProp][prop].apply(this[targetProp], arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function proxyCursorRequestMethods(ProxyClass, targetProp, Constructor, properties) {
|
||||
properties.forEach(function(prop) {
|
||||
if (!(prop in Constructor.prototype)) return;
|
||||
ProxyClass.prototype[prop] = function() {
|
||||
return promisifyCursorRequestCall(this[targetProp], prop, arguments);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function Index(index) {
|
||||
this._index = index;
|
||||
}
|
||||
|
||||
proxyProperties(Index, '_index', [
|
||||
'name',
|
||||
'keyPath',
|
||||
'multiEntry',
|
||||
'unique'
|
||||
]);
|
||||
|
||||
proxyRequestMethods(Index, '_index', IDBIndex, [
|
||||
'get',
|
||||
'getKey',
|
||||
'getAll',
|
||||
'getAllKeys',
|
||||
'count'
|
||||
]);
|
||||
|
||||
proxyCursorRequestMethods(Index, '_index', IDBIndex, [
|
||||
'openCursor',
|
||||
'openKeyCursor'
|
||||
]);
|
||||
|
||||
function Cursor(cursor, request) {
|
||||
this._cursor = cursor;
|
||||
this._request = request;
|
||||
}
|
||||
|
||||
proxyProperties(Cursor, '_cursor', [
|
||||
'direction',
|
||||
'key',
|
||||
'primaryKey',
|
||||
'value'
|
||||
]);
|
||||
|
||||
proxyRequestMethods(Cursor, '_cursor', IDBCursor, [
|
||||
'update',
|
||||
'delete'
|
||||
]);
|
||||
|
||||
// proxy 'next' methods
|
||||
['advance', 'continue', 'continuePrimaryKey'].forEach(function(methodName) {
|
||||
if (!(methodName in IDBCursor.prototype)) return;
|
||||
Cursor.prototype[methodName] = function() {
|
||||
var cursor = this;
|
||||
var args = arguments;
|
||||
return Promise.resolve().then(function() {
|
||||
cursor._cursor[methodName].apply(cursor._cursor, args);
|
||||
return promisifyRequest(cursor._request).then(function(value) {
|
||||
if (!value) return;
|
||||
return new Cursor(value, cursor._request);
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
function ObjectStore(store) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
ObjectStore.prototype.createIndex = function() {
|
||||
return new Index(this._store.createIndex.apply(this._store, arguments));
|
||||
};
|
||||
|
||||
ObjectStore.prototype.index = function() {
|
||||
return new Index(this._store.index.apply(this._store, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(ObjectStore, '_store', [
|
||||
'name',
|
||||
'keyPath',
|
||||
'indexNames',
|
||||
'autoIncrement'
|
||||
]);
|
||||
|
||||
proxyRequestMethods(ObjectStore, '_store', IDBObjectStore, [
|
||||
'put',
|
||||
'add',
|
||||
'delete',
|
||||
'clear',
|
||||
'get',
|
||||
'getAll',
|
||||
'getAllKeys',
|
||||
'count'
|
||||
]);
|
||||
|
||||
proxyCursorRequestMethods(ObjectStore, '_store', IDBObjectStore, [
|
||||
'openCursor',
|
||||
'openKeyCursor'
|
||||
]);
|
||||
|
||||
proxyMethods(ObjectStore, '_store', IDBObjectStore, [
|
||||
'deleteIndex'
|
||||
]);
|
||||
|
||||
function Transaction(idbTransaction) {
|
||||
this._tx = idbTransaction;
|
||||
this.complete = new Promise(function(resolve, reject) {
|
||||
idbTransaction.oncomplete = function() {
|
||||
resolve();
|
||||
};
|
||||
idbTransaction.onerror = function() {
|
||||
reject(idbTransaction.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Transaction.prototype.objectStore = function() {
|
||||
return new ObjectStore(this._tx.objectStore.apply(this._tx, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(Transaction, '_tx', [
|
||||
'objectStoreNames',
|
||||
'mode'
|
||||
]);
|
||||
|
||||
proxyMethods(Transaction, '_tx', IDBTransaction, [
|
||||
'abort'
|
||||
]);
|
||||
|
||||
function UpgradeDB(db, oldVersion, transaction) {
|
||||
this._db = db;
|
||||
this.oldVersion = oldVersion;
|
||||
this.transaction = new Transaction(transaction);
|
||||
}
|
||||
|
||||
UpgradeDB.prototype.createObjectStore = function() {
|
||||
return new ObjectStore(this._db.createObjectStore.apply(this._db, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(UpgradeDB, '_db', [
|
||||
'name',
|
||||
'version',
|
||||
'objectStoreNames'
|
||||
]);
|
||||
|
||||
proxyMethods(UpgradeDB, '_db', IDBDatabase, [
|
||||
'deleteObjectStore',
|
||||
'close'
|
||||
]);
|
||||
|
||||
function DB(db) {
|
||||
this._db = db;
|
||||
}
|
||||
|
||||
DB.prototype.transaction = function() {
|
||||
return new Transaction(this._db.transaction.apply(this._db, arguments));
|
||||
};
|
||||
|
||||
proxyProperties(DB, '_db', [
|
||||
'name',
|
||||
'version',
|
||||
'objectStoreNames'
|
||||
]);
|
||||
|
||||
proxyMethods(DB, '_db', IDBDatabase, [
|
||||
'close'
|
||||
]);
|
||||
|
||||
// Add cursor iterators
|
||||
// TODO: remove this once browsers do the right thing with promises
|
||||
['openCursor', 'openKeyCursor'].forEach(function(funcName) {
|
||||
[ObjectStore, Index].forEach(function(Constructor) {
|
||||
Constructor.prototype[funcName.replace('open', 'iterate')] = function() {
|
||||
var args = toArray(arguments);
|
||||
var callback = args[args.length - 1];
|
||||
var request = (this._store || this._index)[funcName].apply(this._store, args.slice(0, -1));
|
||||
request.onsuccess = function() {
|
||||
callback(request.result);
|
||||
};
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// polyfill getAll
|
||||
[Index, ObjectStore].forEach(function(Constructor) {
|
||||
if (Constructor.prototype.getAll) return;
|
||||
Constructor.prototype.getAll = function(query, count) {
|
||||
var instance = this;
|
||||
var items = [];
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
instance.iterateCursor(query, function(cursor) {
|
||||
if (!cursor) {
|
||||
resolve(items);
|
||||
return;
|
||||
}
|
||||
items.push(cursor.value);
|
||||
|
||||
if (count !== undefined && items.length == count) {
|
||||
resolve(items);
|
||||
return;
|
||||
}
|
||||
cursor.continue();
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
var exp = {
|
||||
open: function(name, version, upgradeCallback) {
|
||||
var p = promisifyRequestCall(indexedDB, 'open', [name, version]);
|
||||
var request = p.request;
|
||||
|
||||
request.onupgradeneeded = function(event) {
|
||||
if (upgradeCallback) {
|
||||
upgradeCallback(new UpgradeDB(request.result, event.oldVersion, request.transaction));
|
||||
}
|
||||
};
|
||||
|
||||
return p.then(function(db) {
|
||||
return new DB(db);
|
||||
});
|
||||
},
|
||||
delete: function(name) {
|
||||
return promisifyRequestCall(indexedDB, 'deleteDatabase', [name]);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = exp;
|
||||
}
|
||||
else {
|
||||
self.idb = exp;
|
||||
}
|
||||
}());
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
|
||||
function promptInstall() {
|
||||
if (deferredPrompt == null) {
|
||||
console.log('beforeinstallprompt has not been triggerred yet');
|
||||
return;
|
||||
}
|
||||
// never show it if the app was launched standalone
|
||||
if (navigator.standalone) {
|
||||
return;
|
||||
}
|
||||
// Show the prompt
|
||||
deferredPrompt.prompt();
|
||||
// Wait for the user to respond to the prompt
|
||||
deferredPrompt.userChoice
|
||||
.then((choiceResult) => {
|
||||
if (choiceResult.outcome === 'accepted') {
|
||||
console.log('User accepted the A2HS prompt');
|
||||
} else {
|
||||
console.log('User dismissed the A2HS prompt');
|
||||
}
|
||||
deferredPrompt = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var deferredPrompt;
|
||||
|
||||
/// cf https://developers.google.com/web/fundamentals/app-install-banners/
|
||||
///
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
console.log('beforeinstallprompt triggered');
|
||||
// Prevent Chrome 67 and earlier from automatically showing the prompt
|
||||
e.preventDefault();
|
||||
// Stash the event so it can be triggered later.
|
||||
deferredPrompt = e;
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', (evt) => {
|
||||
console.log('a2hs installed');
|
||||
});
|
||||
|
||||
const isInStandaloneMode = () =>
|
||||
(window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone);
|
||||
|
||||
console.log('EdeclicLib loaded');
|
||||
|
||||
|
||||
|
||||
// window.addEventListener('error', function (e) {
|
||||
// var errorText = [
|
||||
// e.message,
|
||||
// 'URL: ' + e.filename,
|
||||
// 'Line: ' + e.lineno + ', Column: ' + e.colno,
|
||||
// 'Stack: ' + (e.error && e.error.stack || '(no stack trace)')
|
||||
// ].join('');
|
||||
// console.log(errorText);
|
||||
// // Example: log errors as visual output into the host page.
|
||||
// // Note: you probably don’t want to show such errors to users, or
|
||||
// // have the errors get indexed by Googlebot; however, it may
|
||||
// // be a useful feature while actively debugging the page.
|
||||
// var DOM_ID = 'rendering-debug-pre';
|
||||
// if (!document.getElementById(DOM_ID)) {
|
||||
// var log = document.createElement('pre');
|
||||
// log.id = DOM_ID;
|
||||
// log.style.whiteSpace = 'pre-wrap';
|
||||
// log.textContent = errorText;
|
||||
// if (!document.body) document.body = document.createElement('body');
|
||||
// document.body.insertBefore(log, document.body.firstChild);
|
||||
// } else {
|
||||
// document.getElementById(DOM_ID).textContent += ' ' + errorText;
|
||||
// }
|
||||
// });
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "E-Declic PWA",
|
||||
"short_name": "E-Declic",
|
||||
"lang": "fr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco16x16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco32x32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco57x57.png",
|
||||
"sizes": "57x57",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco114x114.png",
|
||||
"sizes": "114x114",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco180x180.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/icons/e-declicIco512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#21b8cc",
|
||||
"gcm_sender_id":"103953800507"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import ReactGA from 'react-ga'
|
||||
|
||||
export const initGA = () => {
|
||||
ReactGA.initialize('UA-129143411-1')
|
||||
}
|
||||
|
||||
export const logPageView = () => {
|
||||
ReactGA.set({ page: window.location.pathname })
|
||||
ReactGA.pageview(window.location.pathname)
|
||||
}
|
||||
|
||||
export const logEvent = (category = '', action = '') => {
|
||||
if (category && action) {
|
||||
ReactGA.event({ category, action })
|
||||
}
|
||||
}
|
||||
|
||||
export const logException = (description = '', fatal = false) => {
|
||||
if (description) {
|
||||
ReactGA.exception({ description, fatal })
|
||||
}
|
||||
}
|
||||