Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Julien Jerphanion
Rex Dri
Commits
ff8b40ab
Commit
ff8b40ab
authored
Feb 27, 2019
by
Florent Chehab
Browse files
Merge branch 'handle_money_in_markdown' into 'master'
Handle money in markdown See merge request
!56
parents
114ea1cf
1227c0ff
Changes
7
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/map/UnivMap.js
View file @
ff8b40ab
import
React
from
"
react
"
;
import
CustomComponentForAPI
from
"
../CustomComponentForAPI
"
;
import
React
,
{
Component
}
from
"
react
"
;
import
{
connect
}
from
"
react-redux
"
;
import
{
Map
,
TileLayer
,
LayersControl
,
LayerGroup
}
from
"
react-leaflet
"
;
import
PropTypes
from
"
prop-types
"
;
import
{
Map
,
TileLayer
,
LayersControl
,
LayerGroup
}
from
"
react-leaflet
"
;
import
UnivMarkers
from
"
./UnivMakers
"
;
import
{
saveMainMapStatus
}
from
"
../../actions/map
"
;
...
...
@@ -12,9 +12,9 @@ import { saveMainMapStatus } from "../../actions/map";
* Component to create the map of universities
*
* @class UnivMap
* @extends {
Custom
Component
ForAPI
}
* @extends {Component}
*/
class
UnivMap
extends
Custom
Component
ForAPI
{
class
UnivMap
extends
Component
{
// Initial state
state
=
{
...
...
@@ -22,6 +22,12 @@ class UnivMap extends CustomComponentForAPI {
height
:
800
,
}
constructor
(
props
)
{
super
(
props
);
// Make sure to set the correct height on mount
this
.
updateDimensions
();
}
/**
* Custom function to update the appropriate height of the map
*
...
...
@@ -37,15 +43,10 @@ class UnivMap extends CustomComponentForAPI {
catch
(
err
)
{
}
}
componentWillMount
()
{
// Make sure to set the correct height on mount
this
.
updateDimensions
();
}
componentDidMount
()
{
// add an event listener to resize the map when needed
window
.
addEventListener
(
"
resize
"
,
this
.
updateDimensions
.
bind
(
this
));
super
.
componentDidMount
();
this
.
updateDimensions
();
}
componentWillUnmount
()
{
...
...
@@ -93,7 +94,7 @@ class UnivMap extends CustomComponentForAPI {
}));
}
customR
ender
()
{
r
ender
()
{
const
stamenName
=
"
Stamen Watercolor
"
,
osmFrName
=
"
OpenStreetMap France
"
,
esriName
=
"
Esri WorldImagery
"
,
...
...
@@ -154,6 +155,11 @@ class UnivMap extends CustomComponentForAPI {
}
UnivMap
.
propTypes
=
{
map
:
PropTypes
.
object
.
isRequired
,
saveMainMap
:
PropTypes
.
func
.
isRequired
};
const
mapStateToProps
=
(
state
)
=>
{
return
{
map
:
state
.
app
.
mainMap
...
...
frontend/src/components/pages/PageHome.js
View file @
ff8b40ab
...
...
@@ -49,6 +49,11 @@ Les objectifs de ce service sont :
| Ancien départs | ✔ |
| Départs possibles | ✔ |
| Informatio sur les universités | ✔ |
## Autre fun feature
You can format money:
\`
:100CHF:
\`
=> :100CHF:
`
;
...
...
frontend/src/components/shared/Markdown.js
View file @
ff8b40ab
...
...
@@ -2,8 +2,13 @@
/* eslint-disable react/display-name */
// Inspired by : https://github.com/mui-org/material-ui/blob/master/docs/src/pages/page-layout-examples/blog/Markdown.js
import
React
from
"
react
"
;
import
React
,
{
Component
}
from
"
react
"
;
import
{
connect
}
from
"
react-redux
"
;
import
PropTypes
from
"
prop-types
"
;
import
ReactMarkdown
from
"
react-markdown
"
;
import
parseMoney
from
"
../../utils/parseMoney
"
;
import
convertAmountToEur
from
"
../../utils/convertAmountToEur
"
;
import
withStyles
from
"
@material-ui/core/styles/withStyles
"
;
import
Typography
from
"
@material-ui/core/Typography
"
;
...
...
@@ -175,15 +180,71 @@ const renderers = {
tableBody
:
props
=>
(
<
TableBody
>
{
props
.
children
}
<
/TableBody>
)
,
tableRow
:
props
=>
(
<
TableRow
hover
=
{
true
}
>
{
props
.
children
}
<
/TableRow>
)
,
tableCell
:
props
=>
(
<
TableCell
>
{
props
.
children
}
<
/TableCell>
)
,
thematicBreak
:
()
=>
(
<
Divider
/>
)
thematicBreak
:
()
=>
(
<
Divider
/>
)
,
};
export
default
function
Markdown
(
props
)
{
return
<
ReactMarkdown
renderers
=
{
renderers
}
allowedTypes
=
{[...
Object
.
keys
(
renderers
),
"
text
"
,
"
root
"
,
"
strong
"
]}
// Only allow custom nodes and basic ones
mode
=
{
"
escape
"
}
{...
props
}
/>
;
/**
* Custom Markdown component renderer to make use of material UI
*
* We don't make use of Custom Component for API since currencies should be loaded at app startup.
* We don't need to fetch them here
*
* @class Markdown
* @extends {Component}
*/
class
Markdown
extends
Component
{
/**
* Function to "compile" the markdown source
* It adds the money conversion information if the custom tag is present.
*
* @param {string} source
* @returns {string}
* @memberof Markdown
*/
compileSource
(
source
)
{
let
compiled
=
""
;
parseMoney
(
source
).
forEach
(
el
=>
{
if
(
!
el
.
isMoney
)
{
compiled
+=
el
.
text
;
}
else
{
const
{
amount
,
currency
}
=
el
,
{
currencies
}
=
this
.
props
;
if
(
currency
===
"
EUR
"
)
{
compiled
+=
`
${
amount
}
€`
;
}
else
{
const
converted
=
convertAmountToEur
(
amount
,
currency
,
currencies
);
compiled
+=
`
${
amount
}${
currency
}
[*(≈
${
converted
}
€)*](https://www.xe.com/currencyconverter/convert/?Amount=
${
amount
}
&From=
${
currency
}
&To=EUR)`
;
// add money converted information in markdown format
}
}
});
return
compiled
;
}
render
()
{
const
compiledSource
=
this
.
compileSource
(
this
.
props
.
source
);
return
<
ReactMarkdown
renderers
=
{
renderers
}
allowedTypes
=
{[...
Object
.
keys
(
renderers
),
"
text
"
,
"
emphasis
"
,
"
root
"
,
"
strong
"
]}
// Only allow custom nodes and basic ones
mode
=
{
"
escape
"
}
source
=
{
compiledSource
}
/>
;
}
}
Markdown
.
propTypes
=
{
currencies
:
PropTypes
.
array
.
isRequired
,
source
:
PropTypes
.
string
};
const
mapStateToPropsTextRenderer
=
(
state
)
=>
({
currencies
:
state
.
api
.
currenciesAll
.
readSucceeded
.
data
,
});
export
default
connect
(
mapStateToPropsTextRenderer
)(
Markdown
);
frontend/src/components/university/shared/Scholarship.js
View file @
ff8b40ab
...
...
@@ -3,6 +3,7 @@ import PropTypes from "prop-types";
import
withStyles
from
"
@material-ui/core/styles/withStyles
"
;
import
Markdown
from
"
../../shared/Markdown
"
;
import
moneyConversion
from
"
../../../utils/convertAmountToEur
"
;
import
Typography
from
"
@material-ui/core/Typography
"
;
const
styles
=
theme
=>
{
...
...
@@ -63,9 +64,8 @@ class Scholarship extends React.Component {
}
convertAmountToEur
(
amount
)
{
const
{
currencies
,
currency
}
=
this
.
props
;
const
rate
=
currencies
.
find
(
c
=>
c
.
id
==
currency
).
one_EUR_in_this_currency
;
return
Math
.
trunc
(
amount
/
rate
);
const
{
currency
,
currencies
}
=
this
.
props
;
return
moneyConversion
(
amount
,
currency
,
currencies
);
}
getAmounts
()
{
...
...
frontend/src/utils/convertAmountToEur.js
0 → 100644
View file @
ff8b40ab
/**
* Function for converting money amounts to euros.
*
* @export
* @param {number} amount
* @param {string} currency
* @param {Array[string]} currencies
* @returns {number}
*/
export
default
function
convertAmountToEur
(
amount
,
currency
,
currencies
)
{
if
(
currency
===
"
EUR
"
)
{
return
amount
;
}
const
rate
=
currencies
.
find
(
c
=>
c
.
id
==
currency
).
one_EUR_in_this_currency
;
return
Math
.
trunc
(
amount
/
rate
);
}
frontend/src/utils/parseMoney.js
0 → 100644
View file @
ff8b40ab
/**
* Function to get a regex object for money parsing
*
* @returns
*/
function
getMoneyRegex
()
{
return
/
(?<
!`
)
:
(\d
*
[
.,
]?\d
*
)(\w{3})
:/g
;
}
/**
* Parses a string to determine if there are some currency in it.
*
* For example, the string: "Hi, I earn :10.15CHF:" will be converted to:
* [{ isMoney: false, text: 'Hi, I earn ' },
* { isMoney: true, amount: '10.15', currency: 'CHF' }]
*
* In the string: amount can be an int, a float with ',' or '.' as separator
* And the currency can be in mixed case, but will always be return in uppercase.
*
* @export
* @param {string} str
* @returns {Array}
*/
export
default
function
parseMoney
(
str
)
{
if
(
str
===
""
)
{
return
[];
}
// reusable function
const
getOutputText
=
(
str
)
=>
({
isMoney
:
false
,
text
:
str
});
if
(
!
getMoneyRegex
().
test
(
str
))
{
// if the string doesn't contain anything interesting
return
[
getOutputText
(
str
)];
}
else
{
let
matches
=
[],
match
,
moneyRegEx
=
getMoneyRegex
();
while
((
match
=
moneyRegEx
.
exec
(
str
))
!==
null
)
{
const
matchStartIndex
=
match
.
index
,
// index of the starting ':'
matchLastIndex
=
moneyRegEx
.
lastIndex
-
1
,
// index of the ending ':'
amount
=
parseFloat
(
match
[
"
1
"
].
replace
(
"
,
"
,
"
.
"
)),
// fix numbers with "," as decimal separators
currency
=
match
[
"
2
"
].
toUpperCase
();
// make sure the currency is uppercase
matches
.
push
({
matchStartIndex
,
matchLastIndex
,
amount
,
currency
});
}
let
res
=
[],
lastIndex
=
0
;
matches
.
forEach
((
el
)
=>
{
if
(
lastIndex
!==
el
.
matchStartIndex
)
{
// we need to add a classic string that was before the currency marker
res
.
push
(
getOutputText
(
str
.
substring
(
lastIndex
,
el
.
matchStartIndex
)));
}
// We add the element corresponding to money mount
res
.
push
({
isMoney
:
true
,
amount
:
el
.
amount
,
currency
:
el
.
currency
});
lastIndex
=
el
.
matchLastIndex
+
1
;
});
// we need to add the eventual trailing text:
if
(
lastIndex
!==
str
.
length
)
{
res
.
push
(
getOutputText
(
str
.
substring
(
lastIndex
)));
}
return
res
;
}
}
frontend/tests/utils/parseMoney.test.js
0 → 100644
View file @
ff8b40ab
import
parseMoney
from
"
../../src/utils/parseMoney
"
;
test
(
"
parse empty string
"
,
()
=>
{
const
str
=
""
;
expect
(
parseMoney
(
str
).
length
).
toBe
(
0
);
});
test
(
"
Parse string with no money
"
,
()
=>
{
const
str
=
"
A random classic string
"
;
expect
(
parseMoney
(
str
).
length
).
toBe
(
1
);
expect
(
parseMoney
(
str
)[
0
].
text
).
toBe
(
str
);
});
test
(
"
Parse string with only money
"
,
()
=>
{
const
str
=
"
:100CHF:
"
,
parsed
=
parseMoney
(
str
);
expect
(
parsed
.
length
).
toBe
(
1
);
expect
(
parsed
[
0
].
amount
).
toBe
(
100
);
expect
(
parsed
[
0
].
currency
).
toBe
(
"
CHF
"
);
});
test
(
"
Parse complicated string
"
,
()
=>
{
const
str
=
"
Hi, I earn :0,0Chf: but he earned :100.12EUR: this year !
"
,
parsed
=
parseMoney
(
str
);
expect
(
parsed
.
length
).
toBe
(
5
);
expect
(
parsed
[
0
].
isMoney
).
toBe
(
false
);
expect
(
parsed
[
1
].
isMoney
).
toBe
(
true
);
expect
(
parsed
[
2
].
isMoney
).
toBe
(
false
);
expect
(
parsed
[
3
].
isMoney
).
toBe
(
true
);
expect
(
parsed
[
4
].
isMoney
).
toBe
(
false
);
expect
(
parsed
[
0
].
text
).
toBe
(
"
Hi, I earn
"
);
expect
(
parsed
[
2
].
text
).
toBe
(
"
but he earned
"
);
expect
(
parsed
[
4
].
text
).
toBe
(
"
this year !
"
);
expect
(
parsed
[
1
].
amount
).
toBe
(
0
);
expect
(
parsed
[
3
].
amount
).
toBe
(
100.12
);
expect
(
parsed
[
1
].
currency
).
toBe
(
"
CHF
"
);
expect
(
parsed
[
3
].
currency
).
toBe
(
"
EUR
"
);
});
test
(
"
Money directly in code is returned as text
"
,
()
=>
{
const
str
=
"
You can use `:120CHF:` to tag money infos
"
,
parsed
=
parseMoney
(
str
);
expect
(
parsed
.
length
).
toBe
(
1
);
expect
(
parsed
[
0
].
isMoney
).
toBe
(
false
);
});
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment