Commit 1b16a19d authored by Cheng Lou's avatar Cheng Lou

Upgrade to React 0.4, fully compilant

- Compilant with coding style.
- Removed app.css. No need anymore since we freed id.
- Render info footer separately (specs).
- Tweak footer to make it pass jshint when compiled:
  - {''} after 'left'.
  - {''} around `clear completed`.
  - Breaking style for footer clearButton, `>` on the same line.
- Rename cx and put it in Utils.
- Routing with director.
- Change some keyUp to keyDown (specs).
- Strip form tags (specs? Referred to the stable ones).
- Trim after editing item.
- Manually autofocus (attribute not supported in ie9).
- Input focus now triggers as a callback (because of batch rendering).
- Controlled input.
- Gutter at 80.
- Class `completed` trim whitespace.
- Remove bind.
parent 26bdbb2d
......@@ -3,6 +3,6 @@
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.7",
"react": "~0.3.2"
"react": "~0.4.0"
}
}
{
"name": "react",
"version": "0.4.0",
"main": "react.js",
"homepage": "https://github.com/facebook/react-bower",
"_release": "0.4.0",
"_resolution": {
"type": "version",
"tag": "v0.4.0",
"commit": "54334ad626d26dff4c214d308cefd30ad80fb8e9"
},
"_source": "git://github.com/facebook/react-bower.git",
"_target": "~0.4.0"
}
\ No newline at end of file
{
"name": "react",
"version": "0.4.0",
"main": "react.js"
}
\ No newline at end of file
{
"name": "todomvc-common",
"version": "0.1.7",
"homepage": "https://github.com/tastejs/todomvc-common",
"_release": "0.1.7",
"_resolution": {
"type": "version",
"tag": "v0.1.7",
"commit": "e5b3251c95f29d872636b761e32a2296dc97c3e0"
},
"_source": "git://github.com/tastejs/todomvc-common.git",
"_target": "~0.1.7"
}
\ No newline at end of file
......@@ -136,10 +136,6 @@
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
......
/** Reset base.css #todoapp */
#todoapp {
background: none;
margin: auto;
border: 0;
position: static;
box-shadow: none;
}
#todoapp:before {
display: none;
left: 100%;
}
.todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
.todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
.header {
padding-top: 15px;
border-radius: inherit;
}
.header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
.new-todo {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
.main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
.toggle-all-label {
display: none;
}
.toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: '✔';
line-height: 43px; /* 40 + a couple of pixels visual adjustment */
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
.todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
.todo-list li label {
word-break: break-word;
padding: 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
.todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
.todo-list li .destroy:after {
content: '✖';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
.filters li a.selected {
font-weight: bold;
}
.clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
.clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
.info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
.info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (min-width: 899px) {
/**body*/.learn-bar .todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}
#benchmark {
position: absolute;
left: 0;
top: 0;
padding: 10px;
}
.submitButton {
display: none;
}
......@@ -5,19 +5,20 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>React • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="todoapp"></div>
<section id="todoapp"></section>
<footer id="info"></footer>
<div id="benchmark"></div>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/react/react.js"></script>
<script src="bower_components/react/JSXTransformer.js"></script>
<script src="../../../assets/director.min.js"></script>
<script type="text/jsx" src="js/utils.js"></script>
<script type="text/jsx" src="js/todoItem.js"></script>
<script type="text/jsx" src="js/footer.js"></script>
<script type="text/jsx" src="js/app.js"></script>
<script type="text/jsx" src="js/utils.jsx"></script>
<script type="text/jsx" src="js/todoItem.jsx"></script>
<script type="text/jsx" src="js/footer.jsx"></script>
<script type="text/jsx" src="js/app.jsx"></script>
</body>
</html>
/**
* @jsx React.DOM
*/
/*jshint quotmark:false */
/*jshint white:false */
/*jshint trailing:false */
/*jshint newcap:false */
/*global Utils, ALL_TODOS, ACTIVE_TODOS, COMPLETED_TODOS,
TodoItem, TodoFooter, React, Router*/
(function (window, React) {
'use strict';
window.ALL_TODOS = 'all';
window.ACTIVE_TODOS = 'active';
window.COMPLETED_TODOS = 'completed';
var ENTER_KEY = 13;
var TodoApp = React.createClass({
getInitialState: function () {
var todos = Utils.store('react-todos');
return {
todos: Utils.store('react-todos'),
todos: todos,
nowShowing: ALL_TODOS,
editing: null
};
},
handleSubmit: React.autoBind(function () {
componentDidMount: function () {
var router = Router({
'/': this.setState.bind(this, {nowShowing: ALL_TODOS}),
'/active': this.setState.bind(this, {nowShowing: ACTIVE_TODOS}),
'/completed': this.setState.bind(this, {nowShowing: COMPLETED_TODOS})
});
router.init();
this.refs.newField.getDOMNode().focus();
},
handleNewTodoKeyDown: function (event) {
if (event.which !== ENTER_KEY) {
return;
}
var val = this.refs.newField.getDOMNode().value.trim();
var todos;
var newTodo;
......@@ -24,26 +53,26 @@
title: val,
completed: false
};
this.setState({ todos: todos.concat([newTodo]) });
this.setState({todos: todos.concat([newTodo])});
this.refs.newField.getDOMNode().value = '';
}
return false;
}),
},
toggleAll: function (event) {
var checked = event.nativeEvent.target.checked;
var checked = event.target.checked;
this.state.todos.map(function (todo) {
this.state.todos.forEach(function (todo) {
todo.completed = checked;
});
this.setState({ todos: this.state.todos });
this.setState({todos: this.state.todos});
},
toggle: function (todo) {
todo.completed = !todo.completed;
this.setState({ todos: this.state.todos });
this.setState({todos: this.state.todos});
},
destroy: function (todo) {
......@@ -51,11 +80,15 @@
return candidate.id !== todo.id;
});
this.setState({ todos: newTodos });
this.setState({todos: newTodos});
},
edit: function (todo) {
this.setState({ editing: todo.id });
edit: function (todo, callback) {
// refer to todoItem.js `handleEdit` for the reasoning behind the
// callback
this.setState({editing: todo.id}, function () {
callback();
});
},
save: function (todo, text) {
......@@ -63,9 +96,9 @@
this.setState({todos: this.state.todos, editing: null});
},
cancel: React.autoBind(function () {
cancel: function () {
this.setState({editing: null});
}),
},
clearCompleted: function () {
var newTodos = this.state.todos.filter(function (todo) {
......@@ -86,16 +119,27 @@
var activeTodoCount;
var completedCount;
this.state.todos.map(function (todo) {
var shownTodos = this.state.todos.filter(function (todo) {
switch (this.state.nowShowing) {
case ACTIVE_TODOS:
return !todo.completed;
case COMPLETED_TODOS:
return todo.completed;
default:
return true;
}
}.bind(this));
shownTodos.forEach(function (todo) {
todoItems[todo.id] = (
<TodoItem
todo={ todo }
onToggle={ this.toggle.bind(this, todo) }
onDestroy={ this.destroy.bind(this, todo) }
onEdit={ this.edit.bind(this, todo) }
editing={ this.state.editing === todo.id }
onSave={ this.save.bind(this, todo) }
onCancel={ this.cancel }
todo={todo}
onToggle={this.toggle.bind(this, todo)}
onDestroy={this.destroy.bind(this, todo)}
onEdit={this.edit.bind(this, todo)}
editing={this.state.editing === todo.id}
onSave={this.save.bind(this, todo)}
onCancel={this.cancel}
/>
);
}.bind(this));
......@@ -109,19 +153,24 @@
if (activeTodoCount || completedCount) {
footer =
<TodoFooter
count={ activeTodoCount }
completedCount={ completedCount }
onClearCompleted={ this.clearCompleted.bind(this) }
count={activeTodoCount}
completedCount={completedCount}
nowShowing={this.state.nowShowing}
onClearCompleted={this.clearCompleted}
/>;
}
if (this.state.todos.length) {
main = (
<section class="main">
<input class="toggle-all" type="checkbox" onChange={ this.toggleAll.bind(this) } checked={activeTodoCount === 0} />
<label class="toggle-all-label">Mark all as complete</label>
<ul class="todo-list">
{ todoItems }
<section id="main">
<input
id="toggle-all"
type="checkbox"
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
<ul id="todo-list">
{todoItems}
</ul>
</section>
);
......@@ -129,31 +178,30 @@
return (
<div>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<form onSubmit={ this.handleSubmit }>
<input
ref="newField"
class="new-todo"
placeholder="What needs to be done?"
autofocus="autofocus"
/>
<input type="submit" class="submitButton" />
</form>
</header>
{main}
{footer}
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by{' '}<a href="http://github.com/petehunt/">petehunt</a></p>
<p>Part of{' '}<a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<header id="header">
<h1>todos</h1>
<input
ref="newField"
id="new-todo"
placeholder="What needs to be done?"
onKeyDown={this.handleNewTodoKeyDown}
/>
</header>
{main}
{footer}
</div>
);
}
});
React.renderComponent(<TodoApp />, document.getElementById('todoapp'));
React.renderComponent(
<div>
<p>Double-click to edit a todo</p>
<p>Created by{' '}
<a href="http://github.com/petehunt/">petehunt</a>
</p>
<p>Part of{' '}<a href="http://todomvc.com">TodoMVC</a></p>
</div>,
document.getElementById('info'));
})(window, React);
/**
* @jsx React.DOM
*/
/*jshint quotmark:false */
/*jshint white:false */
/*jshint trailing:false */
/*jshint newcap:false */
/*global React, ALL_TODOS, ACTIVE_TODOS, Utils, COMPLETED_TODOS */
(function (window) {
'use strict';
......@@ -12,18 +17,39 @@
if (this.props.completedCount > 0) {
clearButton = (
<button
class="clear-completed"
onClick={ this.props.onClearCompleted }>
Clear completed ({ this.props.completedCount })
id="clear-completed"
onClick={this.props.onClearCompleted}>
{''}Clear completed ({this.props.completedCount}){''}
</button>
);
}
var show = {
ALL_TODOS: '',
ACTIVE_TODOS: '',
COMPLETED_TODOS: ''
};
show[this.props.nowShowing] = 'selected';
return (
<footer class="footer">
<span class="todo-count">
<strong>{this.props.count}</strong>{' '}{ activeTodoWord }{' '}left
<footer id="footer">
<span id="todo-count">
<strong>{this.props.count}</strong>
{' '}{activeTodoWord}{' '}left{''}
</span>
<ul id="filters">
<li>
<a href="#/" class={show[ALL_TODOS]}>All</a>
</li>
{' '}
<li>
<a href="#/active" class={show[ACTIVE_TODOS]}>Active</a>
</li>
{' '}
<li>
<a href="#/completed" class={show[COMPLETED_TODOS]}>Completed</a>
</li>
</ul>
{clearButton}
</footer>
);
......
/**
* @jsx React.DOM
*/
/*jshint quotmark: false */
/*jshint white: false */
/*jshint trailing: false */
/*jshint newcap: false */
/*global React, Utils */
(function (window) {
'use strict';
var ESCAPE_KEY = 27;
var ENTER_KEY = 13;
window.TodoItem = React.createClass({
handleSubmit: React.autoBind(function () {
handleSubmit: function () {
var val = this.state.editText.trim();
if (val) {
this.props.onSave(val);
this.setState({ editText: val });
this.setState({editText: val});
} else {
this.props.onDestroy();
}
return false;
}),
handleEdit: React.autoBind(function () {
this.props.onEdit();
var node = this.refs.editField.getDOMNode();
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}),
},
handleEdit: function () {
// react optimizes renders by batching them. This means you can't call
// parent's `onEdit` (which in this case triggeres a re-render), and
// immediately manipulate the DOM as if the rendering's over. Put it as a
// callback. Refer to app.js' `edit` method
this.props.onEdit(function () {
var node = this.refs.editField.getDOMNode();
node.focus();
node.setSelectionRange(node.value.length, node.value.length);
}.bind(this));
},
handleKey: React.autoBind(function (event) {
if (event.nativeEvent.keyCode === ESCAPE_KEY) {
this.setState({ editText: this.props.todo.title });
handleKeyDown: function (event) {
if (event.keyCode === ESCAPE_KEY) {
this.setState({editText: this.props.todo.title});
this.props.onCancel();
} else if (event.keyCode === ENTER_KEY) {
this.handleSubmit();
} else {
this.setState({ editText: event.target.value });
this.setState({editText: event.target.value});
}
}),
},
handleChange: function (event) {
this.setState({editText: event.target.value});
},
getInitialState: function () {
return { editText: this.props.todo.title };
return {editText: this.props.todo.title};
},
componentWillReceiveProps: function (nextProps) {
......@@ -47,7 +62,7 @@
render: function () {
return (
<li class={cx({
<li class={Utils.stringifyObjKeys({
completed: this.props.todo.completed,
editing: this.props.editing
})}>
......@@ -55,22 +70,22 @@
<input
class="toggle"
type="checkbox"
checked={ this.props.todo.completed ? 'checked' : null }
onChange={ this.props.onToggle }
checked={this.props.todo.completed ? 'checked' : null}
onChange={this.props.onToggle}
/>
<label onDoubleClick={ this.handleEdit }>{ this.props.todo.title }</label>
<button class='destroy' onClick={ this.props.onDestroy } />
<label onDoubleClick={this.handleEdit}>
{this.props.todo.title}
</label>
<button class='destroy' onClick={this.props.onDestroy} />
</div>
<form onSubmit={this.handleSubmit}>
<input
ref="editField"
class="edit"
value={ this.state.editText }
onBlur={ this.handleSubmit }
onKeyUp={ this.handleKey }
/>
<input type="submit" class="submitButton" />
</form>
<input
ref="editField"
class="edit"
value={this.state.editText}
onBlur={this.handleSubmit}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
/>
</li>
);
}
......
......@@ -12,7 +12,8 @@
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)).toString(16);
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
.toString(16);
}
return uuid;
......@@ -29,19 +30,20 @@
var store = localStorage.getItem(namespace);
return (store && JSON.parse(store)) || [];
}
};
},
window.cx = function (obj) {
var s = '';
var key;
stringifyObjKeys: function (obj) {
var s = '';
var key;
for (key in obj) {
if (obj.hasOwnProperty(key) && obj[key]) {
s += key + ' ';
for (key in obj) {
if (obj.hasOwnProperty(key) && obj[key]) {
s += key + ' ';
}
}
}
return s;
return s.trim();
}
};
})(window);
\ No newline at end of file
})(window);
......@@ -11,9 +11,28 @@ The [React getting started documentation](http://facebook.github.io/react/docs/g
Here are some links you may find helpful:
* [Tutorial](http://facebook.github.io/react/docs/tutorial.html)
* [Philosophy](http://www.quora.com/Pete-Hunt/Posts/React-Under-the-Hood)
* [Common Questions](http://facebook.github.io/react/docs/common-questions.html)
* [Documentation](http://facebook.github.io/react/docs/getting-started.html)
* [API Reference](http://facebook.github.io/react/docs/reference.html)
* [Blog](http://facebook.github.io/react/blog/)
* [React on GitHub](https://github.com/facebook/react)
* [Support](http://facebook.github.io/react/support.html)
Articles and guides from the community:
* [Philosophy](http://www.quora.com/Pete-Hunt/Posts/React-Under-the-Hood)
* [How is Facebook's React JavaScript library](http://www.quora.com/React-JS-Library/How-is-Facebooks-React-JavaScript-library)
* [React: Under the hood](http://www.quora.com/Pete-Hunt/Posts/React-Under-the-Hood)
Get help from other React users:
* [React on StackOverflow](http://stackoverflow.com/questions/tagged/reactjs)
* [Mailing list on Google Groups](https://groups.google.com/forum/#!forum/reactjs)
*
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Running
The app is built with [JSX](http://facebook.github.io/react/docs/jsx-in-depth.html) and compiled at runtime for a lighter and more fun code reading experience. As stated in the link, JSX is not mandatory.
To run the app, spin up an HTTP server (e.g. `python -m SimpleHTTPServer`) and visit http://localhost/.../myexample/.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment