Tuesday, 29 April 2025

Sidestepping the year-end bonus trap

By the second year in my current job, I had gone for years without a year-end bonus due to a combination of factors such as constant job-hopping and working for cheap financially-prudent employers. But I made do. Not only that, I thrived.

Thus it was with some puzzlement when I received an email from HR informing me that I was due for a year-end bonus. An entire month's worth, no strings attached.

Extra money out of nowhere.

Wow. Imagine. My. Shock. Having spent so many years learning to live without a year-end windfall, now I faced an entirely different problem: what in the blue hell was I supposed to do with that chunk of change that had just appeared out of nowhere and didn't fit in my budget?

As a married man, the solution was easy. I just handed it over to my wife.

When the Missus received that fat stack of cash, the first question she asked, with a hint of suspicion, was why I was suddenly giving her money. I put on my most macho-bullshit act and declared, "Woman, I told you when you married me, whatever money I make from here onwards, you will make." Famous last words. Because every subsequent year, right at the tail end, she would inquire about my year-end bonus and now I was expected to give it to her every time.

Moral of the story: Be very careful what you tell your woman. She's going to hold you to it. Believe that!

The real reason

When I told my friends what I'd been doing with my year-end bonus for the past few years, what I got was the mind-numbing chorus of "wow, that's true love" and "what a great husband".

Have people always been this simple, or is this just happening now?

This is about freedom, not love. I was once trapped in a job I hated due to having developed a dependency on their year-end bonus. And whenever I tried to leave, I always found myself waiting to get the bonus before making plans to leave. Plenty of people find themselves in the same predicament now. It's that Singaporean mentality of not wanting to lose out.

Yes, I know that this isn't necessarily some cynical move by the company to hold their employees ransom. A year-end bonus is a pretty common practice and nobody was holding a gun to my head in order to force me to stay at the company. It was purely my reluctance to forfeit that lump sum of cash by leaving at year end, that kept me there. However, even taking full responsibility for it, does not make the dependency on the year-end bonus any less of a trap. It merely turns this into a self-imposed trap.

No! It's a trap!

Now having tailored my cash flow around not having a year-end bonus after so many years of going without, I have effectively weaned myself off this dependency. Why would it be a good idea to voluntarily redevelop the same crippling dependency now? Why would I give up the freedom to leave whenever I damn well feel like it, even if there are no plans to leave at the moment?

Not only do I give my wife my yearly bonus, I've also developed the habit of giving Mom my salary increases. Why? Same reasoning. It's a bit of extra money that I don't care about, it keeps 'em happy, and that's a great ROI. Most importantly, it keeps me lean. And hungry. It was that same hunger that had me going back to school. Learning new tech tools. Picking up skillsets from related disciplines such as DevOps, Data Analytics and Mobile Development. It was that very hunger that had me build this blog and keep honing my craft.

It's the principle of the thing. Financially, I'm in a good place and don't need the money. I have worked long and hard, over the course of the last decade, to have the luxury of being able to easily ignore the lure of immediate money, in favor of things of greater value. And approaching the age of 50, autonomy in my career is arguably more important than ever. Definitely more important than an extra few thousand bucks.

Conclusion

Losing my job isn't high on the list of things to worry about. Come to think of it, it might not even be on the list. No matter what you tell yourself so you can get out of bed in the morning, a job is ultimately just a job. Your ability to land a job is far more important than the job itself.

Gotta maintain that edge.

It's more my edge I'm afraid of losing. Not being some special tech talent that can walk into any company and demand the sky, I only got this far due to enthusiasm and motivation. Not complacency. Due to careful financial management, I already have way more money than I realistically need.

I am in charge of my own career, and will give that up when hell freezes over.

Be$t wi$he$,
T___T

Wednesday, 23 April 2025

The case of the ill-considered feature quotation

When you're in-house tech personnel dealing with external tech vendors, sometimes experience and common sense can be useful.

Also, the willingness to prioritize professional duty over being liked. I mention this because that's not me - I actively try not to be the guy who ruins everyone's workday by nitpicking on small details in the name of being "thorough". Work does suck for a lot of people, and there's just no point in making it suck more than it already does, without good reason.

That's far enough.

Except, sometimes, there is a good reason. Sometimes, there are multiple good reasons to dig your heels in and say "that's far enough, buddy".

This is such a story I'm going to tell today.

What happened

My company at the time had contracted a vendor to store our data, which would be sent to them by means of an API endpoint they provided us. Admittedly, they weren't the solution provider I would have gone with, but in the interest of saving time (and also because I didn't have a better alternative), I played nice. After ascertaining that the solution worked - i.e, our system would send data though that API endpoint call and the data would be saved in their system, it was time to talk about security.

Just a key.

Our proposed solution was to have an extra property in the JSON object that we were sending them, with a password that they would provide. Like an API key. Any calls made using that key would be validated against their records, so that they could at least be confident that the origin of the data would be correct.

The solution was simple enough, and their Sales Representative told us it could be done. But then he made the mistake of telling us that we would be quoted an extra charge for it. And that was when I drew a line in the sand, and told them, no, this was absolutely not going to happen.

In retrospect, the fact that I was the one who had to broach the subject of security was a red flag. If I hadn't, would they just have carried on? Alarming if one considered that we weren't their only clients.

Why I put my foot down

The extra feature we requested was for security. Security should always be considered a basic, rather than extra, feature. Especially when the service providers in question are holding on to client data. If these vendors had our data in their storage, why should we have to pay extra for a basic security feature?

Also, in the event of a data breach, could these vendors really afford not to be able to show subsequent audit that they, at the very least, had previously done their due diligence?

Security should be a
basic feature.

From my experience with small vendors like these, they usually aren't using separate database servers for different clients. More often than not, it's some sort of shared virtual hosting. Which meant that any data breach on our part could affect their other clients, and vice versa.

All in all, the vendors had significantly more to lose than we did, from a security breach. From that viewpoint, it was patently ridiculous for them to want to charge us for security. That would be like me demanding payment from the locksmith to install a lock on my own door.

There is the possibility that their Sales Representative was not really thinking clearly, and that he was asking for extra payment out of sheer habit. Because that was the way he had been trained. Honestly, I don't think this made it better. Definitely didn't make my company's data feel more secure in their hands.

Conclusion

Going with the flow is easy. Standing firm on principle is harder. In fact, I would argue that standing firm when the situation is as egregious as this, is easy. The hard part is really identifying such situations in the first place. I don't think there's exactly a playbook for things like that.

Thanks for reading, that's far enough!
T___T

Saturday, 19 April 2025

Web Tutorial: The Easter Poem Generator (Part 2/2)

We actually already created functions for validating and submitting earlier, so let's fill them up now.

We start with the handleChecking() function. We first get name, value and checked from the target object of the e object. target will be the checkbox that is checked. So we have finalValue. If the checkbox is checked, then finalValue has the value value. If not, its value is an empty string.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

};


Whatever the value of finalValue, it will be used in setFormData(). We use the spread operator to destructure formData, then add in a name-value pair using name and finalValue. If name already appears in formData, it will thus be overwritten by the value of finalValue.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

  setFormData({
    ...formData,
    [name]: finalValue
  });

};


What we want to do is ensure that at least one checkbox is checked. So we'll test this. Declare tempFormData as a clone of formData by using stringify() and parse() methods. Modify tempFormData's property, pointed to by name, changing its value to finalValue.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

  var tempFormData = JSON.stringify(formData);
  tempFormData = JSON.parse(tempFormData);
  tempFormData[name] = finalValue;


  setFormData({
    ...formData,
    [name]: finalValue
  });
};


Now we use an If block to check if all the string values are empty strings. If that's true, we reset the value of finalValue to value. This ensures that there is never  a situation where all the properties are empty strings.

src/app/page.js
const handleChecking = (e) => {
  const { name, value, checked } = e.target;
  var finalValue = (checked ? value : "");

  var tempFormData = JSON.stringify(formData);
  tempFormData = JSON.parse(tempFormData);
  tempFormData[name] = finalValue;

  if (tempFormData.symbol_bunny + tempFormData.symbol_chicks + tempFormData.symbol_cross + tempFormData.symbol_eggs + tempFormData.symbol_lillies === "") {
    finalValue = value;
  }


  setFormData({
    ...formData,
    [name]: finalValue
  });
};


Try it... you can select multiple options but you will find that you'll never be able to deselect all of the options.

That's it for validation! Now we want to handle submitting.

We first ensure that the form is not automatically submitted, by using the preventDefault() method. We want to implement our own submit logic. After that, we use setGeneration() to set a loading message.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

};


We'll add a div, styled using the CSS class poemContainer, with the generation object's poem property as content.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>
          <input type="checkbox" name={key} value={key.replace("symbol_", "")} onChange={handleChecking} checked={ (value !== "") } />
           {key.replace("symbol_", "")}
           <Link href={ ("/" +key.replace("_", "/")) } target="_blank">►</Link>
          <br />
        </label>
      ))}    
      <button type="submit" className={styles.button}>Go!</button>
      <br style={{ clear:"both" }} />
    </form>
    <div className={styles.poemContainer} >{ generation.poem } </div>
  </div>
);


Here's poemContainer. Just a bit of alignment and layout. Nothing crazy.

src/app/page.module.css
.generationContainer form {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  border-radius: 10px;
  border: 1px solid rgb(255, 100, 100);
}

.poemContainer {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  text-align: center;
}


.label {
  display: inline-block;
  width: 45%;
  float: left;
}


Now try Clicking the SUBMIT button. Looks like the HTML is being treated like text content!


We will rectify this using dangerouslySetInnerHTML. This renders raw HTML in a React component.

src/app/page.js
<div className={styles.poemContainer} dangerouslySetInnerHTML={{ __html: generation.poem }} />


This time, the loading message is properly formatted.



Now that this is done, we're going to make a call to the backend using fetch(). We'll pair it with await because its very nature is async. The endpoint is poemgen and we will be creating it after this. The result of the call to the backend will be assigned to the object response.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {

  });

};


In fetch(), the second argument is an object. It contains the method of the call (in this case, it's a POST), the headers object (which in this context basically tells fetch() that we're ending data in JSON format, and the body property. The last one contains formData as a JSON-formatted string.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(formData)

  });
};


We then declare result and use it to store the value of response as a JSON object. Since the method we're using, json(), is asynchronous, we have to use await.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(formData)
  });
  const result = await response.json();

};


We then check if status is 200. If so, we run setGeneration() and set the value of the poem property to the message property of result. If not, we handle the error by logging it in the console.

src/app/page.js
const handleSubmit = async (e) => {
  e.preventDefault();
  setGeneration( { poem: "<h2>Pease wait...</h2>" } );

  const response = await fetch("/api/poemgen", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(formData)
  });
  const result = await response.json();

  if (response.status === 200) {
    setGeneration( { poem: result.message } );
  } else {
    console.log(result);
  }

};


Now, in the app directory, create the api subdirectory, and in it, the poemgen subdirectory. In that, create route.js. This is the NextJS App router method of creating backend API endpoints. In this file, we export an async function that responds to the POST method. There is a parameter, req, that represents the request.

src/app/api/poemgen/route.js
export async function POST(req) {

}


We declare formData and use await to obtain the JSON object req. Then we declare symbols as an empty string.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
}


This is followed by a series of If blocks that correspond to each property in formData. If the value is not an empty string, concatenate that value to symbols, with a comma and space. Remember, there will never be a case where all the strings are empty. Thus, there will always be at least one comma and space. We remove the one at the end using the slice() method.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

}


Next, we declare headers as an object, and populate it with standard properties such as Authorization and Content-Type. Since this is for ChatGPT, we'll need OpenAI-Oganization also. Authorization and OpenAI-Oganization will have values that can be accessed from your .env file.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
     "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
     "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
     "Content-Type": "application/json"    
  };  

}


The values will look like this in the .env file. They won't, of course, be "xxx".

.env
NEXT_PUBLIC_API_KEY=xxx
NEXT_PUBLIC_ORG=org-xxx


Now create an empty array, messages. Then create an object, obj. It should have the property role, which has a value of "user", and the property content. Its value should be a prompt we create from symbols. After that, push obj into messages.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

}


Create body as an object. In there, we specify the AI model and set the number of tokens to be used, to 2000. The messages property value will be the array messages.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
     "messages" : messages,
     "max_tokens" : 2000
  };

}


Here, we have a Try-catch block. In here, we use fetch() to grab data via a REST endpoint provided by OpenAI, sending header and body as the data. The response is assigned to the variable apiResponse.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
    "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
     "messages" : messages,
    "max_tokens" : 2000
  };

  try {
    const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
         method: "POST",
         headers: headers,
         body: JSON.stringify(body)
     });
  } catch (error) {

  }

}


We then check the ok property of apiResponse. If this is false, we return a Response object with an appropriate status. We return the same thing if there is an exception thrown.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
    "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
    "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
    "role": "user",
     "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
     "messages" : messages,
     "max_tokens" : 2000
  };

  try {
    const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
         method: "POST",
         headers: headers,
         body: JSON.stringify(body)
     });

    if (!apiResponse.ok) {
      return new Response(JSON.stringify({ message: "Error in poem generation" }), {
        status: 500,
        headers: { "Content-Type": "application/json" },
       });
     }

  } catch (error) {
     return new Response(JSON.stringify({ message: error.message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });

  }
}


Here, we obtain data by converting apiResponse to JSON using the asynchronous method json(). And we'll define html_content as the generated content but with HTML breaks in place of line breaks.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
    "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
     "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
     "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
     "role": "user",
     "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
    "model": "gpt-3.5-turbo",
     "messages" : messages,
     "max_tokens" : 2000
  };

  try {
      const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
           method: "POST",
           headers: headers,
           body: JSON.stringify(body)
       });

       if (!apiResponse.ok) {
        return new Response(JSON.stringify({ message: "Error in poem generation" }), {
          status: 500,
          headers: { "Content-Type": "application/json" },
      });
    }

    const data = await apiResponse.json();
    var html_content =  data.choices[0].message.content.replaceAll("\n", "<br />");

  } catch (error) {
    return new Response(JSON.stringify({ message: error.message }), {
        status: 500,
        headers: { "Content-Type": "application/json" },
    });
  }
}


And once that is done, we will return a new Response with the status 200 and html_content as the message.

src/app/api/poemgen/route.js
export async function POST(req) {
  const formData = await req.json();

  var symbols = "";
  if (formData.symbol_bunny !== "") symbols += (formData.symbol_bunny + ", ");
  if (formData.symbol_chicks !== "") symbols += (formData.symbol_chicks + ", ");
  if (formData.symbol_cross !== "") symbols += (formData.symbol_cross + ", ");
  if (formData.symbol_eggs !== "") symbols += (formData.symbol_eggs + ", ");
  if (formData.symbol_lillies !== "") symbols += (formData.symbol_lillies + ", ");
  symbols = symbols.slice(0, -2);

  var headers = {
     "Authorization": "Bearer " + process.env.NEXT_PUBLIC_API_KEY,
     "OpenAI-Oganization": process.env.NEXT_PUBLIC_ORG,
     "Content-Type": "application/json"    
  };  

  var messages = [];
  var obj = {
     "role": "user",
     "content" : "Generate an Easter-themed poem with the following element(s): " + symbols + "."
  };
  messages.push(obj);

  var body = {
     "model": "gpt-3.5-turbo",
    "messages" : messages,
    "max_tokens" : 2000
  };

  try {
    const apiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
        method: "POST",
        headers: headers,
        body: JSON.stringify(body)
    });

    if (!apiResponse.ok) {
       return new Response(JSON.stringify({ message: "Error in poem generation" }), {
          status: 500,
          headers: { "Content-Type": "application/json" },
      });
    }

    const data = await apiResponse.json();
    var html_content =  data.choices[0].message.content.replaceAll("\n", "<br />");

    return new Response(JSON.stringify({ message: html_content }), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });

  } catch (error) {
     return new Response(JSON.stringify({ message: error.message }), {
       status: 500,
       headers: { "Content-Type": "application/json" },
     });
  }
}


Now try this! First, try it with one item checked.


Then try it with multiple items checked, and see the difference!


Enjoy your Easter!

This has been my virgin NextJS tutorial. We covered a fair amount of territory, and hopefully had fun in the process!

May your Easter eggs all be found,
His Teochewness will see you around!
T___T

Wednesday, 16 April 2025

Web Tutorial: The Easter Poem Generator (Part 1/2)

As of early 2025, the create-react-app method of ReactJS development was officially deprecated. As such, an alternative way forward was to use the ReactJS framework, NextJS. For today's Easter-themed web tutorial, I will be walking through some basic features of NextJS.

Do note that not a lot of things will be new here; the bulk of it is still doable in a standard ReactJS setup, or even NodeJS.

To begin, I will assume that NextJS has been installed on your machine. We'll create the app with this command. "easter-poem-generator" is the name of the app, and consequently, the folder that it will be created in.
npx create-next-app@latest easter-poem-generator


The script will ask some questions. These are the options I have chosen.
Would you like to use TypeScript?  No
Would you like to use ESLint?  No
Would you like to use Tailwind CSS?  No
Would you like your code inside a `src/` directory?  Yes
Would you like to use App Router? (recommended)  Yes
Would you like to use Turbopack for `next dev`?  No
Would you like to customize the import alias (`@/*` by default)?  No

Once done, navigate to the src folder. We'll make some changes to the layout.

src/app/layout.js
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata = {
  title: "Easter Poem Generator",
  description: "Generated by create next app",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        {children}
      </body>
    </html>
  );
}


Next, in the public directory, add these images.

public/symbol_bunny.jpg

public/symbol_chicks.jpg

public/symbol_cross.jpg

public/symbol_eggs.jpg

public/symbol_lillies.jpg

Time to create the form. We start with "use client" to state that this file is only for front-end JavaScript. Since JavaScript is being used for both front and back ends of the application, it's important to specify because this determines what features of JavaScript you'll get to use in the file.

src/app/page.js
"use client"


Then, because we have links in this page, we'll import the Link component. We will also import styles that have already been compiled. Finally, we will import useState because we will be using state variables.

src/app/page.js
"use client"

import Link from "next/link";
import styles from "./page.module.css";

import { useState } from "react";


Next, we create a main function for this page. We will create the return statement in advance.

src/app/page.js
"use client"

import Link from "next/link";
import styles from "./page.module.css";

import { useState } from "react";

export default function Home() {
  return (

  );
}


Now, if you know ReactJS at all, this should be familiar ground. This is why we imported useState earlier, so we can set state variables. The first thing we want is form data. Thus, formData will be an object containing these properties. As  default, only symbol_bunny is not an empty string.

src/app/page.js
export default function Home() {
  const [formData, setFormData] = useState({
    symbol_bunny: "bunny",
    symbol_chicks: "",
    symbol_cross: "",
    symbol_eggs: "",
    symbol_lillies: ""
  });


  return (

  );
}


And then we have generation, which is an object containing the property poem.

src/app/page.js
export default function Home() {
  const [formData, setFormData] = useState({
    symbol_bunny: "bunny",
    symbol_chicks: "",
    symbol_cross: "",
    symbol_eggs: "",
    symbol_lillies: ""
  });

  const [generation, setGeneration] = useState({
    poem: ""
  });


  return (

  );
}


Next, we have two functions. handleChecking() is something standar that happens instantaneously, to ensure that the input is correct. handleSubmit() is an asynchronous function because we are going to have to call API endpoints and wait for responses in this function.

src/app/page.js
const [formData, setFormData] = useState({
  symbol_bunny: "bunny",
  symbol_chicks: "",
  symbol_cross: "",
  symbol_eggs: "",
  symbol_lillies: ""
});

const [generation, setGeneration] = useState({
  poem: ""
});

const handleChecking = (e) => {

};

const handleSubmit = async (e) => {

};


return (

);


In the return statement there will be JSX. In here, we have a div styled using the CSS class generationContainer, and a form. The form will run handleSubmit() when data is submitted.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
    </form>
  </div>

);


Clear out this file. We will add in the generationContainer CSS class. It's basically a centered div with a fixed width. Then we'll style the form. It has rounded corners and a solid light red border.

src/app/page.module.css
.generationContainer {
  width: 500px;
  margin: 0 auto 0 auto;
}

.generationContainer form {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  border-radius: 10px;
  border: 1px solid rgb(255, 100, 100);
}


Not much to look at right now, but this will change soon.


We then have a line of instructions, and we will display a series of checkboxes based on the formData object. For this, we use the map() method on formData. Since formData is an object, we need to make it iterable by using the entries() method of Object, on it. entries() will turn formData into an array of objects containing properties and values.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (

      ))}    

    </form>
  </div>
);


In this, we have a label tag styled using CSS class label. Because the ReactJS engine will complain if we don't, use key to populate the value of the key attribute.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>

        </label>

      ))}    
    </form>
  </div>
);


We will then have a checkbox within that label tag. The name attribute will be key. handleChecking() will be run for the onchange() attribute. The checked value will depend if value is an empty string.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>
          <input type="checkbox" name={key} value={key.replace("symbol_", "")} onChange={handleChecking} checked={ (value !== "") } />
          <br />
        </label>
      ))}    
    </form>
  </div>
);


Then add the text label key, again less the string "symbol_".

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>
          <input type="checkbox" name={key} value={key.replace("symbol_", "")} onChange={handleChecking} checked={ (formData[key] === key.replace("symbol_", "")) } />
           {key.replace("symbol_", "")}
          <br />
        </label>
      ))}    
    </form>
  </div>
);


Back to styles, label has a width that will take up less than half of its container, and be floated left.

src/app/page.module.css
.generationContainer form {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  border-radius: 10px;
  border: 1px solid rgb(255, 100, 100);
}

.label {
  display: inline-block;
  width: 45%;
  float: left;
}


This is nothing that a CSS break won't fix.


Add this CSS break after adding a SUBMIT button.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>
          <input type="checkbox" name={key} value={key.replace("symbol_", "")} onChange={handleChecking} checked={ (formData[key] === key.replace("symbol_", "")) } />
           {key.replace("symbol_", "")}
          <br />
        </label>
      ))}    
      <button type="submit" className={styles.button}>Go!</button>
      <br style={{ clear:"both" }} />

    </form>
  </div>
);


Just some styling for the button.

src/app/page.module.css
.generationContainer form {
  width: 450px;
  padding: 10px;
  margin: 0 auto 0 auto;
  border-radius: 10px;
  border: 1px solid rgb(255, 100, 100);
}

.label {
  display: inline-block;
  width: 45%;
  float: left;
}

.button {
  display: inline-block;
  width: 5em;
  height: 2em;
  float: right;
}


All falling into place nicely.


OK, we'll now add a Link component. Each link's route will be symbol followed by the identifier such as "bunny" or "eggs", thus I cleverly just took key and replaced the underscore with a forward slash.

src/app/page.js
return (
  <div className={styles.generationContainer} >
    <form onSubmit={handleSubmit}>
      <h1>Generate an Easter Poem!</h1>
      <b>Include at least one of these elements</b>
      {
        Object.entries(formData).map(([key, value]) => (
        <label className={styles.label} key={key}>
          <input type="checkbox" name={key} value={key.replace("symbol_", "")} onChange={handleChecking} checked={ (formData[key] === key.replace("symbol_", "")) } />
           {key.replace("symbol_", "")}
           <Link href={ ("/" +key.replace("_", "/")) } target="_blank">&#9658;</Link>
          <br />
        </label>
      ))}    
      <button type="submit" className={styles.button}>Go!</button>
      <br style={{ clear:"both" }} />
    </form>
  </div>
);


In order for the link to work, the route needs to work. So create the symbol directory in app. And in that directory, create the [symbolName] directory, square brackets and all. This tells NextJS that symbolName is a parameter value rather than an actual fixed value. And in that directory, create page.js. We'll want to import Image because we'll be working with images. As in the previous page we were working on, we want to import the CSS.

src/app/symbol/[symbolName]/page.js
import Image from "next/image";
import styles from "../../page.module.css";


We'll want to export this function. As always, we add a return statement inside the function.

src/app/symbol/[symbolName]/page.js
import Image from "next/image";
import styles from "../../page.module.css";

export default function SymbolPage({ params }) {
  return (

  );
}


params will contain symbolName, which will typically be "bunny", "lillies", etc. Thankfully, we've already saved those in the public directory, so they're accessible with just a slash. They can thus be used as the value of the src attribute for the Image component. Here, I have included the width and height of the image. The Image component will be placed inside a div styled using the symbolImage CSS class.

src/app/symbol/[symbolName]/page.js
export default function SymbolPage({ params }) {
  return (
    <div className={ styles.symbolImage } >
        <Image src={ "/symbol_" + params.symbolName + ".jpg" } alt={ params.symbolName } width={ 300 } height={ 300 } />
    </div>

  );
}


I am using symbolImage mostly for positioning. I have also set it so that any img tag inside symbolImage will have rounded corners.

src/app/page.module.css
.symbolImage {
  width: 100%;
  min-height: auto;
  margin: 10px auto 10px auto;
  text-align: center;
}

.symbolImage img{
  border-radius: 10px;
}


.generationContainer {
  width: 500px;
  margin: 0 auto 0 auto;
}


You can see the arrows. Now click on maybe the one beside "bunny".


You see a picture, in a new tab, with rounded corners!


We now add an object, texts. It has properties that mirror the possible values of symbolName.

src/app/symbol/[symbolName]/page.js
import Image from "next/image";
import styles from "../../page.module.css";

export default function SymbolPage({ params }) {
  const texts = {
   "bunny": ,
   "chicks": ,
   "cross": ,
   "eggs": ,
   "lillies":
  };


  return (
    <div className={ styles.symbolImage } >
        <Image src={ "/symbol_" + params.symbolName + ".jpg" } alt={ params.symbolName } width={ 300 } height={ 300 } />
    </div>
  );
}


Here, I'm going to add text strings as values for these properties. These were generated using Copilot.

src/app/symbol/[symbolName]/page.js
import Image from "next/image";
import styles from "../../page.module.css";

export default function SymbolPage({ params }) {
  const texts = {
   "bunny": "The bunny, or Easter rabbit, is a symbol of fertility and new life, which aligns with the themes of rebirth and renewal celebrated during Easter. Originating from ancient pagan traditions, the rabbit was associated with the goddess Ēostre, who represented spring and fertility. As Christianity spread, the symbolism of the rabbit was incorporated into Easter celebrations. The Easter bunny is often depicted delivering eggs, which are also symbols of new life and rebirth, to children, adding a playful and joyful element to the holiday.",
   "chicks": "Chicks are another symbol of new life and rebirth, aligning with the overall themes of Easter. Just as chicks hatch from eggs, they represent the idea of life emerging from the seemingly lifeless, echoing the resurrection of Jesus. They also symbolize innocence and the start of new beginnings, adding to the joyful and hopeful atmosphere of Easter celebrations. The imagery of chicks, along with eggs and bunnies, helps to convey the sense of renewal and the promise of new life that Easter embodies.",
   "cross": "The crucifix is a powerful symbol in Christianity, especially during Easter. It represents the crucifixion of Jesus Christ, an event central to Christian belief. It signifies the ultimate sacrifice Jesus made by dying on the cross to atone for humanity's sins. It also symbolizes the immense suffering Jesus endured during the crucifixion. The crucifix serves as a reminder of the redemption and salvation offered through Jesus' sacrifice. Easter celebrates Jesus' resurrection, signifying victory over death and the promise of eternal life for believers. This powerful imagery serves as a reminder of Jesus' love and the hope that comes with his resurrection, making the crucifix an integral part of Easter celebrations.",
   "eggs": "Eggs are a rich and multifaceted symbol in Easter traditions. They embody the themes of new life, rebirth, and resurrection, which are central to the celebration of Easter. They represent the emergence of new life from within, mirroring the resurrection of Jesus from the tomb. Just as a chick hatches from an egg, the concept of rebirth is symbolized, signifying the idea of starting anew. Historically, eggs have been seen as symbols of fertility and renewal, aligning with the arrival of spring. The egg, appearing lifeless on the outside but containing life within, represents transformation and the miracle of life. Eggs are often decorated and used in Easter egg hunts, making them a playful and engaging way to celebrate the holiday while reflecting on its deeper meanings.",
   "lillies": "Lilies, particularly white lilies, hold significant symbolism in Easter celebrations. They represent purity, virtue, and the resurrection of Jesus Christ. The trumpet shape of the lily flower is said to symbolize the trumpet call of God, heralding the resurrection. Lilies are often used in Easter decorations and church services to convey a sense of hope, renewal, and new beginnings. Their association with new life and purity makes them a fitting symbol for the themes of Easter."
  };

  return (
    <div className={ styles.symbolImage } >
        <Image src={ "/symbol_" + params.symbolName + ".jpg" } alt={ params.symbolName } width={ 300 } height={ 300 } />
    </div>
  );
}


Then we add a paragraph styled using CSS class symbolText. The HTML in here will be the current property of texts being pointed to by symbolName.

src/app/symbol/[symbolName]/page.js
return (
  <div className={ styles.symbolImage } >
      <Image src={ "/symbol_" + params.symbolName + ".jpg" } alt={ params.symbolName } width={ 300 } height={ 300 } />
      <p className={styles.symbolText} >{ texts[params.symbolName] }</p>
  </div>
);


The symbolText CSS class just imposes some padding.

src/app/page.module.css
.symbolImage {
  width: 100%;
  min-height: auto;
  margin: 10px auto 10px auto;
  text-align: center;
}

.symbolImage img{
  border-radius: 10px;
}

.symbolText {
  padding: 10px;
}


.generationContainer {
  width: 500px;
  margin: 0 auto 0 auto;
}


You see the paragraph appears!


We're done for the time being. So far, we've covered a bit of NextJS routing and built-in components.

Next

Form validation, submitting and API usage.

Saturday, 12 April 2025

Buttons or Divs? What to use, and when

With the power of CSS, HTML is significantly more visually versatile than it was in its inception more than two decades ago. Especially with divs. You can make divs appear as anything - paragraphs, block quotes and images. In extreme examples, you could even render entire paintings using many, many divs. 

A huge variety of shapes,
especially rectangular.

The humble div tag, coupled with CSS, is no longer just a rectangle on the browser. Using properties such as transform, border-radius, width and height, among others, a web developer can achieve a myriad of looks.

And this manifests quite frequently, in buttons. Previously, I discussed whether button or input tags would be preferable, but today we make a separate comparison between divs and buttons.

Divs as buttons

Making divs look like buttons is simple enough. How about behavior? Well, for that, JavaScript accomplishes this fairly easily.

One is a button and the other is a div.
<button>This is a button</button>
<div style="cursor:pointer; background-color:rgb(230, 230, 230); border:1px solid rgb(100, 100, 100); border-radius: 3px; font-family: sans-serif; font-size: 12px; width: 8em; padding: 0.2em; text-align: center">
This is a div
</div>


But they can both be made to perform certain actions on a click.
<button onclick="alert('I am a button');">This is a button</button>
<div onclick="alert('I am a div');" style="cursor:pointer; background-color:rgb(230, 230, 230); border:1px solid rgb(100, 100, 100); border-radius: 3px; font-family: sans-serif; font-size: 12px; width: 8em; padding: 0.2em; text-align: center">
This is a div
</div>


Depending on the browser, you should see no appreciable difference between these.
This is a div


How about submitting a form? Well, a button usually does this.
<form id="frmTest">
    <button>Submit<button>
</form>


But if you want a div to do this, all you really need is a bit more code.
<form id="frmTest">
    <div onclick="document.getElementById('frmTest').submit()">Submit<div>
</form>


Definitely possible, but should we?

Visually, there's not a lot of difference. In fact, styling divs to look like buttons, could even potentially offset visual differences of button rendering between browsers. For example, we take the code written earlier.

This is how it looks on Chrome.


This is how it looks on Safari. See? There's no visual change in the div we styled, but the button looks remarkably different.


However, not everything is about the visual. Especially not to the visually-impaired. The button tag and a div tag reads differently in semantics. On a screen reader, the button tag immediately stands out as a control to be clicked, while a div is semantically no different from any other div.

That is the greatest, and most significant difference. Not being blind, it is understandably difficult to imagine perceiving anything other than in visual terms, since a large part of what people like us perceive, is in the visual medium.

Conclusion

The internet was not only made for people like myself. The internet was meant as an equalizer where it came to information access. Not only did it mean that the average person now had access to information that was not available readily in the past, people with visual disabilities were supposed to be able to access this information.

And that access could be compromised if code was written with the visual intent in mind rather than the semantic.


T___T

Monday, 7 April 2025

Fare Thee Well, Skype!

It was with a fair bit of wistfulness when I read the news that Skype would be officially decommissioned by Microsoft on the 5th of May, right next month. Hard to believe that just a few short years ago, I was between jobs and on Skype speaking to interviewers during the COVID-19 pandemic.

Skype is dead soon.

Skype first appeared in 2003 as a telecommunications software application, when I was still at my Desktop Support job. Back then, my Indian boss amusingly referred to it as "Sky Pee". The setup was smooth and easy, and with this software we could talk to people all over the world instead of paying some hideously expensive phone charges. The user experience just seemed to be one of the smoothest around. The installation process was simple, a joy to embark on. I remember thinking that even just the chat feature was a lot more pleasant to use than Microsoft Messenger's.

Somewhere along the way, in 2011, Skype was acquired by Microsoft. Fourteen years later, Microsoft has decided that it is no longer tenable to continue maintaining Skype.

Why did Skype fail?

Most people have heard of the Microsoft BSOD - the Blue Screen of Death. Well, it appears what we have here is the Microsoft Kiss of Death. It may seem a little unfair to Microsoft. After all, all they really did was acquire Skype. And yes, perhaps they tinkered with Skype's inner workings just a little. And... you know what, you're right, this is all on Microsoft.

I think COVID-19 provided an even greater environment for Skype to shine. In those dark days, due to the need to avoid close physical proximity, video calls became a necessity. However, COVID-19 ironically was also one of the factors hastening Skype's downfall, because it provided that very same environment for Skype's competitors to shine.

Left behind in the race.

And shine they did.

Google Hangouts, Oracle Zoom, Slack, Microsoft Teams. WhatsApp. Instagram. Even TikTok. All of them provided video conference features (Instagram and TikTok to a limited degree) and it was clear that Skype was being quickly outpaced. And since Microsoft was understandably more concerned with supporting Teams, Slack had become an afterthought. 

And like most afterthoughts, Skype eventually faded into obscurity. Software has to be relevant in order to survive. And in order to be relevant, software has to evolve. Under Microsoft, Skype was dead in the water. 

Goodbye, Skype!

We had great memories together. Countless memorable conversations were had in your pale blue-and-white interface. There should be a special placed reserved for Skype in the annals of software history - arguably the first of its kind.

The Skype's the limit,
T___T

Friday, 4 April 2025

Deemphasis on quality is what drives Artificial Intelligence

As the progress of Artificial Intelligence marches on, I find myself needing to reiterate a point I may have made earlier, simply because it cannot be overstated.

While doing some reading on the internet, I came across this thread and quickly became immersed in the arguments and counter-arguments presented. Something stood out to me: the assertion that LLMs are incapable of genuinely writing software and will always churn out code that is inferior to what is produced by human developers who actually understand what they're doing.

Code written by machines.

It was a fascinating discussion, not least because of the developers who came out to say how useful they found A.I in their work. But in all this back-and-forth, there's one thing which none of them seem to have mentioned. Namely, the importance of quality.

LLM-generated software can work. However, here's the thing software developers know that laypeople don't - working software is the lowest possible bar to meet. This is like calling myself a "good person" just because I've never molested a child. Of course software has to work. But properly-crafted software does not only work; it is also maintainable, clean, extensible and testable. It is robust and won't break after a few changes.

That's quality software.

Is it true? Do LLMs really produce rubbish?

I think people who aren't tech workers, even some tech workers themselves, really underestimate just how defective all software is. Software these days does not exist in a vacuum. It's built on layers of existing software platforms, and connected to other pieces of software, each with its own set of flaws and vulnerabilities.

The end result is that a staggering amount of software in the world today is held together by duct tape and prayers. And that's only a slight exaggeration.

Duct tape and prayers, yo.

What has that got to do with LLMs, you ask? Well, what do you think LLMs are trained on? That's right - existing software. The crap code we've all committed at some point or other, if we're all being honest. And LLMs are gobbling all of it up, warts and all. If rubbish is the input, what are LLMs likely to produce as output?

That's right - more rubbish.

So what if it's true?

The world at large is made out of non-technical people. They are perfectly happy for software to just work, and give zero shits about maintainability, code cleanliness and all those high-minded ideals. They aren't the ones who have to deal with software on that level.

So what if the LLM-generated software is generic bug-ridden rubbish? It works half the time. Maybe even three-quarters of the time. It's good enough until the next LLM-generated software comes along. The important thing is that we meet those business needs now.

When you're hungry
enough, this looks good.

Let's use food as an analogy. Why do we eat? If we disregard luxuries such as pleasure and nutrition, at the core of it, we eat because the human body is designed to starve (and die!) if it goes without food for too long. So, what's the difference between a double-tier cheeseburger with fries slapped together by a fast food chef, or a gourmet meal lovingly prepared by culinary genius Gordon Ramsay? Forget nuances in taste, empty calories, cholesterol, salt and all that jazz. Both do the job of staving off starvation. One is cheap and produced in minutes. The other needs significantly more time and money, plus you have to put up with that smug bastard Gordon Ramsay. What do the majority of people generally choose most of the time? It's a no-brainer; Option A wins.

Let's examine another example. What about art? Some collectors or connoisseurs will pay a hefty sum for certain pieces or works from certain artists. But what if they weren't interested in art? What if they could only afford cheap imitations? What if their purpose was simply to cover the walls with something, anything? Would cheap A.I-generated art, a simulation of the genuine article, suffice? I think we all know the answer to that.

Finally...

The reality is that quality doesn't matter. At least, not as much as most of us think it should. Especially in software. Questions like "will it accommodate future changes?" and "have all security holes been closed?" and "what happens if we give it unexpected input?" have been replaced by "does it generally work?", "is it too costly?" and "is it ready right now?". Artificial Intelligence didn't cause this; years of business culture did. But that's also a big reason Artificial Intelligence isn't going away - because the world is addicted to quick and dirty solutions.


Your quality tech personality,
T___T