Android Camera Permission Denied on MAUI Hybrid App with MediaPipe JS Pose Detection

I’m developing a .NET MAUI Hybrid app that uses a Blazor WebView to run a web interface with MediaPipe JS for pose detection. On Windows everything works fine the camera activates and MediaPipe processes frames. But when I deploy the app to Android devices, I continuously get a “camera permission denied” error even though:

I have added the camera permission (e.g. <uses-permission android:name="android.permission.CAMERA" />) in my Android manifest.

I’ve tried implementing a custom WebClient to request permissions programmatically.

I signed and published the app correctly.

Below is a simplified snippet of my JavaScript code (from my mediapipePose.js file) where I request camera access using navigator.mediaDevices.getUserMedia:

if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    navigator.mediaDevices.getUserMedia({
        video: true
    })
    .then((stream) => {
        videoElement.srcObject = stream;
        videoElement.play();
    })
    .catch((error) => {
        console.error("Error accessing camera:", error);
    });
} else {
    console.error("getUserMedia is not supported in this browser.");
}

Additionally, the HTML includes my custom JS and all MediaPipe scripts in the correct order. Since the same code runs without issues on Windows, I’m not sure what might be causing Android to deny permission.

Has anyone encountered a similar issue or can suggest additional steps to troubleshoot or configure my MAUI Hybrid app so that camera access works on Android?

TailwindCSS v4 not picking classes

I am doing work in a monorepo where I have 2 apps. The first is next JS and the second one is React.js. I have recently upgrade tailwind CSS version to the latest one. But, the issue is my below CSS is not applying after upgrading. I have follow multiple docs and did some customization but did not get success. Could you please have a look and guide me.

/* 
@tailwind base;
@tailwind components;
@tailwind utilities; */

@import 'tailwindcss';

@theme {
  /* Typography */
  --h1: 'text-gray-900';
  --h2: 'text-3xl md:text-4xl font-bold text-gray-900';
  --h3: 'text-2xl md:text-3xl font-bold text-gray-900';
  --h4: 'text-xl md:text-2xl font-semibold text-gray-900';
  --h5: 'text-lg md:text-xl font-semibold text-gray-900';
  --h6: 'text-base md:text-lg font-semibold text-gray-900';
  --subtitle: 'text-xl text-gray-600';
  --body: 'text-base text-gray-700';
  --body-sm: 'text-sm text-gray-700';
  --caption: 'text-sm text-gray-500';
  --color-mint-500: oklch(0.72 0.11 178);

  /* Links */
  --link: 'text-gray-600 hover:text-primary-500 font-medium transition-colors duration-200';
  --link-muted: 'text-gray-500 hover:text-gray-700 transition-colors duration-200';
  --link-primary: 'text-primary-500 hover:text-primary-600 font-medium transition-colors duration-200';
  --link-underline: 'text-gray-600 hover:text-primary-500 underline transition-colors duration-200';

  /* Spacing */
  --section: 'py-20';
  --container-padded: 'container mx-auto px-4';

  /* Buttons */
  --btn: 'px-6 py-3 rounded-lg font-medium transition-colors text-center inline-block';
  --btn-sm: 'px-4 py-2 rounded-lg font-medium transition-colors text-center inline-block text-sm';
  --btn-lg: 'px-8 py-4 rounded-lg font-medium transition-colors text-center inline-block text-lg';
  --btn-primary: 'bg-primary-500 text-white hover:bg-primary-600';
  --btn-secondary: 'bg-white text-primary-500 border border-primary-500 hover:bg-gray-50';
  --btn-white: 'bg-white text-primary-500 hover:bg-gray-100';
  --btn-outline: 'bg-transparent text-white border border-white hover:bg-primary-600'; /* Cards */
  --card: 'bg-white rounded-lg shadow-sm overflow-hidden';
  --card-hover: 'transition-all duration-300 hover:shadow-md';

  /* Features */
  --feature-card: 'bg-gray-50 p-8 rounded-lg';
  --feature-icon: 'text-primary-500 mb-4 h-12 w-12';

  /* Sections */
  --hero-section: 'bg-gradient-to-b from-white to-gray-50';
  --section-title-container: 'text-center mb-16';
  --cta-section: 'bg-primary-500 text-white';
}

@layer components {
  /* Typography */
  .h1 {
    @reference text-4xl md:text-5xl font-bold text-gray-900;
  }

  .h2 {
    @reference text-3xl md:text-4xl font-bold text-gray-900;
  }

  .h3 {
    @reference text-2xl md:text-3xl font-bold text-gray-900;
  }

  .h4 {
    @reference text-xl md:text-2xl font-semibold text-gray-900;
  }

  .h5 {
    @reference text-lg md:text-xl font-semibold text-gray-900;
  }

  .h6 {
    @reference text-base md:text-lg font-semibold text-gray-900;
  }

  .subtitle {
    @reference text-xl text-gray-600;
  }

  .body {
    @reference text-base text-gray-700;
  }

  .body-sm {
    @reference text-sm text-gray-700;
  }

  .caption {
    @reference text-sm text-gray-500;
  }

  /* Links */
  .link {
    @reference text-gray-600 hover:text-primary-500 font-medium transition-colors duration-200;
  }

  .link-muted {
    @reference text-gray-500 hover:text-gray-700 transition-colors duration-200;
  }

  .link-primary {
    @reference text-primary-500 hover:text-primary-600 font-medium transition-colors duration-200;
  }

  .link-underline {
    @reference text-gray-600 hover:text-primary-500 underline transition-colors duration-200;
  }

  /* Spacing */
  .section {
    @reference py-20;
  }

  .container-padded {
    @reference container mx-auto px-4;
  }

  /* Buttons */
  .btn {
    @reference px-6 py-3 rounded-lg font-medium transition-colors text-center inline-block;
  }

  .btn-sm {
    @reference px-4 py-2 rounded-lg font-medium transition-colors text-center inline-block text-sm;
  }

  .btn-lg {
    @reference px-8 py-4 rounded-lg font-medium transition-colors text-center inline-block text-lg;
  }

  .btn-primary {
    @reference bg-primary-500 text-white hover:bg-primary-600;
  }

  .btn-secondary {
    @reference bg-white text-primary-500 border border-primary-500 hover:bg-gray-50;
  }

  .btn-white {
    @reference bg-white text-primary-500 hover:bg-gray-100;
  }

  .btn-outline {
    @reference bg-transparent text-white border border-white hover:bg-primary-600;
  }

  /* Cards */
  .card {
    @reference bg-white rounded-lg shadow-sm overflow-hidden;
  }

  .card-hover {
    @reference transition-all duration-300 hover:shadow-md;
  }

  /* Features */
  .feature-card {
    @reference bg-gray-50 p-8 rounded-lg;
  }

  .feature-icon {
    @reference text-primary-500 mb-4 h-12 w-12;
  }

  /* Sections */
  .hero-section {
    @reference bg-gradient-to-b from-white to-gray-50;
  }

  .section-title-container {
    @reference text-center mb-16;
  }

  .cta-section {
    @reference bg-primary-500 text-white;
  }
}

THREEJS – Ray intersection on instencedMesh that has multiple morph targets

I would like to create an instancedMesh that can handle morphing. I need morph targets because each instance should have different shape so morph targets seems handy. Also each instance should detect rays because I need mouse events on each instance.

instancedMesh is needed because I want to draw large number of objects with the same material but different size.

Mesh approach

First I create a mesh with a geometry that has morph targets to make sure the morphing works as it expected.

mesh_morph

The base geometry have four morph targets top, bottom, left and right. Each morph target just move the edges in the given direction.

I manually calculated the boundingBox and boundingSphere to be able to detect cursor on the morphed parts.

InstancedMesh approach

With a single mesh works well the morphing however if I try to use the same geometry it does not work the same.

First issue

Only the original geometry can detect ray intersection. It was a problem on the first example as well but I manually calculated the boundingBox and boundingSphere which is fixed the problem. On instancedMesh does not matter if I change the bounds or not it will work the same.

instanced_mesh_01

The first rectangle is the original geometry shape. The second rectangle top and bottom morph targets is 0.5. That means moves the top and bottom edges with 0.5 on the y axes.

Second issue

When I move the second instance out of the original bounding box it somehow still detects ray in the original geometry shape.

instanced_mesh_02

My question is how can I detect rays on instancedMesh in morphed parts? My desired result should be a solution that works the same as the first example.


DEMO

No Overload match this call

I’m encountering a TypeScript error when building an authentication using JWT.

No overload matches this call.
Overload 1 of 5, ‘(payload: string | object | Buffer, secretOrPrivateKey: null, options?: (SignOptions & { algorithm: “none”; }) | undefined): string’, gave the following error.
rgument of type ‘string’ is not assignable to parameter of type ‘null’.
Overload 2 of 5, ‘(payload: string | object | Buffer, secretOrPrivateKey: Buffer | Secret | PrivateKeyInput | JsonWebKeyInput, options?: SignOptions | undefined): string’, gave the following error.
Type ‘string’ is not assignable to type ‘number | StringValue | undefined’.
Overload 3 of 5, ‘(payload: string | object | Buffer, secretOrPrivateKey: Buffer | Secret | PrivateKeyInput | JsonWebKeyInput, callback: SignCallback): void’, gave the following error.
Object literal may only specify known properties, and ‘expiresIn’ does not exist in type ‘SignCallback

This is my code

const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';

if (!JWT_SECRET) {
    throw new Error("FATAL ERROR: JWT_SECRET is not defined in environment variables.");
}

class AuthService {
    private saltRounds = 10; 

    public async registerUser(userData: UserCreationData): Promise<Omit<User, 'password'>> {
        const { username, password } = userData;

        // Check if user already exists
        const existingUserResult: QueryResult<User> = await pool.query(
            'SELECT * FROM users WHERE username = $1',
            [username]
        );
        if (existingUserResult.rows.length > 0) {
            throw new Error('Username already exists.'); 
        }

        // Hash password
        const hashedPassword = await bcrypt.hash(password, this.saltRounds);
        // Insert new user
        const query = `
            INSERT INTO users (username, password)
            VALUES ($1, $2)
            RETURNING id, username, created_at; -- Return user data without password
        `;
        try {
            const result: QueryResult<Omit<User, 'password'>> = await pool.query(query, [username, hashedPassword]);
            if (result.rows.length > 0) {
                return result.rows[0];
            } else {
                throw new Error("User registration failed, no rows returned.");
            }
        } catch (error) {
            console.error("Error registering user:", error);
            throw new Error("Failed to register user in database.");
        }
    }

    public async loginUser(userData: UserCreationData): Promise<{ token: string; user: Omit<User, 'password'> }> {
        const { username, password } = userData;

        // ... (database query and password check) ...
        const query = 'SELECT * FROM users WHERE username = $1;';
        const result: QueryResult<User> = await pool.query(query, [username]);

        if (result.rows.length === 0) {
            throw new Error('Invalid username or password.');
        }
        const user = result.rows[0];
        const isMatch = await bcrypt.compare(password, user.password);

        if (!isMatch) {
            throw new Error('Invalid username or password.');
        }

        
        const token = jwt.sign(
            {userId: user.id, username: user.username},
            JWT_SECRET as string, 
            { expiresIn: JWT_EXPIRES_IN }
        );

        const { password: _, ...userWithoutPassword } = user;
        return { token, user: userWithoutPassword };
    }
}

export default AuthService;

Why doesn’t the `loading=”lazy”` attribute of the “ tag work for lazy loading when used inside an `el-carousel` component?

Why doesn’t the loading=”lazy” attribute of the tag work for lazy loading when used inside an el-carousel component?

Online Demo

Element Plus Playground 1

Example Code

<script setup lang="ts">

</script>

<template>
   <div>
    <div style="width: 100%; height: 2000px; display: block">lazy load header</div>
    <el-carousel height="2000px">
      <el-carousel-item>
        <el-row>
          <el-col :span="24">
            <img
              src="https://dummyimage.com/2000x2000/aaa/fff&text=2000x2000"
              loading="lazy"
            />
          </el-col>
        </el-row>
      </el-carousel-item>
      <el-carousel-item>
        <el-row>
          <el-col :span="24">
            <img
              src="https://dummyimage.com/2000x2000/bbb/fff&text=2000x2000"
              loading="lazy"
            />
          </el-col>
        </el-row>
      </el-carousel-item>
      <el-carousel-item>
        <el-row>
          <el-col :span="24">
            <img
              src="https://dummyimage.com/2000x2000/ccc/fff&text=2000x2000"
              loading="lazy"
            />
          </el-col>
        </el-row>
      </el-carousel-item>
      <el-carousel-item>
        <el-row>
          <el-col :span="24">
            <img
              src="https://dummyimage.com/2000x2000/ddd/fff&text=2000x2000"
              loading="lazy"
            />
          </el-col>
        </el-row>
      </el-carousel-item>
      <el-carousel-item>
        <el-row>
          <el-col :span="24">
            <img
              src="https://dummyimage.com/2000x2000/eee/fff&text=2000x2000"
              loading="lazy"
            />
          </el-col>
        </el-row>
      </el-carousel-item>
    </el-carousel>
  </div>
</template>

<style>
</style>

Currently, I’m not sure how to make an attempt. The expected result is that the loading="lazy" attribute of the <img> tag can work for lazy loading when used inside an el-carousel component.

Why subject’s subscriber keeps consume an error, after the first() operator, if the next emit of this subject was called in the inner tap() operator?

I’ve found a strange rxjs behavior that keeps bothering me, and I can’t even imagine a solution to this problem.

I need to create an Observable from Subject that will either emit an error if the consumed value is null, or emit a value. And then emit from Subject a null value.
Everything works as intended, but the subscriber of this Observable keeps consume the error that was emitted after the first subscription, even with the first() operator.

How is this possible?

import { Subject, throwError, of } from 'rxjs';
import { switchMap, tap, first } from 'rxjs/operators';

var subj$ = new Subject();

var obs$ = subj$.asObservable().pipe(
  switchMap((v) => {
    if (!v) {
      return throwError(() => 'ERROR');
    }

    return of(v);
  })
);

obs$
  .pipe(
    first(),
    tap({
      next(v) {
        console.log('obs$', v);
      },
      error(err) {
        console.error('obs$ error', err);
      },
    }),
    tap(() => subj$.next(null))
  )
  .subscribe();

subj$.next('VALUE');

// obs$ VALUE
// obs$ error ERROR <- ???
// ERROR <- ???

https://stackblitz.com/edit/stackblitz-starters-sdx1oqzx?file=index.js

Real world example:

import { Observable, BehaviorSubject, zip, throwError, of } from 'rxjs';
import { first, switchMap, tap } from 'rxjs/operators';

type User {
  id: number,
  name: string
}

class AuthService {
  private isAuthenticatedSource = new BehaviorSubject<boolean>(false);
  private userSource = new BehaviorSubject<User | null>(null);

  isAuthenticated$: Observable<boolean> = this.isAuthenticatedSource.asObservable();

  user$: Observable<User> = zip(
    this.isAuthenticatedSource.asObservable(),
    this.userSource.asObservable(),
  ).pipe(
    switchMap(([ isAuthenticated, user ]) => {
      if (!isAuthenticated || !user) {
        return throwError(() => 'Unauthenticated.');
      }

      return of(user);
    })
  );

  constructor(private socketService: SocketService) {}

  authenticate(user: User) {
    this.isAuthenticatedSource.next(true);
    this.userSource.next(user);

    return this.socketService.emit('join', user);
  }

  // consumes the error from second emission
  deauthenticate() {
    return this.user$.pipe(
      first(),
      switchMap((user: User) => this.socketService.emit('leave', user)),
      tap(() => {
        this.isAuthenticatedSource.next(false);
        this.userSource.next(null);
      })
    );
  }
}

class SocketService {
  socket = {
    emit(event: string, data: any, ack: (res: any) => any) {
      return { event, data, ack };
    }
  }

  emit<T>(event: string, data: any): Observable<T> {
    return new Observable<T>(subscriber => {
      this.socket.emit(event, data, (res) => {
        subscriber.next(res);
        subscriber.complete();
      })
    });
  }
}

In a prestashop, on mobile, something is overwriting my client-side modifications of the classlist [closed]

I’ve got a react app, running in an Iframe in a prestashop page. For some operations, I want the Iframe to go fullscreen. To implement this, my react frame sends a message to the parent requesting fullscreen, and the parent puts an iframe-fullscreen class on the body. Then I’ve got corresponding styles to position the Iframe and hide the floating header. This works fine on desktop, but on mobile the classlists of every element seem to be managed, actively in the browser (by prestashop?), and any savage attempts to modify classlists are immediately countermanded by the page.

Does anyone know about this? Is it indeed prestashop, or a prestashop theme, or a responsive plugin? I don’t own the prestashop page, and I’m not a prestashop developer, but if someone can help me identify what might be doing this, I can see if the behaviour can be customized or paused.

Can i link another html file which stores info to be used in the other html file? [duplicate]

In my index.html i have images and when i click them they expand into modals (barely did it)

So each image consists of two parts
“Thumbnail”(<img…) + Modal for this image

Modals are quite chunky and so i thought that i can put them all in separate html document to pull them from, into my index.html and other webpages I’m going to have.
Because some of the images are going to appear on both pages and I’d have to copy paste those modals along them otherwise.

So i thought that maybe i can link

<link href="modals.html">

In the “head”

But it didn’t seem to work

Does it work? And if yes, then how?

Cannot get ViewShot to output a quality image using calculated dimensions

Having a strange issue with ViewShot in a react native app, where using a calculated value is causing the output file to be very low quality. Hopefully I can explain this well enough to get some advice on where to look.

I’m trying to use ViewShot to output images with a watermark on them This works as intended provided the line style={[{ height: newHeight, width: newWidth }, styles.cardWrapper]} is set to fixed height and widths. For example if the image output is to be 2000×1500, if I set the values then the resulting file is ok (on my testing device the pixel ratio is 1.96875, so that would mean I need to use the values 1016×762 to achieve the correct image dimensions). If I let the code calculate the values and put them in there, the output file is very low quality, even if the values are exactly the same. I can’t fix the output size as the input images could be any size. This is driving me mad as I can’t figure out why it does this.

{selectedImages.map((image, index) => {
  // Find matching image data by uri
  const imageData = imageSizes.find(item => item.uri === image.uri);

  newWidth = 100;
  newHeight = 100;

  if (imageData?.width) {
    newWidth = Math.round(imageData.width / pixelRatio);
    newHeight = Math.round(imageData.height / pixelRatio);
  }

  return (
    <View
      key={`watermark_${index}`}
      style={[{ height: newHeight, width: newWidth }, styles.cardWrapper]}
      ref={viewRefs.current[index]}
      collapsable={false}
    >
      <Image
        source={{ uri: `${APP_CACHE}${image.uri}` }}
        style={{ width: '100%', height: '100%' }}
      />
      <View style={styles.cardBackground}>
        <Text style={styles.cardInfo}>
          {'text text text'}
          {imageData?.width || 0}{' x '}{imageData?.height || 0}
        </Text>
      </View>
    </View>
  );
})}

function is being called with the following component

<TouchableOpacity activeOpacity={0.6} onPress={() => { processImages(); }} style={styles.button}>
  <Text style={styles.buttonText}>
    {`test watermark`}
  </Text>
</TouchableOpacity>

Processing function here. This works as intended provided the above rule is followed.

const processImages = async () => {
let processedUris = [];

for (let i = 0; i < selectedImages.length; i++) {
  if (!viewRefs.current[i]?.current) {
    console.warn(`Ref for image ${i} is not assigned`);
    continue;
  }

  const watermarkedUri = await addWatermark(selectedImages[i], viewRefs.current[i]);
  if (watermarkedUri) {
    processedUris.push(watermarkedUri);
  }
}

setWatermarkedImages(processedUris);
console.log('All images processed:', processedUris);
};

const addWatermark = async (imageUri, viewRef) => {
return new Promise(async (resolve) => {
  setTimeout(async () => {
    try {
      const snapshot = await captureRef(viewRef.current, {
        format: 'jpg',
        quality: 1,
      });

      const fileName = `watermarked_${Date.now()}.jpg`;
      const newUri = APP_CACHE + fileName;

      await FileSystem.moveAsync({
        from: snapshot,
        to: newUri,
      });

      //   console.log('Saved:', newUri);
      resolve(newUri);
    } catch (error) {
      console.error('Error capturing image:', error);
      resolve(null);
    }
  }, 100);
});
};

Original image (dimensions 2000×1500)

Fixed dimension output (dimensions set at 1016×762, output to 2000×1500)

Variable dimension output (calculated at 1016×762, output to 2000×1500

Want a customized logout warning pop up in reactjs

I have a banking website and I am implementing as case where, when a user tries to refresh browser it should show a warning popup which states that you will be sign out and if user click on that button it should logout that user. I am using react and below is my code. But this gives me only option for Reload and Cancel which is predefined and the alert is not working. I want to add my functionality of logout.

 useEffect(() => {
    window.onbeforeunload = function () {
        alert("This will logout.. ")
        return "Leaving this page will reset the wizard";
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
},[]);

How to resolve a React hydration mismatch error so that third-party JavaScript via tags in CKEditor HTML content can be shown in Frontend?

The following third party code is written in the CK editor in drupal in source mode:

<script>
       window.page = 'gidd';
</script><!-- On new release, please update the bundle count so that cache is busted --><!-- Add react-website-component script and styles here -->
<script defer="" src="***/js/runtime.bundle.js?bundle=3"></script>
<script defer="" src="***/js/main.bundle.js?bundle=3"></script>
<link href="***/css/main.css?bundle=3" rel="stylesheet"><!-- end -->

Now, in the following React component, we’re showing the third-party data, that is rendering fine in frontend:

import { graphql } from "gatsby";
import React from "react";
import { Col, Container, Row } from "react-bootstrap";
import ExtractRichText from "../../molecules/ExtractRichText";
import "./database.scss";
import Seo from "../../atoms/seo";

const DatabasePage = ({ data }) => {
  return (
    <>
      <Container className="database-container">
        <Row>
          <Col>
            {/* <ExtractRichText
              richText={data?.nodeDatabasePage?.body?.value}
            ></ExtractRichText> */}
            <div
              dangerouslySetInnerHTML={{
                __html: data.nodeDatabasePage.body.value,
              }}
            />
          </Col>
        </Row>
      </Container>
    </>
  );
};

export default DatabasePage;

export const pageQuery = graphql`
  query ($id: String!) {
    nodeDatabasePage(id: { eq: $id }) {
      title
      body {
        value
      }
      field_meta_tags {
        description
        keywords
      }
      path {
        alias
      }
      relationships {
        field_metatag_image {
          url
        }
      }
    }
  }
`;

export const Head = ({ data }) => (
  <Seo
    title={data?.nodeDatabasePage?.title}
    image={data?.nodeDatabasePage?.relationships?.field_metatag_image?.url}
    description={data?.nodeDatabasePage?.field_meta_tags?.description}
    keywords={data?.nodeDatabasePage?.field_meta_tags?.keywords}
    url={data?.nodeDatabasePage?.path?.alias}
  />
);

Now we’ve modified the above component to the following:

import { graphql } from "gatsby";
import React from "react";
import { Col, Container, Row } from "react-bootstrap";
import ExtractRichText from "../../molecules/ExtractRichText";
import "./database.scss";
import Seo from "../../atoms/seo";

const DatabasePage = ({ data }) => {
  return (
    <>
      <Container className="database-container">
        <Row>
          <Col>
            <ExtractRichText
              richText={data?.nodeDatabasePage?.body?.value}
            ></ExtractRichText>
          </Col>
        </Row>
      </Container>
    </>
  );
};

export default DatabasePage;

export const pageQuery = graphql`
  query ($id: String!) {
    nodeDatabasePage(id: { eq: $id }) {
      title
      body {
        value
      }
      field_meta_tags {
        description
        keywords
      }
      path {
        alias
      }
      relationships {
        field_metatag_image {
          url
        }
      }
    }
  }
`;

export const Head = ({ data }) => (
  <Seo
    title={data?.nodeDatabasePage?.title}
    image={data?.nodeDatabasePage?.relationships?.field_metatag_image?.url}
    description={data?.nodeDatabasePage?.field_meta_tags?.description}
    keywords={data?.nodeDatabasePage?.field_meta_tags?.keywords}
    url={data?.nodeDatabasePage?.path?.alias}
  />
);

We’ve used ExtractRichText component, that’s handing parsing the code and render in the frontend.

Now the ExtractRichText component looks like following:

import * as React from "react";
import parse from "html-react-parser";
import { useFileFile } from "../../hooks/useFileFile";
import { useMediaFileFile } from "../../hooks/useMediaFileFile";
import "./extractRichText.scss"
export const ExtractRichText = ({ richText, extraClasses }) => {
  const drupalIDs = useFileFile().map((e) => e?.node?.drupal_id);
  const allFiles = useFileFile().map((e) => e?.node);
  const allMediafiles = useMediaFileFile().map((e) => e?.node);
  let body = "";
  if (richText) {
    body = extractImage(drupalIDs, richText, extraClasses, allFiles, allMediafiles);
  }
  return <div className={`rich-text ${extraClasses}`}>{body}</div>;
};
function extractImage(drupalIDs, body, extraClasses, allFiles, allMediafiles) {
  return parse(body, {
    transform: (node) => {
      if (node.type === "img" || node.type === "drupal-entity" || node.type === "drupal-media") {
        const currentFile = allFiles.find((file) => node.props["data-entity-uuid"] === file?.drupal_id);
        let imageUrl = currentFile?.publicUrl;
        let mediaUrl;
        let alt;
        if (node.type === "drupal-media") {
          const currentMedia = allMediafiles.find((media) => node.props["data-entity-uuid"] === media?.drupal_id);
          mediaUrl = currentMedia?.relationships?.field_media_image?.publicUrl;
          alt = currentMedia?.field_media_image?.alt;
        }
        const imgAlign = node.props["data-align"] || "";
        const height = node.props["height"];
        const width = node.props["width"];
        let src = imageUrl || mediaUrl || node.props["src"];
        if (src === node.props["src"]) {
          const isRelative = !/^https?:///i.test(src);
          if (isRelative) {
            const base = process.env.GATSBY_DRUPAL_URL.replace(//+$/, '');
            const path = src.replace(/^/+/, '');
            src = `${base}/${path}`;
          }
        }
        const imgStyles = {};
        if (width) {
          imgStyles.width = `${width}px !important`;
        } else {
          // imgStyles.width = `100%`;
        }
        if (height) {
          imgStyles.height = `auto`;
        } else {
          // imgStyles.height = `100%`;
        }
        return (
          <span className={`imgAlign-${imgAlign}`}>
            <img
              style={imgStyles}
              loading="lazy"
              className={`${extraClasses ?? ''} ${node?.type == 'drupal-media' ? 'w-100' : ''}`}
              alt={node?.props?.alt || alt || "image"}
              src={`${src}`}
            />
          </span>
        );
      }
      if (node.type === 'a' && node.props["data-entity-type"] === 'file') {
        const currentFile = allFiles.find((file) => node.props["data-entity-uuid"] === file?.drupal_id);
        const href = node.props["href"] || "";
        const fallbackImage = process.env.GATSBY_DRUPAL_URL + href;
        return (
          <a
            href={`${currentFile?.url || fallbackImage}`}
            target={`${node.props["target"] || ""}`}
            class={`${node.props["className"] || ""}`}
            aria-label={`${node.props['aria-label'] || ""}`}
            id={`${node.props["id"] || ""}`}
            rel={`${node.props["rel"] || ""}`}
          >{`${node.props["children"] || "Link"}`}</a>
        );
      }
      return node;
    },
  });
}
export default ExtractRichText;

But, after using ExtractRichText in DatabasePage component, we’re getting the following runtime errors and third party data are not showing as well:

throw new Error('Text content does not match server-rendered HTML.');
throw new Error('Hydration failed because the initial UI does not match what was ' + 'rendered on the server.');
var recoverableError = createCapturedValueAtFiber(new Error('There was an error while hydrating. Because the error happened outside ' + 'of a Suspense boundary, the entire root will switch to ' + 'client rendering.'), workInProgress); 

How to fix these above errors and ensure that third-party data is rendering fine in frontend?

Full Screen react exam taking app, Controlled full screen exit

1.Can’t able to control full screen mode, if we press escape automatically coming out from the full screen mode.

2.If not possible how other exam taking apps are working, any other ways?

3.Non of the tools providing the controlled exit

4.What is the best approach to implement full screen controlled exit

1.This is the first one
import React, { useState, useEffect, } from 'react';
import Fullscreen from 'react-fullscreen-crossbrowser';
import "./styles.css";

// Dangerous key combinations to block
const DANGEROUS_KEYS = new Set([
  // 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
  'PrintScreen', 'ScrollLock', 'Pause', 'ContextMenu', 'Escape',
]);

const DANGEROUS_COMBOS = [
  { ctrl: true, shift: true, key: 'p' },  // Chrome's print shortcut
  { ctrl: true, shift: true, key: 'i' },  // DevTools
  { ctrl: true, shift: true, key: 'j' },  // DevTools console
  { ctrl: true, shift: true, key: 'c' },  // Force copy
  { alt: true, tab: true },               // Window switching
  { meta: true, tab: true },             // Mac window switching
  { ctrl: true, alt: true, key: 'Delete' } // Windows security screen
];

const SecureExamContainer = () => {
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [showExitWarning, setShowExitWarning] = useState(false);
  const [examStarted, setExamStarted] = useState(false);

  // Handle fullscreen changes
  // const handleFullscreenChange = useCallback((isEnabled) => {
  //   setIsFullscreen(isEnabled);
  //   if (!isEnabled && examStarted) {
  //     setShowExitWarning(true);
  //     // Immediately attempt to re-enter fullscreen
  //     setTimeout(() => setIsFullscreen(true), 100);
  //   }
  // }, [examStarted]);

  // Keyboard and shortcut protection
  useEffect(() => {
    if (!examStarted) return;

    const handleKeyDown = (e) => {
      console.log("e.key ", e.key);
      // Block function keys
      if (DANGEROUS_KEYS.has(e.key)) {
        e.preventDefault();
        e.stopPropogation();
        setShowExitWarning(true);
        // triggerViolation(`Function key pressed: ${e.key}`);
        return;
      }

      // Check for dangerous combinations
      for (const combo of DANGEROUS_COMBOS) {
        const ctrlMatch = !combo.ctrl || e.ctrlKey || e.metaKey;
        const shiftMatch = !combo.shift || e.shiftKey;
        const altMatch = !combo.alt || e.altKey;
        const keyMatch = !combo.key || e.key === combo.key;
        const tabMatch = combo.tab === undefined || e.key === 'Tab';

        if (ctrlMatch && shiftMatch && altMatch && keyMatch && tabMatch) {
          e.preventDefault();
          setShowExitWarning(true);
          // triggerViolation(`Dangerous combination detected: ${e.key}`);
          return;
        }
      }

      // Block Ctrl/Alt/Meta combinations
      if (e.ctrlKey || e.altKey || e.metaKey) {
        const allowedCombos = [
          { ctrl: true, key: 'c' },  // Allow copy
          { ctrl: true, key: 'v' },  // Allow paste
          { ctrl: true, key: 'x' }   // Allow cut
        ];

        const isAllowed = allowedCombos.some(combo =>
          (combo.ctrl ? e.ctrlKey || e.metaKey : true) &&
          e.key === combo.key
        );

        if (!isAllowed) {
          e.preventDefault();
          setShowExitWarning(true);
          // triggerViolation(`Modifier key combination detected: ${e.key}`);
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [examStarted]);

  // Block keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (isFullscreen && (e.key === 'Escape' || e.key === 'F11')) {
        e.preventDefault();
        setShowExitWarning(true);
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isFullscreen]);

  // Block right-click and text selection
  useEffect(() => {
    if (!isFullscreen) return;

    const blockContextMenu = (e) => e.preventDefault();
    const blockSelection = (e) => e.preventDefault();

    document.addEventListener('contextmenu', blockContextMenu);
    document.addEventListener('selectstart', blockSelection);

    return () => {
      document.removeEventListener('contextmenu', blockContextMenu);
      document.removeEventListener('selectstart', blockSelection);
    };
  }, [isFullscreen]);

  // Fullscreen enforcement interval
  useEffect(() => {
    if (!examStarted) return;

    const interval = setInterval(() => {
      if (!isFullscreen) {
        setIsFullscreen(true);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [examStarted, isFullscreen]);

  const startExam = () => {
    setExamStarted(true);
    setIsFullscreen(true);
  };

  const confirmExit = () => {
    setExamStarted(false);
    // setIsFullscreen(false);
    setShowExitWarning(false);
    // Additional cleanup or submission logic here
  };

  const cancelExit = () => {
    setShowExitWarning(false);
    setIsFullscreen(true);
  };
  console.log("showExitWarning ", showExitWarning)
  console.log("isFullscreen ", isFullscreen)
  return (
    <div className="exam-container">
      {!examStarted && (
        <div className="exam-start-screen">
          <h1>Exam Instructions</h1>
          <p>You must complete this exam in fullscreen mode.</p>
          <button onClick={startExam}>Start Exam</button>
        </div>
      )}
      <Fullscreen
        enabled={isFullscreen}
        onChange={(isEnabled) => {
          if (!isEnabled && examStarted) {
            setShowExitWarning(true);
            setIsFullscreen(true); // Immediately re-enable fullscreen
          }
        }}
      >
        <div className="exam-content">
          <h2>Exam in Progress</h2>
          {/* Your exam questions/components here */}

          <button
            className="submit-button"
            onClick={confirmExit}
          >
            Submit Exam
          </button>
        </div>
        {showExitWarning && (
          <div className="exit-warning-modal">
            <div className="modal-content">
              <h3>Warning!</h3>
              <p>You are not allowed to exit fullscreen during the exam.</p>
              <div className="modal-buttons">
                <button onClick={confirmExit}>Submit & Exit</button>
                <button onClick={cancelExit}>Continue Exam</button>
              </div>
            </div>
          </div>
        )}
      </Fullscreen>
    </div>
  );
};

export default SecureExamContainer;


2. This is the second one
import React, { useCallback, useEffect } from 'react'

// Dangerous key combinations to block
const DANGEROUS_KEYS = new Set([
    'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
    'PrintScreen', 'ScrollLock', 'Pause', 'ContextMenu', 'Escape',
]);

const DANGEROUS_COMBOS = [
    { ctrl: true, shift: true, key: 'p' },  // Chrome's print shortcut
    { ctrl: true, shift: true, key: 'i' },  // DevTools
    { ctrl: true, shift: true, key: 'j' },  // DevTools console
    { ctrl: true, shift: true, key: 'c' },  // Force copy
    { alt: true, tab: true },               // Window switching
    { meta: true, tab: true },             // Mac window switching
    { ctrl: true, alt: true, key: 'Delete' } // Windows security screen
];

const StrictFullScreenMdn = () => {

    const handleOpenFullScreen = useCallback(() => {
        document.documentElement.requestFullscreen();
    }, []);

    useEffect(() => {
        const handleKeyDown = (e) => {
            handleOpenFullScreen();
            console.log("e.key ", e.key);
            // Block function keys
            if (DANGEROUS_KEYS.has(e.key)) {
                e.preventDefault();
                e.stopPropogation();
                // setShowExitWarning(true);
                // triggerViolation(`Function key pressed: ${e.key}`);
                return;
            }

            // Check for dangerous combinations
            for (const combo of DANGEROUS_COMBOS) {
                const ctrlMatch = !combo.ctrl || e.ctrlKey || e.metaKey;
                const shiftMatch = !combo.shift || e.shiftKey;
                const altMatch = !combo.alt || e.altKey;
                const keyMatch = !combo.key || e.key === combo.key;
                const tabMatch = combo.tab === undefined || e.key === 'Tab';

                if (ctrlMatch && shiftMatch && altMatch && keyMatch && tabMatch) {
                    e.preventDefault();
                    // setShowExitWarning(true);
                    // triggerViolation(`Dangerous combination detected: ${e.key}`);
                    return;
                }
            }

            // Block Ctrl/Alt/Meta combinations
            if (e.ctrlKey || e.altKey || e.metaKey) {
                const allowedCombos = [
                    { ctrl: true, key: 'c' },  // Allow copy
                    { ctrl: true, key: 'v' },  // Allow paste
                    { ctrl: true, key: 'x' }   // Allow cut
                ];

                const isAllowed = allowedCombos.some(combo =>
                    (combo.ctrl ? e.ctrlKey || e.metaKey : true) &&
                    e.key === combo.key
                );

                if (!isAllowed) {
                    e.preventDefault();
                    // setShowExitWarning(true);
                    // triggerViolation(`Modifier key combination detected: ${e.key}`);
                }
            }
        };

        window.addEventListener('keydown', handleKeyDown);
        return () => window.removeEventListener('keydown', handleKeyDown);
    }, []);

    const handleCloseFullScreen = useCallback(() => {
        document.exitFullscreen();
    }, []);

    return (
        <div>StrictFullScreenMdn

            <button onClick={handleOpenFullScreen}>Open Mode</button>
            <button onClick={handleCloseFullScreen}>close Mode</button>
        </div>
    )
}

export default StrictFullScreenMdn

Need help reproducing a nodered dashboard 2.0 animation

I’m trying to recreate the Dashboard 2.0 ui-gauge-tank with a custom unit (i need somthing else than %). I used the Vue component i found on Dashboard 2.0’s github, and adapted it to change the unit. It works well but I can’t get the wave animation working correctly…

I’m using node-red v4.0.9, NodeJS v22.14.0 and Dashboard 2.0 V1.22.1

Here’s my flow :

[
{
    "id": "7c0a6281fad8692c",
    "type": "tab",
    "label": "Flux 1",
    "disabled": false,
    "info": "",
    "env": []
},
{
    "id": "c26cf52ff92bf8dc",
    "type": "inject",
    "z": "7c0a6281fad8692c",
    "name": "",
    "props": [
        {
            "p": "payload"
        },
        {
            "p": "topic",
            "vt": "str"
        }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "15",
    "payloadType": "num",
    "x": 170,
    "y": 200,
    "wires": [
        [
            "77736be83ff7aa82"
        ]
    ]
},
{
    "id": "77736be83ff7aa82",
    "type": "function",
    "z": "7c0a6281fad8692c",
    "name": "function 2",
    "func": "msg.min = 4nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 380,
    "y": 200,
    "wires": [
        [
            "442aaa386a504831"
        ]
    ]
},
{
    "id": "f2bc295826c5e605",
    "type": "inject",
    "z": "7c0a6281fad8692c",
    "name": "",
    "props": [
        {
            "p": "payload"
        },
        {
            "p": "topic",
            "vt": "str"
        }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "15",
    "payloadType": "num",
    "x": 150,
    "y": 260,
    "wires": [
        [
            "d0ccf216830a748d"
        ]
    ]
},
{
    "id": "442aaa386a504831",
    "type": "ui-template",
    "z": "7c0a6281fad8692c",
    "group": "fb6501fa7d8b6b7b",
    "page": "",
    "ui": "",
    "name": "custom-tank",
    "order": 1,
    "width": "3",
    "height": "3",
    "head": "",
    "format": "<template>n  <div v-resize="onResize" class="nrdb-ui-gauge-tank--container">n    <label class="nrdb-ui-gauge-title"> CUSTOM-TANK</label>nn    <divn        class="nrdb-ui-gauge-tank"n        :style="{'--gauge-fill': color, '--gauge-fill-pc': pc + '%', 'color': color}"n    >n      <div class="nrdb-ui-gauge-tank--center">n        <div ref="fill" class="nrdb-ui-gauge-tank--fill"></div>n        <svg class="WaveBG" :style="`bottom: 0; height: ${svgBottom}px`" :viewBox="`0 ${amplitude_setting} 1000 ${Math.min(100, svgScaleRatio * svgBottom)}`" preserveAspectRatio="xMinYMin meet">n          <path :d="waves[0]">n            <animaten                dur="5s"n                repeatCount="indefinite"n                attributeName="d"n                :values="`${waves[0]}; ${waves[1]}; ${waves[0]};`"n            />n          </path>n        </svg>n        <svg class="Wave" :style="`bottom: 0; height: ${svgBottom}px;`" :viewBox="`0 ${amplitude_setting} 1000 ${Math.min(100, svgScaleRatio * svgBottom)}`" preserveAspectRatio="xMinYMin meet">n          <path :d="waves[0]">n            <animaten                dur="5s"n                repeatCount="indefinite"n                attributeName="d"n                :values="`${waves[0]}; ${waves[1]}; ${waves[0]};`"n            />n          </path>n        </svg>nn      <div ref="labels" class="nrdb-ui-gauge-tank-labels">n        <label class="nrdb-ui-gauge-tank--label" >{{ this.value }} T</label>n      </div>n    </div>n    </div>nn  </div>n</template>n<script>nexport default {nn  data() {n    return {n      labelLineHeight: 0,n      svgBottom: 0,n      amplitude_setting: 15,n      svgScaleRatio: 1,n      minivalue : 0,n      maxivalue : 50n    }n  },n  watch: {n    value: function () {n      this.$nextTick(() => {n        // react to the fill element being updatedn        this.updateMask()n      })n    }n  },n  computed: {nn    color: function ()n    {n      if (this.value <= this.min) {return "#FF0000"}n      return "#00FF00"n    }n    ,n    pc: function () {n      if (typeof this.value !== 'undefined') {n        return Math.max(0, Math.min(Math.round((this.value - this.minivalue) / (this.maxivalue - this.minivalue) * 100), 100))n      } else {nn        return 0n      }n    },n    clipId: function () {n      return `clip-${this.id}`n    },n    waves: function () {n      const amplitude = this.amplitude_setting * this.svgScaleRation      const svgBottom = this.svgBottomn      const svgScaleRatio = this.svgScaleRationn      return [n        `M750,${amplitude} c -125,0 -125,-${amplitude} -250,-${amplitude} s -125,${amplitude} -250,${amplitude} S 125,0, 0,0 v${svgScaleRatio * (svgBottom + amplitude)}h1000 V0 c-125,0 -125,${amplitude} -250,${amplitude}Z`,n        `M750,0 c -125,0 -125,${amplitude} -250,${amplitude} S 375,0 250,0, S 125,${amplitude}, 0,${amplitude} v${svgScaleRatio * svgBottom}h1000 V${amplitude} c-125,0 -125-${amplitude} -250-${amplitude}Z`n      ]n    }n  },n  methods: {n    updateMask () {n      const h = this.$refs.fill?.clientHeight || 0n      const w = this.$refs.fill?.clientWidth || 0nn      this.labelLineHeight =`${this.$refs.labels.clientHeight}px`nn      if (h >= 0 && this.pc !== 0) {n        this.svgBottom = hn        this.svgScaleRatio = w !== 0 ? 1000 / w : 1n      } else {n        this.svgBottom = 0n        this.svgScaleRatio = 1n      }n    },n    onResize () {n      this.updateMask()n    }n  },n  mounted () {nn    this.$nextTick(() => {n      this.updateMask()n    })nn    this.$socket.on('msg-input:' + this.id, (msg) => {n      this.value = msg.payloadn      this.min = msg.minn    })n  },n  unmounted() {nn  }n}nn</script>n<style scoped>n.Wave,n.WaveBG {n  width: 200%;n  position: absolute;n  overflow: visible;n  animation-name: swell;n  animation-fill-mode: forwards;n  animation-iteration-count: infinite;n  animation-timing-function: linear;n  fill: var(--gauge-fill);n  bottom: var(--gauge-fill-pc);n}nn.WaveBG {n  opacity: 0.4;n  /* offset the animation so that the wave's standing nodes never overlap */n  animation-duration: 1.5s;n  animation-delay: 1.2s;n}n.Wave {n  animation-duration: 2s;n}nn@keyframes swell {n  0% {n    transform: scaleX(2) translateX(25%) translateY(1px);n  }n  100% {n    transform: scaleX(2) translateX(-25%) translateY(1px);n  }n}nn.nrdb-ui-gauge-tank--container {n  display: flex;n  flex-direction: column;n  container: tank-container / size;n}n.nrdb-ui-gauge-tank {n  --tank-margin: 6px;n  --tank-padding: 3px;n  --tank-radius: 6px;n  --tank-border: 4px;n}nn@container tank-container (min-width: 75px) and (min-height: 75px) {n  .nrdb-ui-gauge-tank {n    --tank-margin: 12px;n    --tank-radius: 12px;n    --tank-border: 8px;n    --tank-padding: 6px;n  }n}nn.nrdb-ui-gauge-tank {n  border-radius: var(--tank-radius);n  border-width: var(--tank-border);n  padding: var(--tank-padding);n  border-color: var(--gauge-fill);n  border-style: solid;n  flex-grow: 1;n  position: relative;n}n.nrdb-ui-gauge-tank--fill {n  background-color: transparent;n}n.nrdb-ui-gauge-tank-labels {n  position: relative;n  width: 100%;n  height: 100%;n  display: flex;n  container-type: size;n}n.nrdb-ui-gauge-tank label {n  font-weight: bold;n  display:flex;n  resize: both;n  font-size: min(2.5rem, 50cqmin);n  position: absolute;n  z-index: 2;n  width: 100%;n  height: 100%;n  text-align: center;n  justify-content: center;n  align-items: center;n  color: black;n  --text-border: 1px;n  --text-border-inv: calc(-1 * 1px);n  --text-border-color: white;n  text-shadow: var(--text-border) var(--text-border-inv) var(--text-border-color),n  var(--text-border-inv) var(--text-border-inv) var(--text-border-color),n  var(--text-border-inv) var(--text-border) var(--text-border-color),n  var(--text-border) var(--text-border) var(--text-border-color);n}nnnrdb-ui-gauge-title {n  font-weight: bold;n  display:flex;n  resize: both;n  font-size: min(2.5rem, 50cqmin);n  position: absolute;n  z-index: 2;n  width: 100%;n  height: 100%;n  text-align: center;n  justify-content: center;n  align-items: center;n  color: black;nn}nn.nrdb-ui-gauge-tank--center {n  display: flex;n  justify-content: center;n  align-items: center;n  height: 100%;n  position: relative;n  overflow: hidden;n}nn/* text mask */n.nrdb-ui-gauge-tank svg.mask {n  top: 0;n}n.nrdb-ui-gauge-tank svg.mask,n.nrdb-ui-gauge-tank--fill {n  position: absolute;n  left: 0;n}n.nrdb-ui-gauge-tank--fill {n  bottom: 0;n  height: var(--gauge-fill-pc);n  width: 100%;n}n</style>",
    "storeOutMessages": true,
    "passthru": false,
    "resendOnRefresh": true,
    "templateScope": "local",
    "className": "",
    "x": 610,
    "y": 200,
    "wires": [
        []
    ]
},
{
    "id": "c6cc4137fbb3cdda",
    "type": "ui-template",
    "z": "7c0a6281fad8692c",
    "group": "",
    "page": "d30cf1a71bcee0b6",
    "ui": "",
    "name": "Dasboard 2.0 CSS",
    "order": 0,
    "width": 0,
    "height": 0,
    "head": "",
    "format": ".nrdb-ui-text {n    background-color: #eeeeee !important;n}nn.nrdb-ui-gauge {n        background-color: #eeeeee !important;n}nn.nrdb-ui-gauge-title {n        font-size: larger;n        background-color: aqua;n}nn.valid.valid-label label {n    color: black;n}nn.valid.dark-text span {n    color: black;n}nn.valid.valid-text span {n    color: green;n}nn.invalid.invalid-label label {n    color: black;n}nn.invalid.invalid-text span {n    color: red;n}nn",
    "storeOutMessages": true,
    "passthru": false,
    "resendOnRefresh": true,
    "templateScope": "page:style",
    "className": "",
    "x": 190,
    "y": 100,
    "wires": [
        []
    ]
},
{
    "id": "d0ccf216830a748d",
    "type": "ui-gauge",
    "z": "7c0a6281fad8692c",
    "name": "tank",
    "group": "fb6501fa7d8b6b7b",
    "order": 2,
    "width": "3",
    "height": "3",
    "gtype": "gauge-tank",
    "gstyle": "needle",
    "title": "TANK",
    "units": "Bar",
    "icon": "",
    "prefix": "",
    "suffix": "",
    "segments": [
        {
            "from": "0",
            "color": "#ff0000"
        },
        {
            "from": "4",
            "color": "#00ff40"
        }
    ],
    "min": 0,
    "max": "50",
    "sizeThickness": 16,
    "sizeGap": 4,
    "sizeKeyThickness": 8,
    "styleRounded": true,
    "styleGlow": false,
    "className": "custom-tank",
    "x": 590,
    "y": 260,
    "wires": []
},
{
    "id": "fb6501fa7d8b6b7b",
    "type": "ui-group",
    "name": "Group Name",
    "page": "fd51264148764b4f",
    "width": "12",
    "height": "1",
    "order": 1,
    "showTitle": true,
    "className": "",
    "visible": "true",
    "disabled": "false",
    "groupType": "default"
},
{
    "id": "fd51264148764b4f",
    "type": "ui-page",
    "name": "Page Name",
    "ui": "955d24bee66399fa",
    "path": "/exemple",
    "icon": "home",
    "layout": "grid",
    "theme": "4cbed8b158df0175",
    "breakpoints": [
        {
            "name": "Default",
            "px": "0",
            "cols": "3"
        },
        {
            "name": "Tablet",
            "px": "576",
            "cols": "6"
        },
        {
            "name": "Small Desktop",
            "px": "768",
            "cols": "9"
        },
        {
            "name": "Desktop",
            "px": "1024",
            "cols": "12"
        }
    ],
    "order": 1,
    "className": "",
    "visible": "true",
    "disabled": "false"
},
{
    "id": "955d24bee66399fa",
    "type": "ui-base",
    "name": "UI Name",
    "path": "/dashboard",
    "appIcon": "",
    "includeClientData": true,
    "acceptsClientConfig": [
        "ui-notification",
        "ui-control"
    ],
    "showPathInSidebar": false,
    "headerContent": "page",
    "navigationStyle": "default",
    "titleBarStyle": "default",
    "showReconnectNotification": true,
    "notificationDisplayTime": "1",
    "showDisconnectNotification": true
},
{
    "id": "4cbed8b158df0175",
    "type": "ui-theme",
    "name": "Default Theme",
    "colors": {
        "surface": "#ffffff",
        "primary": "#0094CE",
        "bgPage": "#eeeeee",
        "groupBg": "#ffffff",
        "groupOutline": "#cccccc"
    },
    "sizes": {
        "density": "default",
        "pagePadding": "12px",
        "groupGap": "12px",
        "groupBorderRadius": "4px",
        "widgetGap": "12px"
    }
}
]

Thanks in advance for your help!

Argument ‘….’ is not a function, got undefined

Getting this persistent error: “Error: [ng:areq] Argument ‘myController’ is not a function, got undefined” in my console, trying to set up an email registration form within a Piano.io iframe. The angular script is loaded natively by Piano so I don’t have to add it to my code. Ultimately, I’m trying to get the success message to show up after the email is submitted but it’s not showing up (ng-hide is being applied by angular) and given that I have no other errors and the email is being successfully retrieved per my console.log, I suspect this error has to first be resolved. I haven’t coded the logic for POST and GET etc yet, just trying to get this simulation to work. Been trying to work this out for a few days now so any insight at all would be much appreciated!

HTML

<div id="my-app" ng-app="myApp" ng-controller="myController">
<div class="my-content" id="my-content">
<div class="inner-container">
  <div class="some-info">
    <h2>Header</h2>
    <p>Text</p>
  </div> 
  
  <form class="my-form" id="registration-form" ng-submit="registerUser($event)">
    <div class="form">
      <label for="email">Email address:</label>
      <input type="email" id="user-email" ng-model="email" placeholder="Enter email" required>
      <button id="my-btn" type="submit" class="button">Continue</button>
    </div>
  </form>

  <div class="success-message" ng-show="showSuccessMessage">
    <p class="success-text">Thanks for registering! Your email has been submitted.</p>
  </div>
</div>

SCRIPT

<script>
tp = window.tp || [];
tp.push(["init", function() {
tp.pianoId.init({
  displayMode: 'inline',
  containerSelector: '#my-app'
});
}]);

var app = angular.module('myApp', []);

app.controller('myController', function($scope) {

$scope.showSuccessMessage = false;
$scope.successMessage = "";
 
$scope.registerUser = function($event) {
  $event.preventDefault();
  var email = $scope.email;

  if (!email || email.trim() === "") {
    console.error('Error: Email is missing or invalid.');
    alert('Please provide a valid email address to proceed.');
    return;
  }

  console.log('Email successfully retrieved:', email);

  // Register user with Piano
  tp.pianoId.show({
    screen: 'register',
    registrationSuccess: function(data) {
      console.log('Registration successful:', data);

      // Update AngularJS scope to show the success message
      $scope.$apply(function() {
        $scope.showSuccessMessage = true;
        $scope.successMessage = "Thank you for registering! A confirmation email has been sent to " + email + ".";
      });
    },
    registrationFailed: function(error) {
      console.error('Error during registration:', error);

      // Update AngularJS scope to handle registration failure
      $scope.$apply(function() {
        $scope.showSuccessMessage = false;
        alert('There was an issue creating your account. Please try again.');
      });
    }
 });
 };
 });
 </script>

Recursion problem traversing deeply nested function constructors

I’ve already failed to my exam for this task 🙁
I can’t iterate through all nested objects to get only ‘.js’ files. The property check for the function is not working in all of the cases and can not pass: test 3.
Just can not jump in a new Folder([“7.txt”, “9.js”])] to get “9.js” and my
expectedResult: [“1.js”, “2.js”, “4.js”, “6.js”, “7.js”, “8.js” ] must include “9.js” too. I will be appreciated for any help to fix the problem, thanks

mocha.setup("bdd");
let expect = chai.expect;
let assert = chai.assert; 

function findAllJavascriptFiles(folder) {
  return new Promise((resolve) => {
    let jsFiles = [];  
    function processFolder(f) {
      f.read().then((files) => {  
        const promises = files.map((file) => {  
           if (typeof file.read === "function") {  
            return processFolder(file); 
          } 
          
          if (typeof file === "string" ) {  
            if(file.endsWith(".js")) {
              jsFiles.push(file); 
            } 
          }   
        
        }); 
        
        Promise.all(promises).then(() => { 
          resolve(jsFiles);
        });
      });
    } 
    processFolder(folder);
  });
}

describe("let the test started", () => { 
  it("flat file system", function (done) { 
    const tests = [
      {
        root: new Folder(["1.js", "2.js", "3.js", "4.html"]),
        expectedResult: ["1.js", "2.js", "3.js"]
      },
      {
        root: new Folder([
          "1.js",
          "2.js",
          new Folder([new Folder(["3.txt"]), "4.js"]),
          new Folder(["5.png", "6.js", new Folder(["7.txt"])]),
          "8.html"
        ]),
        expectedResult: ["1.js", "2.js", "4.js", "6.js"]
      },
      {
        root: new Folder([
          "1.js",
          "2.js",
          new Folder([new Folder(["3.txt"]), "4.js"]),
          new Folder(["5.png", "6.js", new Folder(["7.txt"])]),
          new Folder([
            "5.png",
            "7.js",
            new Folder([
              "7.txt",
              new Folder(["5.png", "8.js", new Folder(["7.txt", "9.js"])])
            ])
          ]),
          "8.html"
        ]),
        expectedResult: ["1.js", "2.js", "4.js", "6.js", "7.js", "8.js", "9.js"]
      },
      {
        root: new Folder([
          "1.js",
          "2.js",
          new Folder([new Folder(["3.txt"]), "4.js"]),
          new Folder(["5.png", "6.js", new Folder(["7.txt"])]),
          new Folder([
            "5.png",
            "7.js",
            new Folder([
              "7.txt",
              new Folder([
                "5.png",
                "8.js",
                new Folder([
                  "7.txt",
                  "9.js",
                  new Folder([
                    "11.js",
                    "12.js",
                    new Folder([new Folder(["3.txt"]), "14.js"]),
                    new Folder(["5.png", "16.js", new Folder(["7.txt"])]),
                    new Folder([
                      "5.png",
                      "17.js",
                      new Folder([
                        "7.txt",
                        new Folder([
                          "5.png",
                          "18.js",
                          new Folder(["7.txt", "19.js"])
                        ])
                      ])
                    ]),
                    "8.html",
                    new Folder([
                      "21.js",
                      "22.js",
                      new Folder([new Folder(["3.txt"]), "24.js"]),
                      new Folder(["5.png", "26.js", new Folder(["7.txt"])]),
                      new Folder([
                        "5.png",
                        "27.js",
                        new Folder([
                          "7.txt",
                          new Folder([
                            "5.png",
                            "28.js",
                            new Folder(["7.txt", "29.js"])
                          ])
                        ])
                      ]),
                      "8.html"
                    ])
                  ])
                ])
              ])
            ])
          ]),
          "8.html"
        ]),
        expectedResult: [
          "1.js",
          "11.js",
          "12.js",
          "14.js",
          "16.js",
          "17.js",
          "18.js",
          "19.js",
          "2.js",
          "21.js",
          "22.js",
          "24.js",
          "26.js",
          "27.js",
          "28.js",
          "29.js",
          "4.js",
          "6.js",
          "7.js",
          "8.js",
          "9.js"
        ]
      }
    ];

    const testPromises = tests.map(({ root, expectedResult }) => { 
      return findAllJavascriptFiles(root).then((actualResult) => {
        console.log(`RESULT:`, actualResult);
        assert.deepEqual(actualResult.sort(), expectedResult.sort());
      });
    });

    Promise.all(testPromises)
      .then(() => {
        done();
      })
      .catch((err) => {
        console.log("err", err);
        done(err);
      });
  });
});

function Folder(files) {
  return {
    size: () => {
      return new Promise((res) => {
        res(files.length);
      });
    },
    read: () => {
      return new Promise((res) => {
        res(files);
      });
    }
  };
}

mocha.run();