Showing posts with label ReactJS. Show all posts
Showing posts with label ReactJS. Show all posts

Tuesday, 3 March 2026

Ten Years of GitHub Usage

There was a time I shuttled code between the workplace to home, in the most comically retro manner possible - via email. I would be fiddling with some stuff at home, think it would be interesting to use in a workplace project, and send it to my work email account. And at work, if I didn't feel like leaving my code in the workplace and wanted to continue over the weekend, I would send it to my personal email. As the frequency of this increased, it soon became untenable. I actually only really started a GitHub account almost a full two years after starting this blog.

It's been ten years since. I looked at the heatmaps generated from my activity, and there were some interesting patterns. Really took me back. I realize that just going by the number of commits is a poor metric. Almost as poor as the number of lines of code for measuring code quality. But we've all got to start somewhere...

1. 2016 (46 commits)

At this time, I had just set up my GitHub account the previous year. The line chart of my contributions could be charitably described as "tentative".


Looking at the heatmap of my activity, the word that comes to mind is "sporadic". I used it a couple times a month, each time to commit a bunch of stuff.


Looks pitiful, eh? Basically I was just feeling my way around. Getting comfortable with the interface. There were even months where I had no activity whatsoever.

2. 2017 (96 commits)

The second year wasn't that much better, at least in terms of consistency. There were still a couple months where I failed to register any activity.


The line chart shows a marked improvement over the previous year. Though, to be fair, it's hard to do worse.


However, in the months where I did do shit, there was an uptick. Instead of a couple commits here and there, I was starting to register double digits on a semi-regular basis. This was definitely an improvement. Much of this could be attributed to me coding more ambitious projects. Projects that couldn't just be finished in a couple hours, and had to be periodically saved.

3. 2018 (179 commits)

This was the year one could practically see me shifting into third gear. There were no months where I neglected GitHub. More and more months were registering double digit commits. At the highest point at the end of the year, I even registered 50 commits. Compared to what I do consistently today, this is nothing. But it marked a start of something.


The trend line shows that I was still finding my feet, though some months were better than others. I was struggling for consistency as far as GitHub usage was concerned. This was at least partly due to me being busy studying for my ACTA.


Room for improvement? Definitely. But I was still using GitHub pretty much like a layperson. I used it to store code and not much else. I wasn't using GitHub anywhere close to its full potential.

4. 2019 (138 commits)

Things dropped off slightly in 2019. I suspect a lot of it was due to adjusting to my first year of being married and all. (Yeah, way to blame the wife, dude.)


My GitHub contribution was a jagged line. I would be a GitHub hero for a couple of months, then almost a zero the next month.


And in October, I even registered an entire month without a commit. This might have been due to the impending dominion of COVID-19. Suddenly we were all distracted by this potential life-or-death issue.

But for sure, my use of GitHub was still going strong, just not as strong as the previous year.

5. 2020 (286 commits)

This is where it started to get interesting. Some months, my usage climbed sharply, and plummeted just as quickly the next few months.

If you look at the heatmap below, it almost mirrors the line chart - periods of increased activity punctuated by periods of low activity. If there was any consolation, there were less fluctuations than the previous year.


This was because I was retroactively going through all my Readme files, and reformatting them for better readability. Also, now I was actually establishing proper code commits instead of merely updating via copy-pasting directly into repositories. I had GitHub Desktop open on my Lenovo, and open on my MacBook. This led to a whole lot of increased activity.

6. 2021 (227 commits)

This year, activity dropped off slightly, though my usage was arguably more consistent than it had been the previous year. The line chart shows a jagged line, though less jagged than previous years, with a peak near year end.


The heatmap of my activity was looking more spread out.


I was going through school for Data Analytics, and this affected the time I had for code experimentation. I'm proud to say, though, I managed to dedicate at least this much time.

7. 2022 (651 commits)

This was the year my usage really started to take off. For one, I was really seriously beginning to commit code the way GitHub was meant to be used. The lowest number of commits I tracked was 34 in January, and it never fell below that number for the rest of the year. The highest at one point was 76 in March. The line chart still shows a jagged line, but the minimum has risen dramatically.


The heatmap shows an ever wider spread of contributions over the year. A whole lot more heat. Almost three times the previous year's.


I had also begun to manage the contents of the website on GitHub, under a private repository. This was so I could look up previous versions of files and potentially restore them. I really should have done this a lot sooner. The thing was, my Lenovo was beginning to sputter and I really didn't feel comfortable having all my content stored there. Thus, my hand was forced.

That was pretty much how the number of commits jumped that much.

8. 2023 (872 commits)

The number of commits continued to jump. Compared to the peals and valleys registered in previous years for monthly commits, it was a relatively straight, consistent line.


I had begun to use GitHub to store my blogging drafts. Now this may not sound like much, until one considers how I blog. It starts as a skeleton made out of ideas in point form, and slowly I flesh them out bit by bit. Along the way, I may make revisions - rewording and rearranging stuff. I may only have over 70 blogposts a year, but that's multiple commits per blogpost!

This was partly due to my Lenovo being on its last legs. I began the process of writing drafts in GitHub instead of storing them in text files in my Lenovo, and when my Lenovo finally died in the latter half of 2023, my caution was rewarded.


The end result was, there were only three dates in the entire year where I didn't register a single commit. Compared to last year, the number of commits in a single month ranged from 61 to 80. In the heatmap, you can see that almost the entire map is shades of green.

9. 2024 (991 commits)

Now that I was writing drafts in GitHub full-time, that translated to daily commits. I would get an idea, open up GitHub, and commit it. I registered maybe one or two dates the entire year where I didn't commit anything. Most of the time, though, I was supremely consistent. If you look at the graph, the line was even smoother than the previous year's!


Looking at the chart, my usage started out at 70 plus commits per month, then steadily climbed to the high 80s through the course of the year. I remember at that point trying to rein myself in. I didn't want to end up setting a bar I couldn't commit to long term. The heatmap, as in 2024, shows almost total coverage of shades of green, but a lot more is bright green.


In addition to that, some of my projects were a little complex. They required frequent commits. I could push ten commits in an hour on a ReactJS project. This was also the year I started with NodeJS, and as you can probably tell, this also translated to a lot of commits.



10. 2025 (1007 commits)

The trend continued. I was hitting my stride in my usage of GitHub, and the consistency was really starting to show. Commits per month were now in the 80 plus range, until near the end of the year where I decided to give myself a bit of breathing room. Looking at the trend line, it was almost a straight line except for that year-end dip.


In the case of blogpost drafts, sometimes my updates were just little typo corrections and adding a few sentences here and there. Most bloggers will tell you that the incremental nature of writing a blogpost means that potentially a whole bunch of corrections accompany every one. While this was already the case in previous years, I took it up a few notches.


Here, the heatmap shows an entire year with no gaps. There is obviously higher usage during weekends. Of course, one commit could be as small as correcting a single typo, or be as big as including new functions into the code base. Thus, it can't be a complete representation of how hard I work here. But it's a decent indication.

What a decade!

It's interesting to me how my usage of GitHub evolved through the years. From just another online file system to a means of tracking code changes, and from there expanding to tracking all document changes. Even with code, my usage also changed, with more frequent commits due to establishing CI/CD pipelines.

Most of all, I think it shows my growth as a techie. My usage could still be improved, but at this point I think I'm getting close to a sweet spot. What has your usage been like?

With much commitment,
T___T

Tuesday, 25 November 2025

Tech skillsets: Go deep or go wide? (Part 1/2)

"Jack of all trades, master of none" is a term I've heard all too often when fellow techies describe my stint as a web developer. Back then, I was a generalist dipping my sticky fingers into every new and shiny tech I encountered. In a sense, I'm still that web developer. Just with more money.

Go deep or go wide?

I want to explore the pros and cons of generalizing as opposed to specializing, especially in the context of today's tech landscape. The industry needs both specialists and generalists. That's a simple fact that shouldn't need some nobody tech blogger pointing out, but sometimes people get overly emotional about defending their stance. Want to specialize? Cool, do that. Want to generalize? Also cool, go crazy. But we need to be cognizant of the tradeoffs.

The case for specializing over generalizing

Before every tech and his dog started calling themselves "full-stack developers", specialists seemed to earn a whole lot of money. That was back when I was reading job ads asking for "deep expertise" and extensive work experience in more narrow scopes such as back-end programming and database administration. Some areas, such as COBOL programming, pay a lot for the simple reason that COBOL is still largely in use but COBOL programmers appear to be a dying breed. I've touched a lot of programming languages, but just enough to qualify as a hobbyist in each one. I have a decent frame for comparison between them - a Python lecturer of mine was actually quite entertained at my comparisons of the language to PHP and Ruby - but that often isn't very useful in the professional sense.

Specialists earn big.

The companies whose budgets are large enough to afford to pay for specialists, also tend to use these specialists for projects that are grand in scale. Projects that actually require deep expertise. Projects that add incalculable value to one's CV.

Deep knowledge is also useful for judging the extent of one's resolve and commitment. After all, it takes plenty of both to spend the effort. I personally know someone who specializes in HTML and CSS. It's a very narrow niche and perhaps not very profitable, but undeniably impressive.

Generalists - quite unfairly, I might add - seem to have developed a reputation for being easily distracted. Lacking the necessary discipline and focus required to specialize. I would actually say it takes an extraordinary amount of discipline to avoid going too deeply down one rabbit hole and shutting out everything else, but that's not the general sentiment. Therefore, in some circles, generalists are seen as those who were unable to specialize as opposed to specialists being unable to generalize.

The conventional advice when it comes to tech specialization, is to specialize in one area while simultaneously being decent in a few related areas. For instance, if you're a Data Analytics guru, you may want to specialize in statistical analysis and mathematics, but at the same time get reasonably good with Data Visualization tools and learn a bit of Python. If you're a primarily a Front-end Developer, learning some simple database concepts would be a useful addition to HTML, CSS and JavaScript. Maybe pick up a few frameworks such as ReactJS or VueJS, just to round things out.

I won't argue against that advice, but these days it feels like nothing is ever enough.

Next

We explore the opposite case!

Friday, 10 October 2025

TeochewThunder: Year Eleven (Part 1/2)

Well, look who turns 11 this year! It's not me (I wish), but it's this blog, of course. This thing here might just be a substitute for the children I'm never planning to have.

Dear God, please no.

In all seriousness though, it occurs to me that the effort taken to maintain this blog and the website has pretty much kept me sane all these years. I read somewhere about journalling with regard to mental health, and it seems that this blog is a great example of journalling. Why's it different from venting on Facebook or X, you might ask?

Well, for one, Social Media posts tend to be a lot shorter and more unfiltered. Which can be a good thing, don't get me wrong, but not necessarily so if you want a more thorough internal audit. Blog posts go through several revisions, as we examine what's going on in our heads, and why, and maybe even how it pertains to the tech space. The final result is a more measured, more self-examined output into the stratosphere. As such, I consider my blogposts of higher quality than a simple vomiting of my initial reactions on Social Media platforms.

That isn't to say I haven't said stupid shit in the past. I absolutely have. But the beauty of time is that as the years go by, I can evolve into less of s shit-talker and more of a shit-thinker. Yikes, that didn't sound better, did it?

Dedication

Also, this is a blog I'm dedicated to.

Dedication is a measure of how consistent you're willing to be in your efforts even without applause or acknowledgement. It's a measure of how much of a shit I give. And I give a lot.

Think about it. In previous years, I could at least justify the effort by the way prospective employers would look at my entire online portfolio. These days, they don't do that anymore (also, I haven't been looking in a while) because even the demos I put out are kids' stuff. I like to think some of it is really well-done, but well-done or not, it's still kids' stuff. Those are just not the things people hire senior developers for, especially not in the age of Artificial Intelligence and Vibe Coding.

So no... there are no longer practical reasons for maintaining this effort. I do these things because I like doing these things.

That's not to say I don't occasionally benefit from a break. And October is my assigned month for that break. Other than this blogpost, there will be no other visible activity. Emphasis on the word visible.

Invisible hands, invisible effort.

You see, as in most software development, the value is largely in the stuff that users don't see. The optimizations. The security fixes. The fine-tuning in the back. That's not to say there's no value in the stuff that's visible, but sometimes I feel like a lot of that is just to placate laypersons who don't know any better.

That's a controversial statement which we should reserve for another day.

To my original point, there is going to be work done. Just not visible work. Mostly prep for year-end, and 2026.

Content

As with last year, I've been making an effort to use less profanities in my writing. Not because I necessarily think the odd (or even frequent) vulgarity is a bad thing, mind you. More because I don't want to develop an over-reliance on anything, not even swearing. I don't want to have to use foul language as a crutch to express myself. It's just poor form. To that end, I am limiting myself to using it only a few times a year in this blog, usually whenever I review a Black Mirror episode. I certainly won't be using them with the same frequency during, say, 2019 to 2022, around the COVID-19 pandemic.

Speaking of which, as the horrors of the past few years fade behind us, I'll hopefully be speaking less about COVID-19 from this year forth. It was a terrible few years, and my emotion-laden rants during that period are evidence of that, but it's time to move on.

You may have noticed that the posts are getting even shorter than they used to. This is not an accident; rather it is the natural evolution of this blog. I wasn't verbally verbose before (at least I hope not) but reading other blogposts and tuning out halfway has made me realize that the lack of attention span on the internet is a very real thing. As a result, I'm going to curb any impulse I may have, to belabor whatever points I may be making.

What else? Yeah I changed the TeochewThunder logo. Talked about that already, didn't I? Hope you like it. If you don't, too fucking bad, baby. It's staying.

Surprise!

This is a tech blog, so I talked a whole lot about tech this year, as always. In particular, I talked about Artificial Intelligence. I suspect that this will be happening with alarming regularity, especially with the frequency with which laypersons feel the need to chime in. Someone's got to show 'em their place! Just kidding... kinda.

As for web tutorials, there's been a nice mix that includes NodeJS and NextJS. and D3. Along with the almost obligatory HTML, CSS, JavaScript sprinkled with the occasional PHP, of course. I started learning NodeJS, as usual, for the heck of it. It increased my understanding of what I was doing with ReactJS and NextJS, so there was value in it.

I've continued to generate images from AI, but the pendulum has swung back somewhat and once again I've begun to see value in using stock photos.

Next

Highs, lows, hits and misses

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.