Skip to content

Instantly share code, notes, and snippets.

@joduplessis
Created March 1, 2022 11:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joduplessis/2bcd93ccc1affaac1fa546e7fb7bf7b8 to your computer and use it in GitHub Desktop.
Save joduplessis/2bcd93ccc1affaac1fa546e7fb7bf7b8 to your computer and use it in GitHub Desktop.
The Adtriba coding challenge. Form solution + draggable input curve.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<style>
* {
font-family: sans-serif;
outline: none;
text-decoration: none;
font-size: 13px;
-webkit-font-smoothing: antialiased;
font-feature-settings: 'liga', 'kern';
text-rendering: optimizeLegibility;
}
body {
background-color: #FAFAFA;
}
.container {
background: white;
padding: 20px;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
margin-bottom: 10px;
width: 600px;
border-radius: 5px;
}
.buttons {
flex-direction: row;
align-items: center;
align-content: center;
display: flex;
justify-content: flex-end;
box-sizing: border-box;
}
.buttons button:last-child {
margin-left: 5px;
}
.row {
flex-direction: row;
align-items: center;
align-content: center;
display: flex;
justify-content: center;
box-sizing: border-box;
width: 100%;
}
.error-message {
font-size: 10px;
background-color: #EF4056;
font-weight: 600;
margin-bottom: 20px;
color: white;
padding: 10px;
border-radius: 4px;
font-family: 'menlo', monospace;
}
.column {
flex-direction: column;
align-items: flex-start;
align-content: center;
display: flex;
justify-content: center;
flex: 1;
padding: 10px;
}
label {
display: block;
margin-bottom: 5px;
color: #202428;
font-size: 11px;
font-weight: 900;
font-family: 'menlo', monospace;
}
input {
display: block;
padding: 10px;
border-radius: 7px;
border: 4px solid #f1f3f5;
color: #202428;
box-sizing:border-box;
font-size: 16px;
font-weight: 500;
width: 100%;
}
input.error {
border: 4px solid #EF4056;
}
input::placeholder {
color: #adb5bd;
}
.coordinates, .highlight {
color: #8e43e7;
font-size: 11px;
font-weight: 500;
font-family: 'menlo', monospace;
}
button {
color: white;
background: #8e43e7;
font-size: 13px;
font-weight: 700;
padding: 10px;
border: none;
border-radius: 7px;
cursor: pointer;
}
table {
width: 100%;
box-sizing: border-box;
margin-top: 30px;
margin-bottom: 30px;
}
table th {
padding: 10px;
font-size: 11px;
font-weight: 900;
font-family: 'menlo', monospace;
}
table td.title {
color: #202428;
font-weight: bold;
}
.chart {
flex-direction: row;
align-items: center;
align-content: center;
display: flex;
justify-content: center;
}
</style>
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/utils/Draggable.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
const EXCLUDE = "EXCLUDE"
const CONSTANT = "CONSTANT"
const validateCurrency = (value) => {
const regex = /^\d+(?:\.\d{0,2})$/
if (regex.test(value)) return true
return false
}
const Row = ({ title, updateTotal }) => {
const [amount, setAmount] = React.useState('')
const [type, setType] = React.useState('')
const [error, setError] = React.useState(false)
return (
<tr>
<td className="title">{title}</td>
<td>
<input
className={error ? "error": ""}
value={amount}
placeholder="0.00"
type="text"
onChange={e => setAmount(e.target.value)}
onBlur={e => {
if (validateCurrency(amount)) {
setError(false)
updateTotal(amount)
} else {
setError(true)
}
}}
/>
</td>
<td>
<input
checked={type == CONSTANT}
type="checkbox"
onChange={() => type == CONSTANT ? setType('') : setType(CONSTANT)}
/>
</td>
<td>
<input
checked={type == EXCLUDE}
type="checkbox"
onChange={() => type == EXCLUDE ? setType('') : setType(EXCLUDE)}
/>
</td>
</tr>
)
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
name: '',
start: '',
end: '',
error: '',
sea: 0,
display: 0,
social: 0,
affiliate: 0,
remarketing: 0,
x: 0,
y: 0,
}
this.copyOrSave = this.copyOrSave.bind(this)
this.chartRef = React.createRef()
this.getTotal = this.getTotal.bind(this)
}
copyOrSave() {
this.setState({ error: '' })
// Check the null values!
if (
this.state.name == ''
|| !this.state.start
|| !this.state.end
|| !this.state.sea
|| !this.state.display
|| !this.state.social
|| !this.state.affiliate
|| !this.state.remarketing
) {
return this.setState({ error: 'Please complete all the details!' })
}
const startDate = new Date(this.state.start).getTime()
const endDate = new Date(this.state.end).getTime()
// Check that the date make sense
if (startDate >= endDate) {
return this.setState({ error: 'These dates are incorrect' })
}
// Otherwise all good
alert('Done!')
}
getTotal() {
// These should all be validating correctly from the child component
const total = parseFloat(this.state.sea)
+ parseFloat(this.state.display)
+ parseFloat(this.state.social)
+ parseFloat(this.state.affiliate)
+ parseFloat(this.state.remarketing)
return (Math.round(total * 100) / 100).toFixed(2)
}
componentDidMount() {
this.createChart()
}
createChart() {
const margin = { top: 10, right: 30, bottom: 30, left: 60 }
const width = 460 - margin.left - margin.right
const height = 400 - margin.top - margin.bottom
const svg = d3.select(this.chartRef)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
d3.csv("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/data_IC.csv", (data) => {
const x = d3
.scaleLinear()
.domain([1,100])
.range([ 0, width ])
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
// Add Y axis
const y = d3
.scaleLinear()
.domain([0, 13])
.range([ height, 0 ])
svg.append("g").call(d3.axisLeft(y))
// Show confidence interval
svg.append("path")
.datum(data)
.attr("fill", "#f4ebff")
.attr("stroke", "none")
.attr("d", d3.area()
.x(function(d) { return x(d.x) })
.y0(function(d) { return y(d.CI_right) })
.y1(function(d) { return y(d.CI_left) })
)
// Add the line
svg
.append("path")
.datum(data)
.attr("id", "rail")
.attr("fill", "none")
.attr("stroke", "#8e43e7")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.x) })
.y(function(d) { return y(d.y) })
)
const circle = svg.append("circle")
.attr("fill", "black")
.attr("id", "knob")
.attr("cx", 0)
.attr("cy", height - 0)
.attr("r", 5)
const rail = document.getElementById("rail")
const knob = document.getElementById("knob")
const W = rail.getBBox().width
const railLength = rail.getTotalLength()
// So we still have access to the class
const context = this
// Important here - we need access to 'this'
function update (e) {
const P = rail.getPointAtLength(this.x / W*railLength);
TweenLite.set(knob, { attr: { cx:P.x, cy:P.y }})
context.setState({
x: P.x,
y: P.y,
})
}
Draggable.create(document.createElement('div'), {
type:'x',
throwProps:true,
bounds:{ minX: 0, maxX: W },
trigger: knob,
overshootTolerance: 0,
onDrag: update,
onThrowUpdate: update
})
})
}
render() {
return (
<React.Fragment>
<div className="container">
{!!this.state.error && (
<div className="error-message">{this.state.error}</div>
)}
<div className="row">
<div className="column">
<label>
Mediaplan - <span className="highlight">USD {this.getTotal()}</span>
</label>
<input
type="text"
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
/>
</div>
</div>
<div className="row">
<div className="column">
<label>Start date</label>
<input
value={this.state.start}
type="date"
onChange={e => this.setState({ start: e.target.value })}
/>
</div>
<div className="column">
<label>End date</label>
<input
value={this.state.end}
type="date"
onChange={e => this.setState({ end: e.target.value })}
/>
</div>
</div>
<table>
<thead>
<tr>
<th>Channel</th>
<th>Budget</th>
<th>Keep constant</th>
<th>Exclude</th>
</tr>
</thead>
<tbody>
<Row
title="SEA"
updateTotal={(sea) => this.setState({ sea })}
/>
<Row
title="Display"
updateTotal={(display) => this.setState({ display })}
/>
<Row
title="Social"
updateTotal={(social) => this.setState({ social })}
/>
<Row
title="Affiliate"
updateTotal={(affiliate) => this.setState({ affiliate })}
/>
<Row
title="Remarketing"
updateTotal={(remarketing) => this.setState({ remarketing })}
/>
</tbody>
</table>
<div className="buttons">
<button onClick={this.copyOrSave}>Copy plan</button>
<button onClick={this.copyOrSave}>Save plan</button>
</div>
</div>
<div className="container">
<div className="chart" ref={ref => this.chartRef = ref}>
</div>
<div className="coordinates">
x: {this.state.x}, y: {this.state.y}
</div>
</div>
</React.Fragment>
)
}
}
const domContainer = document.querySelector('#app');
ReactDOM.render(<App />, domContainer);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment