mirror of
https://github.com/status-im/fathom.git
synced 2025-02-28 19:10:36 +00:00
rewrite mithril to react
This commit is contained in:
parent
fbd591547c
commit
a7af934f23
@ -15,6 +15,12 @@ This is a general draft document for thoughts and todo's.
|
||||
- Referral's
|
||||
- Search keywords
|
||||
|
||||
## Notes
|
||||
|
||||
- Every API response has to be JSON
|
||||
- Envelope every response with `data` and `message` props.
|
||||
- JS client for consuming API.
|
||||
|
||||
|
||||
### Admin themes
|
||||
|
||||
|
59
assets/js/components/Graph.js
Normal file
59
assets/js/components/Graph.js
Normal file
@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
import { h, render, Component } from 'preact';
|
||||
import Chart from 'chart.js'
|
||||
|
||||
class Graph extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
data: []
|
||||
}
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
initChart() {
|
||||
new Chart(this.ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: this.state.data.map((d) => d.Label),
|
||||
datasets: [{
|
||||
label: '# of Visitors',
|
||||
data: this.state.data.map((d) => d.Count),
|
||||
backgroundColor: 'rgba(0, 155, 255, .2)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scale: {
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
return fetch('/api/visits/count/day', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
this.setState({ data: data})
|
||||
this.initChart()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="block">
|
||||
<canvas width="600" height="220" ref={(el) => { this.ctx = el; }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Graph
|
59
assets/js/components/LoginForm.js
Normal file
59
assets/js/components/LoginForm.js
Normal file
@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
|
||||
import { h, render, Component } from 'preact';
|
||||
|
||||
class LoginForm extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.setState({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
fetch('/api/session', {
|
||||
method: "POST",
|
||||
data: {
|
||||
email: this.state.email,
|
||||
password: this.state.password,
|
||||
},
|
||||
credentials: 'include'
|
||||
}).then((r) => {
|
||||
if( r.status == 200 ) {
|
||||
this.props.onAuth();
|
||||
console.log("Authenticated!");
|
||||
}
|
||||
|
||||
// TODO: Handle errors
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="block">
|
||||
<h2>Login</h2>
|
||||
<p>Please enter your credentials to access your Ana dashboard.</p>
|
||||
<form method="POST" onSubmit={this.handleSubmit}>
|
||||
<div class="form-group">
|
||||
<label>Email address</label>
|
||||
<input type="email" name="email" onChange={this.linkState('email')} required="required" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" onChange={this.linkState('password')} required="required" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Sign in" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginForm
|
33
assets/js/components/LogoutButton.js
Normal file
33
assets/js/components/LogoutButton.js
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
import { h, render, Component } from 'preact';
|
||||
|
||||
class LogoutButton extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
fetch('/api/session', {
|
||||
method: "DELETE",
|
||||
credentials: 'include',
|
||||
}).then((r) => {
|
||||
if( r.status == 200 ) {
|
||||
this.props.onSuccess();
|
||||
console.log("No longer authenticated!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<a href="#" onClick={this.handleSubmit}>Sign out</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogoutButton
|
58
assets/js/components/Pageviews.js
Normal file
58
assets/js/components/Pageviews.js
Normal file
@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
import { h, render, Component } from 'preact';
|
||||
|
||||
class Pageviews extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
records: []
|
||||
}
|
||||
this.fetchRecords = this.fetchRecords.bind(this);
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
fetchRecords() {
|
||||
return fetch('/api/pageviews', {
|
||||
credentials: 'include'
|
||||
}).then((r) => {
|
||||
if( r.ok ) {
|
||||
return r.json();
|
||||
}
|
||||
}).then((data) => {
|
||||
this.setState({ records: data })
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const tableRows = this.state.records.map( (p, i) => (
|
||||
<tr>
|
||||
<td>{i+1}</td>
|
||||
<td><a href={p.Path}>{p.Path}</a></td>
|
||||
<td>{p.Count}</td>
|
||||
<td>{p.CountUnique}</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<div class="block">
|
||||
<h2>Pageviews</h2>
|
||||
<table class="table pageviews">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>URL</th>
|
||||
<th>Pageviews</th>
|
||||
<th>Unique</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{tableRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Pageviews
|
37
assets/js/components/Realtime.js
Normal file
37
assets/js/components/Realtime.js
Normal file
@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
import { h, render, Component } from 'preact';
|
||||
|
||||
class Realtime extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
count: 0
|
||||
}
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
return fetch('/api/visits/count/realtime', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
this.setState({ count: data })
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let visitors = this.state.count == 1 ? 'visitor' : 'visitors';
|
||||
return (
|
||||
<div class="block">
|
||||
<span class="count">{this.state.count}</span> <span>{visitors} on the site right now</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Realtime
|
@ -1,70 +0,0 @@
|
||||
import m from 'mithril';
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
fetch('/api/session', {
|
||||
method: "POST",
|
||||
data: {
|
||||
email: this.data.email(),
|
||||
password: this.data.password()
|
||||
},
|
||||
credentials: 'include'
|
||||
}).then((r) => {
|
||||
if( r.status == 200 ) {
|
||||
this.onAuth();
|
||||
console.log("Authenticated!");
|
||||
}
|
||||
|
||||
// TODO: Handle errors
|
||||
});
|
||||
}
|
||||
|
||||
const Login = {
|
||||
controller(args) {
|
||||
this.onAuth = args.onAuth;
|
||||
this.data = {
|
||||
email: m.prop(''),
|
||||
password: m.prop(''),
|
||||
}
|
||||
this.onSubmit = handleSubmit.bind(this);
|
||||
},
|
||||
|
||||
view(c) {
|
||||
return m('div.block', [
|
||||
m('h2', 'Login'),
|
||||
m('p', 'Please enter your credentials to login to your Ana dashboard.'),
|
||||
m('form', {
|
||||
method: "POST",
|
||||
onsubmit: c.onSubmit
|
||||
}, [
|
||||
m('div.form-group', [
|
||||
m('label', 'Email address'),
|
||||
m('input', {
|
||||
type: "email",
|
||||
name: "email",
|
||||
required: true,
|
||||
onchange: m.withAttr("value", c.data.email )
|
||||
}),
|
||||
]),
|
||||
m('div.form-group', [
|
||||
m('label', 'Password'),
|
||||
m('input', {
|
||||
type: "password",
|
||||
name: "password",
|
||||
required: true,
|
||||
onchange: m.withAttr("value", c.data.password )
|
||||
}),
|
||||
]),
|
||||
m('div.form-group', [
|
||||
m('input', {
|
||||
type: "submit",
|
||||
value: "Sign in"
|
||||
}),
|
||||
]),
|
||||
])
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export default Login
|
@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import m from 'mithril';
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
fetch('/api/session', {
|
||||
method: "DELETE",
|
||||
credentials: 'include',
|
||||
}).then((r) => {
|
||||
if( r.status == 200 ) {
|
||||
this.cb();
|
||||
console.log("No longer authenticated!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const LogoutButton = {
|
||||
controller(args) {
|
||||
this.cb = args.cb;
|
||||
this.onSubmit = handleSubmit.bind(this);
|
||||
},
|
||||
|
||||
view(c) {
|
||||
return m('a', { href: "#", onclick: c.onSubmit }, 'Sign out');
|
||||
}
|
||||
}
|
||||
|
||||
export default LogoutButton
|
@ -1,49 +0,0 @@
|
||||
import m from 'mithril';
|
||||
|
||||
function fetchRecords() {
|
||||
return fetch('/api/pageviews', {
|
||||
credentials: 'include'
|
||||
}).then((r) => {
|
||||
if( r.ok ) {
|
||||
return r.json();
|
||||
}
|
||||
}).then((data) => {
|
||||
m.startComputation();
|
||||
this.records(data);
|
||||
m.endComputation();
|
||||
});
|
||||
}
|
||||
|
||||
const Pageviews = {
|
||||
controller() {
|
||||
this.records = m.prop([]);
|
||||
fetchRecords.call(this) && window.setInterval(fetchRecords.bind(this), 60000);
|
||||
},
|
||||
view(c) {
|
||||
const tableRows = c.records().map((p, i) => m('tr', [
|
||||
m('td', i+1),
|
||||
m('td', [
|
||||
m('a', { href: p.Path }, p.Path)
|
||||
]),
|
||||
m('td', p.Count),
|
||||
m('td', p.CountUnique)
|
||||
]));
|
||||
|
||||
return m('div.block', [
|
||||
m('h2', 'Pageviews'),
|
||||
m('table.table.pageviews', [
|
||||
m('thead', [
|
||||
m('tr', [
|
||||
m('th', '#'),
|
||||
m('th', 'URL'),
|
||||
m('th', 'Pageviews'),
|
||||
m('th', 'Unique'),
|
||||
]) // tr
|
||||
]), // thead
|
||||
m('tbody', tableRows )
|
||||
]) // table
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export default Pageviews
|
@ -1,33 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import m from 'mithril';
|
||||
|
||||
function fetchData() {
|
||||
return fetch('/api/visits/count/realtime', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
this.count = data;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
const RealtimeVisits = {
|
||||
controller(args) {
|
||||
this.count = 0;
|
||||
fetchData.bind(this) && window.setInterval(fetchData.bind(this), 6000);
|
||||
},
|
||||
|
||||
view(c) {
|
||||
let visitors = c.count > 1 ? 'visitors' : 'visitor';
|
||||
|
||||
return m('div.block', [
|
||||
m('span.count', c.count),
|
||||
' ',
|
||||
m('span', visitors + " on the site right now.")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export default RealtimeVisits
|
@ -1,61 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
import m from 'mithril';
|
||||
import Chart from 'chart.js'
|
||||
|
||||
Chart.defaults.global.tooltips.xPadding = 12;
|
||||
Chart.defaults.global.tooltips.yPadding = 12;
|
||||
|
||||
function fetchData() {
|
||||
return fetch('/api/visits/count/day', {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
this.data = data;
|
||||
initChart.call(this);
|
||||
});
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
let ctx = this.chartCtx;
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: this.data.map((d) => d.Label),
|
||||
datasets: [{
|
||||
label: '# of Visitors',
|
||||
data: this.data.map((d) => d.Count),
|
||||
backgroundColor: 'rgba(0, 155, 255, .2)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scale: {
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const VisitsGraph = {
|
||||
controller(args) {
|
||||
this.data = [];
|
||||
fetchData.call(this);
|
||||
},
|
||||
|
||||
view(c) {
|
||||
return m('div.block', [
|
||||
m('canvas', {
|
||||
width: 600,
|
||||
height: 200,
|
||||
config: (el) => { c.chartCtx = el; }
|
||||
})
|
||||
])
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default VisitsGraph;
|
@ -1,56 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const m = require('mithril');
|
||||
import Login from './components/login.js';
|
||||
import Pageviews from './components/pageviews.js';
|
||||
import RealtimeVisits from './components/realtime.js';
|
||||
import VisitsGraph from './components/visits-graph.js';
|
||||
import LogoutButton from './components/logoutButton.js';
|
||||
import { h, render, Component } from 'preact';
|
||||
import LoginForm from './components/LoginForm.js';
|
||||
import LogoutButton from './components/LogoutButton.js';
|
||||
import Pageviews from './components/Pageviews.js';
|
||||
import Realtime from './components/Realtime.js';
|
||||
import Graph from './components/Graph.js';
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const App = {
|
||||
controller(args) {
|
||||
this.state = {
|
||||
authenticated: document.cookie.indexOf('auth') > -1
|
||||
};
|
||||
|
||||
this.setState = function(nextState) {
|
||||
m.startComputation();
|
||||
for(var k in nextState) {
|
||||
this.state[k] = nextState[k];
|
||||
}
|
||||
m.endComputation();
|
||||
authenticated: document.cookie.indexOf('auth') > -1,
|
||||
}
|
||||
},
|
||||
view(c) {
|
||||
if( ! c.state.authenticated ) {
|
||||
return m('div.container', [
|
||||
m.component(Login, {
|
||||
onAuth: () => {
|
||||
c.setState({ authenticated: true })
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
// logged-in
|
||||
if( this.state.authenticated ) {
|
||||
return (
|
||||
<div class="container">
|
||||
<header class="header cf">
|
||||
<h1 class="pull-left">Ana <small>open web analytics</small></h1>
|
||||
<div class="pull-right">
|
||||
<LogoutButton onSuccess={() => { this.setState({ authenticated: false })}} />
|
||||
</div>
|
||||
</header>
|
||||
<Realtime />
|
||||
<Graph />
|
||||
<Pageviews />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
m('div.container', [
|
||||
m('div.header.cf', [
|
||||
m('h1.pull-left', 'Ana'),
|
||||
m('div.pull-right', [
|
||||
m.component(LogoutButton, {
|
||||
cb: () => {
|
||||
c.setState({ authenticated: false })
|
||||
}
|
||||
})
|
||||
]),
|
||||
]),
|
||||
m.component(RealtimeVisits),
|
||||
m.component(VisitsGraph),
|
||||
m.component(Pageviews),
|
||||
])
|
||||
]
|
||||
// logged-out
|
||||
return (
|
||||
<div class="container">
|
||||
<header class="header cf">
|
||||
<h1 class="pull-left">Ana <small>open web analytics</small></h1>
|
||||
</header>
|
||||
<LoginForm onAuth={() => { this.setState({ authenticated: true })}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
m.mount(document.getElementById('root'), App)
|
||||
render(<App />, document.getElementById('root'));
|
||||
|
@ -9,6 +9,12 @@ body {
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #111;
|
||||
|
||||
small {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
|
@ -9,9 +9,11 @@
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babelify": "^7.3.0",
|
||||
"chart.js": "^2.4.0",
|
||||
"mithril": "^0.2.5"
|
||||
"mithril": "^0.2.5",
|
||||
"preact": "^6.4.0"
|
||||
}
|
||||
}
|
||||
|
31
yarn.lock
31
yarn.lock
@ -176,6 +176,15 @@ babel-generator@^6.18.0:
|
||||
lodash "^4.2.0"
|
||||
source-map "^0.5.0"
|
||||
|
||||
babel-helper-builder-react-jsx@^6.8.0:
|
||||
version "6.18.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.18.0.tgz#ab02f19a2eb7ace936dd87fa55896d02be59bf71"
|
||||
dependencies:
|
||||
babel-runtime "^6.9.0"
|
||||
babel-types "^6.18.0"
|
||||
esutils "^2.0.0"
|
||||
lodash "^4.2.0"
|
||||
|
||||
babel-helper-call-delegate@^6.18.0:
|
||||
version "6.18.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.18.0.tgz#05b14aafa430884b034097ef29e9f067ea4133bd"
|
||||
@ -263,6 +272,10 @@ babel-plugin-check-es2015-constants@^6.3.13:
|
||||
dependencies:
|
||||
babel-runtime "^6.0.0"
|
||||
|
||||
babel-plugin-syntax-jsx@^6.8.0:
|
||||
version "6.18.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
|
||||
|
||||
babel-plugin-transform-es2015-arrow-functions@^6.3.13:
|
||||
version "6.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.8.0.tgz#5b63afc3181bdc9a8c4d481b5a4f3f7d7fef3d9d"
|
||||
@ -432,6 +445,14 @@ babel-plugin-transform-es2015-unicode-regex@^6.3.13:
|
||||
babel-runtime "^6.0.0"
|
||||
regexpu-core "^2.0.0"
|
||||
|
||||
babel-plugin-transform-react-jsx:
|
||||
version "6.8.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.8.0.tgz#94759942f70af18c617189aa7f3593f1644a71ab"
|
||||
dependencies:
|
||||
babel-helper-builder-react-jsx "^6.8.0"
|
||||
babel-plugin-syntax-jsx "^6.8.0"
|
||||
babel-runtime "^6.0.0"
|
||||
|
||||
babel-plugin-transform-regenerator@^6.16.0:
|
||||
version "6.16.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.16.1.tgz#a75de6b048a14154aae14b0122756c5bed392f59"
|
||||
@ -528,7 +549,7 @@ babel-types@^6.16.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.8.
|
||||
lodash "^4.2.0"
|
||||
to-fast-properties "^1.0.1"
|
||||
|
||||
babelify:
|
||||
babelify@^7.3.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/babelify/-/babelify-7.3.0.tgz#aa56aede7067fd7bd549666ee16dc285087e88e5"
|
||||
dependencies:
|
||||
@ -1119,7 +1140,7 @@ escape-string-regexp@^1.0.2:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
||||
esutils@^2.0.2:
|
||||
esutils@^2.0.0, esutils@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
|
||||
|
||||
@ -2174,7 +2195,7 @@ minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
mithril:
|
||||
mithril@^0.2.5:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/mithril/-/mithril-0.2.5.tgz#c1a50438a93ac23f11ada91188bb784c755404c2"
|
||||
|
||||
@ -2480,6 +2501,10 @@ pinkie@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
|
||||
|
||||
preact:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-6.4.0.tgz#1b8c99754b002639a1c33e68175eefa98a01cc14"
|
||||
|
||||
preserve@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||
|
Loading…
x
Reference in New Issue
Block a user