Why does this SVG filter not work in a canvas context using Chrome?

I have a SVG filter saved in a file hosted on the same website as the canvas. The canvas attempts to use that filter to draw a filtered image, but the original image is rendered instead, ignoring the filter.

The path to the SVG file is correct and the ID of the filter is correct. What am I doing wrong?

Here is the content of the “spherize.svg” file…

<svg>
    <defs>
        <filter id="sphereFilter">
            <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
            <feOffset in="blur" dx="2" dy="2" result="offset"/>
            <feComposite in="SourceGraphic" operator="normal" in2="offset" result="final"/>
        </filter>
    </defs>
</svg>

And here is the call made in the canvas rendering function…

context.filter = "url(/resources/filters/spherize.svg#sphereFilter)";
context.drawImage(image, 0, 0, width, height);

I am using the most recent public version of Chrome (131.0.6778.205) on the most recent public version of macOS (15.1.1). Thanks for any insights.

Mail function in php not sending emails

Im trying to send an alert to an email depending on the value of a variable $alert. However, I can’t send any emails no matter what email address I use, but I do get the echo “Email enviado(sent)”, so I know the if structure is working properly! I don’t know if it is of any relevance, but I’m hosting my php file at IONOS. Any help please!

<?php
    date_default_timezone_set('America/Mexico_City');
    $hoy = getdate();
    $fecha = $hoy['hours'] . ":" . $hoy['minutes'] . ":" . $hoy['seconds'] . " " . $hoy['mday'] . "/" . $hoy['mon'] . "/" . $hoy['year'];

    $servername = "db5001424197.hosting-data.io";

    // REPLACE with your Database name
    $dbname = "dbs1200873";
    // REPLACE with Database user
    $username = "dbu300985";
    // REPLACE with Database user password
    $password = "Batman1998#";

    // Keep this API Key value to be compatible with the ESP32 code provided in the project page. If you change this value, the ESP32 sketch needs to match
    $api_key_value = "tPmAT5Ab3j7F9";

    $api_key = $ID = $porcentaje = "";

    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $api_key = test_input($_POST["api_key"]);
        if($api_key == $api_key_value) {
            $ID = test_input($_POST["ID"]);
            $alarma = test_input($_POST["alarma"]);
            $pin = test_input($_POST["pin"]);

            if ($pin == 0 ){
                $pin = "none";
            }
            // Create connection
            $conn = new mysqli($servername, $username, $password, $dbname);
            // Check connection
            if ($conn->connect_error) {
                die("Connection failed: " . $conn->connect_error);
            } 
            //**********SISTEMA ANTERIOR**********//
            $sql = "UPDATE instituto SET ultCon = '$fecha' WHERE ID = '$ID'";
            $sql2 = "UPDATE instituto SET pin = '$pin' WHERE ID = '$ID'";
              
            $alter = $conn->query($sql);
            $alter2 = $conn->query($sql2);
            
            if ($alarma == 1){
                $email_from ="[email protected]";

                $email_subject = "Alarma encendida";

                $email_body = "Pin Activado: $pin.n".
                    "Botonera Activada: Si.n".
                    "Sirena Activada: Si.n";

                $to = "[email protected]";

                $headers = "From: $email_from rn";

                $headers .= "Reply-To: $email_from rn";

                mail($to,$email_subject,$email_body,$headers);

                echo "Email enviado";
            }


            echo "¡Conexión exitosa!";
        
            $conn->close();
        }
        else {
            echo "Wrong API Key provided.";
        }

    } else {
        echo "No data posted with HTTP POST.";
    }

    function test_input($data) {
        $data = trim($data);
        $data = stripslashes($data);
        $data = htmlspecialchars($data);
        return $data;
    }
    ?>

How to return blob in Joomla 5 API

I am writing a custom plugin API that should return PDF, I am using Joomla 5 but I see that it only supports JSON as a return via the View, not only that, but it also requires you to be working with Full MVC structure, is there a away to not do that? I want the flexibility to return a blob or text or whatever from inside the Controller.

class ProductsController extends ApiController
{
 protected $contentType = '';
 protected $default_view = '';

 public function displayList()
 {
 // I need to be able to return here a blob or whatever i want, right now if i return anything the content type in postmen is always "text/html; charset=UTF-8"
 }
}

And this is the view that I don’t really need, but it’s mandatory by Joomla:

class JsonapiView extends BaseApiView
{
        protected $fieldsToRenderList = [];

        protected $fieldsToRenderItem = [];
}

For reference, these are the docs: https://docs.joomla.org/J4.x:Adding_an_API_to_a_Joomla_Component/en

I did try to just return inside the displayList method but it’s not working.

XML Reader Not Found in cPANEL php v8.3

I am trying to use One Click Demo Import on WordPress which works fine on my local. When I moved to a fresh install on cpanel, I get this error.

PHP Fatal error:  Uncaught Error: Class "XMLReader" not found in....

Below is my phpinfo

enter image description here

enter image description here

Question: Why I am getting error in v8.3 on cpanel

Working Solution

I downgraded to version 8.2 and it worked

Laravel Defer on API requests

Is it possible to use defer functions on API requests? I can run defer on my website, but through API is not working at all.

Sample scenario:

// Endpoint accessible through browser https://mywebsite.com/something
public function index(Request $request): void
{
    Log::info('Before response sent');
    defer(function () {
        Log::info('Deferred task executed');
    });
    Log::info('After response sent');
}

// Endpoint accessible through API request https://mywebsite/api/something
public function search(Request $request): JsonResponse
{
    Log::info('Before response sent.');
    defer(function () {
        Log::info('Deferred task executed.');
    });
    Log::info('After response sent.');
    
    return response()->json(true);
}

This sample only works when acessing the endpoint through browser. With the API endpoint, by using either tests or Postman, the message Deferred task executed. is never written.

I tried to create a middleware, applied to the API endpoints, in order to make sure the app is terminated so the defer functions execute, but got no luck.

class EnforceDeferForApi
{
    public function handle(Request $request, Closure $next)
    {
        return $next($request);
    }

    public function terminate(Request $request, $response): void
    {
        app()->terminate();
    }
} 

Any solution?

The update and destroy parameters cannot be used in CRUD Laravel 11

I have a problem when editing my CRUD. When I click edit on my index.tsx page, I can’t display the data stored in the database for editing and can’t be deleted, how is the solution?

Here’s the route for my crud

Route::resource('galeri', GalleryController::class);

this is my index.tsx

import React from "react";
import { Link, usePage, router } from "@inertiajs/react";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { PageProps } from "@/types";
import { route } from 'ziggy-js';


const createPostRoute = route('galeri.create');

// Define the type for a single post
interface Post {
    id: number;
    nama: string;
    deskripsi: string;
    image: string;
}

// Adjust the type to reflect the correct structure of posts
interface Posts {
    data: Post[];
}

const Index = ({ auth }: PageProps) => {
    const { posts } = usePage<{ posts: Posts; auth: PageProps["auth"] }>().props;
    const data: Post[] = posts.data; 

    console.log(data); 

    // Function to handle delete action
    const handleDelete = (id: number) => {
        if (confirm("Are you sure you want to delete this post?")) {
            router.delete(route("galeri.destroy", id));
        }
    };
    

    return (
        <AuthenticatedLayout
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Dashboard</h2>}
        >
            <div className="container mx-auto max-w-7xl mt-4">
                <h1 className="mb-8 text-4xl font-bold text-center">Posts Index</h1>
                <div className="flex items-center justify-between mb-6">
                    <Link
                        className="px-3 py-1.5 text-white bg-blue-500 rounded-md focus:outline-none"
                        href={route("galeri.create")}
                    >
                        Create Post
                    </Link>
                </div>

                <div className="overflow-x-auto">
                    <table className="min-w-full bg-white">
                        <thead>
                            <tr>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    #
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Nama
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    deskripsi
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    image
                                </th>
                                <th className="px-4 py-2 bg-gray-200 text-gray-600 border-b border-gray-300 text-left text-sm uppercase font-semibold">
                                    Actions
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            {data && data.length > 0 ? (
                                data.map(({ id, nama, deskripsi, image }) => (
                                    <tr key={id}>
                                        <td className="px-4 py-2 border-b border-gray-300">{id}</td>
                                        <td className="px-4 py-2 border-b border-gray-300">{nama}</td>
                                        <td className="px-4 py-2 border-b border-gray-300">{deskripsi}</td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            
                                            <img 
                                                src={`/storage/${image}`} 
                                                alt={nama} 
                                                className="h-20 w-20 object-cover rounded"
                                            />
                                        </td>
                                        <td className="px-4 py-2 border-b border-gray-300">
                                            <Link
                                                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded text-xs mr-1"
                                                href={route("galeri.edit", id)}
                                            >
                                                Edit
                                            </Link>
                                            <button
                                                className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded text-xs"
                                                onClick={() => handleDelete(id)}
                                            >
                                                Delete
                                            </button>
                                        </td>
                                    </tr>
                                ))
                            ) : (
                                <tr>
                                    <td className="px-4 py-2 border-b border-gray-300" colSpan={5}>
                                        No posts found.
                                    </td>
                                </tr>
                            )}
                        </tbody>

                    </table>
                </div>
            </div>
        </AuthenticatedLayout>
    );
};

export default Index;

this is my controller

<?php

namespace AppHttpControllers;

use AppHttpRequestsStoreGalleryRequest;
use AppModelsGallery;
use IlluminateSupportFacadesAuth;
use IlluminateHttpRequest;
use IlluminateSupportFacadesRedirect;
use InertiaInertia;

class GalleryController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
{
    $postgaleris = Gallery::all();

    return Inertia::render('Gallery/index', [
        'auth' => [
            'user' => [
                'name' => Auth::user()->name,
                'email' => Auth::user()->email,
            ],
        ],
        'posts' => ['data' => $postgaleris],
    ]);
}


    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return Inertia::render('Gallery/post');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreGalleryRequest $request)
    {

        $data = $request->validated();


        if ($request->hasFile('image')) {
            $imagePath = $request->file('image')->store('gallery_fotos', 'public');
            $data['image'] = $imagePath;
        } else {
            $data['image'] = null;
        }

        Gallery::create($data);

        return Redirect::route('galeri.index');
    }


    /**
     * Display the specified resource.
     */
    public function show(Gallery $gallery)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Gallery $gallery)
{
    return Inertia::render('Gallery/edit', [
        'post' => [
            'id' => $gallery->id,
            'nama' => $gallery->nama,
            'deskripsi' => $gallery->deskripsi,
            'image' => $gallery->image ? asset('storage/' . $gallery->image) : null,
        ],
    ]);
}


    /**
     * Update the specified resource in storage.
     */
    public function update(StoreGalleryRequest $request, Gallery $gallery)
    {
        $data = $request->validated();

        if ($request->hasFile('image')) {
            if ($gallery->image && Storage::disk('public')->exists($gallery->image)) {
                Storage::disk('public')->delete($gallery->image);
            }
            $data['image'] = $request->file('image')->store('gallery_fotos', 'public');
        }

        $gallery->update($data);

        return Redirect::route('galeri.index');
    }



    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Gallery $gallery)
    {
        $gallery->delete();

        return Redirect::route('galeri.index');
    }

}

I’ve tried to find the problem, but I don’t know where it is because there is no error message for the destroy and update functions

Three Contenteditable Divs that connect as one larger text box with maxChars of 274

I’m building a tool to help me with multi-part tweets when I need to separate the character limitations into separate tweets. I want it built so that if I copy a large body of text or freely type into the first div (Tweet1), it will split it up as needed.

I’ve attempted some script using AI but I believe that is not allowed on this page so I did not share it below. After countless attempts and tweaks, I cannot get this to flow very well.

When I use my current code, the backspace acts all wonky and adds more lines of spaces below instead of deleting it. The first div will only allow one character at a time when I type before moving down a row. If I paste the text into the first div, it will overflow below, but it adds large blank lines. If I try to delete or edit, it adds more lines or deletes the end of that div instead of where the carrot is

Style:

    .Tweet {
    height: 25%;
    padding: 10px;
    font-size: 14px;
    overflow: auto;
    word-wrap: break-word;
    white-space: pre-wrap;
    border: 1px solid black;
    margin: 10px;
    }

Code:

    Tweet 1
    <div id='Tweet1' class='Tweet BlueBorder' contenteditable="true" oninput="countText1()"></div>
    Tweet 2
    <div id='Tweet2' class='Tweet BlueBorder' contenteditable="true" oninput="countText2()"></div>
    Tweet 3
    <div id='Tweet3' class='Tweet BlueBorder' contenteditable="true" oninput="countText3()"></div>

Script:

<script>
const Tweet1 = document.getElementById("Tweet1");
const Tweet2 = document.getElementById("Tweet2");
const Tweet3 = document.getElementById("Tweet3");
const maxChars = 274;
const urlCharCount = 23;

const tweets = [Tweet1, Tweet2, Tweet3];

tweets.forEach((div, index) => {
  div.addEventListener("input", () => handleInput(index));
  div.addEventListener("keydown", (e) => handleBackspace(e, index));
  div.addEventListener("paste", handlePaste);
});

function handleInput(index) {
  redistributeText();
}

function handleBackspace(event, index) {
  const currentDiv = tweets[index];
  if (event.key === "Backspace" && currentDiv.innerText.trim() === "" && index > 0) {
    event.preventDefault();
    const previousDiv = tweets[index - 1];
    previousDiv.focus();
    moveCaretToEnd(previousDiv);
    redistributeText();
  }
}

function handlePaste(event) {
  event.preventDefault();
  const text = (event.clipboardData || window.clipboardData).getData("text/plain");
  const targetDiv = event.target;

  // Insert pasted text and redistribute
  const selection = window.getSelection();
  if (selection.rangeCount) {
    const range = selection.getRangeAt(0);
    range.deleteContents();
    range.insertNode(document.createTextNode(text));
    redistributeText();
  }
}

function redistributeText() {
  const allText = tweets.map(div => div.innerText).join("n");
  const words = splitTextIntoWordsAndNewLines(allText);
  let remainingWords = [...words];

  tweets.forEach((div, index) => {
    if (index < tweets.length - 1) {
      const [visibleWords, remaining] = fitWordsWithUrlHandling(remainingWords, maxChars);
      div.innerText = visibleWords.join("");
      remainingWords = remaining;
    } else {
      div.innerText = remainingWords.join("");
    }
  });

  // Restore caret position if redistribution affected typing
  restoreCaret();
}

function splitTextIntoWordsAndNewLines(text) {
  const wordsAndLines = text.match(/([^sn]+|s+|n)/g) || [];
  return wordsAndLines;
}

function fitWordsWithUrlHandling(words, limit) {
  let visibleWords = [];
  let charCount = 0;

  for (const word of words) {
    const isUrl = isValidUrl(word.trim());
    const wordLength = word.trim() === "n" ? 1 : isUrl ? urlCharCount : word.length;

    if (charCount + wordLength <= limit) {
      visibleWords.push(word);
      charCount += wordLength;
    } else {
      break;
    }
  }

  const remainingWords = words.slice(visibleWords.length);
  return [visibleWords, remainingWords];
}

function isValidUrl(word) {
  const urlRegex = /^(https?://)?([a-zA-Z0-9.-]+.[a-zA-Z]{2,})(/[^s]*)?$/;
  return urlRegex.test(word);
}

function moveCaretToEnd(element) {
  const range = document.createRange();
  const selection = window.getSelection();
  range.selectNodeContents(element);
  range.collapse(false);
  selection.removeAllRanges();
  selection.addRange(range);
}

function restoreCaret() {
  const selection = window.getSelection();
  if (!selection.rangeCount) return;

  const focusNode = selection.focusNode;
  const focusOffset = selection.focusOffset;

  tweets.forEach(div => {
    const range = document.createRange();
    range.selectNodeContents(div);
    range.setStart(focusNode, focusOffset);
    range.collapse(false);
    selection.removeAllRanges();
    selection.addRange(range);
  });
}

// Initialize divs
tweets.forEach(div => {
  div.innerText = "";
});
</script>


Screenshot of layout

I can either paste a large paragraph into or free-type text into and split the text into three separate Contenteditable Divs so that Tweet1 and Tweet2 will not allow any more than 274 characters before spilling down to the next div below. I want it so that it won’t cut off words either so it uses a break-word to keep it moving down. I want it so that the three divs flow seamlessly between them so if I delete or add more text to any of the three sections it pushes or pulls text in or out of another div as needed.

Unable to locate tests within a folder

i’m trying to run all my Cypress tests by importing them in a all.cy.js file but i’m not getting the desired results.

import "./folder1/test1.cy.js";
import "./folder2/test2.cy.js";
import "./folder3/test3.cy.js";
import "./test4.cy.js";

but Cypress is only reading/testing test4.cy.js
Folder structure: e2e > folder1, folder2, folder3, all.cy.js, test4.js

How to play HLS live-stream from the end with Bitmovin player

I want to play live streams from “live-edge” instead of playing from the start (first segment of manifest) with Bitmovin player.

With some HLS streams, the playback starts from the beginning of the live event. The user must manually click the “Live” button to jump to the current part of the stream. While this behavior is acceptable for VODs, it is not appropriate for live streams.

My config is as follow:

{key: '-', playback: {live: { edgeThreshold: 5 }, autoplay: true, muted: true}}

Also, with desktop browser element inject document.getElementById("#bmpui-id-185").click(); does work, but that’s nasty and doesn’t work on mobile.

Why Does Putting a Custom HTML Element Inside Another Leaves the Second Hidden?

TL;DR: When one custom element is inside of another, the second fails to render. Why is this and how is it fixed?

Explanation

I created three custom HTML elements and two templates using JS (seen below).

const baseTemplate = document.createElement("template");
baseTemplate.innerHTML = `
<slot name="left-pane">Warning: left-pane not included</slot>
<slot name="right-pane">Warning: right-pane not included</slot>
`;

const paneTemplate = document.createElement("template");
paneTemplate.innerHTML = `<slot name="content">Warning: no content included</slot>`;

class PageBase extends HTMLElement {
    constructor() {
        super();

        const shadow = this.attachShadow({ mode: "closed" });

        let clone = baseTemplate.content.cloneNode(true);
        shadow.append(clone);
    }
}

class LeftPane extends HTMLElement {
    constructor() {
        super();

        const shadow = this.attachShadow({ mode: "closed" });

        let clone = paneTemplate.cloneNode(true);
        shadow.append(clone);
    }
}

class RightPane extends HTMLElement {
    constructor() {
        super();

        const shadow = this.attachShadow({ mode: "closed" });

        let clone = paneTemplate.cloneNode(true);
        shadow.append(clone);
    }
}

customElements.define("page-base", PageBase);
customElements.define("left-pane", LeftPane);
customElements.define("right-pane", RightPane);

In the HTML document (seen below), when right-pane or left-pane is put inside another custom element (in this case page-base), it is not rendered in the browser.

<!DOCTYPE html>
<html lang="en">

<head>
    [...]
    <script src="js/layout.js" type="module"></script>   <!---This is the JS file seen above--->
</head>

<body>
    <page-base>
        <div slot="left-pane">
            <p>Thing 1</p>
        </div>
        <div slot="right-pane">
            <right-pane>
                <p slot="content">Thing 3</p>
            </right-pane>
            <p>Thing 2</p>
        </div>
    </page-base>
</body>

</html>

Question: Thing 1 and Thing 2 are rendered, but not Thing 3. Why is this and how do I fix it?

I’ve tried using each custom element on their own, and they work fine putting header or paragraph tags inside, but not for the custom elements.

filter out object in array if object key value is null

I need to filter out objects in array if specific key value is null.

const getPlayerData = async () => {
  const allPlayers = await fetchData("https://api.sleeper.app/v1/players/nfl");
  const players = Object.keys(allPlayers).map(function(key) {
    return allPlayers[key]
  })

  const activePlayers = await Promise.all(players?.filter(async(player: any) => {player.search_rank !== null}
).sort((a, b) => a.search_rank - b.search_rank));
  
  console.log(activePlayers)
  return activePlayers;
}

it filters if {player.active = true} but i need to filter if player.search_rank is null aswell

How to properly handle AES encryption in React Native and generate Random Key for AES encryption?

$aesKey = random_bytes(32); // 256-bit key
$iv = random_bytes(openssl_cipher_iv_length('aes-256-cbc'));

The above code is from PHP, and I need to do the same in React Native. I tried many packages but didn’t get the expected result.

Also, is there any way to get the same logic for the below PHP code in React Native

$encryptedData = openssl_encrypt($data, 'aes-256-cbc', $aesKey, OPENSSL_RAW_DATA, $iv);

React Leaflet custom marker with NextJS window undefined

When I try to use custom marker icons with leaflet, building fails. Everything works in development, but when I run next build, I get: ReferenceError: window is not defined.

Here’s my code for the Leaflet map:

"use client";

import React from "react";
import { EnrichedPrice } from "@/types/enriched_price";
import dynamic from "next/dynamic";
import Load from "@/components/Load";
import { Icon } from "leaflet";
import { useRouter } from "next/navigation";

// Dynamically import MapContainer to avoid SSR issues
const MapContainer = dynamic(
  () => import("react-leaflet").then((mod) => mod.MapContainer),
  { ssr: false }
);
const TileLayer = dynamic(
  () => import("react-leaflet").then((mod) => mod.TileLayer),
  { ssr: false }
);
const Marker = dynamic(
  () => import("react-leaflet").then((mod) => mod.Marker),
  { ssr: false }
);
const Popup = dynamic(() => import("react-leaflet").then((mod) => mod.Popup), {
  ssr: false,
});

const customIcon = new Icon({
  iconUrl: "/marker.png",
  iconSize: [28, 45],
  iconAnchor: [12, 41],
  popupAnchor: [1, -34],
  shadowSize: [41, 41],
});

export default function ResortMap({
  prices,
  loading,
}: {
  prices: EnrichedPrice[];
  loading: boolean;
}) {
  const router = useRouter();

  if (loading) return <Load />;

  return (
    <div className="h-[80vh] w-full p-4">
      <MapContainer
        center={[39.8283, -98.5795]} // Center of US
        zoom={4}
        className="w-full h-full rounded-lg"
        scrollWheelZoom={true}
      >
        <TileLayer
          attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        {prices.map((price) => (
          <Marker
            key={price.id}
            position={[price.location.lat, price.location.lng]}
            icon={customIcon}
            eventHandlers={{
              click: () => router.push(price.links),
              mouseover: (e) => e.target.openPopup(),
              mouseout: (e) => e.target.closePopup(),
            }}
          >
            <Popup className="font-roboto">
              <div className="flex items-center justify-between gap-2">
                <div className="font-bold">{price.resort_name}</div>
                <div className="text-sky-500 font-extrabold">
                  {price.price === -1 ? (
                    <span className="text-red-500">Unavailable</span>
                  ) : (
                    `$${price.price}`
                  )}
                </div>
              </div>
            </Popup>
          </Marker>
        ))}
      </MapContainer>
    </div>
  );
}

I’ve tried a few different things, but most were for the default marker and none of them worked. I know it’s the icon, because when I comment out the icon assignment, building goes smoothly. Does anyone have any ideas? Thanks!

How to Upload an Image to Supabase Storage and Store the Public URL in a Form Using Zod and React Hook Form in Next.js?

I am working on a Next.js application where users can add books using a form. Each book should have an uploaded cover image that gets stored in Supabase Storage, and its public URL should be saved in my book database table under the column bookImageUrl.

What I Have So Far:

  • A React Hook Form (react-hook-form) handling the book details.

  • Supabase Storage setup to store book images

  • A separate component (UploadBookImage.tsx) to handle image uploads.

  • I need the uploaded image URL to be stored in the form state and submitted when saving
    the book.

Expected Behavior:

  • The user selects an image file.

  • The image is uploaded to Supabase Storage.

  • The public URL of the uploaded image is retrieved and set in the form
    state

  • The form is submitted, and the bookImageUrl is saved in the book
    database.

Current Implementation
UploadBookImages.tsx Handle Images Upload

import { createClient } from "../../../../../utils/supabase/client";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useState } from "react";
export default function UploadBookImage({
  onUpload,
}: {
  size: number;
  url: string | null;
  onUpload: (url: string) => void;
}) {
  const supabase = createClient();
  const [uploading, setUploading] = useState(false);

  const uploadAvatar: React.ChangeEventHandler<HTMLInputElement> = async (
    event
  ) => {
    try {
      setUploading(true);

      if (!event.target.files || event.target.files.length === 0) {
        throw new Error("You must select an image to upload.");
      }

      const file = event.target.files[0];
      const fileExt = file.name.split(".").pop();
      const filePath = `books/${Date.now()}.${fileExt}`;

      const { error: uploadError } = await supabase.storage
        .from("avatars")
        .upload(filePath, file);

      if (uploadError) {
        throw uploadError;
      }

      onUpload(filePath);
    } catch (error) {
      alert(`Error uploading avatar! ${error}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <div className="grid w-full max-w-sm items-center gap-1.5">
        <Label htmlFor="picture">
          {uploading ? "Uploading ..." : "Upload"}
        </Label>
        <Input
          id="picture"
          type="file"
          accept="image/**"
          onChange={uploadAvatar}
          disabled={uploading}
          name="bookImageUrl"
        />
      </div>
    </div>
  );
}

Form

 const BookForm: React.FC<BookFormProps> = ({ authors }) => {
      const [state, action, pending] = useActionState(addBook, undefined);
      const [bookImageUrl, setBookImageUrl] = useState<string | null>(null);
    
  // React Hook Form with default values
  const form = useForm<BookInferSchema>({
    resolver: zodResolver(BookSchema),
    defaultValues: {
      //rest of the values
      bookImageUrl: "",
    },
  });

  //submitting the forms
  async function onSubmit(data: BookInferSchema) {
    try {
      const formData = new FormData();
      if (bookImageUrl) {
        data.bookImageUrl = bookImageUrl; // Attach uploaded image URL
      }

      Object.entries(data).forEach(([key, value]) => {
        formData.append(
          key,
          value instanceof Date ? value.toISOString() : value.toString()
        );
      });

      //sending the formData to the action.ts for submitting the forms
      const response = (await action(formData)) as {
        error?: string;
        message?: string;
      } | void;

      //Error or success messages for any submissions and any errors/success from the server
      if (response?.error) {
        toast({
          title: "Error",
          description: `An error occurred: ${response.error}`,
        });
      } else {
        form.reset();
      }
    } catch {
      toast({
        title: "Error",
        description: "An unexpected error occured.",
      });
    }
  }

  //Error or success messages for any submissions and any errors/success from the server


  return (
        <Form {...form}>
          <form
            className="space-y-8"
            onSubmit={(e) => {
              e.preventDefault();
              startTransition(() => {
                form.handleSubmit(onSubmit)(e);
              });
            }}
          >
            <UploadBookImage
              size={150}
              url={bookImageUrl}
              onUpload={(url) => setBookImageUrl(url)}
            />

           //rest of the input fields
  );
};

export default BookForm;

action.ts For saving the data in the database

"use server"


export async function addBook(state: BookFormState, formData: FormData) {
  // Validate form fields
  // Log all form data to debug
  for (const pair of formData.entries()) {
    console.log(`${pair[0]}: ${pair[1]}`);
  }

  const validatedFields = BookSchema.safeParse({
    //rest of the values
    bookImageUrl: formData.get("bookImageUrl"),
  });

   // Check if validation failed
   if (!validatedFields.success) {
    console.error("Validation Errors:", validatedFields.error.format()); // Log errors
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

 // Prepare for insertion into the new database
 const {..rest of the values, bookImageUrl} = validatedFields.data

  // Insert the new author into the database
  const supabase = createClient();
  const {data, error} = await (await supabase).from('books').insert({ ...rest of the values, bookImageUrl});

  if(data){
    console.log(data,"data in the addBook function")
  }

  
  if (error) {
    return {
      error: true,
      message: error.message,
    };
  }

  return {
    error: false,
    message: 'Book updated successfully',
  };

}

Data definition from Supabase and RLS policy

create table
  public.books (
   //rest of the columns
    "bookImageUrl" text null,
    constraint books_pkey primary key (isbn),
    constraint books_author_id_fkey foreign key (author_id) references authors (id) on delete cascade
  ) tablespace pg_default;

RLS policy for now:

alter policy "Enable insert for authenticated users only"

on "public"."books"

to authenticated
with check (

  true

);

Storage bucket:
enter image description here

My schema

import { z } from "zod";

export const BookSchema = z.object({
  //rest of the values
  bookImageUrl :z.string().optional()
});

// TypeScript Type for Book
export type BookInferSchema = z.infer<typeof BookSchema>;

//Form state for adding and editing books
export type BookFormState =
  | {
      errors?: {
        //rest of the values
        bookImageUrl?: string[];
      };
      message?: string;
    }
  | undefined;

Issues I’m facing:

  • Unable to upload in the storage bucket book-pics. Hence, I am unable to save the bookImageURL when I submit the form.