Base du projet 'application ticketing'

This commit is contained in:
2020-02-17 22:28:57 +01:00
parent 2a72ac97ac
commit afa734f96d
64 changed files with 1798 additions and 685 deletions

View File

View File

@ -1,20 +0,0 @@
export const SEND_MESSAGE = 'SEND_MESSAGE'
export const SEND_MESSAGE_SUCCESS = 'SEND_MESSAGE_SUCCESS';
export const SEND_MESSAGE_FAILURE = 'SEND_MESSAGE_FAILURE';
export function sendMessage (channel, text) {
return { type: SEND_MESSAGE, channel, text }
}
export const FETCH_MESSAGES = 'FETCH_MESSAGES'
export const FETCH_MESSAGES_SUCCESS = 'FETCH_MESSAGES_SUCCESS';
export const FETCH_MESSAGES_FAILURE = 'FETCH_MESSAGES_FAILURE';
export function fetchMessages (channel) {
return { type: FETCH_MESSAGES, channel }
}
export const STREAM_EVENTS = 'STREAM_EVENTS'
export function streamEvents (channel) {
return { type: STREAM_EVENTS, channel }
}

View File

@ -1,7 +0,0 @@
export const LOGIN = 'LOGIN'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export function login (username, password) {
return { type: LOGIN, username, password }
}

View File

@ -1,11 +0,0 @@
export const ADD_PRODUCT = 'ADD_PRODUCT'
export function addProduct (name, price) {
return {type: ADD_PRODUCT, product: {name, price}}
}
export const REMOVE_PRODUCT = 'REMOVE_PRODUCT'
export function removeProduct (name) {
return {type: REMOVE_PRODUCT, productName: name}
}

View File

@ -1,12 +1,8 @@
import { Component, Fragment } from 'react'
import { hot } from 'react-hot-loader'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route, Switch, Redirect } from 'react-router'
import LoginPage from './pages/login';
import ChatPage from './pages/chat';
require('bulma/css/bulma.min.css')
import HomePage from './pages/home';
class App extends Component {
render () {
@ -14,10 +10,8 @@ class App extends Component {
<Fragment>
<HashRouter>
<Switch>
<Route path='/login' exact component={LoginPage} />
<Route path='/chat/:channel' component={ChatPage} />
<Route path='/chat' component={ChatPage} />
<Route component={() => <Redirect to="/login" />} />
<Route path='/home' exact component={HomePage} />
<Route component={() => <Redirect to="/home" />} />
</Switch>
</HashRouter>
</Fragment>

View File

View File

@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Counter snapshot 1`] = `
<div>
Count:
<span>
0
</span>
 
<button
onClick={[Function]}
>
+1
</button>
<button
onClick={[Function]}
>
-1
</button>
</div>
`;

View File

@ -1,32 +0,0 @@
import React from 'react'
export default class Clock extends React.Component {
constructor(props) {
super(props)
// Initialisation du "state" du composant
this.state = {
time: new Date(),
foo: "bar"
}
this.tick = this.tick.bind(this);
// On appelle la méthode tick() du composant
// toutes les secondes
setInterval(this.tick, this.props.interval);
}
// Méthode de rendu du composant
render() {
return (
<div>Time: { this.state.time.toString() }</div>
)
}
// La méthode tick() met à jour le state du composant avec
// la date courante
tick() {
this.setState({ time: new Date() });
}
}

View File

@ -1,33 +0,0 @@
import React from 'react'
export default class Counter extends React.Component {
constructor(props) {
super(props)
// Initialisation du "state" du composant
this.state = {
count: 0
}
// On "lie" les méthodes de la classe à l'instance
this.increment = this.increment.bind(this)
this.decrement = this.decrement.bind(this)
}
// Méthode de rendu du composant
render() {
console.log(this.props.match);
return (
<div>
Count: <span>{ this.state.count }</span>&nbsp;
<button onClick={ this.increment }>+1</button>
<button onClick={ this.decrement }>-1</button>
</div>
)
}
// La méthode increment() incrémente la valeur du compteur de 1
increment() {
this.setState(prevState => ({ count: prevState.count+1 }))
}
// La méthode decrement() décrémente la valeur du compteur de 1
decrement() {
this.setState(prevState => ({ count: prevState.count-1 }))
}
}

View File

@ -1,26 +0,0 @@
/* globals test, expect */
import React from 'react';
import Counter from './counter'
import renderer from 'react-test-renderer'
test('Counter snapshot', () => {
const component = renderer.create(<Counter />)
let tree = component.toJSON()
// Vérifier que le composant n'a pas changé depuis le dernier
// snapshot.
// Voir https://facebook.github.io/jest/docs/en/snapshot-testing.html
// pour plus d'informations
expect(tree).toMatchSnapshot()
// L'API expect() de Jest est disponible à l'adresse
// https://facebook.github.io/jest/docs/en/expect.html
// Il est possible d'effectuer des vérifications plus avancées
// grâce au projet Enzyme (vérification du DOM, etc)
// Voir http://airbnb.io/enzyme/ et
// https://facebook.github.io/jest/docs/en/tutorial-react.html#dom-testing
})

View File

@ -1,41 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { addProduct } from '../actions/products'
class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(evt) {
this.setState({ name: evt.target.value });
}
handleSubmit(evt) {
console.log(`Votre nom est ${this.state.name}`);
evt.preventDefault();
}
componentDidMount() {
this.props.dispatch(addProduct('pomme', 10));
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Nom:
<input type="text" value={this.state.name} onChange={this.handleChange} />
</label>
<input type="submit" value="Soumettre" />
</form>
);
}
}
export default connect()(MyForm)

View File

@ -2,7 +2,7 @@
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Insight</title>
<title>PleaseWait</title>
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
<% } %>

View File

@ -2,6 +2,7 @@ import ReactDOM from 'react-dom'
import App from './app'
import { configureStore } from './store/store'
import { Provider } from 'react-redux'
import 'bulma/css/bulma.min.css';
const store = configureStore()

View File

@ -1,82 +0,0 @@
import React from 'react'
import Page from './page';
import { connect } from 'react-redux';
import { fetchMessages, sendMessage, streamEvents } from '../actions/chat';
export class ChatPage extends React.Component {
constructor(props) {
super(props);
this.state = {
message: ''
}
this.handleSendMessage = this.handleSendMessage.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
render() {
const channel = "default";
const { chat } = this.props;
const messages = channel in chat.messagesByChannel ? chat.messagesByChannel[channel] : [];
const users = [];
return (
<Page>
<div className="columns">
<div className="column is-8 is-offset-2">
<div className="box">
<div className="tile">
<div className="tile is-parent is-vertical is-10">
<div className="tile is-child">
<b>Chat</b>
<ul>
{
messages.map(msg => {
return (
<li key={msg.ID}><span className="is-uppercase">[{msg.Username}]</span> <span>{msg.Text}</span></li>
)
})
}
</ul>
<div className="field">
<div className="control">
<input className="input" type="text" placeholder="Hello everyone !"
value={this.state.message}
onChange={this.handleSendMessage}
onKeyDown={this.handleKeyDown} />
</div>
</div>
</div>
</div>
<div className="tile is-parent is-2">
<b>Utilisateurs</b>
</div>
</div>
</div>
</div>
</div>
</Page>
);
}
componentDidMount() {
this.props.dispatch(fetchMessages("default"));
this.props.dispatch(streamEvents("default"));
}
handleSendMessage(evt) {
this.setState({ message: evt.target.value });
}
handleKeyDown(evt) {
if (evt.keyCode !== 13) return;
this.props.dispatch(sendMessage("default", this.state.message));
this.setState({message: ""});
}
}
export default connect(state => {
return {
chat: state.chat
}
})(ChatPage)

View File

@ -0,0 +1,15 @@
import React from 'react'
import Page from './page';
export default class HomePage extends React.PureComponent {
render() {
return (
<Page title="home">
<div className="section">
<h1 className="title">Bienvenue sur PleaseWait !</h1>
<h2 className="subtitle">Le gestionnaire de ticket simplifié.</h2>
</div>
</Page>
);
}
}

View File

@ -1,53 +0,0 @@
import React from 'react'
import Page from './page';
import { connect } from 'react-redux';
import { login } from '../actions/login';
export class LoginPage extends React.Component {
constructor(props) {
super(props);
this.login = this.login.bind(this);
}
render() {
return (
<Page>
<div className="columns">
<div className="column is-4 is-offset-4">
<div className="box">
<div className="field">
<label className="label">Login</label>
<div className="control">
<input type="text" className="input" placeholder="My login..." />
</div>
</div>
<div className="field">
<label className="label">Password</label>
<div className="control">
<input className="input" type="password" />
</div>
</div>
<button className="button is-primary" onClick={this.login}>Login</button>
</div>
</div>
</div>
</Page>
);
}
componentDidUpdate() {
if (this.props.user.isLoggedIn) this.props.history.push("/chat");
}
login() {
this.props.dispatch(login("foo", "bar"))
}
}
export default connect(state => {
return {
user: state.user
}
})(LoginPage)

View File

@ -1,11 +1,13 @@
import React from 'react'
import React, { useEffect } from 'react'
export default class Page extends React.PureComponent {
render() {
return (
<div className="container-fluid">
{ this.props.children }
</div>
);
}
export default function Page({ title, children }) {
useEffect(() => {
document.title = title ? `${title } - PleaseWait` : 'PleaseWait';
});
return (
<div className="container-fluid">
{ children }
</div>
);
}

View File

View File

@ -1,35 +0,0 @@
import { LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/login';
import { FETCH_MESSAGES_SUCCESS } from '../actions/chat';
const defaultState = {
messagesByChannel: {},
}
export default function chatReducer(state = defaultState, action) {
switch (action.type) {
case FETCH_MESSAGES_SUCCESS:
return {
...state,
messagesByChannel: {
...state.messagesByChannel,
[action.channel]: [...action.data.Messages]
}
};
case 'CHANNEL_EVENT':
switch(action.event) {
case "message":
return {
...state,
messagesByChannel: {
...state.messagesByChannel,
[action.data.Channel]: [
...state.messagesByChannel[action.data.Channel],
action.data.Message
]
}
};
}
return state;
}
return state;
}

View File

@ -1,16 +0,0 @@
import { LOGIN_SUCCESS, LOGIN_FAILURE } from '../actions/login';
const defaultState = {
isLoggedIn: false,
username: null,
}
export default function loginReducer(state = defaultState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, isLoggedIn: true, username: action.data.Username };
case LOGIN_FAILURE:
return { ...state, isLoggedIn: false, username: null };
}
return state;
}

View File

@ -1,31 +0,0 @@
import { ADD_PRODUCT, REMOVE_PRODUCT } from '../actions/products';
export const rootReducer = (state, action) => {
console.log(`Action: ${JSON.stringify(action)}`)
switch (action.type) {
case ADD_PRODUCT:
// L'action est de type ADD_PRODUCT
// On ajoute le produit dans la liste et
// on retourne un nouvel état modifié
return {
products: [...state.products, action.product]
}
case REMOVE_PRODUCT:
// L'action est de type REMOVE_PRODUCT
// On filtre la liste des produits et on
// retourne un nouvel état modifié
return {
products: state.products.filter(p => p.name !== action.productName)
}
}
// Si l'action n'est pas gérée, on retourne l'état
// sans le modifier
return state
}

View File

@ -1,98 +0,0 @@
import { call, put, take } from 'redux-saga/effects';
import { eventChannel } from 'redux-saga'
import {
SEND_MESSAGE_FAILURE, SEND_MESSAGE_SUCCESS,
FETCH_MESSAGES_FAILURE, FETCH_MESSAGES_SUCCESS
} from '../actions/chat';
export function* sendMessageSaga(action) {
let result;
try {
result = yield call(sendMessage, action.channel, action.text);
} catch(err) {
yield put({ type: SEND_MESSAGE_FAILURE, err });
}
if ('Error' in result) {
yield put({type: SEND_MESSAGE_FAILURE, err: result.Error});
return
}
yield put({type: SEND_MESSAGE_SUCCESS, data: result.Data });
}
function sendMessage(channel, text) {
return fetch(`http://192.168.0.126:3000/channels/${channel}`, {
method: 'POST',
body: JSON.stringify({
Text: text,
}),
mode: 'cors',
credentials: 'include'
})
.then(res => res.json())
}
export function* fetchMessagesSaga(action) {
let result;
try {
result = yield call(fetchMessages, action.channel);
} catch(err) {
yield put({ type: FETCH_MESSAGES_FAILURE, err });
}
if ('Error' in result) {
yield put({type: FETCH_MESSAGES_FAILURE, err: result.Error});
return
}
yield put({type: FETCH_MESSAGES_SUCCESS, channel: action.channel, data: result.Data });
}
function fetchMessages(channel) {
return fetch(`http://192.168.0.126:3000/channels/${channel}`, {
mode: 'cors',
credentials: 'include'
})
.then(res => res.json())
}
function channelEvents(channel) {
return eventChannel(emitter => {
const eventSource = new EventSource(
`http://192.168.0.126:3000/channels/${channel}/stream`,
{ withCredentials: true }
);
const emit = evt => {
emitter({type: evt.type, data: evt.data});
};
eventSource.addEventListener("joined", emit);
eventSource.addEventListener("left", emit);
eventSource.addEventListener("message", emit);
return () => {
eventSource.removeEventListener("joined", emit)
eventSource.removeEventListener("left", emit)
eventSource.removeEventListener("message", emit)
eventSource.close()
}
}
)
}
export function* streamEventsSaga(action) {
const stream = yield call(channelEvents, action.channel)
while (true) {
let event = yield take(stream)
yield put({ type: 'CHANNEL_EVENT', event: event.type, data: JSON.parse(event.data) });
}
}

View File

@ -1,34 +0,0 @@
import { call, put } from 'redux-saga/effects';
import { LOGIN_FAILURE, LOGIN_SUCCESS } from '../actions/login';
export default function* loginSaga(action) {
let result;
try {
result = yield call(doLogin, action.username, action.password);
} catch(err) {
yield put({ type: LOGIN_FAILURE, err });
}
if ('Error' in result) {
yield put({type: LOGIN_FAILURE, err: result.Error});
return
}
yield put({type: LOGIN_SUCCESS, data: result.Data });
}
function doLogin(username, password) {
return fetch('http://192.168.0.126:3000/login', {
method: 'POST',
body: JSON.stringify({
Username: username,
Password: password
}),
mode: 'cors',
credentials: 'include'
})
.then(res => res.json())
}

View File

@ -1,14 +1,7 @@
import { all, takeLatest } from 'redux-saga/effects';
import loginSaga from './login';
import { LOGIN } from '../actions/login';
import { SEND_MESSAGE, FETCH_MESSAGES, STREAM_EVENTS } from '../actions/chat';
import { sendMessageSaga, fetchMessagesSaga, streamEventsSaga } from './chat';
export default function* rootSaga() {
yield all([
takeLatest(LOGIN, loginSaga),
takeLatest(SEND_MESSAGE, sendMessageSaga),
takeLatest(FETCH_MESSAGES, fetchMessagesSaga),
takeLatest(STREAM_EVENTS, streamEventsSaga)
]);
}

View File

@ -1,14 +1,11 @@
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'
import loginReducer from '../reducers/login'
import rootSaga from '../sagas/root'
import createSagaMiddleware from 'redux-saga'
import chatReducer from '../reducers/chat';
import rootSaga from '../sagas/root'
const sagaMiddleware = createSagaMiddleware()
const rootReducer = combineReducers({
user: loginReducer,
chat: chatReducer,
// Ajouter vos reducers ici
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

View File

@ -1,39 +0,0 @@
/* globals test, expect, jest */
import { addProduct, removeProduct } from '../actions/products'
import { configureStore } from './store'
test('Ajout/suppression des produits', () => {
// On crée une instance de notre store
// avec le state par défaut
const store = configureStore()
// On crée un "faux" subscriber
// pour vérifier que l'état du store
// a bien été modifié le nombre de fois voulu
const subscriber = jest.fn()
// On attache notre faux subscriber
// au store
store.subscribe(subscriber)
// On "dispatch" nos actions
store.dispatch(addProduct('pomme', 5))
store.dispatch(addProduct('orange', 7))
store.dispatch(addProduct('orange', 10))
store.dispatch(removeProduct('pomme'))
// On s'assure que notre subscriber a bien été
// appelé
expect(subscriber).toHaveBeenCalledTimes(4)
const state = store.getState()
// On s'assure que l'état du store correspond
// à ce qu'on attend
expect(state).toMatchObject({
products: [
{name: 'orange', price: 7}
]
})
})