[The past Year of Literature is dedicated to]
It was another Friday in a quiet, cozy bar with best friends ... The conversation went as usual: news, work, jokes, and again in a circle. In search of a topic of conversation, sipping from beer mugs, for some reason they remembered the verses :) And then everyone began to recall that he still remembers from those distant school years. If I stumbled, the rest suggested, if anyone remembered, it was quite fun and interesting. Returning home that evening, I thought: what if you make a simple web application so that everyone can remember these beautiful works of Russian poetic thought? The design of the application is already spinning in my head, and I sat down to develop ...
It is necessary to write a web application where the user randomly drops a poem, which he must finish by appending the "missing" words as the story goes. At the end, the user should be shown the result and the opportunity to open another poem.
Additional application requirements:
For the design, the old editions of the poems were specially studied in order to draw on the style of the print editions of the time. I wanted something printed, the feeling of "paper" and at the same time simple. The current flat-design fascination makes things easier, especially for a programmer, because he is not a designer. IMHO, it turned out tolerably:
A good project needs a good name. After checking several domains, I quickly found a suitable name for the project - literator.io . It was only later that I found out how much a domain in the .io zone costs, but it was already too late to change something, "art requires sacrifice."
What can please a programmer at work more than the opportunity to write good code and not be bound by deadlines?
But the order is still needed, and I muddied a small Kanban in Trello. I love order.
Yes, I noticed that now on GitHub you can also create boards. Perhaps I will move there.
The choice of a framework for building an application fell on AngularJS , because several projects from the main place of work used it. It was a year ago, Angular 2 was still in deep ass beta, and React was unfamiliar to me. I also wanted to hone my skills in AngularJS, so as not to lose my knack, because at work I had to write more on vanilla JS under the Titanium SDK, and this is a completely different area.
Having considered the possible architecture and having estimated various options, I came to the following structure:
As you can see, two entities stand out: authors and poems. Each of the entities is described by metadata, the files of which are located in the same directories. Verses (verses) additionally have a content.txt file, where the verse is stored.
Separately, the structure.json file is highlighted. If you remember, one of the conditions of the task was to make an application for which no backend will be needed. And since there is no backend, we cannot go through the available directories to find out the structure of our data. Just for this you need the structure.json file, which stores all the structure and metadata. In order not to change this file manually each time, a Node-utility was written, which goes through all the available directories and collects metadata (it’s not for nothing that we laid them out, besides it’s convenient).
As the saying goes: "Divide and conquer." A good developer will not store data (albeit static) along with the source code in the same repository, so a separate repository was created for the poems. It also allows you to use all the power of pull requests so that anyone can add new poems and new authors. This repository connects to the main repository via Git Submodules , which gives additional control over which data revision is currently used.
It remained to change the task grunt build
, so that everything was collected and copied to their places.
Separately, I wanted to dwell on how the auto-completion of words was implemented and, in general, breaking a verse into fragments that need to be filled in by the user. There was a choice between the algorithmic choice of blocks and the explicit indication of these blocks in the verse itself. I also wanted to make the choice of difficulty in the future, so the blocks had to be chosen differently. I chose the latter — an explicit indication of these blocks in the verse itself. This requires preliminary preparation of the text of the verse, but allows you to get the best result, because allows you to choose [subjectively] the most successful fragments that will correspond to the rhyme / flow of the poem and the meaning of the story. Also, the person who prepares the verse can estimate in advance how difficult / simple it will be to solve a specific fragment.
{{}}. {} {}, {} , {}.
As you can see, the fragments are enclosed in braces. Nesting is necessary to specify fragments of different complexity. The brackets "open" from inner to outer, so the "innermost" brackets in the concrete selection correspond to the "easy" level of complexity. A fragment of the form {{}}
corresponds to the fact that it will be used only for "medium" complexity and skipped to "easy", since nested brackets contain nothing. Thus, you can add any complexity, in principle, but currently only two are used in the markup, and in the application so far only the “light” is available.
When you select a poem, it is normalized for the selected complexity and the extra brackets are removed, after which it is broken into fragments. If interested, the code below. I tried to make the normalization regular, but I could not assemble a worker, although I think it is possible (it should work for any nesting, not only for two).
Verse.prototype = { // … /** * Returns string, which normalized to passed difficulty (removes other difficulties' markup) * @param {String} string * @param {String} difficulty * @returns {String} */ normalizeStringToDifficulty: function(string, difficulty) { var self = this; // Convert difficulty string into int of complexity var complexity = difficulty === self.DIFFICULTY_EASY ? 1 : 2; // Normalize verse content to passed difficulty by removing block separators for other difficulties // (if somebody know easier solution, drop me pull request :) var contentArray = string.split(''); var startCharPositions = []; var endCharPositions = []; contentArray.forEach(function(char, index){ // Count separators switch (char) { case self.BLOCK_SEPARATOR_START: startCharPositions.push(index); break; case self.BLOCK_SEPARATOR_END: endCharPositions.push(index); break; } // Check if we counted all separators for one block (Note: current algo is not working with mixed groups) if (startCharPositions.length && startCharPositions.length === endCharPositions.length) { var blockComplexity = Math.min(startCharPositions.length, complexity); // if block has lower complexity, use its maximum // Cleanup unnecessary blocks' separators startCharPositions.reverse().forEach(cleanup); endCharPositions.forEach(cleanup); // Reset stored positions startCharPositions = []; endCharPositions = []; } }); return contentArray.join(''); function cleanup(position, index) { if (index + 1 !== blockComplexity) { delete contentArray[position]; } }; } // … /** * Returns content divided into pieces to display it later * @param options * @returns {Array} */ getPieces: function(options) { var self = this; options = angular.extend({ difficulty: 'easy', }, options); // Get normalized content var contentArray = self.normalizeStringToDifficulty(self.content, options.difficulty).split(''); // Divide into pieces var pieces = []; var isInBlock = false; var blockPiece = null; contentArray.forEach(function(char){ switch (char) { case self.BLOCK_SEPARATOR_START: isInBlock = true; blockPiece = ''; break; case self.BLOCK_SEPARATOR_END: isInBlock = false; if (blockPiece.length) { pieces.push(new VerseBlock(blockPiece)); } break; default: if (isInBlock) { blockPiece += char; } else { pieces.push(char); } } }); return pieces; }
Special attention was paid to usability and compatibility with mobile platforms, because as you know, the user experience there is significantly different from the desktop one. In addition, mobile web surfing has already overtaken the desktop.
Optimization for iOS Safari brought some pain, mainly due to the fact that you cannot programmatically put the focus on the input field if the user has not performed any touch actions. Therefore, there was a need to add a special hint for the user to tap into any place on the screen - only then can we set the focus on the desired element, which slightly spoils usability. If someone knows how to solve this problem - write! My last attempt here is https://jsfiddle.net/6tfrh7qn/5/ (open in iPhone Simulator). Still not found how to pull the carriage away .
Still, there is some kind of bug with a cursor that may not be displayed after focusing on the field.
In the process of development, User Testing was conducted regularly (mostly for relatives and friends) in order to identify errors in the UI, UX (user experience) and in general to check the correctness of the idea presentation. The tests gave very good results, allowing a significant improvement in the UX.
On one of the tests, it turned out that the user thought that in the place where the narration stopped, you need to write everything that you remember next. Then the first clue was changed from "Start typing and finish the poem" to "Start typing the next word."
It also turned out that the user did not understand whether he was typing correctly and how much he needed to type, because he was mostly looking at the keyboard. Then I added a sound that is played with the correct completion of the fragment. It turned out quite intuitively.
In general, a very useful practice, do not neglect the user tests!
Also, all the code is covered by Unit-tests and E2E-tests; not every project can be carved out for this time. Writing them was a pleasure, in some places the development was on TDD. Yes, the E2E tests on the Protractor can be skipped, if the browser window launched by the test is in the background or invisible. If anyone knows how to fix it, please report it.
Web application: http://literator.io
Application repository: https://github.com/bobrosoft/literator.io
Poems repository: https://github.com/bobrosoft/literator.io-verses
The development went slowly, and more than a year has passed since its launch. Although the main part was completed fairly quickly, it took time to polish and finish everything - the Pareto principle in action :) Sometimes there was a desire to continue, because completely new ideas came to mind, but I made an effort, otherwise this gestalt would not give rest.
I think that for the beginning I added all the verses that should be known to most of us. I tried not to take long poems (although there is a "Borodino"), so as not to tire the user. If you undeservedly missed something, write in the comments, add.
Project development ideas (in order of importance):
Thanks for attention! I hope someone will find this project interesting and will bring positive emotions :)
UPD: anyone interested in further development of the project, join the group https://vk.com/literatorio or https://www.facebook.com/LiteratorioApp/ so as not to miss the update announcement.
Source: https://habr.com/ru/post/311300/
All Articles