I originally found this article describing how Jaap Meijers made a literary clock using an old Amazon Kindle and a quote list generated by The Guardian. I used the same quote list and created a rather simple web page that also worked as a literary clock. (This is not the original version as I wasn't using GitHub when I first coded a working solution.)
As many developers will tell you, I revisited the code much later and found that I didn't like it. The original version had everything as one giant function. It was poorly organized. It just made me cringe, as all old code can and often does.
I decided to create an updated version of the application. My days are currently (as of this writing) spent working with Angular applications. Being the most familiar to me, I went with that framework. The current iteration is built using Angular 9, although ng10 has recently been released.
After building the application, I was not satisfied that I had a multiple gaping holes in the dataset. I did some more searching and found a very functional, very complete literary clock at LiteraryClock.com. I reached out to the site's creator, Justin Whalley, who gladly handed over his pains-takingly curated dataset. So a MASSIVE thank you to Justin!
This actually breaks into two problems, as the dataset I have is written using a 24hr time model and I want to display as 12hr time model. Either way, I need the current time. So I use the ES5 Date
method.
// clock.component.ts #SetTime()
const now = new Date();
Now that I have the current time stamp, I need to get the hours, the minutes, and also decide if I need to display 'AM' or 'PM'.
let h = now.getHours();
const m = now.getMinutes();
const i = now.getHours() > 11 ? 'PM' : 'AM';
You'll notice that I use let
for h instead of const
. This is on purpose as I have other transformations to also do on the hours.
Since any time after 12:59 PM has an hour value larger than 12, I need to account for that value being non-conforming with my 12hr clock. So I account for it, using a method that returns a boolean.
if (this.AdjustTime24to12(h)) {
h = h - 12;
}
I could have used a ternary operator, but this is a little more readable, as it clearly indicates what I'm doing and why.
private AdjustTime24to12(hours: number): boolean {
return hours > 12 ? true : false;
}
I also need a valid 24hr time value to compare with the dataset, which has leading zeros. I first tackled the very simple task fo padding the time.
// in #SetTime()
this.time.next(`${h}:${this.Padding(m)} ${i}`);
// Method to pad zeros
private Padding(time: number): string {
return time < 10 ? `0${time}` : time.toString();
}
It's ok to return a string, because I'm looking to match the value against a JSON value. The time
value being set is an RxJs Behavior Subject
.
Now, for the 24hr time. I didn't want to generate a new time, even though this code will run fast enough that no one would notice. I pass the previously generated now
value into a new method to create my dataset search parameter.
private SetMilTime(now: Date): void {
const h = now.getHours();
const m = now.getMinutes();
this.milTime.next(`${this.Padding(h)}:${this.Padding(m)}`);
}
The milTime
is also a Behavior Subject
. The value is emitted here.
Now, I need to create a function that will get the quote from the dataset.
private GetQuote(): Quote {
return this.quotes.filter(q => q.time === this.milTime.value)[0] || null;
}
The quotes
variable is the imported dataset. It's an array of quote
objects. This is the interface:
export interface Quote {
time: string;
quote: string;
source: string;
author: string;
}
The filter
method returns an array, even if no elements match the search parameter. I found it easier to return a single quote value than to add more logic elsewhere. Also, I added the || or null
statement to deal with the possibility that one of the 1440 quotes could magically disappear.
setInterval
is appropriate for repeating code based on a timer. But where would I invoke the method? I chose to use the Angular ngOnInit
Lifecycle hook. This piece of code only gets called once as the page loads. ngOnChanges
gets called first, but it made no difference in execution. Since no user-initiated changes were ever going to happen, this seemed like the best hook to use.
ngOnInit(): void {
setInterval(() => {
this.SetMilTime(this.SetTime());
this.quote = this.GetQuote();
}, 1000);
}
This method does two things, in sequence: sets the time (and search param) and gets the quote from the dataset.
The code is executed each second. I could tweak this to ensure that this application won't become a resources hog, but for the purposes of such a simple app, it's fine.
The rest of the code is rather straight forward. The HTML is a mere 17-ish lines for the component.
<main>
<section class="time">
{{ time | async }}
</section>
<section>
<article class="quote green">
<p> {{ quote?.quote }}</p>
</article>
</section>
<section class="purple">
<article class="source">
{{ quote?.source}}
</article>
<article class="author">
{{ quote?.author}}
</article>
</section>
</main>
I subscribe to the time
behavior subject using the async
pipe. This ensures the subscribe()
and unsubscribe()
methods will be handled automatically. I used the null-safe
(Elvis) operator on the quote
properties. As the page loads, there actually isn't a valid quote for a few hundred milliseconds. The page will load, but it throws an error into the console.
I decided to go with an artistic flare for this clock. I am a fan of Impressionist paintings, with Matisse being my favorite. Water Lilies is one of my favorite paintings. I used it for the background. I used ColorZilla's Chrome Extension to sample colors from the painting and used those colors (with a 40% opacity) as the background colors for the text. I like the Special Elite font from Google Fonts. The remaining CSS exercise is nothing special.
Ordinarily, I would have crammed all of the logic for this application into a Service
. My display component would have been 'dumb,' merely subscribing to the value and displaying the results. I could possibly revisit this issue in the coming weeks. For now, this is the MVP of a literary clock.
I am open to improvements on the code and will gladly accept any additional quotes for the dataset. Please fork the repo, code your heart out, and submit a pull request.
Please submit any bugs you find. Since the application is only supposed to display a quote, the expected behavior is a given. The observed behavior is not. So be verbose if you have to.