Sunday, 30 March 2025

Disclosing your salary during a job application

Should job applicants have to disclose their last remuneration to the companies that they are interviewing at? That's a debate that has gone on for as long as I can remember. Singapore has not made this practice illegal; however, applicants are similarly not obliged to disclose this information.

A closely guarded secret?

Some will claim that this is not helpful to applicants; companies can simply choose not to hire applicants who decline to disclose these details. That's certainly true. What's not true, I feel, is that this only disadvantages applicants. No, this disadvantages applicants who don't have many options. Similarly, this also disadvantages companies who don't exactly have a large pool of applicants to choose from. Your options, or lack thereof, are not anyone's problem but your own.

Why ask for this info?

Companies usually justify asking for the last drawn pay with this: it saves time for everyone if the applicant's previous remuneration was significantly higher than the budget for this role - thus the company avoids making an uninteresting (or worse, insulting) offer.

If this were the justification, however, it begs the question as to why the budget for this role isn't normally revealed. What applicants tend to feel, is that companies want this information so as to give themselves an edge in negotiations and an opportunity to lowball these applicants. And while this may not be true for every employer, enough employers have done this (and are still doing this) that this accusation isn't entirely unfounded. On the other side of the coin, employers may feel that applicants who aren't trying to pull a fast one on them, should have nothing to hide where their last drawn pay is concerned... and indeed, there are plenty of applicants who act just as shadily to justify those fears. Suffice to say, information, or lack thereof, is seen as the decisive weapon in negotiations.

All about the
strategic advantage.

Purely from a business standpoint, I understand that sentiment behind lowballing applicants perfectly. Let's say, for example's sake, that this was a role for a senior software engineer. If the applicant was last paid SGD 5,000 a month as a software developer, why should the company pay more than SGD 6,000 a month? Sure, SGD 8,000 a month is the budget for the senior software engineer role, but the applicant doesn't know that.

Conversely, applicants might feel that disclosing their last drawn remuneration unnecessarily limits them to a certain pay range. If their last remuneration was SGD 5,000 as a web developer and the software architect role they were applying for potentially paid SGD 10,000, they wouldn't want to have to settle for SGD 6,000 a month even though it was higher than their last drawn pay.

I have to agree, that doesn't make sense either. The value of the role doesn't change depending on who's being interviewed for it, so why should the offer differ based on the last drawn salary? Should applicants be permanently disadvantaged just because their last employers undervalued or underpaid them, or because they were paid lower for an entirely different job?

Here's my opinion: it's rarely just about the money. Sure, saving a little here and there tends to add up, but let's not kid ourselves - companies aren't doing this for the money. If saving a couple hundred or thousand a month meant that much, perhaps they shouldn't be in business at all. No, companies (or just the people running them) don't want to lose. And having to pay applicants significantly more than they were making in their last job, could be seen as losing, even if the job is now significantly different or more challenging. Honestly, again, just purely from a business point of view, employers would pay employees nothing if they thought they could get away with it; what more paying them a lot more than their last boss did?

Common advice

Most literature I've read on this subject, advise applicants to do the following when pressed to reveal their last drawn pay - politely explain that you're looking for a competitive compensation package based on your experience and the role's responsibilities. Turn the question around by inquiring about the company's salary range for the position.

Well... that's certainly an option. Diplomacy is great and all, but a little too indirect for my tastes. And, it doesn't stop the company from insisting on the information anyway.

And then there are some blustery corners of the internet, such as Quora and Reddit, who recommend that the applicant lie. I can't even begin to describe all the ways this would be a horrible idea.

Don't lie.

But, if the risk of being caught doesn't sufficiently scare you out of lying, consider this. The company is just one of a multitude of places to work at. You shouldn't have to lie in order to get employed. No one should.

Perhaps I'm speaking from a position of privilege when I say this: it's just a job opportunity. Don't lie. It's undignified. It's not so much that the company that you're applying to work at, deserves the truth. It's more that you deserve to be better than that.

My advice

Give them what they want. And at the same time, don't give them what they want. Confusing? Let me explain this.

They say they want last drawn pay information. Sure. Tell them. Give them payslips, even. But if what they really want is an edge at pay negotiations, don't give them that. When I provide my last drawn pay, I also explain that all the salary information in the world doesn't change my price. I want what I want for that role. This tends to put a damper on anyone trying to use my pay information in a cynical way.

Sure, you can say that now they know what your last drawn pay was, they'll want you to justify your asking price. And your point is...? If you can't justify that asking price, you have no business asking for it in the first place. Go get 'em, tiger.

Be prepared to walk if you don't get what you want. This is your career, not some wet market where you haggle over the price of a cod steak. The moment you find yourself haggling, you've already surrendered control.

The price of cod.

In no way am I suggesting that one should be inflexible when it comes to job offer negotiations. But that flexibility should always be on terms you are comfortable with. While they should know how much money you were earning in your last job, it should be made abundantly clear that this changes nothing where your asking pay is concerned. Not one iota. If they're unable to get over the fact that you're asking for remuneration significantly higher than your last job's, and you miss out on this opportunity as a result, why is that a bad thing? You dodged a bullet. You don't want a situation where your current employers are happy with your employment only because they thought they were taking advantage of you.

People have tried testing me with lowball offers before. They found out the hard way that His Teochewness doesn't play games. Is that an arrogant thing to say? Yes. Is it profoundly satisfying not to have to put up with this shit? Also yes. Absolutely.

I'm not saying you should take a leaf from my book and laugh in the faces of those who make insulting lowball offers after learning your last drawn pay. But cultivating a habit of not playing stupid games, can only be a good thing in the long run.

Conclusion

People should stop obsessing over "winning", especially if it's at someone else's expense. Candidates who get what they want, should be happy about it even if it turns out that they could have gotten significantly more. Companies who hire candidates on a price they're happy with, should remain happy even if the last company paid half that.

But under no circumstances should anyone's negotiating advantage be built on lies, or on lack of transparency.

What's your last drawn pay?
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

Wednesday, 19 March 2025

Film Review: Black Mirror Series Five (Part 3/3)

The final episode, titled Rachel, Jack and Ashley Too, stars Miley Cyrus!


Yes, you got that right. Black Mirror is full of surprises, eh? What's perhaps even more surprising is that this episode isn't dark and gloomy like most of the other Black Mirror episodes are.

The Premise

Ashley O is a huge star who gets put in a coma. Her consciousness is placed inside robotic dolls electronically. One of these dolls belongs to a girl named Rachel. An adventure ensues as Rachel and her older sister Jack, follow a trail that leads to Ashley's rescue.

The Characters

Miley Cyrus has the main role of Ashley Ortiz. Cyrus is actually in her element here. The song and dance sequences, while sometimes goofy, are on point. Cyrus isn't afraid to get ugly too, in the sequences that see her wake up from her coma.

Angourie Rice
is Rachel Goggins, the lonely kid who's still distanced from her older sister and harbors dreams of being a star like Ashley O. I didn't really like watching her, not sure why. Maybe it was the cringey dancing the script made her do.

Madison Davenport as Jack Goggins, Rachel's older sister. She's mean, snarky and ill-tempered, with a soft center. Her facial expressions are to die for - those looks of WTF did I just hear she throws at Rachel and Ashley Too? Such delicious disdain. Davenport outdid herself here.

Susan Pourfar as Ashley's aunt Catherine Ortiz. At the beginning, she comes across as cheerful and friendly, if a little stressed out, which makes it even more chilling when her true colors as a scheming bitch emerge.

Marc Menchaca is Rachel and Jack's dad, Kevin. He's a basement geek who tries to be a good dad after the girls' mother passed away two years ago. Well-intentioned, if a little oblivious.

Nicholas Pauling as Dr Munk, the goto guy for drugs. He's ostensibly a doctor, but this guy just feels comes across more like some kind of sleazy thug, especially later on as he's choking Jack.

James III as Jackson Hanabera, technical director, who comes up with the scheme to harvest Ashley's dreams while she's in a coma. Weird, the guy just didn't give off that kind of psycho vibe.

Daniel Stewart Sherman as Bear, security guy. Huge and menacing. I loved watching his quizzical expressions when Jack feeds him bullshit.

Jerah Milligan puts in a brief appearance as BusyG, a TV host.

The Mood

It's upbeat and bright, kind of like a Disney movie. Even as the story progresses, it doesn't ever get -that- dark. This episode has more of an adventure movie vibe going for it, and actually ends on a positive note (bad guy gets nabbed, heroine gets rescued, etc).

What I liked

The commercial Ashley's music and lyrics are corny and shallow and major cringe. It's a good contrast to the work that she actually wants to produce.

When the limiter around Ashley Too's "brain" is removed, the ensuing expletive-filled rant it goes on, is marvellous. Later on, the sarcasm-filled exchanges between Ashley Too and Jack, as well.


The entire subplot revolving around Kevin's mousetrapping technology, and how it ultimately helps in freeing Ashley Too.


And of course, that damn car!


I think the scaled-up holographic concert was pretty neat, as well.


This shot of Ashley Too at the end, turned into a punk rocker, was amusing.

What I didn't

The scenes of Rachel dancing with the encouragement of Ashley Too, are a little draggy and pretty cringey. But I suppose that was the whole point.

Watching the 15-year old character Rachel dancing to lyrics like "Oh honey kiss me up against the wall" and "I can't take it so don't you fake it" gave me the ick.

This is a minor one, but the episode title's a little lazy. It's literally just the names of the two sisters, and the doll. Somewhere along the way, someone stopped giving a fuck about catchy episode titles!

Conclusion

The series ends on a high with this one. Although, it has to be said, expectations were low after the last one. But even judged on its own merits, this episode stands strong. Even though the villains were almost cartoony, I couldn't help but enjoy myself. The plot wasn't all that original - pretty sure I've seen some variation of that story somewhere - but boy, was it an engaging hour or so.

My Rating

8 / 10

Final Thoughts on Black Mirror Series Five

The weak link in Black Mirror Series Five was undoubtedly Smithereens. And while Striking Vipers was more thematically Black Mirror, its glaring flaws hinder it significantly. Still, it did serve as a decent opening act.

Rachel, Jack and Ashley Too, conversely, has less of the feel of Black Mirror, but takes the prize due to an almost flawless execution. Or it could merely seem flawless coming right after the severely flawed Smithereens.

All in all, Black Mirror Series Five is a worthy addition to the series, though far from the best offering.

That's all for now... "go to sleep"!
T___T

Monday, 17 March 2025

Film Review: Black Mirror Series Five (Part 2/3)

This next story, Smithereens, is set in the UK - London, to be exact. Smithereen is the name of a Social Media company owned by Billy Bauer.


As the exclamation mark icon would suggest, Smithereen produces some kind of Twitter-like Social Media product.

The Premise

Rideshare driver Chris Gilhaney takes a man hostage in an effort to speak with the founder of Smithereen, Billy Bauer. That's pretty much it; this is, in broad strokes, what takes place. The rest of it is in-character drama and backstory.

The tragedy itself is a commentary on the addictve nature of Social Media.

The Characters

Andrew Scott as Christopher Gilhaney. At first we think he's an unkempt cab driver with an unusually intense stare. We later find out that he's extremely stressed and has a hair-trigger temper, but ultimately a decent chap at heart. He's also got a fair amount of tragedy in his life which leads him to do this.

Damson Idris plays Smithereen intern Jaden Tommins as a scared kid taken hostage. He does this wonderfully too, and really elicits sympathy here. At the end, the character exhibits loads of empathy with his kidnapper Chris and even tries to stop him from killing himself.

Topher Grace as Smithereen CEO Billy Bauer. Grace appears like I've never seen him before - as a new-agey man-bunned hipster out on a "silent retreat". Pretensions aside, he's entirely sympathetic as a tech geek who begins building his product with benign intentions but ends up creating a monster.

Calum Callaghan as male cop, Damien Cullen. Slightly goofy, but the portrayal of a stodgy cop seemed entirely authentic.

Ambreen Razia as female cop, Najma Haque. Portrayed as the more on-the-ball and alert partner.

Amanda Drew is Hayley Blackwood. She is a grieving mother whose daughter took her own life, and has a one-night stand with Chris. I liked her in this role, but I'm not sure if her inclusion was necessary at all.

Caitlin Innes Edwards as Hannah Kent from Smithereen HR. Nothing much to see here, really. Honestly didn't do much. I get that the plot warranted her inclusion; she just wasn't that interesting to watch.

Maggie Bain as Maryam, who appears to be in charge of Smithereen in London. Another superfluous role.

Ruibo Qian as Penelope Wu, COO. She acts as the counterfoil to Billy Bauer's empathetic nature, mostly cold and calculating and concerned with the less human side of things.

Monica Dolan as Chief Superintendent Linda Grace, who tries to take charge but does a really bad job of it.

Daniel Ings as David Gilkes, negotiator. Another role that felt superfluous.

Quincy Dunn-Baker as Don from legal and Mirirai Sithole as Shonelle from Analytics. Fucking superfluous. FFS, they should have just combined the two roles with no loss to the story. Were they trying to set some kind of record here?

Jorge Cordova as Ernesto Cruz from FBI. Wow, this is Superfluous with a capital S. What did the character contribute here, really? Neat pornstache, though.

Adam McNamara as Harris the sniper. Alice Bailey-Johnson as the other sniper. Supremely incompetent at their jobs, considering how much they missed and how little distance it was.

Crystal Clarke does a decent job playing Tipi. Upbeat and earnest. Clarke made the most out of the limited scope of the role as the staff member sent to alert Billy Bauer.

The Mood

It looks like a typical dreary day in London but soon (after an unneccessary sex scene) the action picks up and becomes some kind of hostage thriller. Doubles as tragedy when one realizes what led Chris to take such extreme measures.

What I liked

I really enjoyed the fact that this episode was all about existing tech. Smithereen was like a real-world Twitter (now X), Persona the equivalent of Facebook or Instagram, and Hitcher like Uber. It made it all the more relatable, especially since the bleakness was a dark (or black, hur hur) mirror to the all-too-real ills of Social Media today.


The transcriber being a prude as it recorded Chris delivering a cluster F-bomb, substituting all instances of "fuck" with "duck". I probably shouldn't find it that amusing, but I did.

What I didn't

The cast just felt unnecessarily huge. There was a number of speaking roles in the episode that felt like they were there just to make up the numbers. The characterization for these roles were just flat.

The plot was bloated beyond belief. A lot of it felt like meaningless waiting. Did it ratchet up the tension? Maybe a little. But not enough to warrant sitting through a whole lot of stuff that could have been cut out.

For example, Hayley's subplot. Near the end, we see that Billy Bauer has called in to Persona to request that they give Hayley her daughter's password. But anyone who works in tech (or even has the faintest idea how this shit works) knows that passwords have not been stored in cleartext since the last decade. Basic web security. There is no way for anyone to go into the database and search for a user and simply retrieve a non-hashed password. The best they could have done was reset the password or send Hayley a link to do so.



Now, normally I could have overlooked this... if it weren't for the fact that this password was a plot point (the password apparently matched the number of the boat in a photo that Hayley and her daughter took together), which resulted in the password thing being a gigantic fucking plot hole.

Cut out this entirely, and we would have eliminated Hayley and her one night stand with Chris -and- the plot hole. Though it would have been a pity because I actually liked Amanda Drew as Hayley.

All in all, this amateurish error ruined the episode for me. You want to make a show about tech, get a writer who actually understands tech. Jesus!

Conclusion

A poor, poor episode, especially since it could have been halfway decent once a lot of fat was trimmed. I liked the premise and the Aesop of the episode, but the huge plot hole and execution flaws made this one just about unsalvageable.

My Rating

4.5 / 10

Next

Rachel, Jack and Ashley Too