<p dir="auto"><span>I’ve been following the Blockstack ecosystem for awhile and even found myself as a github contributor. Recently the team started a contribution site that rewards individuals who are able to make meaningful contributions to the team via blog posts, pull requests, etc. (<a href="https://contribute.blockstack.org/" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://contribute.blockstack.org/)
<p dir="auto"><span>If you do not know what Blockstack is or what they do, I recommend you look into their main website <a href="https://blockstack.org/" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://blockstack.org/ and read the white paper. This post will not dive into the goals or vision of the team but rather a technical approach to using the blockstack.js library
<p dir="auto">The contribution program gave me a boosted motivation to really deep dive into the blockstack.js tutorials. The one that really stood out is the multi-layer storage post by Ken Liao. I completed the tutorial but felt like it was incomplete and needed a bit more to fully make it usable for real world use.
<p dir="auto"><span>Before you move on I highly encourage (or demand) that you go through the tutorial here <a href="https://blockstack.org/tutorials/multi-player-storage" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://blockstack.org/tutorials/multi-player-storage …. Otherwise it won’t make any sense at all!
<hr />
<p dir="auto">So you finished the tutorial. You can now see the status of your profile and others. If you want to create a new status it can be done via the input parameters. If you go to another person’s page, you lose the create privileges. However what about deleting a status?
<p dir="auto">I did a minor refactor on the status display portion of the page. I realized there would be a bit of logic/criteria required to have the ability to delete a status.
<p dir="auto">Criteria #1: You have to be the actual user to delete a status
<p dir="auto">Criteria #2: You need to display the delete button
<p dir="auto">Criteria #3: Display to the user a confirmation text if deletion is intentional
<p dir="auto">I decided to create a Status component as shown below in the file <code>Profile.jsx
<pre><code>// Old
{this.state.statuses.map((status) => (
<div className="status" key={status.id}>
{status.text}
</div>
)
)}
// New
{
this.state.statuses.map((status) => (
<Status
key={status.id}
status={status}
handleDelete={this.handleDelete}
isLocal={this.isLocal}
/>
))
}
<p dir="auto">I created a new function called <code>handleDelete . Similar to <code>saveNewStatus , I needed a function that had the ability to hook into the blockstack.js library to filter and remove the intended deleted status and use the putFile function to create new <code>status.json.
<pre><code>handleDelete(id) {
const statuses = this.state.statuses.filter((status) => status.id !== id)
const options = { encrypt: false }
putFile(statusFileName, JSON.stringify(statuses), options)
.then(() => {
this.setState({
statuses
})
})
}
}
<p dir="auto">As you can see the function <code>handleDelete is relatively similar to <code>saveNewStatus<br />
For reference here is the full <code>Profile.jsx file<span> . The function is expecting an argument of id . With this argument we can compare and filter out the id from this.state.statuses . If you’re wondering why I did this approach as opposed to a traditional API DELETE method, I recommend reading this article <a href="https://blockstack.org/tutorials/managing-data-with-gaia" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://blockstack.org/tutorials/managing-data-with-gaia
<pre><code>import React, { Component } from 'react';
import {
isSignInPending,
loadUserData,
Person,
getFile,
putFile,
lookupProfile
} from 'blockstack';
import Status from './Status.jsx';
const avatarFallbackImage = 'https://s3.amazonaws.com/onename/avatar-placeholder.png';
const statusFileName = 'statuses.json'
export default class Profile extends Component {
constructor(props) {
super(props);
this.state = {
person: {
name() {
return 'Anonymous';
},
avatarUrl() {
return avatarFallbackImage;
},
},
username: "",
newStatus: "",
statuses: [],
statusIndex: 0,
isLoading: false
};
this.handleDelete = this.handleDelete.bind(this);
this.isLocal = this.isLocal.bind(this);
}
componentDidMount() {
this.fetchData()
}
handleNewStatusChange(event) {
this.setState({
newStatus: event.target.value
})
}
handleNewStatusSubmit(event) {
this.saveNewStatus(this.state.newStatus)
this.setState({
newStatus: ""
})
}
handleDelete(id) {
const statuses = this.state.statuses.filter((status) => status.id !== id)
const options = { encrypt: false }
putFile(statusFileName, JSON.stringify(statuses), options)
.then(() => {
this.setState({
statuses
})
})
}
saveNewStatus(statusText) {
let statuses = this.state.statuses
let status = {
id: this.state.statusIndex++,
text: statusText.trim(),
created_at: Date.now()
}
statuses.unshift(status)
const options = { encrypt: false }
putFile(statusFileName, JSON.stringify(statuses), options)
.then(() => {
this.setState({
statuses: statuses
})
})
}
fetchData() {
if (this.isLocal()) {
this.setState({ isLoading: true })
const options = { decrypt: false, zoneFileLookupURL: 'https://core.blockstack.org/v1/names/' }
getFile(statusFileName, options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
person: new Person(loadUserData().profile),
username: loadUserData().username,
statusIndex: statuses.length,
statuses: statuses,
})
})
.finally(() => {
this.setState({ isLoading: false })
})
} else {
const username = this.props.match.params.username
this.setState({ isLoading: true })
lookupProfile(username)
.then((profile) => {
this.setState({
person: new Person(profile),
username: username
})
})
.catch((error) => {
console.log('could not resolve profile')
})
const options = { username: username, decrypt: false, zoneFileLookupURL: 'https://core.blockstack.org/v1/names/'}
getFile(statusFileName, options)
.then((file) => {
var statuses = JSON.parse(file || '[]')
this.setState({
statusIndex: statuses.length,
statuses: statuses
})
})
.catch((error) => {
console.log('could not fetch statuses')
})
.finally(() => {
this.setState({ isLoading: false })
})
}
}
isLocal() {
return this.props.match.params.username ? false : true
}
render() {
const { handleSignOut } = this.props;
const { person } = this.state;
const { username } = this.state;
return (
!isSignInPending() && person ?
<div className="container">
<div className="row">
<div className="col-md-offset-3 col-md-6">
<div className="col-md-12">
<div className="avatar-section">
<div className="username">
<h1>
<span id="heading-name">{ person.name() ? person.name()
: 'Nameless Person' }</span>
</h1>
<span>{username}</span>
{this.isLocal() &&
<span>
|
<a onClick={ handleSignOut.bind(this) }>(Logout)</a>
</span>
}
</div>
</div>
</div>
{this.isLocal() &&
<div className="new-status">
<div className="col-md-12">
<textarea className="input-status"
value={this.state.newStatus}
onChange={e => this.handleNewStatusChange(e)}
placeholder="What's on your mind?"
/>
</div>
<div className="col-md-12 text-right">
<button
className="btn btn-primary btn-lg"
onClick={e => this.handleNewStatusSubmit(e)}
>
Submit
</button>
</div>
</div>
}
<div className="col-md-12 statuses">
{this.state.isLoading && <span>Loading...</span>}
{
this.state.statuses.map((status) => (
<Status
key={status.id}
status={status}
handleDelete={this.handleDelete}
isLocal={this.isLocal}
/>
))
}
</div>
</div>
</div>
</div> : null
);
}
}
<p dir="auto">In order for this function to be fully utilized we need our <code>Status component to properly work.<br />
For simplicity sake I decided to show the source code below with some bullet points.
<pre><code>import React, { Component } from 'react';
class Status extends Component {
constructor(props) {
super(props);
this.state = {
showDeleteConfirmation: false
}
this.onDeleteClick = this.onDeleteClick.bind(this);
this.toggleDeleteConfirmation = this.toggleDeleteConfirmation.bind(this);
}
onDeleteClick() {
const { status } = this.props;
this.props.handleDelete(status.id);
}
toggleDeleteConfirmation() {
this.setState({ showDeleteConfirmation: !this.state.showDeleteConfirmation })
}
render() {
const { status, isLocal } = this.props;
if(!isLocal()) {
return (
<div className="status">
<div className="status-text">
{status.text}
</div>
</div>
)
}
return (
<div className="status">
<div className="status-text">
{status.text}
</div>
<div className="status-button-options">
<button
className="btn btn-danger status-delete"
onClick={this.toggleDeleteConfirmation}
disabled={this.state.showDeleteConfirmation}
>
Delete
</button>
</div>
{
this.state.showDeleteConfirmation &&
<div className="status-delete-confirmation">
<h4>ARE YOU SURE?</h4>
<button className="btn btn-danger status-delete-yes" onClick={this.onDeleteClick}>
YES
</button>
<button className="btn btn-info status-delete-no" onClick={this.toggleDeleteConfirmation}>
NO
</button>
</div>
}
</div>
)
}
}
export default Status;
<ol>
<li><p dir="auto">status props is passed down through parent via <code>Profile.jsx
<li><p dir="auto"><code>isLocal function is passed down through parent via <code>Profile.jsx . If isLocal() is false render the status text WITHOUT the ability to delete. (This means you are viewing someone else’s profile)
<li><p dir="auto"><code>showDeleteConfirmation is an internal state of the <code>Status component to determine whether or not to show the confirmation text. If true, show the confirmation text.
<li><p dir="auto">The delete button has a <code>onClick handler that calls <code>this.toggleDeleteConfirmation. This function is responsible for displaying the confirmation text via state change.
<li><p dir="auto">The confirmation text will be rendered like below
<p dir="auto"><img src="https://images.hive.blog/768x0/https://steemitimages.com/DQmZ9r6HBtsHpAHUp7yV27fiwR8gyZ2ndj8YUiaMpD6nHhK/edit%20profile.png" alt="edit profile.png" srcset="https://images.hive.blog/768x0/https://steemitimages.com/DQmZ9r6HBtsHpAHUp7yV27fiwR8gyZ2ndj8YUiaMpD6nHhK/edit%20profile.png 1x, https://images.hive.blog/1536x0/https://steemitimages.com/DQmZ9r6HBtsHpAHUp7yV27fiwR8gyZ2ndj8YUiaMpD6nHhK/edit%20profile.png 2x" />
<p dir="auto">6a. Delete button will be disabled via this.state.showDeleteConfirmation
<p dir="auto">6b. Clicking NO will call this.toggleDeleteConfirmation and reset the state. Thus, hiding the<br />
confirmation buttons.
<p dir="auto">6c. Clicking YES will call this.onDeleteClick . Inside of that function it calls the parent function of Profile’s handleDelete via props this.props.handleDelete(status.id) , which also includes the id of the status. Through this, the status will now be deleted and the statuses.json will render the new results.
<ol>
<li>If viewing a user you will not be shown the Delete option.
<p dir="auto"><img src="https://images.hive.blog/768x0/https://steemitimages.com/DQmRoJcH9peaU6wxx6FsYVHVwNyXZSAZCVbtsYLmQM8HBfQ/non%20edit%20profile.png" alt="non edit profile.png" srcset="https://images.hive.blog/768x0/https://steemitimages.com/DQmRoJcH9peaU6wxx6FsYVHVwNyXZSAZCVbtsYLmQM8HBfQ/non%20edit%20profile.png 1x, https://images.hive.blog/1536x0/https://steemitimages.com/DQmRoJcH9peaU6wxx6FsYVHVwNyXZSAZCVbtsYLmQM8HBfQ/non%20edit%20profile.png 2x" />
<p dir="auto"><span>Example: <a href="http://localhost:8080/kkomaz.id" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">http://localhost:8080/kkomaz.id
<h2>Minor CSS add-ons
<p dir="auto">I added some margin between the buttons to give a cleaner ui look which you can copy and paste at the bottom of the style.css file.
<pre><code>.status-delete{margin-top: 10px}
.status-delete-confirmation {margin-top: 10px}
.status-delete-yes{margin-right: 5px}
<h2>Conclusion
<p dir="auto">In my opinion, this is the standard CRUD blog post when beginners start learning web development. However, this post is configured for a blockstack application.<br />
Prior to our changes, we had the ability to create a status and view the statuses of other profiles.<br />
Now we have the ability to DELETE. What’s next should be the ability to EDIT a status. Stay tuned!
<h2>Things to note:
<p dir="auto">I removed the<code>img tag under the avatar element because steemit was complaining about url not being valid. Refer to my medium article for the full source code.
<p dir="auto"><span><a href="https://medium.com/@kkomaz/blockstack-js-status-multi-layer-followup-part-i-df560a638e6b" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://medium.com/@kkomaz/blockstack-js-status-multi-layer-followup-part-i-df560a638e6b
<p dir="auto">Github Link:<br /><span>
<a href="https://github.com/kkomaz/publik" target="_blank" rel="nofollow noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">https://github.com/kkomaz/publik