Merged with master
This commit is contained in:
commit
1d85c21e91
92
src/App.re
92
src/App.re
|
@ -1,8 +1,92 @@
|
||||||
|
type route =
|
||||||
|
| EAFunds
|
||||||
|
| GlobalCatastrophe
|
||||||
|
| Home
|
||||||
|
| NotFound;
|
||||||
|
|
||||||
|
let routeToPath = route =>
|
||||||
|
switch (route) {
|
||||||
|
| EAFunds => "/ea-funds"
|
||||||
|
| GlobalCatastrophe => "/global-catastrophe"
|
||||||
|
| Home => "/"
|
||||||
|
| _ => "/"
|
||||||
|
};
|
||||||
|
|
||||||
|
module Menu = {
|
||||||
|
module Styles = {
|
||||||
|
open Css;
|
||||||
|
let menu =
|
||||||
|
style([
|
||||||
|
position(`relative),
|
||||||
|
marginTop(em(0.25)),
|
||||||
|
marginBottom(em(0.25)),
|
||||||
|
selector(
|
||||||
|
"a",
|
||||||
|
[
|
||||||
|
borderRadius(em(0.25)),
|
||||||
|
display(`inlineBlock),
|
||||||
|
backgroundColor(`hex("eee")),
|
||||||
|
padding(em(1.)),
|
||||||
|
cursor(`pointer),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selector("a:hover", [backgroundColor(`hex("bfcad4"))]),
|
||||||
|
selector("a:hover", [backgroundColor(`hex("bfcad4"))]),
|
||||||
|
selector(
|
||||||
|
"a:not(:first-child):not(:last-child)",
|
||||||
|
[marginRight(em(0.25)), marginLeft(em(0.25))],
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
module Item = {
|
||||||
|
[@react.component]
|
||||||
|
let make = (~href, ~children) => {
|
||||||
|
<a
|
||||||
|
href
|
||||||
|
onClick={e => {
|
||||||
|
e->ReactEvent.Synthetic.preventDefault;
|
||||||
|
ReasonReactRouter.push(href);
|
||||||
|
}}>
|
||||||
|
children
|
||||||
|
</a>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
[@react.component]
|
[@react.component]
|
||||||
let make = () => {
|
let make = () => {
|
||||||
<div className="w-full max-w-screen-xl mx-auto px-6">
|
<div className=Styles.menu>
|
||||||
<FormBuilder.ModelForm model=EAFunds.Interface.model />
|
<Item href={routeToPath(Home)} key="home"> {"Home" |> E.ste} </Item>
|
||||||
<FormBuilder.ModelForm model=GlobalCatastrophe.Interface.model />
|
<Item href={routeToPath(EAFunds)} key="ea-funds">
|
||||||
<FormBuilder.ModelForm model=Human.Interface.model />
|
{"EA Funds" |> E.ste}
|
||||||
|
</Item>
|
||||||
|
<Item href={routeToPath(GlobalCatastrophe)} key="gc">
|
||||||
|
{"Global Catastrophe" |> E.ste}
|
||||||
|
</Item>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
[@react.component]
|
||||||
|
let make = () => {
|
||||||
|
let url = ReasonReactRouter.useUrl();
|
||||||
|
|
||||||
|
let routing =
|
||||||
|
switch (url.path) {
|
||||||
|
| ["ea-funds"] => EAFunds
|
||||||
|
| ["global-catastrophe"] => GlobalCatastrophe
|
||||||
|
| [] => Home
|
||||||
|
| _ => NotFound
|
||||||
|
};
|
||||||
|
|
||||||
|
<div className="w-full max-w-screen-xl mx-auto px-6">
|
||||||
|
<Menu />
|
||||||
|
{switch (routing) {
|
||||||
|
| EAFunds => <FormBuilder.ModelForm model=EAFunds.Interface.model />
|
||||||
|
| GlobalCatastrophe =>
|
||||||
|
<FormBuilder.ModelForm model=GlobalCatastrophe.Interface.model />
|
||||||
|
| Home => <div> {"Welcome" |> E.ste} </div>
|
||||||
|
| _ => <div> {"Page is not found" |> E.ste} </div>
|
||||||
|
}}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
|
@ -29,6 +29,9 @@ export class CdfChartD3 {
|
||||||
continuous: null,
|
continuous: null,
|
||||||
discrete: null,
|
discrete: null,
|
||||||
},
|
},
|
||||||
|
yMaxContinuousDomainFactor: 1,
|
||||||
|
yMaxDiscreteDomainFactor: 1,
|
||||||
|
options: {},
|
||||||
onHover: (e) => {
|
onHover: (e) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -47,90 +50,21 @@ export class CdfChartD3 {
|
||||||
this.formatDates = this.formatDates.bind(this);
|
this.formatDates = this.formatDates.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
svgWidth(svgWidth) {
|
set(name, value) {
|
||||||
this.attrs.svgWidth = svgWidth;
|
_.set(this.attrs, [name], value);
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
svgHeight(height) {
|
|
||||||
this.attrs.svgHeight = height;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxX(maxX) {
|
|
||||||
this.attrs.maxX = maxX;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
minX(minX) {
|
|
||||||
this.attrs.minX = minX;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
scale(scale) {
|
|
||||||
this.attrs.scale = scale;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
timeScale(timeScale) {
|
|
||||||
this.attrs.timeScale = timeScale;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
onHover(onHover) {
|
|
||||||
this.attrs.onHover = onHover;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
marginBottom(marginBottom) {
|
|
||||||
this.attrs.marginBottom = marginBottom;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
marginLeft(marginLeft) {
|
|
||||||
this.attrs.marginLeft = marginLeft;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
marginRight(marginRight) {
|
|
||||||
this.attrs.marginRight = marginRight;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
marginTop(marginTop) {
|
|
||||||
this.attrs.marginTop = marginTop;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDistributionLines(showDistributionLines) {
|
|
||||||
this.attrs.showDistributionLines = showDistributionLines;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
showDistributionYAxis(showDistributionYAxis) {
|
|
||||||
this.attrs.showDistributionYAxis = showDistributionYAxis;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
verticalLine(verticalLine) {
|
|
||||||
this.attrs.verticalLine = verticalLine;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
showVerticalLine(showVerticalLine) {
|
|
||||||
this.attrs.showVerticalLine = showVerticalLine;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
container(container) {
|
|
||||||
this.attrs.container = container;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
data(data) {
|
data(data) {
|
||||||
this.attrs.data = data;
|
this.attrs.data = data;
|
||||||
this.attrs.data.continuous = data.continuous || {xs: [], ys: []};
|
this.attrs.data.continuous = data.continuous || {
|
||||||
this.attrs.data.discrete = data.discrete || {xs: [], ys: []};
|
xs: [],
|
||||||
|
ys: [],
|
||||||
|
};
|
||||||
|
this.attrs.data.discrete = data.discrete || {
|
||||||
|
xs: [],
|
||||||
|
ys: [],
|
||||||
|
};
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,8 +83,12 @@ export class CdfChartD3 {
|
||||||
// Calculated properties.
|
// Calculated properties.
|
||||||
this.calc.chartLeftMargin = this.attrs.marginLeft;
|
this.calc.chartLeftMargin = this.attrs.marginLeft;
|
||||||
this.calc.chartTopMargin = this.attrs.marginTop;
|
this.calc.chartTopMargin = this.attrs.marginTop;
|
||||||
this.calc.chartWidth = this.attrs.svgWidth - this.attrs.marginRight - this.attrs.marginLeft;
|
this.calc.chartWidth = this.attrs.svgWidth
|
||||||
this.calc.chartHeight = this.attrs.svgHeight - this.attrs.marginBottom - this.attrs.marginTop;
|
- this.attrs.marginRight
|
||||||
|
- this.attrs.marginLeft;
|
||||||
|
this.calc.chartHeight = this.attrs.svgHeight
|
||||||
|
- this.attrs.marginBottom
|
||||||
|
- this.attrs.marginTop;
|
||||||
|
|
||||||
// Add svg.
|
// Add svg.
|
||||||
this.svg = this._container
|
this.svg = this._container
|
||||||
|
@ -159,12 +97,12 @@ export class CdfChartD3 {
|
||||||
.attr('height', this.attrs.svgHeight)
|
.attr('height', this.attrs.svgHeight)
|
||||||
.attr('pointer-events', 'none');
|
.attr('pointer-events', 'none');
|
||||||
|
|
||||||
// Add container g element.
|
// Add container "g" (empty) element.
|
||||||
this.chart = this.svg
|
this.chart = this.svg
|
||||||
.createObject({ tag: 'g', selector: 'chart' })
|
.createObject({ tag: 'g', selector: 'chart' })
|
||||||
.attr(
|
.attr(
|
||||||
'transform',
|
'transform',
|
||||||
'translate(' + this.calc.chartLeftMargin + ',' + this.calc.chartTopMargin + ')',
|
`translate(${this.calc.chartLeftMargin}, ${this.calc.chartTopMargin})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.hasDate('continuous')) {
|
if (this.hasDate('continuous')) {
|
||||||
|
@ -186,32 +124,39 @@ export class CdfChartD3 {
|
||||||
const yMin = d3.min(this.attrs.data.continuous.ys);
|
const yMin = d3.min(this.attrs.data.continuous.ys);
|
||||||
const yMax = d3.max(this.attrs.data.continuous.ys);
|
const yMax = d3.max(this.attrs.data.continuous.ys);
|
||||||
|
|
||||||
// Scales.
|
// X-domains.
|
||||||
let xScale = null;
|
const yMaxDomainFactor = _.get(this.attrs, 'yMaxContinuousDomainFactor', 1);
|
||||||
if (this.attrs.scale === 'linear') {
|
const xMinDomain = xMin;
|
||||||
xScale = d3.scaleLinear()
|
const xMaxDomain = xMax;
|
||||||
.domain([xMin, xMax])
|
const yMinDomain = yMin;
|
||||||
.range([0, this.calc.chartWidth]);
|
const yMaxDomain = yMax * yMaxDomainFactor;
|
||||||
} else {
|
|
||||||
xScale = d3.scaleLog()
|
|
||||||
.base(this.attrs.logBase)
|
|
||||||
.domain([xMin, xMax])
|
|
||||||
.range([0, this.calc.chartWidth]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// X-scale.
|
||||||
|
let xScale = this.attrs.scale === 'linear'
|
||||||
|
? d3.scaleLinear()
|
||||||
|
.domain([xMinDomain, xMaxDomain])
|
||||||
|
.range([0, this.calc.chartWidth])
|
||||||
|
: d3.scaleLog()
|
||||||
|
.base(this.attrs.logBase)
|
||||||
|
.domain([xMinDomain, xMaxDomain])
|
||||||
|
.range([0, this.calc.chartWidth]);
|
||||||
|
|
||||||
|
// Y-scale.
|
||||||
const yScale = d3.scaleLinear()
|
const yScale = d3.scaleLinear()
|
||||||
.domain([yMin, yMax])
|
.domain([yMinDomain, yMaxDomain])
|
||||||
.range([this.calc.chartHeight, 0]);
|
.range([this.calc.chartHeight, 0]);
|
||||||
|
|
||||||
// Axis generator.
|
// X-axis.
|
||||||
let xAxis = null;
|
let xAxis = null;
|
||||||
if (!!this.attrs.timeScale) {
|
if (!!this.attrs.timeScale) {
|
||||||
const zero = _.get(this.attrs.timeScale, 'zero', moment());
|
// Calculates the projection on X-axis.
|
||||||
const unit = _.get(this.attrs.timeScale, 'unit', 'years');
|
const zero = _.get(this.attrs, 'timeScale.zero', moment());
|
||||||
|
const unit = _.get(this.attrs, 'timeScale.unit', 'years');
|
||||||
const diff = Math.abs(xMax - xMin);
|
const diff = Math.abs(xMax - xMin);
|
||||||
const left = zero.clone().add(xMin, unit);
|
const left = zero.clone().add(xMin, unit);
|
||||||
const right = left.clone().add(diff, unit);
|
const right = left.clone().add(diff, unit);
|
||||||
|
|
||||||
|
// X-time-scale.
|
||||||
const xScaleTime = d3.scaleTime()
|
const xScaleTime = d3.scaleTime()
|
||||||
.domain([left.toDate(), right.toDate()])
|
.domain([left.toDate(), right.toDate()])
|
||||||
.nice()
|
.nice()
|
||||||
|
@ -237,6 +182,7 @@ export class CdfChartD3 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Y-axis.
|
||||||
const yAxis = d3.axisRight(yScale);
|
const yAxis = d3.axisRight(yScale);
|
||||||
|
|
||||||
// Objects.
|
// Objects.
|
||||||
|
@ -250,12 +196,14 @@ export class CdfChartD3 {
|
||||||
.y0(this.calc.chartHeight);
|
.y0(this.calc.chartHeight);
|
||||||
|
|
||||||
// Add axis.
|
// Add axis.
|
||||||
this.chart.createObject({ tag: 'g', selector: 'x-axis' })
|
this.chart
|
||||||
.attr('transform', 'translate(0,' + this.calc.chartHeight + ')')
|
.createObject({ tag: 'g', selector: 'x-axis' })
|
||||||
|
.attr('transform', `translate(0, ${this.calc.chartHeight})`)
|
||||||
.call(xAxis);
|
.call(xAxis);
|
||||||
|
|
||||||
if (this.attrs.showDistributionYAxis) {
|
if (this.attrs.showDistributionYAxis) {
|
||||||
this.chart.createObject({ tag: 'g', selector: 'y-axis' })
|
this.chart
|
||||||
|
.createObject({ tag: 'g', selector: 'y-axis' })
|
||||||
.call(yAxis);
|
.call(yAxis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,15 +261,16 @@ export class CdfChartD3 {
|
||||||
|
|
||||||
function mouseover() {
|
function mouseover() {
|
||||||
const mouse = d3.mouse(this);
|
const mouse = d3.mouse(this);
|
||||||
hoverLine.attr('opacity', 1).attr('x1', mouse[0]).attr('x2', mouse[0]);
|
hoverLine
|
||||||
|
.attr('opacity', 1)
|
||||||
|
.attr('x1', mouse[0])
|
||||||
|
.attr('x2', mouse[0]);
|
||||||
const xValue = xScale.invert(mouse[0]);
|
const xValue = xScale.invert(mouse[0]);
|
||||||
// This used to be here, but doesn't seem important
|
|
||||||
// const xValue = (mouse[0] > range[0] && mouse[0] < range[1]) ? : 0;
|
|
||||||
context.attrs.onHover(xValue);
|
context.attrs.onHover(xValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseout() {
|
function mouseout() {
|
||||||
hoverLine.attr('opacity', 0)
|
hoverLine.attr('opacity', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chart
|
this.chart
|
||||||
|
@ -338,49 +287,65 @@ export class CdfChartD3 {
|
||||||
return { xScale, yScale };
|
return { xScale, yScale };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} distributionChart
|
||||||
|
* @param {object} distributionChart.xScale
|
||||||
|
* @param {object} distributionChart.yScale
|
||||||
|
*/
|
||||||
addLollipopsChart(distributionChart) {
|
addLollipopsChart(distributionChart) {
|
||||||
const data = this.getDataPoints('discrete');
|
const data = this.getDataPoints('discrete');
|
||||||
const ys = data.map(item => item.y);
|
|
||||||
const yMax = d3.max(ys);
|
|
||||||
|
|
||||||
// X axis
|
const _yMin = d3.min(this.attrs.data.discrete.ys);
|
||||||
this.chart.append("g")
|
const yMax = d3.max(this.attrs.data.discrete.ys);
|
||||||
.attr("class", 'lollipops-x-axis')
|
|
||||||
.attr("transform", "translate(0," + this.calc.chartHeight + ")")
|
// X axis.
|
||||||
|
this.chart.append('g')
|
||||||
|
.attr('class', 'lollipops-x-axis')
|
||||||
|
.attr('transform', `translate(0, ${this.calc.chartHeight})`)
|
||||||
.call(d3.axisBottom(distributionChart.xScale));
|
.call(d3.axisBottom(distributionChart.xScale));
|
||||||
|
|
||||||
// Y axis
|
// Y-domain.
|
||||||
|
const yMaxDomainFactor = _.get(this.attrs, 'yMaxDiscreteDomainFactor', 1);
|
||||||
|
const yMinDomain = 0;
|
||||||
|
const yMaxDomain = yMax * yMaxDomainFactor;
|
||||||
|
|
||||||
|
// Y-scale.
|
||||||
const yScale = d3.scaleLinear()
|
const yScale = d3.scaleLinear()
|
||||||
.domain([0, yMax])
|
.domain([yMinDomain, yMaxDomain])
|
||||||
.range([this.calc.chartHeight, 0]);
|
.range([this.calc.chartHeight, 0]);
|
||||||
|
|
||||||
this.chart.append("g")
|
// Adds "g" for an y-axis.
|
||||||
.attr("class", 'lollipops-y-axis')
|
this.chart.append('g')
|
||||||
.attr("transform", "translate(" + this.calc.chartWidth + ",0)")
|
.attr('class', 'lollipops-y-axis')
|
||||||
|
.attr('transform', `translate(${this.calc.chartWidth}, 0)`)
|
||||||
.call(d3.axisLeft(yScale));
|
.call(d3.axisLeft(yScale));
|
||||||
|
|
||||||
// Lines
|
// Lines.
|
||||||
this.chart.selectAll("lollipops-line")
|
this.chart.selectAll('lollipops-line')
|
||||||
.data(data)
|
.data(data)
|
||||||
.enter()
|
.enter()
|
||||||
.append("line")
|
.append('line')
|
||||||
.attr("class", 'lollipops-line')
|
.attr('class', 'lollipops-line')
|
||||||
.attr("x1", d => distributionChart.xScale(d.x))
|
.attr('x1', d => distributionChart.xScale(d.x))
|
||||||
.attr("x2", d => distributionChart.xScale(d.x))
|
.attr('x2', d => distributionChart.xScale(d.x))
|
||||||
.attr("y1", d => yScale(d.y))
|
.attr('y1', d => yScale(d.y))
|
||||||
.attr("y2", yScale(0));
|
.attr('y2', yScale(0));
|
||||||
|
|
||||||
// Circles
|
// Circles.
|
||||||
this.chart.selectAll("lollipops-circle")
|
this.chart.selectAll('lollipops-circle')
|
||||||
.data(data)
|
.data(data)
|
||||||
.enter()
|
.enter()
|
||||||
.append("circle")
|
.append('circle')
|
||||||
.attr("class", 'lollipops-circle')
|
.attr('class', 'lollipops-circle')
|
||||||
.attr("cx", d => distributionChart.xScale(d.x))
|
.attr('cx', d => distributionChart.xScale(d.x))
|
||||||
.attr("cy", d => yScale(d.y))
|
.attr('cy', d => yScale(d.y))
|
||||||
.attr("r", "4");
|
.attr('r', '4');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ts
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
formatDates(ts) {
|
formatDates(ts) {
|
||||||
return moment(ts).format("MMMM Do YYYY");
|
return moment(ts).format("MMMM Do YYYY");
|
||||||
}
|
}
|
||||||
|
@ -436,8 +401,8 @@ export class CdfChartD3 {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasDate(key) {
|
hasDate(key) {
|
||||||
const data = _.get(this.attrs.data, key);
|
const xs = _.get(this.attrs, ['data', key, 'xs']);
|
||||||
return !!data;
|
return !!_.size(xs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,26 +34,28 @@ function CdfChartReact(props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new CdfChartD3()
|
new CdfChartD3()
|
||||||
.svgWidth(width)
|
.set('svgWidth', width)
|
||||||
.svgHeight(props.height)
|
.set('svgHeight', props.height)
|
||||||
.maxX(props.maxX)
|
.set('maxX', props.maxX)
|
||||||
.minX(props.minX)
|
.set('minX', props.minX)
|
||||||
.onHover(props.onHover)
|
.set('onHover', props.onHover)
|
||||||
.marginBottom(props.marginBottom || 15)
|
.set('marginBottom',props.marginBottom || 15)
|
||||||
.marginLeft(30)
|
.set('marginLeft', 30)
|
||||||
.marginRight(30)
|
.set('marginRight', 30)
|
||||||
.marginTop(5)
|
.set('marginTop', 5)
|
||||||
.showDistributionLines(props.showDistributionLines)
|
.set('showDistributionLines', props.showDistributionLines)
|
||||||
.showDistributionYAxis(props.showDistributionYAxis)
|
.set('showDistributionYAxis', props.showDistributionYAxis)
|
||||||
.verticalLine(props.verticalLine)
|
.set('verticalLine', props.verticalLine)
|
||||||
.showVerticalLine(props.showVerticalLine)
|
.set('showVerticalLine', props.showVerticalLine)
|
||||||
.container(containerRef.current)
|
.set('container', containerRef.current)
|
||||||
|
.set('scale', scale)
|
||||||
|
.set('timeScale', props.timeScale)
|
||||||
|
.set('yMaxContinuousDomainFactor', 1)
|
||||||
|
.set('yMaxDiscreteDomainFactor', 1)
|
||||||
.data({
|
.data({
|
||||||
continuous: props.continuous,
|
continuous: props.continuous,
|
||||||
discrete: props.discrete,
|
discrete: props.discrete,
|
||||||
})
|
})
|
||||||
.scale(scale)
|
|
||||||
.timeScale(props.timeScale)
|
|
||||||
.render();
|
.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -297,3 +297,5 @@ module JsArray = {
|
||||||
);
|
);
|
||||||
let filter = Js.Array.filter;
|
let filter = Js.Array.filter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let ste = React.string;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user