chore(project): bootstrap project tree

This commit is contained in:
2020-08-08 15:04:59 +02:00
parent c11d55b61c
commit 5806f196c4
77 changed files with 14666 additions and 0 deletions

2
client/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules
/dist

10925
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
client/package.json Normal file
View 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"
}
}

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,4 @@
declare module "*.svg" {
const content: any;
export default content;
}

20
client/src/gql/client.tsx Normal file
View 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,
});

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

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

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

View File

@ -0,0 +1,3 @@
// Use this file to customize client configuration.
// See frontend/src/config.ts for more informations.
window['__CONFIG__'] = {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

View 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

View 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

View File

@ -0,0 +1,3 @@
@import 'bulma/bulma.sass';
@import '_base.scss';
@import '_loader.scss';

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

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

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

@ -0,0 +1,7 @@
export interface User {
id: string
email: string
name?: string
connectedAt?: Date
createdAt?: Date
}

View 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
View 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
View 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' },
],
}),
]
}