Showing posts with label api. Show all posts
Showing posts with label api. Show all posts

Saturday, 19 July 2025

Five Reasons to learn Web Development in 2025

Recent events such as the rise of generative A.I, have made tech work a little less attractive than it used to be. Web development, in particular, has suffered. That's probaby because a large chunk of web development is automatable, and even before A.I came on the scene, there had been numerous tools such as Content Management Systems and Low-code development platforms.

Thus, web development being automated by A.I was par for the course.

Robots writing websites.

Still, not all is lost. While web development might have lost much of its luster, there are still good, strong reasons to pick it up in ones tech career. Unlike the tech reporters and HR executives who write listicles like these, I have actually been a web developer before. I speak from experience, my dudes. And following are some of the most compelling reasons I had, in no particular order of importance, for going down this path.

1. No complicated installation

Ever tried to learn a language like PHP or Java? Every single one of these languages requires you to set up some kind of compiler or interpreter environment. PHP requires an Apache server. Java needs the Java Runtime Environment. You can write all the code you want, but until the code gets compiled or interpreted by the environment that you have to install and set up, you're not getting even a Hello World program done.

All you need is a browser.

HTML, CSS and JavaScript, however, do not. All of them already run in any major browser - Firefox, Chrome, and so on. In effect, the environment is right there for you.

This is not to say that you will never need to do any complicated installation. But for the basic building blocks - again, HTML, CSS and JavaScript - of web development, you don't. You will need to do that when you want to pick up a server-side language and maybe databases and definitely for the NodeJS style of development. But for basic stuff? Even the slightly more advanced stuff? Nope, not even a little bit. That is a lot more than you could ever say about other programming languages or platforms.

2. Good skill spread

When you learn web development, you learn HTML, CSS and JavaScript as a base starting point. That's already a good spread right there.

HTML and CSS are where you learn front-end and possibly even design. When you learn JavaScript, in addition to all the things you pick up when learning a programming language such as operators, arrays, branching and iterative logic, you also learn asynchronous operations and DOM manipulation.

A good spread of tools.

That's not to say that other tech disciplines don't have their own unique perks. But where it comes to the skill spread, web development wins. I don't think anything else even comes close.

Once you get past the basic toolset of HTML, CSS and JavaScript, back-end programming and databases will come into play. It's never just web development. Even if you are averse to the thought of being a humble web developer for the rest of your career, there are far worse places to start.

3. Resources

Now, when I say "resources", I don't just mean documentation, references and learning materials, though there's plenty of that, yes. But web development is not special in that regard because any other tech discipline boasts plenty of learning resources and a community dedicated to helping each other learn.

A good learning
community.

Though, in this case, web development has something extra.

You see, every humble HTML page on the internet can have its source viewed and played with in the browser, reverse engineered, and so on. Every URL on the internet is potentially a resource for learning, much like how I learned to cobble together JavaScript widgets decades ago.

In contrast, it's not possible to just take any desktop application and reverse-engineer the code, because the code has already been compiled and is no longer human-readable.

4. Ready use case

Often, when learning a programming language, it's helpful to be able to use newly-acquired skills to build something, so as to really hammer home the muscle memory. Something both relevant and useful, preferably. Not that Hello World programs don't have their place, but if one wishes to level up, better use cases are the order of the day.

And with web development, those use cases are almost too easy to find. Web development creates web pages, at the minimum. And after that, at varying levels of complexity, web applications. One does not have to stretch too far to find something worth building... and because it already exists, you know that it is both worth building and possible to build.

Applying what you learn.

My larger point is that what you learn can readily be applied. Not just in creating and editing websites, but in general software development. This also means that your chances of landing a job with that skillset cannot be understated. In this day and age, web developers are perhaps not nearly as in demand as they were a decade ago, or paid nearly as well, but the skillset goes beyond just web development.

For example, a lot of existing software already leverage things like REST API endpoints. These are basically URLs, which are pretty much the backbone of the web. REST is an almost inescapable part of the whole web development deal. Ergo, if you deal in web development, at some point you are going to be dealing with REST endpoints, which overlaps a large part of software development regardless of discipline.

Or even mobile development. In case you weren't aware, a large chunk of mobile tech is basically HTML, CSS and JavaScript.

I could go on... but do I really need to?

5. No gatekeeping

In the legal profession, there's the Bar Exam. In the medical profession, there's the Medical Regulatory Authority. In tech? Other than job interviews which exist at almost every industry, there's almost no gatekeeping in tech. Even the requirement for Degrees of Diplomas is not a really hard one.

When I say "no gatekeeping", I don't mean that nobody tries to gatekeep. The fact is that many people try to gatekeep, but it just doesn't work because to gatekeep, one needs a unified set of standards. It's almost impossible to establish said standards in a landscape as varied as tech, whose goalposts shift constantly.

The gatekeeper.

And while this inability to gatekeep exists in many areas of tech, none moreso than web development. HTML, CSS and JavaScript are fairly stable at this point, but these are just the base technologies. Their offshoots - frameworks, libraries and the like - keep springing up like mushrooms. And when you consider databases and backend programming languages, the possibilities multiply even more.

All in all, one could come in anytime in web development, and still be relatively fresh and relevant. No one can stop you from making and publishing web pages and applications, not in the same way they can stop you from practising law. You don't need a license to write code, so nobody can revoke it.

Some clarifications

The reasons stated here are in relation to those for choosing other tech fields. Why, for instance, web development when you could go for Data Analytics or cybersecurity? Reasons specific to web development.

I was inspired to compile this list because there are a lot of vague, generic and - to be brutally honest - trite lists out there on the web that extol the virtues of web development. Hopefully this is a better list.

<html>Bye for now,</html>
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

Monday, 24 March 2025

Web Tutorial: The Movie Poster Generator

Hi, readers. Do I have a fun one today!

We will be leveraging on the code I wrote for the Nike Meme Generator, to generate something else that may require an image upload - a Movie Poster Generator! This one is going to require some ChatGPT finangling as well.

Let's begin by setting up the uploads directory, and adding a default image - an AI-generated portrait of Angelina Jolie.
uploads/angelinajolie.jpg

Then, in the parent folder, we copy over the code from the Nike Meme Generator in index.php. We'll be removing quite a lot of lines. You'll see what we've retained is the file upload functions and the form. We've also made a few text changes as well. The div memeContainer has been replaced by posterContainer, along with styling.
<?php
$filecode = "angelinajolie";
$filetype = "jpg";
//$line1 = "Believe in something.";
//$line2 = "Even if it means sacrificing everything.";
//$slogan = "Just Do It.";

$strmessage="";

if (isset($_POST["btSubmit"]))
{
    //$line1 = $_POST["txtLine1"];
    //$line2 = $_POST["txtLine2"];
    //$slogan = $_POST["txtSlogan"];

    if (basename($_FILES["flUpload"]["name"]) != "")
    {
        $uploadsize = intval($_POST["hidUploadSize"]);
         $filetype = pathinfo($_FILES["flUpload"]["name"],PATHINFO_EXTENSION);
         $filetype = strtolower($filetype);

         if ($_FILES["flUpload"]["size"] > $uploadsize)
         {
             $strmessage = "Error was encountered while uploading file. File cannot exceed " . ($uploadsize/1000) . "kb";
        }
        else
        {
          if (!is_array(getimagesize($_FILES["flUpload"]["tmp_name"])))
          {
           $strmessage = "File type invalid";
          }
          else
          {
              $filecode=strtotime("now").rand();
        
              if (move_uploaded_file($_FILES["flUpload"]["tmp_name"], "uploads/" . $filecode . "." . $filetype))
              {
               $strmessage = "File uploaded.";
              }
              else
              {
                  $strmessage = "Error was encountered while uploading file.";
              }  
          }
         }
    }
    else
    {
         $strmessage="No file selected.";
    }
}
?>

<!DOCTYPE html>
<html>
    <head>
        <title>Movie Poster Generator</title>

        <style>
        #pnlMessage
        {
            width: 100%;
            height: 50px;
            color: #FF0000;
            outline: 0px solid #DDDDDD;
        }

        #formContainer
        {
            width: 400px;
            padding: 5px;
            margin: 5px;
            float: left;
            outline: 0px solid #DDDDDD;
        }

        /*
        #memeContainer
        {
            width: 500px;
            height: 500px;
            padding: 5px;
            margin: 5px;
            float: left;
            outline: 1px solid #DDDDDD;
            background: url(<?php echo "uploads/" . $filecode . "." . $filetype; ?>) center center no-repeat;
            background-size: cover;
            font-family: georgia;
            color: #FFFFFF;
            font-size: 25px;
            -webkit-filter: grayscale(100%);
            filter: grayscale(100%);
            text-align: center;
        }
         */

        #posterContainer
        {
            width: 800px;

            height: 500px;
            margin: 5px;
            float: left;
            text-align: center;
        }

        @media print
        {
               #formContainer, #pnlMessage
               {
                   display: none;
               }
    
               #posterContainer
               {
                    margin: 10% auto 0 auto;
                    float: none;
               }
        }
        </style>
    </head>

    <body>
        <div id="pnlMessage"><?php echo $strmessage; ?></div>

        <div id="formContainer">
            <form id="frmUpload" name="frmUpload" action="" method="POST" enctype="multipart/form-data">
                  <label for="flUpload">File</label>
                  <input type="file" name="flUpload" id="flUpload">
                  <input type="hidden" name="hidUploadSize" id="hidUploadSize" value="50000000">
                  <br /><br />
                  <!---
                 <label for="txtLine1">Line 1</label>
                  <input name="txtLine1" id="txtLine1" maxlength="50" value="<?php echo $line1; ?>" />
                  <br /><br />
                 <label for="txtLine2">Line 2</label>
                  <input name="txtLine2" id="txtLine2" maxlength="50" value="<?php echo $line2; ?>" />
                  <br /><br />
                  <label for="txtSlogan">Slogan</label>
                  <input name="txtSlogan" id="txtSlogan" maxlength="20" value="<?php echo $slogan; ?>" />
                  --->
                  <br /><br />
                  <input type="submit" name="btSubmit" id="btSubmit" value="Create your Movie Poster!">
            </form>
        </div>

        <div id="posterContainer">
             <!---
             <p style="margin-top:50%"><?php echo $line1;?><br /><?php echo $line2;?></p>
             <p style="margin-top:30%"><img src="nikelogo.png"> <?php echo $slogan;?></p>
             --->
        </div>
    </body>
</html>


Because I plan on using jQuery in here, let's include the library as well. And a script tag for custom JavaScript.
<head>
  <title>Movie Poster Generator</title>

  <style>
    #pnlMessage
    {
      width: 100%;
      height: 50px;
      color: #FF0000;
      outline: 0px solid #DDDDDD;
    }

    #formContainer
    {
      width: 400px;
      padding: 5px;
      margin: 5px;
      float: left;
      outline: 0px solid #DDDDDD;
    }

    #posterContainer
    {
      width: 800px;
      height: 500px;
      margin: 5px;
      float: left;
      text-align: center;
    }

    @media print
    {
        #formContainer, #pnlMessage
        {
          display: none;
        }

        #posterContainer
        {
          margin: 10% auto 0 auto;
          float: none;
        }
    }
  </style>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
  <script>

  </script>

</head>


We are going to begin the PHP with some variables other than strmessage, filecode and filetype. You'll see that I've specified some default values too.

space_from_top: controls the vertical position that the block of text occupies.
movie_title: self-explanatory.
movie_title_color: what color it appears in.
movie_title_size: what font size to use for the title.
movie_tagline: the tagline that appears beneath the title.
movie_tagline_color: what color it appears in.
movie_tagline_size: what font size to use for the tagline.
movie_starring: the movie star name(s). Since I'm using a picture of Angeline Jolie, that's her name I'm using.
movie_starring_color: what color it appears in.
movie_starring_size: what font size to use.
reviews: this is an array which will contain the stuff we get from ChatGPT.
reviews_color: what color they appear in.
reviews_bgcolor: what background color the reviews will use.

<?php
$filecode = "angelinajolie";
$filetype = "jpg";

$space_from_top = "20";

$movie_title = "Modern-day Maleficent";
$movie_title_color = "#FFFFFF";
$movie_title_size = "30";

$movie_tagline = "A re-imagining of the original tale of darkness";
$movie_tagline_color = "#FFFFFF";
$movie_tagline_size = "15";

$movie_starring = "Angelina Jolie";
$movie_starring_color = "#FFFFFF";
$movie_starring_size = "10";

$reviews = [];
$reviews_color = "#FFFFFF";
$reviews_bgcolor = "#000000";


$strmessage="";


Inside the posterContainer div, add three divs, left, middle and right. left and right are styled using the CSS class review.
<div id="posterContainer">
    <div id="left" class="review">

    </div>
    <div id="middle">

    </div>
    <div id="right" class="review">

    </div>

</div>


In the styles, both left and right have different text alignments. As they are both styled by the CSS class review, they have a certain width and height, they're floated left with a little padding, and color and background-color properties are determined by reviews_color and reviews_bgcolor respectively. The font type is less important and it's a personal choice.
#posterContainer
{
    width: 800px;
    height: 500px;
    margin: 5px;
    float: left;
     text-align: center;
}

#left
{
    text-align: right;
}

#right
{
    text-align: left;
}

.review
{
    width: 180px;
    height: 480px;
    float: left;
    padding: 10px;
    color: <?php echo $reviews_color;?>;
    background-color: <?php echo $reviews_bgcolor;?>;
    font-family: Georgia;
}

@media print
{
    #formContainer, #pnlMessage
    {
        display: none;
    }

    #posterContainer
    {
        margin: 10% auto 0 auto;
        float: none;
    }
}


Now we have middle. Like the reviews CSS class, it has a certain width and height, and is floated left. The background is determined by filecode, which currently points to angelinajolie.jpg in the uploads folder.
#posterContainer
{
    width: 800px;
    height: 500px;
    margin: 5px;
    float: left;
     text-align: center;
}

#middle
{
    width: 380px;
    height: 500px;
    background: url(<?php echo "uploads/" . $filecode . "." . $filetype; ?>) center center no-repeat;
    background-size: cover;
    float: left;
}

#left
{
    text-align: right;
}

#right
{
    text-align: left;
}


Here's a preview!

In the middle div, add a paragraph tag with the id title_and_tagline. In there, we have span tags with the ids movie_title and movie_tagline respectively.
<div id="middle">
    <p id="title_and_tagline">
    
    <span id="movie_title"></span>
        <br />
        <span id="movie_tagline"></span>
    </p>
</div>


Populate these span tags with the strings for movie_title and movie_tagline.
<div id="middle">
    <p id="title_and_tagline">
        <span id="movie_title"><?php echo $movie_title;?></span>
        <br />
        <span id="movie_tagline"><?php echo $movie_tagline;?></span>
    </p>
</div>


Now add another paragraph with id movie_starring.
<div id="middle">
    <p id="title_and_tagline">
        <span id="movie_title"><?php echo $movie_title;?></span>
        <br />
        <span id="movie_tagline"><?php echo $movie_tagline;?></span>
    </p>
    <p id="movie_starring">

    </p>
</div>


In there, have some PHP. It's possible that movie_starring is an empty string, in which case we want no text in that paragraph. But if it's not an empty string, we want it to say "starring" followed by movie_starring.
<div id="middle">
    <p id="title_and_tagline">
        <span id="movie_title"><?php echo $movie_title;?></span>
        <br />
        <span id="movie_tagline"><?php echo $movie_tagline;?></span>
    </p>
    <p id="movie_starring">
        <?php echo ($movie_starring == "" ? "" : "starring ");?>
        <?php echo $movie_starring;?>
    </p>
</div>


Let's do some more styling. We have styles for title_and_tagline, movie_title, movie_tagline and movie_starring. We're doing largely what we did for the review CSS class, with font-size and color property values determined by the PHP variables. For title_and_tagline, the margin-top property is determined by space_from_top.
.review
{
    width: 180px;
    height: 480px;
    float: left;
    padding: 10px;
    color: <?php echo $reviews_color;?>;
    background-color: <?php echo $reviews_bgcolor;?>;
    font-family: Georgia;
}

#title_and_tagline
{
    margin-top: <?php echo $space_from_top;?>px;
}

#movie_title
{
    color: <?php echo $movie_title_color;?>;
    font-size: <?php echo $movie_title_size;?>px;
    font-weight: bold;
    font-family: Impact, Verdana;
}

#movie_tagline
{
    color: <?php echo $movie_tagline_color;?>;
    font-size: <?php echo $movie_tagline_size;?>px;
    font-family: Arial, Helvetica, Verdana;
}

#movie_starring
{
    color: <?php echo $movie_starring_color;?>;
    font-size: <?php echo $movie_starring_size;?>px;
    font-family: Arial, Helvetica, Verdana;
}

@media print
{
    #formContainer, #pnlMessage
    {
        display: none;
    }

    #posterContainer
    {
        margin: 10% auto 0 auto;
        float: none;
    }
}


You see the text appears!

Now we are going to make randomly-generated reviews appear. Remember the empty array reviews? Basically, we're about to populate it. The code will run only if the form has been submitted, so put it inside the If block. You'll need your own OpenAI Developer Account, so replace "xxx" with your credentials. Then define headers with the appropriate properties for authentication.
if (isset($_POST["btSubmit"]))
{
    if (basename($_FILES["flUpload"]["name"]) != "")
    {
         $uploadsize = intval($_POST["hidUploadSize"]);
         $filetype = pathinfo($_FILES["flUpload"]["name"],PATHINFO_EXTENSION);
         $filetype = strtolower($filetype);

         if ($_FILES["flUpload"]["size"] > $uploadsize)
         {
             $strmessage = "Error was encountered while uploading file. File cannot exceed " . ($uploadsize/1000) . "kb";
         }
         else
         {
             if (!is_array(getimagesize($_FILES["flUpload"]["tmp_name"])))
             {
                  $strmessage = "File type invalid";
             }
             else
             {
                  $filecode=strtotime("now").rand();
        
                  if (move_uploaded_file($_FILES["flUpload"]["tmp_name"], "uploads/" . $filecode . "." . $filetype))
                  {
                       $strmessage = "File uploaded.";
                  }
                  else
                  {
                       $strmessage = "Error was encountered while uploading file.";
                  }  
              }
         }
    }
    else
    {
         $strmessage="No file selected.";
    }

    $key = "xxx";
    $org = "org-xxx";
    $url = 'https://api.openai.com/v1/chat/completions';  

    $headers = [
        "Authorization: Bearer " . $key,
        "OpenAI-Organization: " . $org,
        "Content-Type: application/json"
    ];
}


Here, we're defining the prompt. We use movie_title (and movie_starring, if it's not an empty string), and generate an array, FakeReviews, of 10 objects. Each object will have a one-sentence comment and a string to determine the "source" of the comment.
$key = "xxx";
$org = "org-xxx";
$url = 'https://api.openai.com/v1/chat/completions';  

$headers = [
    "Authorization: Bearer " . $key,
    "OpenAI-Organization: " . $org,
    "Content-Type: application/json"
];

$messages = [];
$obj = [];
$obj["role"] = "user";
$obj["content"] = "Give me a JSON object with one property. The property should be named 'FakeReviews', and should be an array of ten objects. Each object should have the property 'review', which is a random fictional complimentary about the movie '" . $movie_title . "'" . ($movie_starring == "" ? "" : " or celebrity '" . $movie_starring . "'") .  " (range between three to ten words) sentence in a string, and the property 'critic' which contains the fictional publication for that quote.";
$messages[] = $obj;


The rest of the code we've gone through before in The Random Christmas Card and The Self-affirmations Wordpress Plugin
$messages = [];
$obj = [];
$obj["role"] = "user";
$obj["content"] = "Give me a JSON object with one property. The property should be named 'FakeReviews', and should be an array of ten objects. Each object should have the property 'review', which is a random fictional complimentary about the movie '" . $movie_title . "'" . ($movie_starring == "" ? "" : " or celebrity '" . $movie_starring . "'") .  " (range between three to ten words) sentence in a string, and the property 'critic' which contains the fictional publication for that quote.";
$messages[] = $obj;

$data = [];
$data["model"] = "gpt-3.5-turbo";
$data["messages"] = $messages;
$data["max_tokens"] = 1000;


We use cURL to send the data to the API endpoint. The returned response is in result, and we handle any errors before closing out with curl_close().
$data = [];
$data["model"] = "gpt-3.5-turbo";
$data["messages"] = $messages;
$data["max_tokens"] = 1000;

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

$result = curl_exec($curl);
if (curl_errno($curl)) 

{
    echo 'Error:' . curl_error($curl);
}

curl_close($curl);


Then we'll use json_decode() on result, and then extract FakeReviews from it.
$result = curl_exec($curl);
if (curl_errno($curl)) 
{
    echo 'Error:' . curl_error($curl);
}

curl_close($curl);

$result = json_decode($result);
$content = $result->choices[0]->message->content;
$content = json_decode($content);

$reviews = $content->FakeReviews;


In the left div, use a PHP If block to check if reviews has 10 elements, just to filter out any nasty surprises. Then use a For loop to go through the first 5 elements.
<div id="left" class="review">
<?php
if (count($reviews) == 10) 

{
    for ($i = 0; $i < 5; $i++)
    {

    }
}
?>

</div>


For each element, you want a span element, and a nicely formatted review property. Be sure to use htmlspecialchars() on it.
<div id="left" class="review">
<?php
if (count($reviews) == 10) 
{
    for ($i = 0; $i < 5; $i++)
    {
?>
    <span>
    <b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
<?php

    }
}
?>
</div>


We want the font size to be inversely proportional to the length of the string. Thus, if the review was "Go watch it!", it would be in a significantly larger font than "This movie will bring you through a roller-coaster ride of emotions!".
<div id="left" class="review">
<?php
if (count($reviews) == 10) 
{
    for ($i = 0; $i < 5; $i++)
    {
?>
    <span style="font-size:<?php echo (1.5 - (strlen($reviews[$i]->review) / 10)); ?>em">
    <b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
<?php
    }
}
?>
</div>


After that, we have a small tag and the critic property in italics.
<div id="left" class="review">
<?php
if (count($reviews) == 10) 
{
    for ($i = 0; $i < 5; $i++)
    {
?>
    <span style="font-size:<?php echo (1.5 - (strlen($reviews[$i]->review) / 10)); ?>em">
    <b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
    <br />
    <small>
    <i><?php echo $reviews[$i]->critic; ?></i>
    </small>
    <br />
    <br />

<?php
    }
}
?>
</div>


We then want there to be between 3 to 5 stars. So we use the HTML symbol 3 times...
<small>
<i><?php echo $reviews[$i]->critic; ?></i>
&nbsp;&#9733;&#9733;&#9733;
</small>


Then use a For loop and the rand() function to potentially put down a maximum of 2 more stars.
<small>
<i><?php echo $reviews[$i]->critic; ?></i>
&nbsp;&#9733;&#9733;&#9733;
<?php 
for ($j = 0; $j <= 1; $j++)
{
    if (rand(1, 2) == 1) echo "&#9733;";
}
?>

</small>


You have to click the "Create your Movie Poster" button to test this.

Repeat for the other side. This time, set the For loop to iterate through elements 5 to 10 of reviews.
<div id="right" class="review">
<?php
if (count($reviews) == 10) 

{
    for ($i = 5; $i < 10; $i++)
    {
?>
    <span style="font-size:<?php echo (2 - (strlen($reviews[$i]->review) / 10)); ?>em"><b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
    <br />
    <small>
    <i><?php echo $reviews[$i]->critic; ?></i>
    &nbsp;&#9733;&#9733;&#9733;
    <?php 
    for ($j = 0; $j <= 1; $j++)
    {
        if (rand(1, 2) == 1) echo "&#9733;";
    }
    ?>
    </small>
    <br />
    <br />
<?php
    }
}
?>

</div>


Looks like the other side is done!


We're not quite done yet...

We want to make a dashboard to manipulate customizable variables. The good news is, we've done that already before in The Easter Egg Generator, all the way back in 2016. Can't really reuse any code, but the principles are the same.

Let's define some fieldsets, with legends.
<form id="frmUpload" name="frmUpload" action="" method="POST" enctype="multipart/form-data">
    <label for="flUpload">File</label>
    <input type="file" name="flUpload" id="flUpload">
    <input type="hidden" name="hidUploadSize" id="hidUploadSize" value="50000000">
    <br /><br />
    <fieldset>
        <legend>Movie Title</legend>            

    </fieldset>

    <fieldset>
        <legend>Movie Tagline</legend>

    </fieldset>

    <fieldset>
        <legend>Starring</legend>

    </fieldset>           

    <fieldset>
        <legend>Reviews</legend>

    </fieldset>
    <br /><br />

    <input type="submit" name="btSubmit" id="btSubmit" value="Create your Movie Poster!">
</form>


Taking shape!


We'll want controls that the user can use to change the PHP variables. For numeric values, we'll use a range input. For colors, we'll use color inputs. And for text values, we will just have vanilla text inputs. For the latter, I've included maxlength attributes out of sheer habit. Take a note of name and id attributes - those will be relevant real soon.
<input type="hidden" name="hidUploadSize" id="hidUploadSize" value="50000000">
<br /><br />
<label for="txtSpace_from_top">Space From Top</label>
<input type="range" min="10" max="400" name="txtSpace_from_top" id="txtSpace_from_top" value="" />

<br /><br />
<fieldset>
        <legend>Movie Title</legend>
        <label for="txtMovie_title">Text</label>
        <input name="txtMovie_title" id="txtMovie_title" maxlength="20" value="" />
        <br />
        <label for="txtMovie_title_color">Color</label>
        <input type="color" name="txtMovie_title_color" id="txtMovie_title_color" value="" />
        <br />
        <label for="txtMovie_title_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_title_size" id="txtMovie_title_size" value="" />
            
</fieldset>

<fieldset>
        <legend>Movie Tagline</legend>
        <label for="txtMovie_tagline">Text</label>
        <input name="txtMovie_tagline" id="txtMovie_tagline" maxlength="50" value="" />
        <br />
        <label for="txtMovie_tagline_color">Color</label>
        <input type="color" name="txtMovie_tagline_color" id="txtMovie_tagline_color" value="" />
        <br />
        <label for="txtMovie_tagline_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_tagline_size" id="txtMovie_tagline_size" value="" />

</fieldset>

<fieldset>
        <legend>Starring</legend>
        <label for="txtMovie_starring">Text</label>
        <input name="txtMovie_starring" id="txtMovie_starring" maxlength="100" value="" />
        <br />
        <label for="txtMovie_starring_color">Color</label>
        <input type="color" name="txtMovie_starring_color" id="txtMovie_starring_color" value="" />
        <br />
        <label for="txtMovie_starring_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_starring_size" id="txtMovie_starring_size" value="" />

</fieldset>           

<fieldset>
        <legend>Reviews</legend>
        <label for="txtReviews_color">Color</label>
        <input type="color" name="txtReviews_color" id="txtReviews_color" value="" />
        <br />
        <label for="txtReviews_bgcolor">Background Color</label>
        <input type="color" name="txtReviews_bgcolor" id="txtReviews_bgcolor" value="" />

</fieldset>


And here are the inputs. Just a bit messy, so let's clean stuff up.


In the styles, style labels to have a fixed width. I've also styled font. While we're at it, let's have the submit button also given a fixed width, a bit of spacing at the top, and float it right.
#formContainer
{
        width: 400px;
        padding: 5px;
        margin: 5px;
        float: left;
        outline: 0px solid #DDDDDD;
}

label
{
        display: inline-block;
        font-family: arial;
        font-size: 12px;
        width: 10em;
}


#btSubmit
{
        width: 15em;
        margin-top: 1em;
        float: right;
}

#posterContainer
{
        width: 800px;
        height: 500px;
        margin: 5px;
        float: left;
        text-align: center;
}


And then let's populate the values of all these controls with the PHP variables.
<label for="txtSpace_from_top">Space From Top</label>
<input type="range" min="10" max="400" name="txtSpace_from_top" id="txtSpace_from_top" value="<?php echo $space_from_top; ?>" />
<br /><br />
<fieldset>
        <legend>Movie Title</legend>
        <label for="txtMovie_title">Text</label>
        <input name="txtMovie_title" id="txtMovie_title" maxlength="20" value="<?php echo $movie_title; ?>" />
        <br />
        <label for="txtMovie_title_color">Color</label>
        <input type="color" name="txtMovie_title_color" id="txtMovie_title_color" value="<?php echo $movie_title_color; ?>" />
        <br />
        <label for="txtMovie_title_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_title_size" id="txtMovie_title_size" value="<?php echo $movie_title_size; ?>" />             
</fieldset>

<fieldset>
        <legend>Movie Tagline</legend>
        <label for="txtMovie_tagline">Text</label>
        <input name="txtMovie_tagline" id="txtMovie_tagline" maxlength="50" value="<?php echo $movie_tagline; ?>" />
        <br />
        <label for="txtMovie_tagline_color">Color</label>
        <input type="color" name="txtMovie_tagline_color" id="txtMovie_tagline_color" value="<?php echo $movie_tagline_color; ?>" />
        <br />
        <label for="txtMovie_tagline_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_tagline_size" id="txtMovie_tagline_size" value="<?php echo $movie_tagline_size; ?>" />
</fieldset>

<fieldset>
        <legend>Starring</legend>
        <label for="txtMovie_starring">Text</label>
        <input name="txtMovie_starring" id="txtMovie_starring" maxlength="100" value="<?php echo $movie_starring; ?>" />
        <br />
        <label for="txtMovie_starring_color">Color</label>
        <input type="color" name="txtMovie_starring_color" id="txtMovie_starring_color" value="<?php echo $movie_tagline_color; ?>" />
        <br />
        <label for="txtMovie_starring_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_starring_size" id="txtMovie_starring_size" value="<?php echo $movie_starring_size; ?>" />
</fieldset>           

<fieldset>
        <legend>Reviews</legend>
        <label for="txtReviews_color">Color</label>
        <input type="color" name="txtReviews_color" id="txtReviews_color" value="<?php echo $reviews_color; ?>" />
        <br />
        <label for="txtReviews_bgcolor">Background Color</label>
<input type="color" name="txtReviews_bgcolor" id="txtReviews_bgcolor" value="<?php echo $reviews_bgcolor; ?>" />
</fieldset>


There you go.


Now, here's some more PHP code. This ensures that if you change any of the variables in the form and then submit the form, the changes take effect.
if (isset($_POST["btSubmit"]))
{
    if (basename($_FILES["flUpload"]["name"]) != "")
    {
         $uploadsize = intval($_POST["hidUploadSize"]);
         $filetype = pathinfo($_FILES["flUpload"]["name"],PATHINFO_EXTENSION);
         $filetype = strtolower($filetype);

         if ($_FILES["flUpload"]["size"] > $uploadsize)
         {
             $strmessage = "Error was encountered while uploading file. File cannot exceed " . ($uploadsize/1000) . "kb";
         }
         else
         {
             if (!is_array(getimagesize($_FILES["flUpload"]["tmp_name"])))
             {
                  $strmessage = "File type invalid";
             }
             else
             {
                  $filecode=strtotime("now").rand();
        
                  if (move_uploaded_file($_FILES["flUpload"]["tmp_name"], "uploads/" . $filecode . "." . $filetype))
                  {
                      $strmessage = "File uploaded.";
                  }
                  else
                  {
                      $strmessage = "Error was encountered while uploading file.";
                  }  
             }
         }
    }
    else
    {
         $strmessage="No file selected.";
    }

    $space_from_top = $_POST["txtSpace_from_top"];

    $movie_title = $_POST["txtMovie_title"];
    $movie_title_color = $_POST["txtMovie_title_color"];
    $movie_title_size = $_POST["txtMovie_title_size"];

    $movie_tagline = $_POST["txtMovie_tagline"];
    $movie_tagline_color = $_POST["txtMovie_tagline_color"];
    $movie_tagline_size = $_POST["txtMovie_tagline_size"];

    $movie_starring = $_POST["txtMovie_starring"];
    $movie_starring_color = $_POST["txtMovie_starring_color"];
    $movie_starring_size = $_POST["txtMovie_starring_size"];

    $reviews = [];
    $reviews_color = $_POST["txtReviews_color"];
    $reviews_bgcolor = $_POST["txtReviews_bgcolor"];


    $key = "xxx";
    $org = "org-xxx";
    $url = 'https://api.openai.com/v1/chat/completions';  

    $headers = [
        "Authorization: Bearer " . $key,
        "OpenAI-Organization: " . $org,
        "Content-Type: application/json"
    ];

    $messages = [];
    $obj = [];
    $obj["role"] = "user";
    $obj["content"] = "Give me a JSON object with one property. The property should be named 'FakeReviews', and should be an array of ten objects. Each object should have the property 'review', which is a random fictional complimentary about the movie '" . $movie_title . "'" . ($movie_starring == "" ? "" : " or celebrity '" . $movie_starring . "'") .  " (range between three to ten words) sentence in a string, and the property 'critic' which contains the fictional publication for that quote.";
    $messages[] = $obj;

    $data = [];
    $data["model"] = "gpt-3.5-turbo";
    $data["messages"] = $messages;
    $data["max_tokens"] = 1000;

    $curl = curl_init($url);
    curl_setopt($curl, CURLOPT_POST, 1);
    curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

    $result = curl_exec($curl);
    if (curl_errno($curl)) 
    {
         echo 'Error:' . curl_error($curl);
    }

    curl_close($curl);

    $result = json_decode($result);
    $content = $result->choices[0]->message->content;
    $content = json_decode($content);

    $reviews = $content->FakeReviews;
}


See what I mean?


Now, it would be nice to have whatever changes you make, be reflected in "real-time" instead of having to submit the form. Well, image changes have to involve submitting the form, but not the rest! So, in the fields, add the oninput attribute and set it to call the function useVariables().
<label for="txtSpace_from_top">Space From Top</label>
<input type="range" min="10" max="400" name="txtSpace_from_top" id="txtSpace_from_top" value="<?php echo $space_from_top; ?>" oninput="useVariables()" />
<br /><br />
<fieldset>
        <legend>Movie Title</legend>
        <label for="txtMovie_title">Text</label>
        <input name="txtMovie_title" id="txtMovie_title" maxlength="20" value="<?php echo $movie_title; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_title_color">Color</label>
        <input type="color" name="txtMovie_title_color" id="txtMovie_title_color" value="<?php echo $movie_title_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_title_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_title_size" id="txtMovie_title_size" value="<?php echo $movie_title_size; ?>" oninput="useVariables()" />             
</fieldset>

<fieldset>
        <legend>Movie Tagline</legend>
        <label for="txtMovie_tagline">Text</label>
        <input name="txtMovie_tagline" id="txtMovie_tagline" maxlength="50" value="<?php echo $movie_tagline; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_tagline_color">Color</label>
        <input type="color" name="txtMovie_tagline_color" id="txtMovie_tagline_color" value="<?php echo $movie_tagline_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_tagline_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_tagline_size" id="txtMovie_tagline_size" value="<?php echo $movie_tagline_size; ?>" oninput="useVariables()" />
</fieldset>

<fieldset>
        <legend>Starring</legend>
        <label for="txtMovie_starring">Text</label>
        <input name="txtMovie_starring" id="txtMovie_starring" maxlength="100" value="<?php echo $movie_starring; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_starring_color">Color</label>
        <input type="color" name="txtMovie_starring_color" id="txtMovie_starring_color" value="<?php echo $movie_tagline_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_starring_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_starring_size" id="txtMovie_starring_size" value="<?php echo $movie_starring_size; ?>" oninput="useVariables()" />
</fieldset>           

<fieldset>
        <legend>Reviews</legend>
        <label for="txtReviews_color">Color</label>
        <input type="color" name="txtReviews_color" id="txtReviews_color" value="<?php echo $reviews_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtReviews_bgcolor">Background Color</label>
<input type="color" name="txtReviews_bgcolor" id="txtReviews_bgcolor" value="<?php echo $reviews_bgcolor; ?>" oninput="useVariables()" />
</fieldset>


And then we create the useVariables() function in the JavaScript.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{

}

</script>


We will be making changes to these elements...
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{
    $("#title_and_tagline")

    $(".review")

    $("#movie_title")

    $("#movie_tagline")

    $("#movie_starring")

}
</script>


All of these elements will have changes made to the style attribute. As you can see, the changes are mostly about font size and color. In the case of title_and_tagline, the margin-top property is changed. In the case of divs styled using the review CSS class, it's color and background color that's changed.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{
    $("#title_and_tagline")
    .attr("style", "margin-top:" + $("#txtSpace_from_top").val() + "px");

    $(".review")
    .attr("style", "color:" + $("#txtReviews_color").val() + ";background-color: " +       $("#txtReviews_bgcolor").val());

    $("#movie_title")
    .attr("style", "color:" + $("#txtMovie_title_color").val() + ";font-size: " + $("#txtMovie_title_size").val() + "px");

    $("#movie_tagline")
    .attr("style", "color:" + $("#txtMovie_tagline_color").val() + ";font-size: " + $("#txtMovie_tagline_size").val() + "px");

    $("#movie_starring")
    .attr("style", "color:" + $("#txtMovie_starring_color").val() + ";font-size: " + $("#txtMovie_starring_size").val() + "px");
}
</script>


For movie_title, movie_tagline and movie_starring, we use the html() method to change the text. Note that for movie_starring, the same rules apply as they did with the PHP script - if the contents of the txtMovie_starring text box is an empty string, this placeholder will be empty as well; otherwise, prepend with "starring".
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{
    $("#title_and_tagline")
    .attr("style", "margin-top:" + $("#txtSpace_from_top").val() + "px");

    $(".review")
    .attr("style", "color:" + $("#txtReviews_color").val() + ";background-color: " + $("#txtReviews_bgcolor").val());

    $("#movie_title")
    .attr("style", "color:" + $("#txtMovie_title_color").val() + ";font-size: " + $("#txtMovie_title_size").val() + "px")
    .html($("#txtMovie_title").val());

    $("#movie_tagline")
    .attr("style", "color:" + $("#txtMovie_tagline_color").val() + ";font-size: " + $("#txtMovie_tagline_size").val() + "px")
    .html($("#txtMovie_tagline").val());

    $("#movie_starring")
    .attr("style", "color:" + $("#txtMovie_starring_color").val() + ";font-size: " + $("#txtMovie_starring_size").val() + "px")
    .html(($("#txtMovie_starring").val() == "" ? "" : "starring " + $("#txtMovie_starring").val()));
}
</script>


You can't really tell from here, but the screen is reflecting the changes I'm making right now.


Before I forget...

You can even print out your poster. The media queries I specified in the CSS ensure that the dashboard is not visible in print view.

Now, wasn't that fun?!

Generative Artificial Intelligence really injects a little bit of random craziness into tiny projects like these. Love it!

Getting the picture? Hur hur,
T___T