kkomazCoding - Blockstack.js — Status Multi-Player Followup Part I

in #blog6 years ago (edited)

Logo.png

<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> &nbsp;|&nbsp; <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