How can I make sure web crawlers see the navigation menu generated with JavaScript?

I run a website that publishes recipes and I’m having concerns about search engine bots not understanding the structure of the website. This has come to light after realizing that google is indexing very few of the pages, unless I manually submit each URL, which is far from ideal.

The website is created with HTML vanilla JavaScript and CSS.

The raw HTML holds the most of the structure, tags, text, images etc.

CSS implements the styling of course.

JavaScript provides some functionality to the site but more importantly, does some work saving tasks such as creating the navigation menu, the footer and the content for the recipe index page which displays links / images to each individual recipe page.

So I have one file app.js that creates the footer, the website navigation menu and adds simple functionality to each webpage (allows a user to toggle between “ingredients” & “cooking method”.

A separate file recipecards.js holds a program that creates the content of the recipe index page.

I ran an audit scan using ahrefs site audit which came up with one deeply disturbing result for almost all pages:

Page has no outgoing links

Issue details:

If a page has no outgoing links, it is a “dead end” for both website visitors and search engine crawlers.

How to fix:

Check your website navigation and link architecture to make sure your website has no “dead ends.”

I’m assuming that this must be because the crawler hasn’t / isn’t seeing the navigation menu, or the **footer ** for that matter, as each obviously have links that take the user elsewhere. It also makes me wonder if the web crawlers are actually seeing the “cooking methods” which are hidden until a user physically clicks an element to show those methods.

The most important thing here is the navigation menu, which must be present, otherwise the page would be a dead-end for the user (as reported by ‘ahrefs’ above) with no way to get to any other part of the website.

So, back to my question, where should I put the navigation menu script? Currently it is placed before the closing </body> tag of each page. It looks for one DOM element <div id="nav-container" class="nav-container"> which is the very first element in the <body>.

I’m now reading in several sources for example:

Where should I put tags in HTML markup?

and MDN

that perhaps this navigation script needs to be loaded in the head of the document with an attribute of defer and/or async. However, I’m thinking asynccould potentially cause an error on a slow connection if the <div id="nav-container" class="nav-container"> has not been parsed, and I can’t see how putting the script in the head with an attribute of defer would help, as the script still wont run until whole document has been parsed, unless I’m missing something here?

It’s a fairly simple site if anyone wants to take a peek:

cooking website

But essentially how do I get round this problem of crawlers not seeing the nav menu and other content generated by JS.

The site is hosted on github, so there is no opportunity for server-side scripts generating the menu or other parts of the pages.

I appreciate any thoughts on this issue.

Thanks in advance.

Buttons not working in data table | Tailwick | Flask | Jquery

I am using the datatable from the tailwick library (https://themesdesign.in/tailwick/html/tables-datatable.html). But the problem is that the buttons on the 2nd page aren’t working at all. I have gone thorugh tens of forum posts but none of them solves my problem (Probably coz i didnt developed the webpage and the web page isn’t defining a datatable explicitly). So, can anyone please help me?

Here’s the code for table:

<table class="w-full whitespace-nowrap" id="basic_tables">
                    <thead class="ltr:text-left rtl:text-right bg-slate-100 text-slate-500 dark:text-zink-200 dark:bg-zink-600">
                        <tr>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">NO</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">CAMPAIGN ID</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">CAMPAIGN_NAME</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">CLIENT_ID</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">COMPANY</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">CAMPAIGN_LP</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">TARGET_AUDIENCE</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">TESTED_INTERNALLY</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">SALES_PROCESS</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">CPL_RANGE</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">EXTRA_COMMENTS</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">STATUS</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">PAID</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">FORM_FIELDS</th>
                            <th class="px-3.5 py-2.5 first:pl-5 last:pr-5 font-semibold border-y border-slate-200 dark:border-zink-500">Actions</th>
                        </tr>
                    </thead>
                    <tbody id="dataTable">
                        {% if cam != [] %}
                            {% for item in cam %}
                                <tr>
                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">{{ "%02d"|format(loop.index) }}</td>
                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">#CA{{ item['id'] + 1000 }}</td>
                                    {% for key, value in item.items() %}
                                            {% if key == 'client_id' %}
                                                <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">#CL{{ value + 1000 }}</td>
                                            {% endif %}
                                            {% if key != 'id' and key != 'status' and key != 'tested_internally' and key != 'paid' and key != 'cpl_range' and key!= 'client_id' %}
                                                {% if value is none %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500"></td>
                                                {% else %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">{{ value }}</td>
                                                {% endif %}
                                            {% endif %}
                                            {% if key == 'cpl_range' %}
                                                <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">€{{ value }}</td>
                                            {% endif %}
                                            {% if key == 'tested_internally' or key == 'paid' %}
                                                {% if value == 'true' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500" id="yes" style="color : transparent; font-size: 0px;"><svg style="margin-left: 30%" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4ade80" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-lucide="check" class="lucide lucide-check size-4"><path d="M20 6 9 17l-5-5"></path></svg>yes</td>
                                                {% endif %}
                                                {% if value == 'false' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500" id="no" style="color : transparent; font-size: 0px;"><svg style="margin-left: 30%" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-lucide="x" class="lucide lucide-x size-4 text-red-500"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>no</td>
                                                {% endif %}
                                            {% endif %}
                                            {% if key == 'status' %}
                                                {% if value == '' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500"></td>
                                                {% endif %}
                                                {% if value == 'Preparing Test' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                                        <span class="delivery_status px-2.5 py-0.5 text-xs inline-block font-medium rounded border bg-green-100 border-green-200 text-green-500 dark:bg-green-500/20 dark:border-green-500/20">Preparing Test</span>
                                                    </td>
                                                {% endif %}
                                                {% if value == 'Test Live' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                                        <span class="delivery_status px-2.5 py-0.5 text-xs inline-block font-medium rounded border bg-yellow-100 border-yellow-200 text-yellow-500 dark:bg-yellow-500/20 dark:border-yellow-500/20">Test Live</span>
                                                    </td>
                                                {% endif %}
                                                {% if value == 'Analizing' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                                        <span class="delivery_status px-2.5 py-0.5 text-xs inline-block font-medium rounded border bg-purple-100 border-purple-200 text-purple-500 dark:bg-purple-500/20 dark:border-purple-500/20">Analizing</span>
                                                    </td>
                                                {% endif %}
                                                {% if value == 'Paused' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                                        <span class="px-2.5 py-0.5 inline-block text-xs font-medium rounded border bg-red-100 border-transparent text-red-500 dark:bg-red-500/20 dark:border-transparent">Paused</span>
                                                    </td>
                                                {% endif %}
                                                {% if value == 'Proposal' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                                        <span class="px-2.5 py-0.5 inline-block text-xs font-medium rounded border bg-orange-100 border-transparent text-orange-500 dark:bg-orange-500/20 dark:border-transparent">Proposal</span>
                                                    </td>
                                                {% endif %}
                                                {% if value == 'Campaign Live' %}
                                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                                        <span class="px-2.5 py-0.5 inline-block text-xs font-medium rounded border bg-cyan-100 border-transparent text-cyan-500 dark:bg-cyan-500/20 dark:border-transparent">Campaign Live</span>
                                                    </td>
                                                {% endif %}
                                            {% endif %}
                                    {% endfor %}
                                    <td class="px-3.5 py-2.5 first:pl-5 last:pr-5 border-y border-slate-200 dark:border-zink-500">
                                        <div class="relative dropdown">

<-- This is not working -->

                                            <button id="estimateAction1" class="flex items-center justify-center size-[30px] dropdown-toggle p-0 text-slate-500 btn bg-slate-100 hover:text-white hover:bg-slate-600 focus:text-white focus:bg-slate-600 focus:ring focus:ring-slate-100 active:text-white active:bg-slate-600 active:ring active:ring-slate-100 dark:bg-slate-500/20 dark:text-slate-400 dark:hover:bg-slate-500 dark:hover:text-white dark:focus:bg-slate-500 dark:focus:text-white dark:active:bg-slate-500 dark:active:text-white dark:ring-slate-400/20"
                                                onclick="">
                                                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-lucide="more-horizontal" class="lucide lucide-more-horizontal size-3">
                                                    <circle cx="12" cy="12" r="1"></circle>
                                                    <circle cx="19" cy="12" r="1"></circle>
                                                    <circle cx="5" cy="12" r="1"></circle>
                                                </svg>
                                            </button>
                                            <ul id="dropdownMenu" class="absolute z-50 py-2 mt-1 ltr:text-left rtl:text-right list-none bg-white rounded-md shadow-md dropdown-menu min-w-[10rem] dark:bg-zink-600 hidden" aria-labelledby="estimateAction1" data-popper-placement="bottom-end" style="position: absolute; inset: 0px 0px auto auto; margin: 0px; transform: translate(-17px, 26px);">
                                                <li>
                                                    <a data-modal-target="editCampaign" class="block px-4 py-1.5 text-base transition-all duration-200 ease-linear text-slate-600 dropdown-item hover:bg-slate-100 hover:text-slate-500 focus:bg-slate-100 focus:text-slate-500 dark:text-zink-100 dark:hover:bg-zink-500 dark:hover:text-zink-200 dark:focus:bg-zink-500 dark:focus:text-zink-200 edit-button" data-campaign-id="{{ item.id }}">
                                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-lucide="file-edit" class="lucide lucide-file-edit inline-block size-3 ltr:mr-1 rtl:ml-1">
                                                            <path d="M4 13.5V4a2 2 0 0 1 2-2h8.5L20 7.5V20a2 2 0 0 1-2 2h-5.5"></path>
                                                            <polyline points="14 2 14 8 20 8"></polyline>
                                                            <path d="M10.42 12.61a2.1 2.1 0 1 1 2.97 2.97L7.95 21 4 22l.99-3.95 5.43-5.44Z"></path>
                                                        </svg> 
                                                        <span class="align-middle">Edit</span>
                                                    </a>
                                                </li>
                                                <li>
                                                    <a data-modal-target="deleteModal" class="block px-4 py-1.5 text-base transition-all duration-200 ease-linear text-slate-600 dropdown-item hover:bg-slate-100 hover:text-slate-500 focus:bg-slate-100 focus:text-slate-500 dark:text-zink-100 dark:hover:bg-zink-500 dark:hover:text-zink-200 dark:focus:bg-zink-500 dark:focus:text-zink-200" href="javascript:showDeleteConfirmation({{ item.id }})">
                                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-lucide="trash-2" class="lucide lucide-trash-2 inline-block size-3 ltr:mr-1 rtl:ml-1">
                                                            <path d="M3 6h18"></path>
                                                            <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
                                                            <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
                                                            <line x1="10" x2="10" y1="11" y2="17"></line>
                                                            <line x1="14" x2="14" y1="11" y2="17"></line>
                                                        </svg> 
                                                        <span class="align-middle">Delete</span>
                                                    </a>
                                                </li>
                                            </ul>
                                        </div>
                        
                                    </td>
                                </tr>
                            {% endfor %}
                        {% endif %}
                    </tbody>
                </table>
.... Some lines later ........
<script src="{{url_for('static' ,filename='js/datatables/jquery-3.7.0.js')}}"></script>
<script src="{{url_for('static' ,filename='js/datatables/data-tables.min.js')}}"></script>
<script src="{{url_for('static' ,filename='js/datatables/data-tables.tailwindcss.min.js')}}"></script>
<script src="{{url_for('static' ,filename='js/datatables/datatables.init.js')}}"></script>

Can anyone please tell me, how can I fix this error?

I went through different forum posts but none of them worked for me (as most of the guys werent using a library that manages these triggers).
I also tried writing my adding my own function and even listner for opening the dropdown and editing but in vain!

JS how to target a div inside of the clicked div

Kind of a special case of this: Change CSS of the clicked element.

If I have some divs like this:



    <div class="parent">
        <div class="child"></div>
    </div>

     <div class="parent">
        <div class="child"></div>
    </div>


    

I know I can assign a function to each parent div like:

const parent = document.querySelectorAll('.parent');
if(parent.length){
   for (let l of parent){
      l.addEventListener('click', (e)=>{
        l.classList.toggle('visible')
      })
   }
}

But how can I toggle the class .visible for the child div when the parent is clicked? Or alternatively add visibility:visible to the child div style.

Many thanks!

‘Access-Control-Allow-Origin’ -Error with Twitch API

I tried to fetch some data with the twitch.tv API on a little JavaScript project.

Here is the fetch request:

fetch("https://api.twitch.tv/helix/chat/emotes/global", {
                headers: {
                    Authorization: "Bearer XXX",
                    ClientId: "MY_ID"
}}) 

I tried to fetch the data on an vercel site and got this error

Access to fetch at ‘https://api.twitch.tv/helix/chat/emotes/global’ from origin “MY-WEBSITE” has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

None of the solutions I tried worked and I thought its a vercel only error so it uploaded the code on my own website, but still the same error

I have no idea what is wrong

I found this guide but it wasn’t helpful

Also found this site which also didn’t work

How to Get Authentication Code from URL and Store Access Tokens in a React/NextJS App with 3rd party Integration?

I am building an online editor with React and NextJS. I want to add some extra functionality to make a connection with a third party (Strava). By adding this feature, the users should be able to see his activities.

The connection with the 3rd party works as follow:

  1. User clicks a button to open a link of Strava. Its a prompt to ask the user permission.
  2. After the user gave permission, Strava redirects back to the editor with a Search parameter like this: www.example.com?code=abcdefg
  3. The code should get this authentication code, call another api of strava with it to get an access and refresh token
  4. The application should store the access and refresh token in a way that it can be read (and set) in other components.

Since I am still a beginner in React/NextJs, I have troubles with step 3 and 4.

My question: What should be the best way to get this authentication code and save the access/refresh token?
I have seen some possibilities with Middleware or useContext, but I do not know what a are the (dis)advantages of these methods. Maybe there is a another/better way to do it?

Could you give some advice/pointers?

Optimize Redux Data Updates

I am building a chat app with React/Redux. I want to store the messages state using Redux, it currently works with this code:

// chatsReducer.js

import {
  GET_CHATS_PENDING, GET_CHATS_SUCCESS,
  SET_SEND_MESSAGE_SUCCESS
} from '../constants/chatsConstants';

const initialState = {
  chats: null,
  loading_chats: false,
  sending_msg: false
};

export const chatsReducer = (state = initialState, action) => {
  switch (action.type) {
    case GET_CHATS_PENDING:
      return {
        ...state,
        loading_chats: true
      };
    case GET_CHATS_SUCCESS:
      return {
        ...state,
        chats: action.payload,
        loading_chats: false
      }
    case SET_SEND_MESSAGE_SUCCESS:
      const updatedChats = [...state.chats];

      updatedChats[action.payload.chat_id] = {
        ...updatedChats[action.payload.chat_id],
        messages: [...updatedChats[action.payload.chat_id].messages, { msg: action.payload.msg }]
      };

      return {
        ...state,
        chats: updatedChats,
        sending_msg: false
      };

    default:
      return state;
  }
};

This is what the data currently looks like in the store:
enter image description here

I worried about how inefficient SET_SEND_MESSAGE_SUCCESS is, with all the copying etc.
What is a better way to do this.

I was looking dynamically creating reducers for each chat

Unprotected Route in nextjs

I am trying to make my /login page unprotected so I created a file and tried to wrap it in this but failed due to this error

lib_withUnprotectedRoute__WEBPACK_IMPORTED_MODULE_1__.default) is not a function

lib_withUnprotectedRoute__WEBPACK_IMPORTED_MODULE_1__.default) is not a function

here is my code for withUnprotectedRoute.tsx

    "use client"

// lib/withUnprotectedRoute.ts
import { useRouter } from "next/navigation";
import React, { useEffect } from "react";

const withUnprotectedRoute = (WrappedComponent: React.ComponentType<any>) => {
  console.log("withUnprotectedRoute called");
  const Wrapper: React.FC<any> = (props) => {
    const router = useRouter();
    const [loading, setLoading] = React.useState(true);

    useEffect(() => {
      console.log("useEffect called");
      const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;

      if (token) {
        router.push("/dashboard");
      } else {
        setLoading(false);
      }
    }, [router]);

    if (loading) {
      return <div>Loading...</div>;
    }

    return <WrappedComponent {...props} />;
  };

  return Wrapper;
};

export default withUnprotectedRoute;

but this error occurs before calling this console
console.log(“useEffect called”);

and here is my login here where i want to wraped:

import Image from "next/image";
import Link from "next/link";

import { LoginForm } from "@/components/forms/LoginForm";
import withUnprotectedRoute from "@/lib/withUnprotectedRoute";

function Login() {
  return (
<>
<div>
  login here
</div>
</>
  );
};

export default withUnprotectedRoute(Login);

Trying to match what a user inputs into a command, to a google sheet

So i am trying to get this where when a user inputs a command “steamid” it will go through the google sheet “data” and search for the “steamid”. If the steamid is found in the google sheet it will display blacklisted.

 ( async () => {
    const data = await readSheet();
    if (interaction.commandName === 'checkbl') {
        const steamid = interaction.options.get('steamid').value;

        if (steamid == data) {
        interaction.reply(`${steamid} is blacklisted`);
        } else {
        interaction.reply(`${steamid} is not blacklisted`);
        console.log(steamid)
        }
    }
})()

I have used console.log(steamid) and console.log(data) to confirm i am getting both the users input and google sheets data, which i am. I am just confused as to why its not displaying if the steamid is or isn’t blacklisted.

Flow of Execution in Reactjs

UseMemo.jsx

import React, { useEffect, useState } from 'react';
import HeavyComponent from './HeavyComponent'

const UseMemo = () => {
  const[count, set_count] = useState(0);
  console.log("A")

  useEffect(() => {
    console.log("B")
  }, [count])

  return (
    <div>
      <h1>You clicked below btn {count} times</h1>
      <button
        onClick={() => {
          set_count(count + 1);
        }}
      >
        Refresh
      </button>
      <br />
      <HeavyComponent />
    </div>
  )
}

export default UseMemo;

HeavyComponent.jsx

import React, { useEffect, useState } from "react";

const HeavyComponent = () => {
  const [magic, set_magic] = useState([]);
  console.log("C")

  useEffect(() => {
    console.log("HeavyComponent useEffect");
    var numbers = new Array(30000000)
      .fill(976)
      .map((item, ind) => item * 23443423)
      .find((item, ind) => ind === 20000000 - 2);

    set_magic(numbers);
  }, []);
 
  return <div>{magic}</div>;
};

export default HeavyComponent;

when my page loads first time then my console is like:-

A
HeavyComponent.jsx:6 C
HeavyComponent.jsx:9 HeavyComponent useEffect
UseMemo.jsx:11 B
HeavyComponent.jsx:6 C

I got that

  1. First A will be printed when my parent component renders,
  2. Then HeavyComponent’s console will be executed and C will be printed
  3. then useffect of HeavyComponent
  4. then control comes back to parent and its useffect will be peinted
  5. but now everything should finish but why again “C” got printed?

Create Tailwind Plugin Class That Generates Various Hover And Focus States

Currently I’m using the @apply like so to create a simple error class I can add to input elements to highlight them.

.error{
    @apply 
    outline
    outline-red-500
    focus:outline
    focus:outline-red-500
    focus:outline-2
    focus:ring-red-500 
    focus:ring-1
}

However, I want more flexibility. I want to be able to provide a custom color and have the variants recommended to me in vs code intellisense. For example adding the class error-purple-800 should result in the equivalent of this code

.error-purple-800{
    @apply 
    outline
    outline-purple-800
    focus:outline
    focus:outline-purple-800
    focus:outline-2
    focus:ring-purple-800
    focus:ring-1
}

So I was trying to include a plugin in my tailwind.config.js to accomplish this

  plugins: [
    require('./css/tailwind_plugins/error'),
  ],

And sofar my error.js looks something like this

function errorPlugin({ addUtilities, e, theme, variants }) {
  const colors = theme('colors')
  const errorUtilities = Object.keys(colors).reduce((acc, color) => {
    const colorShades = colors[color]
    if (typeof colorShades === 'object') {
      Object.keys(colorShades).forEach(shade => {
        acc[`.error-${color}-${shade}`] = {
          outline: `2px solid ${colorShades[shade]}`,
          'outline-offset': '2px',
          'focus:outline': `2px solid ${colorShades[shade]}`,
          'focus:ring': `${colorShades[shade]} 1px`,
        }
      })
    } else {
      acc[`.error-${color}`] = {
        outline: `2px solid ${colorShades}`,
        'outline-offset': '2px',
        'focus:outline': `2px solid ${colorShades}`,
        'focus:ring': `${colorShades} 1px`,
      }
    }
    return acc
  }, {})

  addUtilities(errorUtilities, variants('outline', ['responsive', 'hover', 'focus']))
}

module.exports = errorPlugin

However, there are two problems I’m running into.

Issue One

Whatever styles I generate in this plugin are only applied directly to the element, they don’t actually include the focus styles for example. focus:outline for example is not a real css style and doesn’t work. I know that these plugins do work with variants though and so i could add the classes error-purple-800 & focus:error-purple-800 to get closer to my desired result. However, I want a solution that only requires me to add a single class in order to have both the base and focus styles applied.

Issue Two

My original tailwind css uses the ring classes which are abit confusing to me in terms of what they should translate too for normal css and more so how to integrate them into my plugin properly.

@apply 
focus:ring-purple-800
focus:ring-1

If anyone could help with this it would be greatly appreciated.

please help me fix this string reversing js code [closed]

I’m trying to reverse a string while maintaining the word orders:

example: “this string is reversed.” ==> “siht gnirts si .desrever”

function reverseWords(str) {
  let splitIndex = str.split(" ");
  let arr = [];
  splitIndex.forEach(letter => {
    for (let i = letter.length; i >= 0; i--) {
      arr.push(letter[i]);
    }
  });
  let joined = arr.join(' ');
  return joined;
}


console.log(reverseWords("this string is reversed."));

it works but it prints all the characters with space:
‘ s i h t g n i r t s s i . d e s r e v e r ‘

I understand it’s because of the space in the join(‘ ‘) method, so how else would I be able to join the letters and create spaces between each word simultaneously? Also ik the code is over-complicated and long so i’m definitely open to a more readable approach. However, I’d appreciate some insight on what I did wrong and how I could fix it for learning purposes.

How to correct implement white labelling with NextJS?

I need to create a project of multiple website using the same NextJS app but with a different backend and a different theme. For the backend it’s quite easy since I just use process.env at building time to get different connection.

But I struggle regarding the theme.

The project is structured with mono repo like this:

- apps/
   - website-1/
      - src/
         - app/ 
            - [[...route]]/
               - page.tsx
   - website-2/
- packages/
   - generic-frontend/
      - src/
         - app/ 
            - [[...route]]/
               - page.tsx

The content page.tsx of the library looks like this:

export default async function Page({ params, searchParams }: PageProps) {

});

And the page.tsx of website-1 and website-2 like this:

import Page from 'generic-frontend/[[...route]]/page';

export default Page;

Now the ugly part, I pass the theme inside apps/website-1/src/app/[[...route]]/layout.tsx like this:

import { theme } from '../theme';
global.theme = theme;

I know it’s not good but until now it was working. If I update NextJS from 14.1.4 to 14.2.0 it does not work anymore.

The theme is needed at building time in server component and probably the update of NextJS changes the way things are loaded. Now global.theme is undefined when I need it.

Also note that sometimes the theme is used directly in the component under the imports like this:

import { CSSProperties } from 'react';
import { getTheme } from '@/themes';

const theme = getTheme();

export default function Button() {
 ...
}

I can’t use context since the theme is needed server side.

So my question is, how should I implement this pattern the right way in NextJS?

How to modify Array with map of another array [closed]

I have 2 array, one is product array and another is product sequence array.
I want to modify prodList sequence array element inside product array with help of product sequence array.

Both array have prodId as common key.

Below is the product array.

const product = [
  {
    prodId: 1,
    prodList: [{ prodName: 'prod1' }, { prodName: 'prod2' }],
  },
  {
    prodId: 2,
    prodList: [{ prodName: 'prod3' }, { prodName: 'prod4' }, { prodName: 'prod5' }],
  },
];

Below array will decide new sequence in product array. Here index is important for all elements.

const prodSequence = [
  {
    prodId: 1,
    prodName: 'prod5',
  },
  {
    prodId: 1,
    prodName: 'prod2',
  },
  {
    prodId: 1,
    prodName: 'prod1',
  },
  {
    prodId: 2,
    prodName: 'prod4',
  },
  {
    prodId: 2,
    prodName: 'prod3',
  },
];

New array after prodList update

const newProduct = [
  {
    prodId: 1,
    prodList: [{ prodName: 'prod5' }, { prodName: 'prod2' }, { prodName: 'prod1' }],
  },
  {
    prodId: 2,
    prodList: [{ prodName: 'prod4' }, { prodName: 'prod3' }],
  },
];

I am struggling with this array mapping. Looking for easy way to make this worked.

Efficiently Remove and Replace Outdated Products in MongoDB with Envato API in Node.js

I have around 200,000 products in my MongoDB database. I need to write a script to remove products that are no longer available on Envato and replace them with new ones. I’ve written the following script to accomplish this, but I’m looking for suggestions to improve its efficiency and robustness.

The main script iterates over the products, checks their availability on Envato using the envatoApi, and replaces unavailable products with similar or new items. The code uses batching, Redis for tracking progress, and handles API rate limiting and retries.

Here’s my main script:

import { envatoApi } from '#apis';
import { productService, redisService, websiteService } from '#services';
import AppError from '#shared/AppError';
import { convert } from 'html-to-text';
import slugify from 'slugify';
import Model from '#models/Product';
import { Mutex } from 'async-mutex';

const config = {
    BATCH_SIZE: 500,
    API_BATCH_SIZE: 5,
    API_DELAY: 3000,
    MAX_ATTEMPTS: 10,
    SKIP_COUNT: 0,
    REDIS_KEY: 'processed_count',
    REDIS_PAGE_KEY: 'current_page',
};

const mutex = new Mutex();

const removeOldProductsAndAddNew = async category => {
    let processedCount = await redisService.get(config.REDIS_KEY);
    processedCount = processedCount ? parseInt(processedCount, 10) : config.SKIP_COUNT;
    let noOfBatches = 1;
    let batch = [];
    let skippedCount = 0;

    try {
        console.log('skipped items', processedCount);
        let productCursor = Model.find({ category: { $in: [category] } }).cursor();
        let run = true;

        while (run) {
            let hasNext = true;
            try {
                for await (const doc of productCursor) {
                    if (skippedCount < processedCount) {
                        skippedCount++;
                        continue;
                    }
                    batch.push(doc);
                    if (batch.length >= config.BATCH_SIZE) {
                        await processBatch(batch);
                        noOfBatches++;
                        console.log(`Mongo batch ${noOfBatches} processed.`);
                        batch = [];
                        processedCount += config.BATCH_SIZE;
                        await redisService.set(config.REDIS_KEY, processedCount);
                    }
                }
                hasNext = false; // No more documents to process
            } catch (error) {
                if (error.code === 43) {
                    console.warn('Cursor not found, reopening cursor...');
                    productCursor = Model.find({ category: { $in: [category] } })
                        .skip(processedCount)
                        .cursor();
                } else {
                    throw error;
                }
            } finally {
                try {
                    await productCursor.close();
                } catch (closeError) {
                    console.error('Error closing cursor:', closeError);
                }
            }

            if (!hasNext) break;
        }

        if (batch.length > 0) {
            await processBatch(batch);
            processedCount += batch.length;
            await redisService.set(config.REDIS_KEY, processedCount);
        }
        console.log(`Processing completed. Number of Mongo batches processed: ${noOfBatches}`);
    } catch (error) {
        console.error('Error processing batches:', error);
        throw new AppError(error || 'An unexpected error occurred', error.statusCode || 500);
    }
};

const processBatch = async batch => {
    for (let i = 0; i < batch.length; i += config.API_BATCH_SIZE) {
        const batchSlice = batch.slice(i, i + config.API_BATCH_SIZE);
        console.log(`Processing API requests: ${i + config.API_BATCH_SIZE}`);
        // await Promise.all(batchSlice.map(product => removeOldProducts(product)));
        for (const product of batchSlice) {
            await removeOldProducts(product);
        }
        await delay(config.API_DELAY);
    }
};

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

const removeOldProducts = async product => {
    const [site, externalId] = parseExternalId(product?.externalId);
    try {
        await envatoApi.getItemDetail(externalId);
    } catch (error) {
        if (error?.error === 404) {
            console.log('Product not found, initiating replacement...');
            await handleProductReplacement({ id: externalId, site }, product);
        } else {
            // console.error('Error fetching item detail:', error);
            throw new AppError(error.description || 'An unexpected error occurred', error.error || 500);
        }
    }
};

const parseExternalId = externalId => {
    if (!externalId) return [null, null];
    const [sitePart, idPart] = externalId.split('_');
    return [sitePart, idPart];
};

const handleProductReplacement = async ({ id, site }, product) => {
    let attempts = 0;

    while (attempts < config.MAX_ATTEMPTS) {
        let newProduct = null;

        await productService.remove(product._id);

        try {
            newProduct = await fetchSimilarOrNewItem(id, site, product);
            if (newProduct) {
                await updateProductReferences(product, newProduct);
                return { message: 'Product updated successfully' };
            }
        } catch (error) {
            const errorCode = error?.error || error?.code || error?.statusCode || 500;
            const errorMessage = error?.message || 'An unexpected error occurred';

            if (errorCode === 404) {
                console.log(`404 Error, item not found, retrying... Attempts: ${attempts}`);
            } else {
                throw new AppError(errorMessage, errorCode);
            }
        }

        attempts++;
    }

    console.log(`Maximum attempts reached for product ${product._id}, removing from all websites.`);
    await websiteService.collection.updateMany({ products: { $in: [product._id] } }, { $pull: { products: product._id } });

    return { message: 'Product removed after maximum attempts' };
};

const fetchSimilarOrNewItem = async (id, site, product) => {
    let newProduct = null;
    let run = true;
    let page = parseInt(await redisService.get(config.REDIS_PAGE_KEY), 10) || 1;

    while (run) {
        console.log('Processing page', page);
        const similarItems = await envatoApi.getSimilarItems(site, page);

        if (!similarItems || !similarItems.matches || similarItems.matches.length === 0) {
            console.log('No similar items found. Exiting loop.');
            break;
        }

        for (const item of similarItems.matches) {
            try {
                newProduct = await createProductFromItem(item, product.category);
                const release = await mutex.acquire();
                try {
                    const existingProduct = await productService.collection.findOne({ slug: newProduct.slug });

                    if (!existingProduct) {
                        console.log(`New product found: ${newProduct.slug}`);
                        await redisService.set(config.REDIS_PAGE_KEY, page);
                        return newProduct;
                    } else {
                        console.log(`Product with slug ${newProduct.slug} already exists, trying another...`);
                    }
                } finally {
                    release();
                }
            } catch (error) {
                console.error('Error adding product from item:', error);
            }
        }

        if (similarItems?.matches === 0) {
            console.log('No more pages left. Exiting loop.');
            break;
        }

        page++;
    }

    throw new AppError('No valid similar or new item found', 400);
};

const createProductFromItem = async (item, category) => {
    if (!item || !item.id) {
        console.error('Invalid item object:', item);
        throw new AppError('Invalid item object', 400);
    }

    const { id: newProductId, name, url, site, attributes, description, price_cents, previews } = await envatoApi.getItemDetail(item.id);
    if (!newProductId || !name || !url || !site) {
        console.error('Missing necessary product details:', { newProductId, name, url, site });
        throw new AppError('Missing necessary product details from item', 400);
    }

    const slug = slugify(name, { lower: true, strict: true });
    return {
        name,
        source: site,
        slug,
        reference: url,
        externalId: `${site}_${newProductId}`,
        attributes,
        price: price_cents / 100,
        description: convert(description, { wordwrap: false }).replace(/n/g, ' ').replace(/s+/g, ' ').trim(),
        previews,
        images: [previews?.icon_preview?.icon_url],
        category,
    };
};

const updateProductReferences = async (oldProduct, newProduct) => {
    const { _id } = await productService.create(newProduct);
    await websiteService.collection.updateMany({ products: { $in: [oldProduct._id] } }, [
        {
            $set: {
                products: {
                    $concatArrays: [
                        {
                            $filter: {
                                input: '$products',
                                as: 'product',
                                cond: { $ne: ['$$product', oldProduct._id] },
                            },
                        },
                        [_id],
                    ],
                },
            },
        },
    ]);
    console.log('Product references updated successfully.');
};

export default {
    removeOldProductsAndAddNew,
};

The envatoApi module used in the script:

import axios from 'axios';
import config from '#config';

const instance = axios.create({
    baseURL: 'https://api.envato.com',
    headers: {
        Authorization: `Bearer ${config.apis.envato.apiKey}`,
    },
});

const onFulfilled = response => {
    const { data } = response;
    return data;
};

const onRejected = err => {
    const { error, description } = err.response.data;
    return Promise.reject({ error, description });
};

instance.interceptors.response.use(onFulfilled, onRejected);

/**
 *
 * @param {string} purchaseCode
 * @returns {Promise<string>}
 */
const getDownloadUrl = async purchaseCode => {
    const { download_url: url } = await instance.get('/v3/market/buyer/download', { params: { purchase_code: purchaseCode, shorten_url: true } });
    return url;
};

const getItemDetail = async id => {
    const data = await instance.get(`/v3/market/catalog/item?id=${id}`);
    return data;
};

const getSimilarItem = async id => {
    const data = await instance.get(`/v1/discovery/search/search/more_like_this?item_id=${id}`);
    return data;
};

const getSimilarItems = async (site, page) => {
    const data = await instance.get(`/v1/discovery/search/search/item?site=${site}&date=this-year&page=${page}`);
    return data;
};

export default {
    getDownloadUrl,
    getItemDetail,
    getSimilarItem,
    getSimilarItems,
};

I have a view more button for my container but i cant expand fully my video

i have a view more function and it works well with only paragraph and image.
when i have insert paragraph and video together in one container. the expand cannot cover the video

In result, the video appear in half meaning the height of the video is half

looks like the box of expand is being limit

<?php
session_start();
$nickname = isset($_SESSION['nickname']) ? $_SESSION['nickname'] : 'Guest';

// Database connection
$conn = new mysqli("localhost", "root", "", "forum");

if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

date_default_timezone_set("Asia/Kuala_Lumpur");
$discussion_id = isset($_GET['id']) ? intval($_GET['id']) : 0;

// Fetch discussion details
$discussion_sql = "SELECT * FROM discussions WHERE id = $discussion_id";
$result = $conn->query($discussion_sql);
if ($result->num_rows > 0) {
    $discussion = $result->fetch_assoc();
} else {
    echo "Discussion not found.";
    exit();
}

// Fetch comments and replies
// Add your existing code here for comments and replies...

$conn->close();
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Discussion</title>
    <style>
        .discussion-video-container {
            overflow: hidden;
            position: relative;
            width: 100%;
            max-width: 800px;
            margin: 10px auto;
            background: #000;
        }

        .discussion-video {
            width: 100%;
            height: auto;
            object-fit: contain;
        }

        .discussion-video.expanded {
            height: auto;
        }

        .view-more {
            display: none;
            cursor: pointer;
            color: #007bff;
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <div class="forum-discussion">
        <!-- Discussion content -->
        <div class="subforum-description">
            <?php if (!empty($discussion['video_path'])): ?>
                <div class="discussion-video-container">
                    <video controls class="discussion-video" id="discussionVideo">
                        <source src="<?php echo htmlspecialchars($discussion['video_path']); ?>" type="video/mp4">
                        Your browser does not support the video tag.
                    </video>
                </div>
            <?php endif; ?>
            <button class="view-more" id="viewMoreBtn" onclick="toggleVideo()">View More</button>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            var videoContainer = document.querySelector('.discussion-video-container');
            var viewMoreButton = document.getElementById('viewMoreBtn');

            // Set the initial max height
            var maxHeight = 450; // Adjust as needed
            videoContainer.style.maxHeight = maxHeight + 'px';

            // Check if the video container exceeds max height
            if (videoContainer.scrollHeight > maxHeight) {
                viewMoreButton.style.display = 'inline-block';
            }
        });

        function toggleVideo() {
            var videoContainer = document.querySelector('.discussion-video-container');
            var viewMoreButton = document.getElementById('viewMoreBtn');

            if (videoContainer.classList.contains('expanded')) {
                videoContainer.classList.remove('expanded');
                videoContainer.style.maxHeight = '450px'; // Collapse
                viewMoreButton.innerText = 'View More';
            } else {
                videoContainer.classList.add('expanded');
                videoContainer.style.maxHeight = videoContainer.scrollHeight + 'px'; // Expand
                viewMoreButton.innerText = 'View Less';
            }
        }
    </script>
</body>
</html>

I want to make sure the video can display fully. No need full-size atleast I want to view the overview of the video

width and height smaller is just fine. as long as the video can be expanded fully