chore(project): bootstrap project tree
This commit is contained in:
2
client/.gitignore
vendored
Normal file
2
client/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/dist
|
10925
client/package-lock.json
generated
Normal file
10925
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
client/package.json
Normal file
69
client/package.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "guesstimate",
|
||||
"version": "0.0.0",
|
||||
"description": "Guesstimate",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"server": "webpack-dev-server"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://forge.cadoles.com/Cadoles/daddy.git"
|
||||
},
|
||||
"author": "William Petit <wpetit@cadoles.com>",
|
||||
"license": "AGPL-3.0",
|
||||
"bugs": {
|
||||
"url": "https://forge.cadoles.com/Cadoles/daddy/issues"
|
||||
},
|
||||
"homepage": "https://forge.cadoles.com/Cadoles/daddy#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@types/node": "^13.13.4",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/uuid": "^7.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.0.2",
|
||||
"css-loader": "^1.0.1",
|
||||
"extract-loader": "^3.1.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"node-sass": "^4.14.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"resolve-url-loader": "^3.0.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^7.0.2",
|
||||
"webpack": "^4.25.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.0.2",
|
||||
"@types/qs": "^6.9.3",
|
||||
"bulma": "^0.9.0",
|
||||
"graphql": "^15.3.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"redux": "^4.0.4",
|
||||
"redux-saga": "^1.1.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"subscriptions-transport-ws": "^0.9.17",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
18
client/src/components/App.tsx
Normal file
18
client/src/components/App.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Route, Redirect, Switch } from "react-router-dom";
|
||||
import { HomePage } from './HomePage/HomePage';
|
||||
import { ProfilePage } from './ProfilePage/ProfilePage';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/profile" exact component={ProfilePage} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
15
client/src/components/HomePage/Dashboard.tsx
Normal file
15
client/src/components/HomePage/Dashboard.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<div className="columns">
|
||||
<div className="column is-6">
|
||||
</div>
|
||||
<div className="column is-3">
|
||||
</div>
|
||||
<div className="column is-3">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
21
client/src/components/HomePage/HomePage.tsx
Normal file
21
client/src/components/HomePage/HomePage.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { Dashboard } from './Dashboard';
|
||||
import { useUserProfile } from '../../gql/queries/user';
|
||||
import { WelcomeContent } from './WelcomeContent';
|
||||
|
||||
export function HomePage() {
|
||||
const { user, loading } = useUserProfile();
|
||||
|
||||
return (
|
||||
<Page title={user.id ? 'Tableau de bord' : 'Accueil'}>
|
||||
<section className="mt-5">
|
||||
{
|
||||
user.id ?
|
||||
<Dashboard /> :
|
||||
<WelcomeContent />
|
||||
}
|
||||
</section>
|
||||
</Page>
|
||||
);
|
||||
}
|
121
client/src/components/HomePage/ItemPanel.tsx
Normal file
121
client/src/components/HomePage/ItemPanel.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React, { FunctionComponent, useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { WithLoader } from "../WithLoader";
|
||||
|
||||
export interface Item {
|
||||
id: string
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface TabDefinition {
|
||||
label: string
|
||||
itemFilter?: (item: Item) => boolean
|
||||
}
|
||||
|
||||
|
||||
export interface ItemPanelProps {
|
||||
className?: string
|
||||
itemIconClassName?: string
|
||||
title?: string
|
||||
newItemUrl: string
|
||||
isLoading?: boolean
|
||||
items: Item[]
|
||||
tabs?: TabDefinition[],
|
||||
itemKey: (item: Item, index: number) => string
|
||||
itemLabel: (item: Item, index: number) => string
|
||||
itemUrl: (item: Item, index: number) => string
|
||||
}
|
||||
|
||||
export const ItemPanel: FunctionComponent<ItemPanelProps> = (props) => {
|
||||
const {
|
||||
title, className, newItemUrl,
|
||||
itemKey, itemLabel,
|
||||
itemIconClassName, itemUrl
|
||||
} = props;
|
||||
|
||||
const [ state, setState ] = useState({ selectedTab: 0, filteredItems: [] });
|
||||
|
||||
const filterItemsForTab = (tab: TabDefinition, items: Item[]) => {
|
||||
const itemFilter = tab && typeof tab.itemFilter === 'function' ? tab.itemFilter : () => true;
|
||||
return items.filter(itemFilter);
|
||||
};
|
||||
|
||||
const selectTab = (tabIndex: number) => {
|
||||
setState(state => {
|
||||
const { tabs, items } = props;
|
||||
const newTab = Array.isArray(tabs) && tabs.length > 0 ? tabs[tabIndex] : null;
|
||||
return {
|
||||
...state,
|
||||
selectedTab: tabIndex,
|
||||
filteredItems: filterItemsForTab(newTab, items)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setState(state => {
|
||||
const { tabs, items } = props;
|
||||
const newTab = Array.isArray(tabs) && tabs.length > 0 ? tabs[state.selectedTab] : null;
|
||||
return {
|
||||
...state,
|
||||
filteredItems: filterItemsForTab(newTab, items),
|
||||
}
|
||||
});
|
||||
}, [props.items, props.tabs]);
|
||||
|
||||
const itemElements = state.filteredItems.map((item: Item, i: number) => {
|
||||
return (
|
||||
<Link to={itemUrl(item, i)} key={`item-${itemKey(item, i)}`} className="panel-block">
|
||||
<span className="panel-icon">
|
||||
<i className={itemIconClassName} aria-hidden="true"></i>
|
||||
</span>
|
||||
{itemLabel(item, i)}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
const tabs = props.tabs || [];
|
||||
|
||||
return (
|
||||
<nav className={`panel ${className}`}>
|
||||
<div className="level is-mobile panel-heading mb-0">
|
||||
<div className="level-left">
|
||||
<p className="level-item">{title}</p>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<Link to={newItemUrl} className="button level-item is-outlined is-info is-inverted">
|
||||
<i className="icon fa fa-plus"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel-block">
|
||||
<p className="control has-icons-left">
|
||||
<input className="input" type="text" placeholder="Filtrer..." />
|
||||
<span className="icon is-left">
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="panel-tabs">
|
||||
{
|
||||
tabs.map((tab, i) => {
|
||||
return (
|
||||
<a key={`workgroup-tab-${i}`}
|
||||
onClick={selectTab.bind(null, i)}
|
||||
className={i === state.selectedTab ? 'is-active' : ''}>
|
||||
{tab.label}
|
||||
</a>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
{
|
||||
itemElements.length > 0 ?
|
||||
itemElements :
|
||||
<a className="panel-block has-text-centered is-block">
|
||||
<em>Aucun élément pour l'instant.</em>
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
)
|
||||
};
|
75
client/src/components/HomePage/WelcomeContent.tsx
Normal file
75
client/src/components/HomePage/WelcomeContent.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React, { FunctionComponent, Fragment } from "react";
|
||||
|
||||
export interface WelcomeContentProps {
|
||||
|
||||
}
|
||||
|
||||
export const WelcomeContent: FunctionComponent<WelcomeContentProps> = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<section className="hero is-normal is-light is-bold">
|
||||
<div className="hero-body has-text-centered">
|
||||
<div className="container">
|
||||
<h1 className="title">
|
||||
Bienvenue sur Guesstimate !
|
||||
</h1>
|
||||
<h2 className="subtitle">
|
||||
La solution pour réaliser simplement des estimations de temps pour vos projets !
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="box cta">
|
||||
<p className="has-text-centered">
|
||||
<span className="tag is-info">Attention</span> Le service est actuellement en alpha. L'accès est restreint aux adresses autorisées.
|
||||
</p>
|
||||
</div>
|
||||
<section className="container mt-5">
|
||||
<div className="columns">
|
||||
<div className="column is-4">
|
||||
<div className="card is-shady">
|
||||
<div className="card-image has-text-centered">
|
||||
<i className="fa fa-at mt-5" style={{fontSize: '8rem'}}></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="content">
|
||||
<h4>Une adresse courriel et c'est parti !</h4>
|
||||
<p>Pas de création de compte, pas de mot de passe à retenir. Entrez votre adresse courriel et commencez directement à travailler !</p>
|
||||
{/* <p><a href="#">En savoir plus</a></p> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-4">
|
||||
<div className="card is-shady">
|
||||
<div className="card-image has-text-centered">
|
||||
<i className="fa fa-edit mt-5" style={{fontSize: '8rem'}}></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="content">
|
||||
<h4>Un méthode simple et efficace.</h4>
|
||||
<p>Découpez votre projet en tâches, faites l'estimation de temps <b>optimiste</b>, <b>probable</b> et <b>pessimiste</b> et obtenez en temps réel vos estimations de temps/financières avec leurs indices de confiance.</p>
|
||||
{/* <p><a href="#">En savoir plus</a></p> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-4">
|
||||
<div className="card is-shady">
|
||||
<div className="card-image has-text-centered">
|
||||
<i className="fa fa-users mt-5" style={{fontSize: '8rem'}}></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="content">
|
||||
<h4>Partager simplement vos estimations.</h4>
|
||||
<p>Une adresse courriel et votre estimation est partagée ! Le niveau d'accès (lecture seule, écriture, vue partielle...) peut être défini pour chaque partage.</p>
|
||||
{/* <p><a href="#">En savoir plus</a></p> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
26
client/src/components/Modal.tsx
Normal file
26
client/src/components/Modal.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
export interface ModalProps {
|
||||
active: boolean
|
||||
showCloseButton?: boolean
|
||||
onClose?: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
export class Modal extends React.PureComponent<PropsWithChildren<ModalProps>> {
|
||||
render() {
|
||||
const { children, active, showCloseButton, onClose } = this.props;
|
||||
return (
|
||||
<div className={`modal ${active ? 'is-active': ''}`}>
|
||||
<div className="modal-background"></div>
|
||||
<div className="modal-content">
|
||||
{children}
|
||||
</div>
|
||||
{
|
||||
showCloseButton ?
|
||||
<button onClick={onClose} className="modal-close is-large" aria-label="close"></button> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
71
client/src/components/Navbar.tsx
Normal file
71
client/src/components/Navbar.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Config } from '../config';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useUserProfileQuery, useUserProfile } from '../gql/queries/user';
|
||||
import { WithLoader } from './WithLoader';
|
||||
|
||||
export function Navbar() {
|
||||
const { user, loading } = useUserProfile();
|
||||
const [ isActive, setActive ] = useState(false);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setActive(active => !active);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar is-fixed-top" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<Link className="navbar-item" to="/">
|
||||
<h1 className="is-size-4">
|
||||
<i className="fa fa-stopwatch mr-1"></i>
|
||||
Guesstimate
|
||||
</h1>
|
||||
</Link>
|
||||
<a role="button"
|
||||
className={`navbar-burger ${isActive ? 'is-active' : ''}`}
|
||||
onClick={toggleMenu}
|
||||
aria-label="menu"
|
||||
aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<WithLoader loading={loading}>
|
||||
<div className="buttons">
|
||||
{
|
||||
user.id ?
|
||||
<Fragment>
|
||||
<Link to="/profile" className="button">
|
||||
<span className="icon">
|
||||
<i className="fas fa-user"></i>
|
||||
</span>
|
||||
</Link>
|
||||
<a className="button" href={Config.logoutURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
</a>
|
||||
</Fragment> :
|
||||
<a className="button" href={Config.loginURL}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-in-alt"></i>
|
||||
</span>
|
||||
<span>Se connecter</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</WithLoader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
30
client/src/components/Page.tsx
Normal file
30
client/src/components/Page.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Navbar } from './Navbar';
|
||||
|
||||
export interface PageProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export class Page extends React.PureComponent<PageProps> {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Navbar />
|
||||
{this.props.children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
updateTitle() {
|
||||
const { title } = this.props;
|
||||
if (title !== undefined) window.document.title = title + ' - Guesstimate';
|
||||
}
|
||||
}
|
38
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
38
client/src/components/ProfilePage/ProfilePage.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { UserForm } from '../UserForm';
|
||||
import { User } from '../../types/user';
|
||||
import { useUserProfile } from '../../gql/queries/user';
|
||||
import { useUpdateUserMutation } from '../../gql/mutations/user';
|
||||
import { WithLoader } from '../WithLoader';
|
||||
|
||||
export function ProfilePage() {
|
||||
const { user, loading } = useUserProfile();
|
||||
const [ updateUser, updateUserMutation ] = useUpdateUserMutation();
|
||||
const isLoading = updateUserMutation.loading || loading;
|
||||
|
||||
const onUserChange = (updatedUser: User) => {
|
||||
if (user.name !== updatedUser.name) {
|
||||
updateUser({ variables: {id: user.id, changes: { name: updatedUser.name }}});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Mon profil">
|
||||
<div className="container is-fluid">
|
||||
<section className="section">
|
||||
<div className="columns">
|
||||
<div className="column is-6 is-offset-3">
|
||||
<h2 className="is-size-2 subtitle">Mon profil</h2>
|
||||
<WithLoader loading={isLoading || !user}>
|
||||
{
|
||||
<UserForm onChange={onUserChange} user={user} />
|
||||
}
|
||||
</WithLoader>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
93
client/src/components/UserForm.tsx
Normal file
93
client/src/components/UserForm.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { User } from '../types/user';
|
||||
|
||||
export interface UserFormProps {
|
||||
user: User
|
||||
onChange?: (user: User) => void
|
||||
}
|
||||
|
||||
export function UserForm({ user, onChange }: UserFormProps) {
|
||||
const [ state, setState ] = useState({
|
||||
changed: false,
|
||||
user: {
|
||||
id: user && user.id ? user.id : '',
|
||||
name: user && user.name ? user.name : '',
|
||||
email: user && user.email ? user.email : '',
|
||||
createdAt: user && user.createdAt ? user.createdAt : null,
|
||||
connectedAt: user && user.connectedAt ? user.connectedAt : null,
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
changed: false,
|
||||
user: {
|
||||
id: user && user.id ? user.id : '',
|
||||
name: user && user.name ? user.name : '',
|
||||
email: user && user.email ? user.email : '',
|
||||
createdAt: user && user.createdAt ? user.createdAt : null,
|
||||
connectedAt: user && user.connectedAt ? user.connectedAt : null,
|
||||
}
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
const onSaveClick = () => {
|
||||
if (!state.changed) return;
|
||||
if (typeof onChange !== 'function') return;
|
||||
onChange(state.user);
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: false,
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const onUserAttrChange = function(attrName: string, evt: ChangeEvent<HTMLInputElement>) {
|
||||
const value = evt.currentTarget.value;
|
||||
setState(state => {
|
||||
return {
|
||||
...state,
|
||||
changed: true,
|
||||
user: {
|
||||
...state.user,
|
||||
[attrName]: value,
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form">
|
||||
<div className="field">
|
||||
<label className="label">Nom d'utilisateur</label>
|
||||
<div className="control">
|
||||
<input type="text" className="input" value={state.user.name}
|
||||
onChange={onUserAttrChange.bind(null, "name")} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Adresse courriel</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Date de dernière connexion</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.connectedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Date de création</label>
|
||||
<div className="control">
|
||||
<p className="input is-static">{state.user.createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="buttons is-right">
|
||||
<button disabled={!state.changed}
|
||||
className="button is-primary" onClick={onSaveClick}>Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
18
client/src/components/WithLoader.tsx
Normal file
18
client/src/components/WithLoader.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { Fragment, PropsWithChildren, FunctionComponent } from 'react';
|
||||
|
||||
export interface WithLoaderProps {
|
||||
loading?: boolean|boolean[]
|
||||
}
|
||||
|
||||
export const WithLoader: FunctionComponent<WithLoaderProps> = ({ loading, children }) => {
|
||||
const isLoading = Array.isArray(loading) ? loading.some(l => l) : loading;
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
isLoading ?
|
||||
<div>Chargement</div> :
|
||||
children
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
15
client/src/config.ts
Normal file
15
client/src/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const Config = {
|
||||
loginURL: get<string>("loginURL", "http://localhost:8081/login"),
|
||||
logoutURL: get<string>("logoutURL", "http://localhost:8081/logout"),
|
||||
graphQLEndpoint: get<string>("graphQLEndpoint", "http://localhost:8081/api/v1/graphql"),
|
||||
subscriptionEndpoint: get<string>("subscriptionEndpoint", "ws://localhost:8081/api/v1/graphql"),
|
||||
};
|
||||
|
||||
function get<T>(key: string, defaultValue: T):T {
|
||||
const config = window['__CONFIG__'] || {};
|
||||
if (config && config.hasOwnProperty(key)) {
|
||||
return config[key] as T;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
4
client/src/custom.d.ts
vendored
Normal file
4
client/src/custom.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
20
client/src/gql/client.tsx
Normal file
20
client/src/gql/client.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
|
||||
import { Config } from '../config';
|
||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||
import { RetryLink } from "@apollo/client/link/retry";
|
||||
import { SubscriptionClient } from "subscriptions-transport-ws";
|
||||
|
||||
const subscriptionClient = new SubscriptionClient(Config.subscriptionEndpoint, {
|
||||
reconnect: true,
|
||||
});
|
||||
|
||||
const link = new RetryLink({attempts: {max: 2}}).split(
|
||||
(operation) => operation.operationName === 'subscription',
|
||||
new WebSocketLink(subscriptionClient),
|
||||
new HttpLink({ uri: Config.graphQLEndpoint, credentials: 'include' })
|
||||
);
|
||||
|
||||
export const client = new ApolloClient<any>({
|
||||
cache: new InMemoryCache(),
|
||||
link: link,
|
||||
});
|
15
client/src/gql/mutations/user.tsx
Normal file
15
client/src/gql/mutations/user.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { gql, useMutation } from '@apollo/client';
|
||||
|
||||
export const MUTATION_UPDATE_USER = gql`
|
||||
mutation updateUser($id: ID!, $changes: UserChanges!) {
|
||||
updateUser(id: $id, changes: $changes) {
|
||||
id,
|
||||
name,
|
||||
createdAt,
|
||||
connectedAt,
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useUpdateUserMutation() {
|
||||
return useMutation(MUTATION_UPDATE_USER);
|
||||
}
|
11
client/src/gql/queries/helper.ts
Normal file
11
client/src/gql/queries/helper.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useQuery, DocumentNode } from "@apollo/client";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useGraphQLData<T>(q: DocumentNode, key: string, defaultValue: T, options = {}) {
|
||||
const query = useQuery(q, options);
|
||||
const [ data, setData ] = useState<T>(defaultValue);
|
||||
useEffect(() => {
|
||||
setData(query.data ? query.data[key] as T : defaultValue);
|
||||
}, [query.loading, query.data]);
|
||||
return { data, loading: query.loading, error: query.error };
|
||||
}
|
25
client/src/gql/queries/user.tsx
Normal file
25
client/src/gql/queries/user.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { User } from '../../types/user';
|
||||
import { useGraphQLData } from './helper';
|
||||
|
||||
export const QUERY_CURRENT_USER = gql`
|
||||
query userProfile {
|
||||
currentUser {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
createdAt,
|
||||
connectedAt
|
||||
}
|
||||
}`;
|
||||
|
||||
export function useUserProfileQuery() {
|
||||
return useQuery(QUERY_CURRENT_USER);
|
||||
}
|
||||
|
||||
export function useUserProfile() {
|
||||
const { data, loading, error } = useGraphQLData<User>(
|
||||
QUERY_CURRENT_USER, 'currentUser', {id: '', email: ''}
|
||||
);
|
||||
return { user: data, loading, error };
|
||||
}
|
22
client/src/index.html
Normal file
22
client/src/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="has-navbar-fixed-top">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Guesstimate</title>
|
||||
<% for (var css in htmlWebpackPlugin.files.css) { %>
|
||||
<link href="/<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
|
||||
<% } %>
|
||||
<% if (htmlWebpackPlugin.files.favicon) { %>
|
||||
<link rel="shortcut icon" href="/<%= htmlWebpackPlugin.files.favicon%>">
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app" class="is-fullheight"></div>
|
||||
<script src="/config.js"></script>
|
||||
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
|
||||
<script type="text/javascript" src="/<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
19
client/src/index.tsx
Normal file
19
client/src/index.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import './sass/_all.scss';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
import { client } from './gql/client';
|
||||
|
||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||
import '@fortawesome/fontawesome-free/js/solid'
|
||||
import '@fortawesome/fontawesome-free/js/regular'
|
||||
import '@fortawesome/fontawesome-free/js/brands'
|
||||
import './resources/favicon.png';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
|
||||
ReactDOM.render(
|
||||
<ApolloProvider client={client}>
|
||||
<App />
|
||||
</ApolloProvider>,
|
||||
document.getElementById('app')
|
||||
);
|
3
client/src/resources/config.sample.js
Normal file
3
client/src/resources/config.sample.js
Normal file
@ -0,0 +1,3 @@
|
||||
// Use this file to customize client configuration.
|
||||
// See frontend/src/config.ts for more informations.
|
||||
window['__CONFIG__'] = {};
|
BIN
client/src/resources/favicon.png
Normal file
BIN
client/src/resources/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 888 B |
1
client/src/resources/favicon.svg
Normal file
1
client/src/resources/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="stopwatch" class="svg-inline--fa fa-stopwatch fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M432 304c0 114.9-93.1 208-208 208S16 418.9 16 304c0-104 76.3-190.2 176-205.5V64h-28c-6.6 0-12-5.4-12-12V12c0-6.6 5.4-12 12-12h120c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12h-28v34.5c37.5 5.8 71.7 21.6 99.7 44.6l27.5-27.5c4.7-4.7 12.3-4.7 17 0l28.3 28.3c4.7 4.7 4.7 12.3 0 17l-29.4 29.4-.6.6C419.7 223.3 432 262.2 432 304zm-176 36V188.5c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12V340c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12z"></path></svg>
|
After Width: | Height: | Size: 660 B |
148
client/src/resources/logo.svg
Normal file
148
client/src/resources/logo.svg
Normal file
@ -0,0 +1,148 @@
|
||||
<?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="256px"
|
||||
height="256px"
|
||||
viewBox="0 0 256 256"
|
||||
version="1.1"
|
||||
id="SVGRoot"
|
||||
sodipodi:docname="daddysvg.svg"
|
||||
inkscape:export-filename="/home/benjamin/Images/daddysvg.svg.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14">
|
||||
<defs
|
||||
id="defs3785">
|
||||
<inkscape:path-effect
|
||||
effect="skeletal"
|
||||
id="path-effect11307"
|
||||
is_visible="true"
|
||||
pattern="M 0,5 C 0,2.24 2.24,0 5,0 7.76,0 10,2.24 10,5 10,7.76 7.76,10 5,10 2.24,10 0,7.76 0,5 Z"
|
||||
copytype="single_stretched"
|
||||
prop_scale="1"
|
||||
scale_y_rel="false"
|
||||
spacing="0"
|
||||
normal_offset="0"
|
||||
tang_offset="0"
|
||||
prop_units="false"
|
||||
vertical_pattern="false"
|
||||
fuse_tolerance="0" />
|
||||
<inkscape:path-effect
|
||||
effect="skeletal"
|
||||
id="path-effect11303"
|
||||
is_visible="true"
|
||||
pattern="M 0,5 C 0,2.24 2.24,0 5,0 7.76,0 10,2.24 10,5 10,7.76 7.76,10 5,10 2.24,10 0,7.76 0,5 Z"
|
||||
copytype="single_stretched"
|
||||
prop_scale="1"
|
||||
scale_y_rel="false"
|
||||
spacing="0"
|
||||
normal_offset="0"
|
||||
tang_offset="0"
|
||||
prop_units="false"
|
||||
vertical_pattern="false"
|
||||
fuse_tolerance="0" />
|
||||
<inkscape:path-effect
|
||||
effect="skeletal"
|
||||
id="path-effect11299"
|
||||
is_visible="true"
|
||||
pattern="M 0,5 C 0,2.24 2.24,0 5,0 7.76,0 10,2.24 10,5 10,7.76 7.76,10 5,10 2.24,10 0,7.76 0,5 Z"
|
||||
copytype="single_stretched"
|
||||
prop_scale="1"
|
||||
scale_y_rel="false"
|
||||
spacing="0"
|
||||
normal_offset="0"
|
||||
tang_offset="0"
|
||||
prop_units="false"
|
||||
vertical_pattern="false"
|
||||
fuse_tolerance="0" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect11297"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<inkscape:path-effect
|
||||
effect="skeletal"
|
||||
id="path-effect4672"
|
||||
is_visible="true"
|
||||
pattern="M 0,5 C 0,2.24 2.24,0 5,0 7.76,0 10,2.24 10,5 10,7.76 7.76,10 5,10 2.24,10 0,7.76 0,5 Z"
|
||||
copytype="single_stretched"
|
||||
prop_scale="1"
|
||||
scale_y_rel="false"
|
||||
spacing="0"
|
||||
normal_offset="0"
|
||||
tang_offset="0"
|
||||
prop_units="false"
|
||||
vertical_pattern="false"
|
||||
fuse_tolerance="0" />
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect4670"
|
||||
is_visible="true"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="2.8284271"
|
||||
inkscape:cx="47.812546"
|
||||
inkscape:cy="121.69356"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1865"
|
||||
inkscape:window-height="1029"
|
||||
inkscape:window-x="55"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:grid-bbox="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid11360" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata3788">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#058eff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||
d="m 32.583136,21.416152 c 0.432923,-0.183441 1.65138,1.815536 3.85459,5.558711 1.876009,3.187273 5.638052,9.780818 10.540554,14.926476 4.902145,5.186689 12.7877,10.329902 22.509118,11.171913 5.287649,0.501505 10.82326,0.01212 16.981963,-1.006244 6.160916,-1.036929 12.451394,-2.633697 19.096969,-4.720546 5.92081,-1.862873 11.96851,-4.083209 18.09205,-6.704773 4.70577,-2.01913 9.47684,-4.242646 13.98868,-6.84685 2.78157,-1.651835 5.40309,-3.206 7.1713,-5.122172 0.72442,-1.142891 1.45398,-1.672372 1.05724,-1.417617 0.32165,0.358747 -0.21409,0.06426 -0.74301,-0.731311 -1.10708,-1.096321 -2.77281,-2.009494 -4.71571,-3.103565 0,0 -2e-5,-8e-6 -2e-5,-8e-6 -5.02038,-2.724381 -10.68908,-5.025077 -16.52146,-7.931398 -2.71668,-1.344975 -5.54564,-2.85375 -8.31782,-4.709758 0,0 -2e-5,-10e-6 -2e-5,-10e-6 -2.15905,-1.3673133 -4.52019,-3.2751851 -6.52692,-6.0493324 -1.61911,-2.4211048 -2.44027,-5.21000004 -2.40705,-8.0392852 0,0 0,-2.59e-5 0,-2.59e-5 0.09,-2.7949565 0.69429,-5.2119617 1.41807,-7.2341845 0.42972,-1.302744 0.886,-2.553107 1.33877,-3.764434 1.25978,-3.370408 2.45493,-6.346665 2.94174,-9.192843 0.88436,-4.684509 0.5051,-9.593748 -1.02641,-13.97674 -1.81489,-5.355405 -5.58765,-10.060675 -10.09284,-13.565898 -6.185708,-4.82811 -13.850872,-7.899477 -21.424861,-10.19356 -7.846124,-2.284214 -15.31769,-3.712304 -22.49963,-3.291965 0,0 -10e-6,4e-6 -10e-6,4e-6 -4.941703,0.320675 -9.556379,1.724903 -12.759349,4.515646 -3.340828,2.680913 -5.806968,6.910841 -8.090315,11.031815 -6.254309,11.724587 -9.740983,22.044246 -10.398302,32.001908 -0.845296,10.957356 2.454185,22.2768522 4.090997,27.981816 1.895369,6.606141 3.015663,10.171026 2.441687,10.414234 -0.439344,0.186161 -2.566206,-2.934783 -5.446076,-9.308474 -2.557029,-5.6591822 -6.989301,-16.7224702 -7.020222,-29.295099 0.07855,-11.034421 3.227368,-22.756907 9.435014,-35.331317 2.142237,-4.508984 5.001492,-9.589461 9.535428,-13.760978 4.883182,-4.244584 11.147908,-6.571491 17.615613,-7.025057 10e-7,5e-6 1.6e-5,-10e-7 1.6e-5,-10e-7 8.592767,-0.650065 17.191925,0.693474 25.668062,3.084144 8.282922,2.247398 17.023837,5.688766 24.741208,11.430798 5.91486,4.416957 10.85903,10.581991 13.55164,18.017862 2.14173,6.040892 2.69157,12.662434 1.49516,19.106381 -0.80091,4.041083 -2.14135,7.582602 -3.37362,10.870104 -0.44272,1.1811067 -0.86613,2.3161062 -1.24092,3.4206913 -0.51854,1.6952301 -0.95487,3.091231 -0.91259,4.1877256 0,0 0,7.6e-6 0,7.6e-6 -0.0118,0.8349371 0.21925,1.7074536 0.6526,2.28958094 0.66465,1.13442943 2.14357,2.18347446 3.86618,3.44382626 0,0 2e-5,8.3e-6 2e-5,8.3e-6 2.17771,1.4948012 4.63292,2.8070937 7.18494,4.1111063 5.3624,2.7221697 11.13485,5.1455167 16.74875,8.2559417 0,0 2e-5,8e-6 2e-5,8e-6 2.13483,1.143298 4.46579,2.583477 6.67927,4.65126 1.80636,1.426501 3.29256,4.006836 3.65258,7.555011 -0.34386,3.964935 -2.0161,6.713128 -3.85914,8.18127 -3.01783,2.949299 -6.35207,4.9621 -9.36964,6.570362 -5.03606,2.76332 -10.17639,5.028534 -15.13018,6.999205 -6.47792,2.583408 -12.87124,4.723785 -19.12085,6.469439 -7.00088,1.959402 -13.740645,3.397556 -20.366513,4.235038 -6.575016,0.84831 -12.824726,1.051204 -18.888844,0.183956 C 56.987331,58.018531 48.354967,51.422103 43.313256,45.056121 38.316131,38.685001 35.474015,31.775565 34.132073,28.055142 32.633631,23.900835 32.212485,21.573206 32.583136,21.416152 Z"
|
||||
id="path4668"
|
||||
inkscape:connector-curvature="0"
|
||||
inkscape:path-effect="#path-effect4670;#path-effect4672"
|
||||
inkscape:original-d="m 30,18.5 c 8.834333,17.665667 17.667667,35.332333 26.5,53 C 90.50177,58.998717 124.501,46.499 158.5,34 155.33445,29.165838 152.16767,24.332333 149,19.5 133.33512,13.999275 117.66767,8.4989999 102,2.9999999 108.66791,-4.6679494 115.33433,-12.334333 122,-20 c -2.83194,-12.832552 -5.66567,-25.667667 -8.5,-38.5 -21.497755,-5.667339 -42.999,-11.334334 -64.5,-17 -5.499176,7.49924 -10.999,14.999 -16.5,22.5 -4.499676,12.000804 -8.999,23.999 -13.5,36 3.668035,11.8335217 7.334333,23.6656665 11,35.5 z"
|
||||
transform="matrix(0,-1.3691421,1.8997251,0,140.89907,239.44713)" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
3
client/src/sass/_all.scss
Normal file
3
client/src/sass/_all.scss
Normal file
@ -0,0 +1,3 @@
|
||||
@import 'bulma/bulma.sass';
|
||||
@import '_base.scss';
|
||||
@import '_loader.scss';
|
27
client/src/sass/_base.scss
Normal file
27
client/src/sass/_base.scss
Normal file
@ -0,0 +1,27 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
// Generated with https://www.svgbackgrounds.com/
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='351' height='292.5' viewBox='0 0 1080 900'%3E%3Cg fill-opacity='0.04'%3E%3Cpolygon fill='%23444' points='90 150 0 300 180 300'/%3E%3Cpolygon points='90 150 180 0 0 0'/%3E%3Cpolygon fill='%23AAA' points='270 150 360 0 180 0'/%3E%3Cpolygon fill='%23DDD' points='450 150 360 300 540 300'/%3E%3Cpolygon fill='%23999' points='450 150 540 0 360 0'/%3E%3Cpolygon points='630 150 540 300 720 300'/%3E%3Cpolygon fill='%23DDD' points='630 150 720 0 540 0'/%3E%3Cpolygon fill='%23444' points='810 150 720 300 900 300'/%3E%3Cpolygon fill='%23FFF' points='810 150 900 0 720 0'/%3E%3Cpolygon fill='%23DDD' points='990 150 900 300 1080 300'/%3E%3Cpolygon fill='%23444' points='990 150 1080 0 900 0'/%3E%3Cpolygon fill='%23DDD' points='90 450 0 600 180 600'/%3E%3Cpolygon points='90 450 180 300 0 300'/%3E%3Cpolygon fill='%23666' points='270 450 180 600 360 600'/%3E%3Cpolygon fill='%23AAA' points='270 450 360 300 180 300'/%3E%3Cpolygon fill='%23DDD' points='450 450 360 600 540 600'/%3E%3Cpolygon fill='%23999' points='450 450 540 300 360 300'/%3E%3Cpolygon fill='%23999' points='630 450 540 600 720 600'/%3E%3Cpolygon fill='%23FFF' points='630 450 720 300 540 300'/%3E%3Cpolygon points='810 450 720 600 900 600'/%3E%3Cpolygon fill='%23DDD' points='810 450 900 300 720 300'/%3E%3Cpolygon fill='%23AAA' points='990 450 900 600 1080 600'/%3E%3Cpolygon fill='%23444' points='990 450 1080 300 900 300'/%3E%3Cpolygon fill='%23222' points='90 750 0 900 180 900'/%3E%3Cpolygon points='270 750 180 900 360 900'/%3E%3Cpolygon fill='%23DDD' points='270 750 360 600 180 600'/%3E%3Cpolygon points='450 750 540 600 360 600'/%3E%3Cpolygon points='630 750 540 900 720 900'/%3E%3Cpolygon fill='%23444' points='630 750 720 600 540 600'/%3E%3Cpolygon fill='%23AAA' points='810 750 720 900 900 900'/%3E%3Cpolygon fill='%23666' points='810 750 900 600 720 600'/%3E%3Cpolygon fill='%23999' points='990 750 900 900 1080 900'/%3E%3Cpolygon fill='%23999' points='180 0 90 150 270 150'/%3E%3Cpolygon fill='%23444' points='360 0 270 150 450 150'/%3E%3Cpolygon fill='%23FFF' points='540 0 450 150 630 150'/%3E%3Cpolygon points='900 0 810 150 990 150'/%3E%3Cpolygon fill='%23222' points='0 300 -90 450 90 450'/%3E%3Cpolygon fill='%23FFF' points='0 300 90 150 -90 150'/%3E%3Cpolygon fill='%23FFF' points='180 300 90 450 270 450'/%3E%3Cpolygon fill='%23666' points='180 300 270 150 90 150'/%3E%3Cpolygon fill='%23222' points='360 300 270 450 450 450'/%3E%3Cpolygon fill='%23FFF' points='360 300 450 150 270 150'/%3E%3Cpolygon fill='%23444' points='540 300 450 450 630 450'/%3E%3Cpolygon fill='%23222' points='540 300 630 150 450 150'/%3E%3Cpolygon fill='%23AAA' points='720 300 630 450 810 450'/%3E%3Cpolygon fill='%23666' points='720 300 810 150 630 150'/%3E%3Cpolygon fill='%23FFF' points='900 300 810 450 990 450'/%3E%3Cpolygon fill='%23999' points='900 300 990 150 810 150'/%3E%3Cpolygon points='0 600 -90 750 90 750'/%3E%3Cpolygon fill='%23666' points='0 600 90 450 -90 450'/%3E%3Cpolygon fill='%23AAA' points='180 600 90 750 270 750'/%3E%3Cpolygon fill='%23444' points='180 600 270 450 90 450'/%3E%3Cpolygon fill='%23444' points='360 600 270 750 450 750'/%3E%3Cpolygon fill='%23999' points='360 600 450 450 270 450'/%3E%3Cpolygon fill='%23666' points='540 600 630 450 450 450'/%3E%3Cpolygon fill='%23222' points='720 600 630 750 810 750'/%3E%3Cpolygon fill='%23FFF' points='900 600 810 750 990 750'/%3E%3Cpolygon fill='%23222' points='900 600 990 450 810 450'/%3E%3Cpolygon fill='%23DDD' points='0 900 90 750 -90 750'/%3E%3Cpolygon fill='%23444' points='180 900 270 750 90 750'/%3E%3Cpolygon fill='%23FFF' points='360 900 450 750 270 750'/%3E%3Cpolygon fill='%23AAA' points='540 900 630 750 450 750'/%3E%3Cpolygon fill='%23FFF' points='720 900 810 750 630 750'/%3E%3Cpolygon fill='%23222' points='900 900 990 750 810 750'/%3E%3Cpolygon fill='%23222' points='1080 300 990 450 1170 450'/%3E%3Cpolygon fill='%23FFF' points='1080 300 1170 150 990 150'/%3E%3Cpolygon points='1080 600 990 750 1170 750'/%3E%3Cpolygon fill='%23666' points='1080 600 1170 450 990 450'/%3E%3Cpolygon fill='%23DDD' points='1080 900 1170 750 990 750'/%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.is-fullheight {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-margin-top-normal {
|
||||
margin-top: $size-normal;
|
||||
}
|
||||
|
||||
.has-padding-small {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: #ffffff;
|
||||
}
|
44
client/src/sass/_loader.scss
Normal file
44
client/src/sass/_loader.scss
Normal file
@ -0,0 +1,44 @@
|
||||
.loader-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid $grey;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 36px;
|
||||
left: 36px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
35
client/src/types/decision.tsx
Normal file
35
client/src/types/decision.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Workgroup } from "./workgroup";
|
||||
|
||||
export enum DecisionSupportFileStatus {
|
||||
Draft = "draft",
|
||||
Ready = "ready",
|
||||
Voted = "voted",
|
||||
Closed = "closed",
|
||||
}
|
||||
|
||||
export interface DecisionSupportFileSection {
|
||||
name: string
|
||||
}
|
||||
|
||||
// aka Dossier d'aide à la décision
|
||||
export interface DecisionSupportFile {
|
||||
id: string
|
||||
title: string
|
||||
sections: {[name: string]: any}
|
||||
status: DecisionSupportFileStatus
|
||||
workgroup?: Workgroup,
|
||||
createdAt: Date
|
||||
votedAt?: Date
|
||||
closedAt?: Date
|
||||
}
|
||||
|
||||
export function newDecisionSupportFile(): DecisionSupportFile {
|
||||
return {
|
||||
id: '',
|
||||
title: '',
|
||||
sections: {},
|
||||
status: DecisionSupportFileStatus.Draft,
|
||||
workgroup: null,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
}
|
7
client/src/types/user.ts
Normal file
7
client/src/types/user.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
connectedAt?: Date
|
||||
createdAt?: Date
|
||||
}
|
18
client/src/types/workgroup.ts
Normal file
18
client/src/types/workgroup.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { User } from "./user";
|
||||
export interface Workgroup {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: Date
|
||||
closedAt: Date
|
||||
members: User[]
|
||||
}
|
||||
|
||||
export function inWorkgroup(u: User, wg: Workgroup): boolean {
|
||||
for (let m, i = 0; (m = wg.members[i]); i++) {
|
||||
if(m.id === u.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
16
client/tsconfig.json
Normal file
16
client/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "es6",
|
||||
"lib": ["dom", "es6"],
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"strict": false,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"files": [
|
||||
"./src/custom.d.ts"
|
||||
]
|
||||
}
|
83
client/webpack.config.js
Normal file
83
client/webpack.config.js
Normal file
@ -0,0 +1,83 @@
|
||||
const path = require('path');
|
||||
|
||||
// Plugins
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const env = process.env;
|
||||
|
||||
module.exports = {
|
||||
mode: `${env.NODE_ENV ? env.NODE_ENV : 'production'}`,
|
||||
entry: './src/index.tsx',
|
||||
devtool: 'inline-source-map',
|
||||
output: {
|
||||
filename: '[name].[contenthash].js',
|
||||
path: path.join(__dirname, 'dist')
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
||||
},
|
||||
devServer: {
|
||||
contentBase: path.join(__dirname, 'dist'),
|
||||
compress: true,
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
historyApiFallback: true,
|
||||
writeToDisk: true,
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.s(a|c)ss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
loader: "resolve-url-loader",
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
sourceMap: true,
|
||||
sourceMapContents: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},{
|
||||
test: /\.(woff(2)?|ttf|eot|svg|png)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash].[ext]',
|
||||
outputPath: '/resources/'
|
||||
}
|
||||
}]
|
||||
},{
|
||||
test: /\.(t|j)sx?$/,
|
||||
exclude: /node_modules/,
|
||||
loaders: ['ts-loader']
|
||||
}]
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/[name].[contenthash].css",
|
||||
chunkFilename: "css/[id].css"
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
inject: false,
|
||||
favicon: "./src/resources/favicon.png",
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: './src/resources/config.sample.js', to: 'config.js' },
|
||||
],
|
||||
}),
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user