Tuesday, 9 June 2026

Film Review: Black Mirror Series Seven, Redux (Part 2/3)

The next episode is Eulogy, and it's a bit of a mood whiplash from the preceding episode, but just as good, if not better.

The Premise

Philip receives a call that his old flame Carol has passed away. In order to collect recollections for her memorial, Philip goes through a program by Eulogy designed to read his memories, unearthing some interesting ones...


The Characters

Paul Giamatti absolutely knocks it out of the park in the role of Philip Connarty. Philip is a surly, bitter, cranky old guy. Giamatti carried this episode by serving up an entire buffet of emotional delivery. He also serves as narrator for most of the recollections. He gives us a moving portrayal of a deeply flawed man who loved passionately, experienced guilt, lashed out in anger, and finds closure in forgiveness. Giamatti was immediately recognizable to me as the antagonist in Shoot Em Up. Upon watching this episode, I gained a newfound respect for this man's range.

Patsy Ferran as Kelly Royce, Carol's daughter and the AI that serves Eulogy. She acted as the foil to Philip's general grumpiness, appearing upbeat and chirpy most of the time, occasionally delivering quite the witty barb. The meat of this episode belonged to Giamatti, but without Ferran to play off against, it wouldn't have worked as well.

There were plenty of others in the cast, but they either had no speaking parts, extremely limited screen time or merely appeared as still images... or all at the same time.

The Mood

It begins with a scene of an elderly man trimming rose bushes in some quiet countryside or other. A very placid scene. And the pacing stays slow and steady even as more upsetting memories and twists are revealed. There's a pervading sense of sorrow, old anger and sentimentality, though at no point does the viewer get the sense that anyone is in any particular danger. It's not that kind of Black Mirror episode, and yet it absolutely works.

What I liked

It's a very small cast. Only two actors have any sort of work to do - the rest either appear in non-speaking and/or non-moving roles. Thus, it's up to Paul Giamatti and Patsy Ferran to hold up this episode all by themselves. And these two, quite amazingly, get it done.

Black Mirror is famously full of violence, vulgarity and downer endings. On the rare occasion when things get sentimental and sweet, like now, even admidst the sorrow permeating the episode, it really hits home.



The visuals are amazing. They give us a pretty darn good idea of what it's like to virtually dive into a still photograph, and have AI render everything in 3D. The side effects of Philip using sharpies and knives to erase Carol's face from photographs, are disturbingly rendered as well. It's glorious... with just a hint of spooky.

What I didn't

DHL sending the package by drone was a detail I could have done without. That was so out of left field. I don't actually think drone delivery is too far-fetched, though having it take place in such a casual manner, probably is. There are regulatory issues to think about, especially since the company Eulogy is in the UK and Philip is in the USA or something. And it wasn't even really relevant to the story.


Still, it's a very minor detail in what was otherwise an excellent episode.

Conclusion

This episode was -genius-. It had the Black Mirror DNA - fantastical tech being used in novel ways, flawed characters acting out and inevitable human tragedy. Though, in this case, the tech brings about a somewhat happy ending. This might actually be my favorite episode in an above-average season of Black Mirror.

My Rating

9.5 / 10

Next

USS Callister: Into Infinity

Sunday, 7 June 2026

Film Review: Black Mirror Series Seven, Redux (Part 1/3)

Welcome back! We're going to continue the review of Black Mirror Series Seven. I promise you, it doesn't let up!

The next episode is Plaything, and it's quite the mindfuck.

The Premise

Mentally disturbed Cameron Walker is hauled into a police station where he is interviewed by the police for a possible murder. What follows is Walker's telling of a bizzarre story that has a terrifying conclusion in the present day.

The Characters

Peter Capaldi provides a gripping portrayal of Cameron Walker, a computer nerd who gets sucked into the world of the Throng. And boy, does he deliver. When we first see him, he's a friendly but awfully goofy character who talks silly. By the end of the episode, he's become full-blown deranged and the twist in that master plan is served with such maniacal glee it almost hurts to watch.

Lewis Gribben is a younger Cameron Walker. Had a physical resemblance to Paul Bettany. We get to see him slowly unravel from a shy nerd to murderous lunatic. Gribben brings across the shitload of anxiety this role probably requires, really well.

James Nelson-Joyce as DCI Kano. Stern, cold, uncompromising. Chomping at the bit to prosecute Walker. I had fun watching this guy grind his teeth and try to curb his impulse to beat Cameron Walker to a pulp.

Michele Austin as Jen Minter. She's in the interview for psychological evaluation. We see glimpses of a sense of humor. She's the reasonable authority figure in the room.

Darryl Foster as PC Mo Raiker. Kind of fades into the background if not for that afro, if I'm being honest.

Ami Tredrea as WPC Yvonne Best. She looks constantly serious and focused. Of her and her partner, she's the one that gets most of the dialogue.

Will Poulter reprises his role as Colin Ritman from Bandersnatch. Poulter brings his intense stare back into the Black Mirror universe, and I am here for it! When Colin Ritman emphasizes that the Throng are living and sentient, his deadly earnest delivery really sells it. Poulter has limited screen time in this one, but he makes the absolute most of it.

Asim Chaudhry, too, reprises his role as Mohan Thakur from Bandersnatch. Part manic, part affable, all boss. "My name's Mo but you can call me God."

Josh Finan plays Lump the drug dealer with much goofiness and gusto, complete with annoying shit-eating grin and wild-eyed stare. He was hugely entertaining.

Jay Simpson is Gordon, Walker's boss at PC Zone. At first I thought I was looking at John Cusack. There's a very unsettling air of menace about him as he's on the phone pressuring Cameron to meet a deadline. This actor should have been off playing a mob boss or something, instead of this. Wasted here.

Special mention to Kave Niku as the shopkeeper in the beginning sequence. That look of exasperation was golden.

The Mood

The subdued colors, smoke and debris suggests a dystopian future at the start. In the first few minutes, the mood shifts to a police procedural vibe, with all the trappings of modern-day police drama. Halfway through, flashbacks take us in a new direction - it's a tech thriller both creepy and uncanny. Other than that, once the episode goes into the interview room, the action doesn't leave it, other than through the narrative supplied by Cameron Walker.

What I liked

The game itself looks pretty damn awesome. It's available to download on Google Play. You need a Netflix account to play it. My plate's a little full at the moment, but dammit I want to play this game.


Another thing I loved were the tie-ins to Bandersnatch. Thronglets is a game, and Bandersnatch features a gaming company. So yes, the link to Tuckersoft was genius. Bonus points for that poster of Bandersnatch II!


That twist at the end with the sharpie and how it was used. It was a deliciously horrific end.


Did I mention the sound effects from the game? The squeals, the shrilling, and the ominous hum. It all totally got me.

What I didn't

I have mixed feelings on this. On one hand, that whole surgically inserting a port into the back of his head sequence was suitably gross for a series like Black Mirror.


And on the other hand, I've always liked Black Mirror for tech that is fantastic, but at least plausible. This is just... not. It is pretty bizarre and creepy though, I'll give them that.

Conclusion

What an episode. What. An. Episode!

Plaything is probably one of the creepiest things Black Mirror has produced ever, and has one of the darkest ever endings. I don't know what the writer of this epsiode had been smoking while creating this, but whatever it was, it worked!

My Rating

9 / 10

Next

Eulogy

Monday, 1 June 2026

Bolt CEO's dangerous logic for axing his entire HR Team

It's been a couple weeks since I last heard the news that the entire HR Team at Bolt was fired by CEO Ryan Breslow.

Now I'm not a fan of HR, and I like to warn people that mistaking HR as your friend is a mistake they should make only once, if at all. Hell, "nobody at work is your friend" is a good principle to have, and HR should probably head that list.


But anyone expecting me to join the pile-on and gloat about "HR finally getting a taste of their own medicine"? Sorry lads, I'm going to have to disappoint you today. This is a dodgy move at best.

What happened

Bolt was valued at around $11 billion in 2022 when Breslow stepped away, and lost up to 90% of its valuation by 2026. Breslow returned to make sweeping changes, starting with removing about 30% of staff, including the HR function.

Breslow was on record glibly saying the following.
"We had an HR team, right? And that HR team was creating problems that didn't exist. And those problems disappeared when I let them go."

Don't know about you, but this simultaneously caused me amusement and discomfort. It reminded me of a joke I used to make as a smoker.
"I've been smoking for years, but when I read about all the different toxins I've been inhaling into my body, I got so disturbed that I stopped reading."

Turn that racket off!

I mean, would you turn off the fire alarm just because it made a loud distracting sound every time there was a fire? Or remove the brakes on your car to make it incapable of slowing down?

Yes, that's the TeochewThunder equivalent of sticking your head in the sand.

Another disturbing idea

Breslow was also quoted as saying, sounding somewhat conciliatory.
"Those HR professionals have really important insights when you're in a peacetime at a larger company. But we're a remote company, it's not like - a lot of the potential issues that you would have in a workplace don't really exist because you're not in the same room as somebody."

Did our boy just say we don't really need HR if we're remote? Maybe not, but it sounded an awful lot like it. And in case that's the takeaway anyone gets from this, I'm going to shut it down right now.

HR problems don't exist only if people are in the same room together. Remote workers still have to get paid, appraised and onboarded. They still interact with other workers, even online. Company leadership may still, in their zeal, make errors in judgement they need to be warned about. And in certain cases, HR problems get more intense precisely because it's remote. Workplace bullying, for example. I've encountered wankers who became even bigger wankers simply because being remote meant they weren't in immediate danger of being bitch-slapped for talking shit taken to task for being impolite. Come now, we all know someone like that.

What did he just
say to me?!

The only way remote work lessens the need for HR would be if HR's only function was just to look pretty and ask how everyone's day was. And that's not how it works. Not even a little bit.

So no... HR does not become less relevant because the company structure is remote. Sometimes, in those cases, HR becomes more relevant.

The real danger

Let's give Breslow the benefit of the doubt. Let's say HR was really creating problems where there were none, or making issues out of non-issues. Mountains out of molehills. Playing office politics, or worse, identity politics. Overreaching like they were cosplaying Dhalsim in Street Fighter.

Maybe. And even then, saying "they made problems so I got rid of them, problem solved" is a dangerous path to go down.

The point is, what often looks like non-issues slowing things down unnecessarily, are actual problems that laypersons just aren't equipped to handle. And let me be clear; this is not about defending HR.

Not interested in
defending HR.

What if it was an entire team of software developers who were fired because business people started thinking that software devs and their trifling concerns were slowing shit down? We've got LLMs that can generate code now, so business people don't actually need techies to write code.

However, business people are not software engineers, and being able to produce code doesn't magically make them so. Business people have largely the same concerns as software developers - aesthetics, functionality, compliance, security, maintainability - but ask a business person and software developer to rank all these in order of importance, and their answers would look very different. Go ahead, ask business people what they'd do if you told them implementing unit tests would add another week to deployment time at minimum. I'm not saying that all business people would give you an answer that would have you internally questioning your life choices, but the fact is that business and tech people have very different priorities. That tends to introduce resistance into the decision-making process.

What if people started thinking that, armed with LLMs, they could just replace software engineers and get rid of that resistance? Y'all know it's a matter of time someone starts going down this road, if they haven't already. I've seen business people make apps using LLMs, with sometimes comical results. But let's not pretend that eventually replacing techies isn't on the cards.

Au Revoir, Bolt HR!

Still think the firing the entire HR team isn't a big deal? You're right, it's probably not. But the reasoning - that's something we need to look at.

Time for me to Bolt,
T___T

Thursday, 28 May 2026

Web Tutorial: VueJS Financial Projector (Part 3/3)

At long last, we get to the exciting part! No more CRUD functions - now it's purely display.

Add this to the data. monthItems is an array of data from January to December. monthItemsTemplate is an array that shows us what that data is supposed to look like. The idea here is that the display will refresh when monthItems changes... but I don't want it to render constantly when I recalculate, so I have a temporary array to hold the new completely recalculated array in until I'm ready to replace monthItems.
data: {
    currentIndex: 0,
    errors: {
        name: "",
        amount: "",
    },
    items:[
        {
            name: "",
            month: 0,
            amount: 0
        }
    ],
    monthItems: [],
    monthItemsTemplate: [

    ],

    months: [
        "All", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    ]
},


So right now, this is it. The table looks like this. The first element at index 0 is an object that's just a placeholder. The other rows correspond with the month names - index 1 for "Jan", index 2 for "Feb", etc.
monthItems: [],
monthItemsTemplate: [
    { monthName: "", itemsIn:[], itemsInTotal:"", itemsOut:[], itemsOutTotal:"", cumulative: "" },
    { monthName: "Jan", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Feb", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Mar", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Apr", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "May", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Jun", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Jul", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Aug", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Sep", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Oct", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Nov", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 },
    { monthName: "Dec", itemsIn:[], itemsInTotal:0, itemsOut:[], itemsOutTotal:0, cumulative: 0 }

],


After this, we need a new method, sortItemOrder(). This populates the monthItems array with the correct data. Because elements added, updated or removed in items will trigger a recalculation of cumulative values. The idea for monthItems is that each month, from January to December, will show totals after calculating incoming and outgoing funds.
    setCurrentIndex: function(val) {
        this.currentIndex = val;
    },
    sortItemOrder: function() {

    }

}


We declare a temporary array, tempMonthItems. As its value, we use a copy of monthItemsTemplate using the structuredClone() function. At the end of this method, we're going to replace monthItems with tempMonthItems after processing it. (For more about the structuredClone() function, follow this link.)
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    this.monthItems = structuredClone(tempMonthItems);

}


As part of that process, we're going to fix the elements in tempMonthItems corresponding to months 1 to 12, i.e, January to December. Index 0 is the placeholder element.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {

    }


    this.monthItems = structuredClone(tempMonthItems);
}


We populate itemsIn and itemsOut. itemsIn in each element of tempMonthItems, is the array of every element in items where month is "All" or the current month, and amount is more than 0. itemsOut in each element of tempMonthItems, is the array of every element in items where month is "All" or the current month, and amount is more than 0. To get this data, we use the filter() method.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});
    }

    this.monthItems = structuredClone(tempMonthItems);
}


Then we want the sum of all items in itemsIn and itemsOut, and assign those values to the itemsInTotal and itemsOutTotal properties, using the reduce() method. (For more about the reduce() method, follow this link.)
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});

        tempMonthItems[i].itemsInTotal = tempMonthItems[i].itemsIn.reduce((sum, item) => sum + item.amount, 0);
        tempMonthItems[i].itemsOutTotal = tempMonthItems[i].itemsOut.reduce((sum, item) => sum + item.amount, 0);

    }

    this.monthItems = structuredClone(tempMonthItems);
}


The cumulative property is 0 if we're looking at January because we haven't accumulated anything yet at the start of the year. Thus, if i is 1 or less, cumulative is 0.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});

        tempMonthItems[i].itemsInTotal = tempMonthItems[i].itemsIn.reduce((sum, item) => sum + item.amount, 0);
        tempMonthItems[i].itemsOutTotal = tempMonthItems[i].itemsOut.reduce((sum, item) => sum + item.amount, 0);

        tempMonthItems[i].cumulative = (i > 1 ? : 0);
    }

    this.monthItems = structuredClone(tempMonthItems);
}


If we're at February or later, we take the remainding funds of the current month (itemsInTotal + itemsOutTotal) and add the cumulative property of the previous month, to derive the current month's cumulative value.
sortItemOrder: function() {
    let tempMonthItems = structuredClone(this.monthItemsTemplate);

    for (let i = 1; i <= 12; i++) {
        tempMonthItems[i].itemsIn = this.items.filter((item) => {return item.amount > 0 && [0, i].indexOf(item.month) != -1});
        tempMonthItems[i].itemsOut = this.items.filter((item) => {return item.amount < 0 && [0, i].indexOf(item.month) != -1});

        tempMonthItems[i].itemsInTotal = tempMonthItems[i].itemsIn.reduce((sum, item) => sum + item.amount, 0);
        tempMonthItems[i].itemsOutTotal = tempMonthItems[i].itemsOut.reduce((sum, item) => sum + item.amount, 0);

        tempMonthItems[i].cumulative = (i > 1 ? tempMonthItems[i].itemsInTotal + tempMonthItems[i].itemsOutTotal + tempMonthItems[i - 1].cumulative : 0);
    }

    this.monthItems = structuredClone(tempMonthItems);
}


With that done, we make sure to run sortItemOrder() at the end of addUpdateItem() and removeItem().
    this.setCurrentIndex(0);
    this.sortItemOrder();
},
removeItem: function(index) {
    this.items.splice(index, 1);
    this.setCurrentIndex(0);
    this.sortItemOrder();
},
setCurrentIndex: function(val) {
    this.currentIndex = val;
},


Now, for the HTML!
There, in pnlFinancialProjection, have a table. These are the headers.
<div id="pnlFinancialProjection" class="panel">
    <table>
        <tr class="header">
            <td width="20%">MONTH</td>
            <td width="20%" class="numeric">IN</td>
            <td width="20%" class="numeric">OUT</td>
            <td width="20%" class="numeric">REMAINING</td>
            <td width="20%" class="numeric">CUMULATIVE</td>
        </tr>
    </table>

</div>


And here's your table taking shape...


Then we render rows for every element in monthItems other than the one at index 0.
<table>
        <tr class="header">
            <td width="20%">MONTH</td>
            <td width="20%" class="numeric">IN</td>
            <td width="20%" class="numeric">OUT</td>
            <td width="20%" class="numeric">REMAINING</td>
            <td width="20%" class="numeric">CUMULATIVE</td>
        </tr>

        <tr v-for="(monthItem, monthItemIndex) in monthItems" v-bind:key="monthItemIndex" v-if="monthItemIndex > 0">

        </tr>

</table>


We put in the month name, itemsInTotal, itemsOutTotal, and in the case of the REMAINING column, we calculate it on the spot. If the result is positive, we style it using inText, otherwise we use outText. In both cases, we also use numeric. We do the same for cumulative.
<tr v-for="(monthItem, monthItemIndex) in monthItems" v-bind:key="monthItemIndex" v-if="monthItemIndex > 0">
    <td>{{ monthItem.monthName }}</td>
    <td class="numeric inText">{{ monthItem.itemsInTotal }}</td>
    <td class="numeric outText">{{ monthItem.itemsOutTotal }}</td>
    <td v-bind:class=" monthItem.itemsInTotal + monthItem.itemsOutTotal > 0 ? 'numeric inText' : 'numeric outText'">{{ monthItem.itemsInTotal + monthItem.itemsOutTotal }}</td>
    <td v-bind:class=" monthItem.cumulative > 0 ? 'numeric inText' : 'numeric outText'">{{ monthItem.cumulative }}</td>

</tr>



Let's test this app!
Add the item "Salary" for all months. I'm just going to put SGD 6,000 here.


Then we'll balance it out with "Expenses", which is an outgoing item for all months. We set it at SGD 1,000. Look at the financial projection now.


Then we have "Income Tax", which is to be paid in May.


Then I declare "Annual Bonus" in December, an incoming item at another SGD 6,000.

And let's say I pay "AIA Insurance" in September. Now in the financial projection, you can see that the REMAINING column is starting to show red.

Here, I add "Womb Tax" for all months, which is my quaint codeword for money I give my mother.


And "Investments" for all months. Now you can see , more of the REMAINING column has turned red. The CUMULATIVE column adjusts automatically as well.


Right on the money,
T___T

Monday, 25 May 2026

Web Tutorial: VueJS Financial Projector (Part 2/3)

The next part is to display items in a table, so you can see what items you added.

In the HTML, inside the pnlItems div, add a table. The header will be styled using the CSS class header, while the AMOUNT column will be styled using the CSS class numeric.
<div id="pnlItems" class="panel">
    <table>
        <tr class="header">

            <td width="20%">MONTH</td>
            <td width="40%">NAME</td>
            <td width="20%" class="numeric">AMOUNT</td>
            <td width="10%"></td>
            <td width="10%"></td>
        </tr>
    </table>
</div>


header will be in bold, and numeric means that text is aligned right. That's really all there is to it.
.panel
{
    float: left;
    padding: 10px;
    border-radius: 20px;
    outline: 1px solid rgba(255, 150, 0, 0.5);
    margin-right: 10px;
    margin-bottom: 10px;
}

.numeric
{
    text-align: right;
}

.header
{
    font-weight: bold;
}


label
{
    width: 5em;
    font-size: 0.5em;
    float: left;
}


So we've got the beginnings of a table.


Now let's add content. We want the rows to render for as many elements there are in items. We also want to set key because it's a repeated HTML element we're creating.
<div id="pnlItems" class="panel">
    <table>
        <tr class="header">
            <td width="20%">MONTH</td>
            <td width="40%">NAME</td>
            <td width="20%" class="numeric">AMOUNT</td>
            <td width="10%"></td>
            <td width="10%"></td>
        </tr>

        <tr v-for="(i, index) in items" v-bind:key="index">
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
        </tr>
    </table>
</div>


In here, we add the month, the name and amount. Note that the amount column is styled using the numeric CSS class.
<tr v-for="(i, index) in items" v-bind:key="index">
    <td>{{ months[i.month] }}</td>
    <td>{{ i.name }}</td>
    <td class="numeric">{{ i.amount }}</td>
    <td></td>
    <td></td>
</tr>


Now we can test this. When you refresh, the first item in items appears.


Now add something. Here I call it "Investment Dividend" and say it's incoming of SGD 1,500. Hey, a guy can dream.


Click the ADD button and you see it appears. So far so good.


Now add something else, an outgoing amount. Wifey's birthday isn't in March and I wish I spent only a thousand, but this is just an example.


See? Some things to correct.
- The amount appears negative. Would be nice if we could color code this.
- It appears in order of entry, which is fine until you have like 50 items.
- We also want to not show the first item.


We start off by adding these CSS classes, inText and outText. So incoming money is marked in green, and outgoing money in red.
label
{
    width: 5em;
    font-size: 0.5em;
    float: left;
}

.inText
{
    color: rgb(0, 200, 0);
}

.outText
{
    color: rgb(200, 0, 0);
}


input[type=text], input[type=number], select
{
    width: 10em;
    padding: 0em;
}


Then we change the class. Instead of just styling using numeric, we use a combination of numeric and inText or outText, depending on whether the amount is positive.
<tr v-for="(i, index) in items" v-bind:key="index">
    <td>{{ months[i.month] }}</td>
    <td>{{ i.name }}</td>
    <td v-bind:class="i.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ i.amount }}</td>
    <td></td>
    <td></td>
</tr>


Now in the HTML, we add a conditional. This means the HTML element renders only if index is greater than 0.
<tr v-for="(i, index) in items" v-bind:key="index" v-if="index > 0">


If you retry everything, you should see that incoming and outgoing amounts are colored differently, and the first row no longer appears. But soon, we'll be doing something bigger.

In computed, add the method sortedItems(). This actually returns the sorted view of the items array.
computed: {
    sortedItems: function() {
        return this.items

    }
},


Here, we use the map() method to iterate through items and return the index and the element as a new object. This serves to preserve both the index, index and the element, item.
computed: {
    sortedItems: function() {
        return this.items
        .map((item, index) => ({ item, index }))
    }
},


And we continue by chaining on a sort() method, sorting by the month property of item. This works because we need the index... but sorting items and adding or removing from it, might change index.
computed: {
    sortedItems: function() {
        return this.items
        .map((item, index) => ({ item, index }))
        .sort((a, b) => a.item.month - b.item.month);
    }
},


Now we'll need to change this. Instead of iterating through items, we iterate through sortedItems and we change all mentions of i to si. Since each element of sortedItems is made of index and item, if we want to refer to the element's properties, we have to refer to it as item.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
    <td>{{ months[si.item.month] }}</td>
    <td>{{ si.item.name }}</td>
    <td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
    <td></td>
    <td></td>
</tr>


We'll then add two buttons. One is an UPDATE button and will run the setCurrentIndex() method, passing in index as an argument. The other is a DELETE button that runs the removeItem() method, also passing in index as an argument.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
    <td>{{ months[si.item.month] }}</td>
    <td>{{ si.item.name }}</td>
    <td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
    <td><input type="button" value="UPDATE" @click="setCurrentIndex(si.index)" /></td>
    <td><input type="button" value="DELETE" @click="removeItem(si.index)" /></td>
</tr>


There be buttons! And you may notice, if you enter a June item first and then a February item, they are now sorted properly by month regardless of what order they were entered in. The first default element from items is no longer there, filtered out by the conditional.


Create these two methods. removeItem() has a parameter, index. setCurrentIndex() has a parameter as well, val.
methods: {
    addUpdateItem: function() {
        this.errors.name = "";
        this.errors.amount = "";

        let nameValue = this.$refs.itemName.value.trim();
        let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
        let monthValue = parseInt(this.$refs.itemMonth.value);
        let errors = 0;

        if (nameValue == "") { this.errors.name = "Required"; errors++; }
        if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
        if (errors > 0) return;

        if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;

        if (this.currentIndex == 0) {
            this.items.push({
                name: nameValue,
                month: monthValue,
                amount: amountValue
            });
        }
    },
    removeItem: function(index) {

    },
    setCurrentIndex: function(val) {

    }

}


setCurrentIndex() is straightforward - simply assign the value of val to currentIndex.
removeItem: function(index) {

},
setCurrentIndex: function(val) {
    this.currentIndex = val;
}


removeItem() uses the splice() method to remove the element at position index in items, then resets currentIndex to 0 (just in case it was something else).
removeItem: function(index) {
    this.items.splice(index, 1);
    this.setCurrentIndex(0);

},
setCurrentIndex: function(val) {
    this.currentIndex = val;
}


Now, let's test this. Add this item - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click on UPDATE for "Bonus B". setCurrentIndex() should ensure that Bonus B's details appear in the upper right! Also note that the button now says "UPDATE"! That's because currentIndex is no longer 0.


Click on DELETE for "Bonus A". The item vanishes, and setCurrentIndex() changes currentIndex back to 0, so the upper right panel changes as well.


Update the addUpdateItem() method. Before, we only handled the case for currentIndex being 0. Now if currentIndex is not 0, this means it's an update. And we update the values accordingly. The values, of course, have already been validated.
addUpdateItem: function() {
    this.errors.name = "";
    this.errors.amount = "";

    let nameValue = this.$refs.itemName.value.trim();
    let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
    let monthValue = parseInt(this.$refs.itemMonth.value);
    let errors = 0;

    if (nameValue == "") { this.errors.name = "Required"; errors++; }
    if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
    if (errors > 0) return;

    if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;

    if (this.currentIndex == 0) {
        this.items.push({
            name: nameValue,
            month: monthValue,
            amount: amountValue
        });
    } else {
        this.items[this.currentIndex].name = nameValue;
        this.items[this.currentIndex].amount = amountValue;
        this.items[this.currentIndex].month = monthValue;
    }


    this.setCurrentIndex(0);
},


In pnlItem, we add another button. It says "NEW", and when you click on it, it sets currentIndex back to 0. And it renders only if currentIndex is greater than 0.
<p>
    <label for="itemAmount">Amount</label>
    <br />
    <input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
    <span class="error">{{ errors.amount }}</span>
</p>
<input type="button" value="NEW" @click="setCurrentIndex(0)" v-if="currentIndex > 0" />
<input type="button" v-bind:value="currentIndex == 0 ? 'ADD' : 'UPDATE'" @click="addUpdateItem" />


Again, add these items - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click UPDATE for Bonus A. The NEW button appears!



Ignore that button for now. Set the amount to 8000 and click UPDATE (the one in the top right corner). It should reflect the new value in the list of items below.


Now click the UPDATE button on "Bonus B". See the NEW button appear again? What happens when you click it? That's right - it should set currentIndex to 0 and give you the "New Item" view.


Next

Showing the Financial Projection.The next part is to display items in a table, so you can see what items you added.

In the HTML, inside the pnlItems div, add a table. The header will be styled using the CSS class header, while the AMOUNT column will be styled using the CSS class numeric.
<div id="pnlItems" class="panel">
    <table>
        <tr class="header">

            <td width="20%">MONTH</td>
            <td width="40%">NAME</td>
            <td width="20%" class="numeric">AMOUNT</td>
            <td width="10%"></td>
            <td width="10%"></td>
        </tr>
    </table>
</div>


header will be in bold, and numeric means that text is aligned right. That's really all there is to it.
.panel
{
    float: left;
    padding: 10px;
    border-radius: 20px;
    outline: 1px solid rgba(255, 150, 0, 0.5);
    margin-right: 10px;
    margin-bottom: 10px;
}

.numeric
{
    text-align: right;
}

.header
{
    font-weight: bold;
}


label
{
    width: 5em;
    font-size: 0.5em;
    float: left;
}


So we've got the beginnings of a table.


Now let's add content. We want the rows to render for as many elements there are in items. We also want to set key because it's a repeated HTML element we're creating.
<div id="pnlItems" class="panel">
    <table>
        <tr class="header">
            <td width="20%">MONTH</td>
            <td width="40%">NAME</td>
            <td width="20%" class="numeric">AMOUNT</td>
            <td width="10%"></td>
            <td width="10%"></td>
        </tr>

        <tr v-for="(i, index) in items" v-bind:key="index">
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
        </tr>
    </table>
</div>


In here, we add the month, the name and amount. Note that the amount column is styled using the numeric CSS class.
<tr v-for="(i, index) in items" v-bind:key="index">
    <td>{{ months[i.month] }}</td>
    <td>{{ i.name }}</td>
    <td class="numeric">{{ i.amount }}</td>
    <td></td>
    <td></td>
</tr>


Now we can test this. When you refresh, the first item in items appears.


Now add something. Here I call it "Investment Dividend" and say it's incoming of SGD 1,500. Hey, a guy can dream.


Click the ADD button and you see it appears. So far so good.


Now add something else, an outgoing amount. Wifey's birthday isn't in March and I wish I spent only a thousand, but this is just an example.


See? Some things to correct.
- The amount appears negative. Would be nice if we could color code this.
- It appears in order of entry, which is fine until you have like 50 items.
- We also want to not show the first item.


We start off by adding these CSS classes, inText and outText. So incoming money is marked in green, and outgoing money in red.
label
{
    width: 5em;
    font-size: 0.5em;
    float: left;
}

.inText
{
    color: rgb(0, 200, 0);
}

.outText
{
    color: rgb(200, 0, 0);
}


input[type=text], input[type=number], select
{
    width: 10em;
    padding: 0em;
}


Then we change the class. Instead of just styling using numeric, we use a combination of numeric and inText or outText, depending on whether the amount is positive.
<tr v-for="(i, index) in items" v-bind:key="index">
    <td>{{ months[i.month] }}</td>
    <td>{{ i.name }}</td>
    <td v-bind:class="i.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ i.amount }}</td>
    <td></td>
    <td></td>
</tr>


Now in the HTML, we add a conditional. This means the HTML element renders only if index is greater than 0.
<tr v-for="(i, index) in items" v-bind:key="index" v-if="index > 0">


If you retry everything, you should see that incoming and outgoing amounts are colored differently, and the first row no longer appears. But soon, we'll be doing something bigger.

In computed, add the method sortedItems(). This actually returns the sorted view of the items array.
computed: {
    sortedItems: function() {
        return this.items

    }
},


Here, we use the map() method to iterate through items and return the index and the element as a new object. This serves to preserve both the index, index and the element, item.
computed: {
    sortedItems: function() {
        return this.items
        .map((item, index) => ({ item, index }))
    }
},


And we continue by chaining on a sort() method, sorting by the month property of item. This works because we need the index... but sorting items and adding or removing from it, might change index.
computed: {
    sortedItems: function() {
        return this.items
        .map((item, index) => ({ item, index }))
        .sort((a, b) => a.item.month - b.item.month);
    }
},


Now we'll need to change this. Instead of iterating through items, we iterate through sortedItems and we change all mentions of i to si. Since each element of sortedItems is made of index and item, if we want to refer to the element's properties, we have to refer to it as item.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
    <td>{{ months[si.item.month] }}</td>
    <td>{{ si.item.name }}</td>
    <td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
    <td></td>
    <td></td>
</tr>


We'll then add two buttons. One is an UPDATE button and will run the setCurrentIndex() method, passing in index as an argument. The other is a DELETE button that runs the removeItem() method, also passing in index as an argument.
<tr v-for="si in sortedItems" v-bind:key="si.index" v-if="si.index > 0">
    <td>{{ months[si.item.month] }}</td>
    <td>{{ si.item.name }}</td>
    <td v-bind:class=" si.item.amount > 0 ? 'numeric inText' : 'numeric outText'">{{ si.item.amount }}</td>
    <td><input type="button" value="UPDATE" @click="setCurrentIndex(si.index)" /></td>
    <td><input type="button" value="DELETE" @click="removeItem(si.index)" /></td>
</tr>


There be buttons! And you may notice, if you enter a June item first and then a February item, they are now sorted properly by month regardless of what order they were entered in. The first default element from items is no longer there, filtered out by the conditional.


Create these two methods. removeItem() has a parameter, index. setCurrentIndex() has a parameter as well, val.
methods: {
    addUpdateItem: function() {
        this.errors.name = "";
        this.errors.amount = "";

        let nameValue = this.$refs.itemName.value.trim();
        let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
        let monthValue = parseInt(this.$refs.itemMonth.value);
        let errors = 0;

        if (nameValue == "") { this.errors.name = "Required"; errors++; }
        if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
        if (errors > 0) return;

        if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;

        if (this.currentIndex == 0) {
            this.items.push({
                name: nameValue,
                month: monthValue,
                amount: amountValue
            });
        }
    },
    removeItem: function(index) {

    },
    setCurrentIndex: function(val) {

    }

}


setCurrentIndex() is straightforward - simply assign the value of val to currentIndex.
removeItem: function(index) {

},
setCurrentIndex: function(val) {
    this.currentIndex = val;
}


removeItem() uses the splice() method to remove the element at position index in items, then resets currentIndex to 0 (just in case it was something else).
removeItem: function(index) {
    this.items.splice(index, 1);
    this.setCurrentIndex(0);

},
setCurrentIndex: function(val) {
    this.currentIndex = val;
}


Now, let's test this. Add this item - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click on UPDATE for "Bonus B". setCurrentIndex() should ensure that Bonus B's details appear in the upper right! Also note that the button now says "UPDATE"! That's because currentIndex is no longer 0.


Click on DELETE for "Bonus A". The item vanishes, and setCurrentIndex() changes currentIndex back to 0, so the upper right panel changes as well.


Update the addUpdateItem() method. Before, we only handled the case for currentIndex being 0. Now if currentIndex is not 0, this means it's an update. And we update the values accordingly. The values, of course, have already been validated.
addUpdateItem: function() {
    this.errors.name = "";
    this.errors.amount = "";

    let nameValue = this.$refs.itemName.value.trim();
    let amountValue = parseFloat(this.$refs.itemAmount.value.trim());
    let monthValue = parseInt(this.$refs.itemMonth.value);
    let errors = 0;

    if (nameValue == "") { this.errors.name = "Required"; errors++; }
    if (amountValue <= 0 || isNaN(amountValue)) { this.errors.amount = "Must be positive"; errors++; }
    if (errors > 0) return;

    if (this.$refs.itemTypeOut.checked) amountValue = amountValue * -1;

    if (this.currentIndex == 0) {
        this.items.push({
            name: nameValue,
            month: monthValue,
            amount: amountValue
        });
    } else {
        this.items[this.currentIndex].name = nameValue;
        this.items[this.currentIndex].amount = amountValue;
        this.items[this.currentIndex].month = monthValue;
    }


    this.setCurrentIndex(0);
},


In pnlItem, we add another button. It says "NEW", and when you click on it, it sets currentIndex back to 0. And it renders only if currentIndex is greater than 0.
<p>
    <label for="itemAmount">Amount</label>
    <br />
    <input ref="itemAmount" id="itemAmount" type="number" v-bind:value="Math.abs(items[currentIndex].amount)">
    <span class="error">{{ errors.amount }}</span>
</p>
<input type="button" value="NEW" @click="setCurrentIndex(0)" v-if="currentIndex > 0" />
<input type="button" v-bind:value="currentIndex == 0 ? 'ADD' : 'UPDATE'" @click="addUpdateItem" />


Again, add these items - "Bonus A" at SGD 5,000 in May. Then add "Bonus B" at SGD 15,000 in June. Click UPDATE for Bonus A. The NEW button appears!



Ignore that button for now. Set the amount to 8000 and click UPDATE (the one in the top right corner). It should reflect the new value in the list of items below.


Now click the UPDATE button on "Bonus B". See the NEW button appear again? What happens when you click it? That's right - it should set currentIndex to 0 and give you the "New Item" view.


Next

Showing the Financial Projection.