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.

How to Retrieve Logs from %temp%OfficeAddins.log.txt and Display Them in an Excale Add-in Side Panel

I have developed an Office JavaScript Excel add-in that provides live product pricing using custom streaming functions. The office add-in javascript-only function is designed to log errors in a file located at %temp%OfficeAddins.log.txt on the user’s machine.

To enhance troubleshooting for customer issues, I want to:

  1. Programmatically access the %temp%OfficeAddins.log.txt file
  2. Read the file contents
  3. Display the logs dynamically in the side panel of the add-in, allowing users to view error details

Questions:

  • Is there a way to programmatically access the %temp% directory and read the OfficeAddins.log.txt file from within the add-in side panel?
  • If direct file access is not possible, what alternative solutions could be used to fetch and display the logs from that file in the side panel?
  • Can the Office.js API facilitate secure access to that file?

Additional Information

  • The add-in is built using HTML, CSS and JavaScript, and it operates within the Office.js framework

Why does the iterator close after a break in a for…of loop?

I have a generator that yields 1000 items. I noticed that when I use a for...of loop to partially consume the generator and then break out of the loop, the iterator seems to close. As a result, my subsequent for...of loop doesn’t resume where the first one left off.

Here’s the code:

function* test() {
  const array = Array.from({ length: 1000 }, (_, index) => index);

  for (let item of array) {
    yield item;
  }
}

const iterator = test();
console.log('Before first loop', iterator);

let j = 0;
for (const i of iterator) {
  console.log('A', i);

  if (j++ === 3) {
    break; // Break after consuming 4 items
  }
}

console.log('Before second loop', iterator);

j = 0;
for (const i of iterator) {
  console.log('B', i);

  if (j++ === 3) {
    break;
  }
}

and here is the log:

Before first loop test {<suspended>}
A 0
A 1
A 2
A 3
Before second loop test {<closed>}

I expected the second loop to pick up where the first one left off, but it doesn’t. From my understanding, for...of works with iterators and should resume the iteration unless explicitly reset.

  • Why does breaking out of the first for...of loop close the iterator?
  • How exactly does for...of interact with the generator’s lifecycle?
  • Is there a way to prevent this closure without switching to next()?

I understand I could manually call next() on the generator, but I’m specifically asking about how for...of works in this context.

Turning a horizontal scroll image gallery for desktop (using vertical scroll gesture) into a vertical scroll gallery (using vertical scroll gesture))

I am trying to figure out how I can make this code turn a gallery that scrolls horizontally on desktop to a vertical scrolling gallery on mobile breakpoint sizes. I currently have it working properly for the horizontal scroll on desktop using the vertical scroll gesture, but I can’t figure out how to have the code switch to have the gallery scroll vertically on mobile breakpoints. I tried an “if else” statement (isMobile) that should check to see if the width of the screen is a certain size and then the gallery should scroll vertically but it does not work. Code setup below.

<script>
  import { gsap } from "gsap";

  let target = 0;
  let current = 0;
  let ease = 0.075;
  const isMobile = window.innerWidth <= 768;
  const sliderWrapper = document.querySelector(".slider-wrapper");
  let maxScroll = sliderWrapper?.offsetWidth - window.innerWidth;

  function lerp(start:number, end:number, factor:number) {
    return start + (end - start) * factor;
  }

  function update() {
    current = lerp(current, target, ease);

    gsap.set(".slider-wrapper", {
      x: -current,
    });

    requestAnimationFrame(update);
  }

  window.addEventListener("resize", () => {
    maxScroll = sliderWrapper?.offsetWidth - window.innerWidth;
  });

  if (!isMobile) {
    window.addEventListener("wheel", (e) => {
      target += e.deltaY;
      e.preventDefault();

      target = Math.max(0, target);
      target = Math.min(maxScroll, target);
    });
  } else {
    window.addEventListener("wheel", (e) => {
      target += e.deltaY;
        gsap.set(".slider-wrapper", {
        Y: -current,
      });
    });
  }

  update();
</script>

onPointerOver and onPointerOut detect all child elements

The problem is that I have onPointerOver and onPointerOut triggered on each child element, but I need it to be on a common group, because then the component is re-rendered too often.

There is a group here, and now when I move the mouse cursor, the component is redrawn many times, I want my state to change only 1 time when I move the mouse cursor and move it away.

This is changing, but the fact is that my head consists of many InstancedMesh, and it turns out that this state changes for each square that my head consists of.

How do I make this work only for the entire group, that is, so that when I hover the cursor, it only reacts to the entire group, and not to individual squares?

    <group
      position={position}
      onPointerOver={(e) => {
        e.stopPropagation();  
        handlePointerOver(e); 
      }}
      onPointerOut={(e) => {
        e.stopPropagation();
        handlePointerOut(e);
      }}
    >
      <Head scale={scale} isVisibleGrid={isVisibleGrid} position={scaledPosition(0, 24, 0)} />

    </group>
  const handlePointerOver = (e: ThreeEvent<PointerEvent>) => {
    setIsVisibleGrid(true);
    e.stopPropagation();
  };

  const handlePointerOut = (e: ThreeEvent<PointerEvent>) => {
    setIsVisibleGrid(false);
    e.stopPropagation();
  };