Receiving duplicate FCM notifications on Android phone, works normally on desktop

I am making a Flask web app that uses the Google Sheets API to scan a school bus position spreadsheet and determine which section a bus is in. Then, it sends a notification with the user’s bus number, quadrant/section, and the buses it’s in between. The app works fine on desktop devices, but on Android, it sends duplicate notifications. One contains the site favicon, while the other doesn’t.

I thought this was a problem with ngrok, the tunneling service I was using to connect my phone to my laptop which is hosting the app over HTTPS, but as it turns out, connecting from a desktop device still doesn’t send duplicate notifications and works as expected, so I don’t think this is a problem with ngrok.Here is an extremely simplified version of my code, with all the irrelevant parts removed. It has the same issue as the extensive code.

Flask app:

from flask import Flask, request, jsonify, render_template, send_from_directory
import firebase_admin
from firebase_admin import credentials, messaging
from flask_cors import CORS
import os
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

app = Flask(__name__,
    template_folder='templates',
    static_folder='static',
    static_url_path=''
)
CORS(app)

# Initialize Firebase Admin SDK
cred = credentials.Certificate('Core/firetoken.json')  # Your Firebase credentials file
firebase_admin.initialize_app(cred)

@app.route('/firebase-messaging-sw.js')
def sw():
    response = send_from_directory(app.static_folder, 'firebase-messaging-sw.js')
    response.headers['Content-Type'] = 'application/javascript'
    response.headers['Service-Worker-Allowed'] = '/'
    return response

@app.route('/')
def home():
    return render_template('index.html',
        firebase_config=dict(
            api_key=os.getenv('FIREBASE_API_KEY'),
            auth_domain=os.getenv('FIREBASE_AUTH_DOMAIN'),
            project_id=os.getenv('FIREBASE_PROJECT_ID'),
            storage_bucket=os.getenv('FIREBASE_STORAGE_BUCKET'),
            messaging_sender_id=os.getenv('FIREBASE_MESSAGING_SENDER_ID'),
            app_id=os.getenv('FIREBASE_APP_ID'),
            measurement_id=os.getenv('FIREBASE_MEASUREMENT_ID')
        ),
        vapid_key=os.getenv('VAPID_KEY')
    )

@app.route('/store_token', methods=['POST'])
def store_token():
    data = request.json
    token = data.get('token')
    
    if not token:
        return jsonify({'error': 'Token is required'}), 400

    try:
        # Send a test notification
        message = messaging.Message(
            notification=messaging.Notification(
                title="Test Notification",
                body="This is a test notification!"
            ),
            token=token
        )
        messaging.send(message)
        return jsonify({'status': 'Notification sent successfully'})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True)

HTML Template:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Notification Test</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .container {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 10px 0;
        }
        #status {
            margin: 20px 0;
            padding: 10px;
            border-radius: 4px;
        }
        .success { background-color: #dff0d8; color: #3c763d; }
        .error { background-color: #f2dede; color: #a94442; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Notification Test</h1>
        <button id="send-notification">Send Test Notification</button>
        <p id="status"></p>
    </div>

    <script type="module">
        import { initializeApp } from "https://www.gstatic.com/firebasejs/11.0.1/firebase-app.js";
        import { getMessaging, getToken, onMessage } from "https://www.gstatic.com/firebasejs/11.0.1/firebase-messaging.js";

        const firebaseConfig = {
            apiKey: "{{ firebase_config.api_key }}",
            authDomain: "{{ firebase_config.auth_domain }}",
            projectId: "{{ firebase_config.project_id }}",
            storageBucket: "{{ firebase_config.storage_bucket }}",
            messagingSenderId: "{{ firebase_config.messaging_sender_id }}",
            appId: "{{ firebase_config.app_id }}",
            measurementId: "{{ firebase_config.measurement_id }}"
        };

        const vapidKey = "{{ vapid_key }}";

        try {
            const app = initializeApp(firebaseConfig);
            const messaging = getMessaging(app);

            // Register service worker
            if ('serviceWorker' in navigator) {
                navigator.serviceWorker.register('/firebase-messaging-sw.js')
                    .then(registration => console.log('Service Worker registered'))
                    .catch(err => console.error('Service Worker registration failed:', err));
            }

            document.getElementById('send-notification').addEventListener('click', async () => {
                try {
                    const permission = await Notification.requestPermission();
                    if (permission === 'granted') {
                        const currentRegistration = await navigator.serviceWorker.getRegistration();
                        const token = await getToken(messaging, { 
                            vapidKey: vapidKey,
                            serviceWorkerRegistration: currentRegistration
                        });

                        const response = await fetch('/store_token', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ token: token })
                        });

                        const result = await response.json();
                        if (!response.ok) throw new Error(result.error);

                        document.getElementById('status').innerText = 'Notification sent successfully!';
                        document.getElementById('status').className = 'success';
                    } else {
                        throw new Error('Notification permission denied');
                    }
                } catch (error) {
                    document.getElementById('status').innerText = `Error: ${error.message}`;
                    document.getElementById('status').className = 'error';
                }
            });

            // Listen for messages
            onMessage(messaging, (payload) => {
                document.getElementById('status').innerText = `Received: ${payload.notification.title} - ${payload.notification.body}`;
                document.getElementById('status').className = 'success';
            });

        } catch (error) {
            console.error('Initialization error:', error);
            document.getElementById('status').innerText = `Error: ${error.message}`;
            document.getElementById('status').className = 'error';
        }
    </script>
</body>
</html>

Here is a screenshot of the problem:
Screenshot of duplicate Android notifications

The dropdown menu is not expanding. CSS and JS are connected correctly

I am a beginner in web development. I need some help. The dropdown menu is not expanding. CSS and JS are connected correctly. What could be the reason?

I checked the CSS/JS classes and connected files. Everything seems to be correct, I even used GPT, but no results. 🙁 I can provide other project files if needed. Here is the code from the main file layout.html.

{% load static %}
<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Головна{% endblock %}</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{% static 'main/css/main.css' %}">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/5.10.1/main.min.css">
</head>
<body>
    <button class="btn btn-primary" id="sidebarToggle">
        <i class="fas fa-bars"></i>
    </button>
    <div class="d-flex">
        <aside class="flex-shrink-0 p-3 bg-dark text-white sidebar" id="sidebar">
            <a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
                <img src="{% static 'main/img/2.png' %}" alt="Logo" class="small-logo">
            </a>
            <hr>
            <ul class="nav nav-pills flex-column mb-auto">
                <li class="nav-item">
                    <a href="{% url 'layout' %}" class="nav-link text-white {% if request.resolver_match.url_name == 'layout' %}active{% endif %}" aria-current="page">
                        <i class="fas fa-heart"></i> Головна
                    </a>
                </li>
                <li>
                    <a href="{% url 'staff' %}" class="nav-link text-white {% if request.resolver_match.url_name == 'staff' %}active{% endif %}">
                        <i class="fas fa-users"></i> Особовий склад
                    </a>
                </li>
                <li>
                    <a href="{% url 'calendar' %}" class="nav-link text-white {% if request.resolver_match.url_name == 'duty' %}active{% endif %}">
                        <i class="fas fa-list"></i> Подати наряд
                    </a>
                </li>
                <li>
                    <a href="{% url 'profile' %}" class="nav-link text-white {% if request.resolver_match.url_name == 'profile' %}active{% endif %}">
                        <i class="fas fa-user"></i> Профіль
                    </a>
                </li>
                <li>
                    <a href="{% url 'calendar' %}" class="nav-link text-white {% if request.resolver_match.url_name == 'calendar' %}active{% endif %}">
                        <i class="fas fa-calendar"></i> Календар
                    </a>
                </li>
            </ul>
            <li class="nav-item">
                <a class="nav-link dropdown-toggle text-white" href="#" id="dropdownMenuLink" data-bs-toggle="dropdown" aria-expanded="false">
                    Dropdown
                </a>
                <ul class="dropdown-menu" aria-labelledby="dropdownMenuLink">
                    <li><a class="dropdown-item" href="#">Action</a></li>
                    <li><a class="dropdown-item" href="#">Another action</a></li>
                    <li><a class="dropdown-item" href="#">Something else here</a></li>
                </ul>
            </li>
        </ul>
        
        </aside>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.1.3/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/5.10.1/main.min.js"></script>
    <script src='https://cdn.jsdelivr.net/npm/@fullcalendar/[email protected]/index.global.min.js'></script>
    <script src='https://cdn.jsdelivr.net/npm/@fullcalendar/[email protected]/index.global.min.js'></script>
    <script src='https://cdn.jsdelivr.net/npm/@fullcalendar/[email protected]/index.global.min.js'></script>
    <script>
        document.getElementById('sidebarToggle').addEventListener('click', function () {
            document.getElementById('sidebar').classList.toggle('active');
            this.classList.toggle('active');
        });

        document.addEventListener('DOMContentLoaded', function() {
            var calendarEl = document.getElementById('calendar');
            if (calendarEl) {
                var calendar = new FullCalendar.Calendar(calendarEl, {
                    initialView: 'dayGridMonth',
                    locale: 'uk',
                    editable: true,
                    selectable: true,
                    events: '/get_duties/',  // Завантаження нарядів з сервера
                    select: function(info) {
                        var dutyDateInput = document.getElementById('dutyDate');
                        dutyDateInput.value = info.startStr;
                        var dutyFormModal = new bootstrap.Modal(document.getElementById('dutyFormModal'));
                        dutyFormModal.show();
                        calendar.unselect();
                    },
                    eventClick: function(info) {
                        if (confirm('Ви впевнені, що хочете видалити цю подію?')) {
                            $.ajax({
                                type: 'POST',
                                url: '{% url "delete_duty" %}',
                                data: {
                                    'id': info.event.id,
                                    'csrfmiddlewaretoken': '{{ csrf_token }}'
                                },
                                success: function(response) {
                                    if (response.status === 'success') {
                                        info.event.remove();
                                    } else {
                                        alert('Сталася помилка при видаленні наряду.');
                                    }
                                },
                                error: function(response) {
                                    alert('Сталася помилка при видаленні наряду.');
                                }
                            });
                        }
                    }
                });
                calendar.render();
            }

            $('#dutyForm').on('submit', function(event) {
                event.preventDefault();
                $.ajax({
                    type: 'POST',
                    url: $(this).attr('action'),
                    data: $(this).serialize(),
                    success: function(response) {
                        $('#dutyFormModal').modal('hide');
                        calendar.refetchEvents();
                    },
                    error: function(response) {
                        var errors = response.responseJSON.errors;
                        var errorMessage = 'Сталася помилка при подачі наряду:n';
                        for (var field in errors) {
                            errorMessage += field + ': ' + errors[field].join(', ') + 'n';
                        }
                        alert(errorMessage);
                    }
                });
            });
        });
    </script>
</body>
</html>

Vue JS event listeners don’t work and vanilla work ( composition api+ script setup )

Problem: I created a custom component that works as a searchable selection which via vue’s dom event handlers (@input, @focus, watch) detects client input and performs the search on the server.

My component works perfectly (maybe it could be perfected) on all devices except my phone (Xiamo Redmi 12) in which I use a custom keyboard for convenience. i thought that was the problem instead not.
Trying to detect the input with a vanilla javascript eventListener works while with vue handlers, the input is detected only when I ‘unselect’ the input.

<template>
<div ref="inputContainer" class="input-select-container" >
    <label v-if="label">{{ label }}</label>
    <input 
      v-model="model" 
      :type 
      id="search-input"
      :placeholder 
      :required="required"
      @focus="handleShowList"  
      @input="handleShowList"
      autocomplete="off"
      :class="class"
    />
    <div class="input-select-list" v-if="insideList">
      <ul v-if="showList" ref="selectList">
        <slot></slot>
      </ul>
    </div>
  </div>

</template>

<script setup>
  import { ref, onUnmounted, onMounted, watch } from 'vue';
  

  const model     = defineModel();
  const emits     = defineEmits(['show-list']);

  const props     = defineProps({
    type: {
      type: String,
      default: 'search'
    },
    id: {
      type: String,
      default: 'input'
    },
    label: {
      type: String,
      default: null
    },
    placeholder: {
      type: String,
      default: ''
    },
    required: {
      type: Boolean,
      default: false
    },
    class: {
      type: String,
      default: ''
    },
    insideList: {
      type: Boolean,
      default: true
    }
  });

  const showList        = ref(false);
  const inputContainer  = ref(null);
  const selectList      = ref(null);


  function handleClickOutside(event) {
    if (inputContainer.value && !inputContainer.value.contains(event.target)) {
      handleHideList();
    }
  }

  function handleShowList(){
    showList.value = true;
  }

  function handleHideList(){
    showList.value = false;
  }
  //not work
  watch(showList, (value) => {
    emits('show-list', value);
  });
  //not work
  watch(model, (newValue, oldValue) => {
    console.log('Input value changed:', newValue); // Log per debug
    showList.value = true; // Mostra la lista ogni volta che l'input cambia
  });

  

  onMounted(() => {
    document.addEventListener('click', handleClickOutside);
    //WORK
    document.getElementById('search-input').addEventListener('input', function(){
      console.log('Input value changed:', this.value);
    });
  });

  onUnmounted(() => {
    document.removeEventListener('click', handleClickOutside);
  });

  defineExpose({
    handleHideList
  });


</script>

Firefox – Javascript setInterval WAY WAY off

I’m trying to scroll text up the screen in about 4min +/- a few seconds doesn’t even matter, along with a 4min audio.

To follow it, I’ve muted the audio 1 second every 10 seconds.

I display the elapsed time in the console. In Chrome and Edge, the counter in the console is precisely on the 10 second mark every time the song mutes. 10, 20, 30, 40, etc., and the text follows the audio.

In Firefox, 133.0.3 (64bit), the song seems to mute when the counter counts 8 seconds, 8, 16, 24, 32… Despite the song still playing its exact time duration.

My script has 1400 lines, here are the lines in question. Perhaps I’m not writing the setInterval properly for Firefox?? Is this a Firefox bug? Any idea what’s going on?

var pri;
var tm3 = 70;
var pry = 650;
var pryc = 0;

pri = setInterval(rosary.promise, tm3);

this.promise = function() {
  pryc++;
  console.log(pryc * tm3 / 1000); // Display elapsed seconds
  pry -= 1;
  texts.promise(pry);
  if (pry < -2677) {
    clearInterval(pri);
  }
}

var Texts = function() {
  this.promise = function(yp) {
    yp = (yi + yp) * rtr;
    t[gdPro].style.top = yp + 'px';
    t[gdPro].style.visibility = 'visible';
  }
}
var texts = new Texts();

react-navigation/native error: viewmanagerresolver returned null for either RNSScreenContentWrapper or RCTRNSScreenContentWrapper

in a fresh created react native project, bootstrapped using @react-native-community/cli, i installed @react-navigation/native-stack guided by youtube video.
I ended up with code:

import {NavigationContainer} from "@react-navigation/native"
import {createNativeStackNavigator} from "@react-navigation/native-stack";
import WelcomeReactNative from "./screens/WelcomeReactNative.tsx";

const Stack = createNativeStackNavigator()

function App(): React.JSX.Element {

    return (
        // <WelcomeReactNative/>
        <NavigationContainer>
            <Stack.Navigator initialRouteName='WelcomeReactNative'>
                <Stack.Screen name='WelcomeReactNative' component={WelcomeReactNative}/>
            </Stack.Navigator>
        </NavigationContainer>
    );
}

export default App;

It didn’t work. Error pop up in emulator from with i cant copy text
enter image description here.

and i dont fined it DevTools console

without it, prj runs fine

return (
        <WelcomeReactNative/>
        // <NavigationContainer>
        //     <Stack.Navigator initialRouteName='WelcomeReactNative'>
        //         <Stack.Screen name='WelcomeReactNative' component={WelcomeReactNative}/>
        //     </Stack.Navigator>
        // </NavigationContainer>
    );

I want to add navigation functionality, but i am not sure how to move forward.
After error message showing, i have to press”a – run on Android” in Metro console, in order to be able to run app again.

my package.json

{
  "name": "TestApp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start",
    "test": "jest"
  },
  "dependencies": {
    "@react-navigation/native-stack": "^7.2.0",
    "react": "18.3.1",
    "react-native": "0.76.5"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@babel/preset-env": "^7.25.3",
    "@babel/runtime": "^7.25.0",
    "@react-native-community/cli": "15.0.1",
    "@react-native-community/cli-platform-android": "15.0.1",
    "@react-native-community/cli-platform-ios": "15.0.1",
    "@react-native/babel-preset": "0.76.5",
    "@react-native/eslint-config": "0.76.5",
    "@react-native/metro-config": "0.76.5",
    "@react-native/typescript-config": "0.76.5",
    "@types/react": "^18.2.6",
    "@types/react-test-renderer": "^18.0.0",
    "babel-jest": "^29.6.3",
    "eslint": "^8.19.0",
    "jest": "^29.6.3",
    "prettier": "2.8.8",
    "react-test-renderer": "18.3.1",
    "typescript": "5.0.4"
  },
  "engines": {
    "node": ">=18"
  }
}

GitHub

Redirecting (wp safe redirect) with delay for clientdata to be collected

I’ve written a custom script that generates a fingerprint based on both server-side and client-side data to block users who keep coming back with different IP addresses. I know it’s not 100% foolproof, but it’s a last-resort failsafe that is currently working smoothly.

The issue I’m encountering occurs when a page is first loaded in a new session. The PHP code is executed before the JavaScript code has a chance to collect the client-side data. Specifically, the fingerprint function is called, but since there is no fingerprint in the session at that point, generate_fingerprint() is triggered. However, when the client-side data is collected, there is no POST data available (because the JavaScript hasn’t run yet), causing the function to return null. This causes the blocking mechanism to fail in this scenario.

This is the code piece I currently have (in combination with a javascript file) to generate the client-data based information for the fingerprint:

function generate_advanced_fingerprint() {
    $components = [
        'screen_resolution' => $_POST['screen_resolution'] ?? '',
        'color_depth' => $_POST['color_depth'] ?? '',
        'canvas_fingerprint' => $_POST['canvas_fingerprint'] ?? '',
        'webgl_fingerprint' => $_POST['webgl_fingerprint'] ?? '',
        'audio_fingerprint' => $_POST['audio_fingerprint'] ?? '',
        'installed_fonts' => $_POST['installed_fonts'] ?? '',
        'hardware_concurrency' => $_POST['hardware_concurrency'] ?? '',
        'device_memory' => $_POST['device_memory'] ?? '',
        'browser_features' => $_POST['browser_features'] ?? ''
    ];

    if (in_array('', $components, true)) {
        return null;
    }
    ksort($components);
    $structured_data = json_encode($components);
    $fingerprint = md5($structured_data);
    return $fingerprint;
}

Note: The javascript is execute in the top of the <head> section

This is the code I use to generate the fingerprint and do the block check against the blocklist (a JSON file):

function handle_fingerprint() {
    if (!is_page('test')) {
        return;
    }

    $is_post_request = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST']));

    if ($is_post_request) {
        // Process the POST data with client-side information
        $raw_post_data = file_get_contents('php://input');
        $json_data = json_decode($raw_post_data, true);
        if ($json_data) {
            $_POST = array_merge($_POST, $json_data);
        }
    }

    // Generate or retrieve the fingerprint
    if (!isset($_SESSION['fingerprint_generated']) || $is_post_request) {
        $fingerprint = generate_fingerprint();
        $_SESSION['fingerprint_generated'] = true;
        $_SESSION['current_fingerprint'] = $fingerprint;

        // Save the fingerprint if it is a POST request
        if ($is_post_request) {
            $ip = get_visitor_ip();
            save_fingerprint($ip, $fingerprint);
            
            // Send a response to the POST request
            header('Content-Type: application/json');
            echo json_encode(['success' => true]);
            exit;
        }
    } else {
        $fingerprint = $_SESSION['current_fingerprint'];
    }

    // Run the check
    $blocked_fingerprints = get_blocked_fingerprints();

    // Check fallback fingerprint
    if (in_array($fingerprint['fallback_fingerprint'], $blocked_fingerprints)) {
        wp_safe_redirect(home_url('/'));
        exit;
    }

    // Check advanced fingerprint
    if (isset($fingerprint['advanced_fingerprint']) && in_array($fingerprint['advanced_fingerprint'], $blocked_fingerprints)) {
        wp_safe_redirect(home_url('/'));
        exit;
    }

}

// Check the fingerprint on every pageload
add_action('wp', 'handle_fingerprint');

// Process POST requests for fingerprint data
add_action('init', function() {
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST'])) {
        handle_fingerprint();
    }
});

In this scenario (with a new session), after the page loads, the JavaScript executes window.getFingerprint(). This function collects client-side data and sends it to the server via a POST request. This triggers the handle_fingerprint() function again. Now the POST data is available, so generate_advanced_fingerprint() can execute successfully. A complete fingerprint is generated.

The result is a double call (which I can make visible via my error logs).

I’ve tried generating the fingerprint only once all the data is available:

    $is_post_request = ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['HTTP_X_FINGERPRINT_REQUEST']));

    if ($is_post_request) {
        // Process the POST data with client-side information
        $raw_post_data = file_get_contents('php://input');
        $json_data = json_decode($raw_post_data, true);
        if ($json_data) {
            $_POST = array_merge($_POST, $json_data);
        }
    }

Which works, but by the time the clientdata is available, the content has already been served, and the redirect mechanism doesn’t work anymore. Now, I’m trying to find a way to delay the blocking check until the client-side data is available, but so far, I haven’t been successful.

Note:

  • I want to avoid showing error messages. I rather have them redirected to a “faux” page designed to distract rather than making them any wiser than they are.

  • I also want to avoid using javascript for the redirect. I would like to stay with the wp_safe_redirect() or something likewise if possible.

Can anyone help me out with a fresh look at it?

Update with C# and React [closed]

I need to update my database using C# in the back-end and React in front-end.

Back-end

[HttpPost("update")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<Resultado<Analise>> AnaliseAsync([FromBody] Analise analise)

{
    _telemetria.AdicionarPropriedade("NomeUsuario", _login);
    _telemetria.EscreverTelemetria(GetType().Name, "Atualizar análise", TipoSeveridade.Informacao);

    if (analise == null)
    {
        return Resultado<Analise>.AdicionarErro(
          null,
          CreditOn.Dominio.Enums.StatusCode.Status400BadRequest,
          "Dados inválidos."
        );
    }

    var resultado = await _servicoAnalise.AtualizarAnalise(analise.AnaliseId, _login);

    if (resultado.Erro)
    {
        return Resultado<Analise>.AdicionarErro(
          null,
          resultado.StatusCode,
          resultado.Erros
        );
    }

    _telemetria.EscreverTelemetria(
      "AtualizarAnálise",
      "Análise atualizada com sucesso.",
      TipoSeveridade.Informacao,
      propriedades: new Dictionary<string, object> { { "ID_Analise", analise.AnaliseId } }
    );
    return Resultado<Analise>.AdicionarSucesso(analise);
}```


**Front-end**


interface EditableTableProps {
    resultado: Resultado[];
    onUpdate: (updatedData: Resultado[]) => void;
}


const EditableTable = ({ resultado, onUpdate }: EditableTableProps) => {
    const [data, setData] = useState<Resultado[]>(Array.isArray(resultado) ? resultado : []);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);
    

    console.log('Análise:', data)
    

    const handleInputChange = (
        e: React.ChangeEvent<HTMLInputElement>,
        field: string,
        itemId: string
    ) => {
        const updatedData = data.map((analise) => ({
            ...analise,
            analiseDemonstrativo: analise.analiseDemonstrativo?.map((item) =>
                item.analiseDemonstrativoId === itemId
                    ? { ...item, [field]: e.target.value }
                    : item
            ) || [],
        }));
    
        setData(updatedData);
        onUpdate(updatedData);
    };
    console.log('resultado:', resultado)
    console.log('result:', result)
    const handleSave = async () => {
        setLoading(true);
        setError(null);
    
        try {
            const payload = {
                analiseId: analiseId, // Adicionar o analiseId aqui
                analises: data.map((analise) => ({
                    analiseId: analise.analiseId,
                    analiseDemonstrativo: analise.analiseDemonstrativo?.map((item) => ({
                        analiseDemonstrativoId: item.analiseDemonstrativoId,
                        tipoParametro: item.tipoParametro,
                        valor: item.valor,
                        // Adicione outros campos necessários aqui
                    })) || [],
                })),
            };
            
            console.log('payload:', JSON.stringify(payload, null, 2));
    
            const response = await axios.post(
                'https://localhost:7116/analise/update',
                payload,
                { headers: { 
                    'Authorization': `Bearer ${token}`,
                    'Content-Type': 'application/json' 
                } }
            );
    
            console.log('Dados enviados com sucesso:', response.data);
            setResult(response.data.valor || []);
            alert('Dados atualizados com sucesso!');
        } catch (err) {
            console.error('Erro ao atualizar dados:', err);
            setError('Ocorreu um erro ao enviar os dados. Verifique os valores e tente novamente.');
            
        } finally {
            setLoading(false);
        }
    };
    
    return (
        <div>
            <table style={{ width: '100%', border: '1px solid black', borderCollapse: 'collapse' }}>
                <thead>
                    <tr>
                        <th style={{ border: '1px solid black', padding: '8px' }}>Tipo Parâmetro</th>
                        <th style={{ border: '1px solid black', padding: '8px' }}>Valor</th>
                    </tr>
                </thead>
                <tbody>
                {data.map((analise) =>
                    analise.analiseDemonstrativo?.map((item: any) => (
                        <tr key={item.analiseDemonstrativoId}>

                            <td style={{ border: '1px solid black', padding: '8px' }}>
                                <input
                                    type="number"
                                    value={item.tipoParametro}
                                    onChange={(e) => handleInputChange(e, 'tipoParametro', item.analiseDemonstrativoId)}
                                    style={{ width: '100%' }} />
                            </td>

                            <td style={{ border: '1px solid black', padding: '8px' }}>
                                <input
                                    type="number"
                                    value={item.valor}
                                    onChange={(e) => handleInputChange(e, 'valor', item.analiseDemonstrativoId)}
                                    style={{ width: '100%' }} />
                            </td>

                        </tr>
                    )) || []
                )}
                </tbody>
            </table>

            {/* Botão de envio */}
            <button
                onClick={handleSave}
                style={{
                    marginTop: '10px',
                    padding: '8px 16px',
                    backgroundColor: '#4CAF50',
                    color: 'white',
                    border: 'none',
                    cursor: 'pointer',
                }}
                disabled={loading}
            >
                {loading ? 'Salvando...' : 'Salvar Alterações'}
            </button>

            {/* Feedback de erro */}
            {error && <p style={{ color: 'red' }}>{error}</p>}
        </div>
    );
};

const TableAnalysis: React.FC<{ resultado: any[] }> = ({ resultado }) => {
    if (!resultado || resultado.length === 0) {
        return <p>Nenhum dado encontrado.</p>;
    }

    const handleUpdate = (updatedData: Resultado[]) => {
        // Atualize o estado ou faça algo com os dados atualizados
        console.log('Dados atualizados:', updatedData);
    };

    return <EditableTable resultado={resultado} onUpdate={handleUpdate} />;
};

    const handleUpdate = (updatedData: Resultado[]) => {
        setResult(updatedData);
    };

    if (!pdfUrl) {
        return <p>Carregando ou URL do PDF não encontrada.</p>;
    }

    return (
        <Container>
            <main>
                <div className="Container1">
                    <h1>Page</h1>
                    
                    <div className="row">
                        <div className="col1">
                            <h1>Visualização do PDF</h1>
                           <VisualizationPage pdfUrl={pdfUrl} />
                            <PdfButton url="https://extratordev.blob.core.windows.net/95e6b155-8152-4c3c-8430-bef3333be2aa/07486111-c9fe-44ce-a5df-707985b7d2f7-Balanco_Patrimonial_2022-CostaSul.pdf" label="Abrir PDF 1" onClick={setPdfUrl} />
                            <PdfButton url="https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK" label="Abrir PDF 2" onClick={setPdfUrl} />
                            <PdfButton url="./Costa_Sul.pdf" label="Abrir PDF 3" onClick={setPdfUrl} /> 
                        </div>
                        <div className="col2">
                            <h2>Indicadores</h2>
                            <div className="Indicators">
                                <h2>Análise Demonstrativa</h2>
                                <TableAnalysis
                                    resultado={(result || [])}
                                />

                            </div>
                            <MyButton />
                        </div>
                    </div>
                </div>
            </main>
        </Container>
    );
};

My payload in the console

payload: {
  "analiseId": "30d972df-9f8f-491c-8e****",
  "analises": [
    {
      "analiseId": "30d972df-9f8f-491***",
      "analiseDemonstrativo": [
        {
          "analiseDemonstrativoId": "45b93269-e007-4f23-a04***",
          "tipoParametro": "2",
          "valor": 30000
        }
      ]
    }
  ]
}

I got data updated with success:

{statusCode: 200, erro: false, erros: Array(0), valor: {…}}
erro
:
false
erros
:
[]
statusCode
:
200
valor
:
{analiseId: ’30d972df-9f8f-491c-8e76-fdae76eeac2c’, cnpjOuCpf: null, statusId: ‘00000000-0000-0000-0000-000000000000’, tipoAnalise: 0, observacao: null, …}
[[Prototype]]
:
Object

An the empty result like this:

analiseCliente
:
[]
analiseCredito
:
[]
analiseCriterio
:
[]
analiseDemonstrativo
:
[]
analiseDocumentacao
:
[]
analiseEmpresa
:
[]
analiseId
:
“30d972df-9f8f-491c-8e76-fdae76eeac2c”
analiseIndicador
:
[]
analiseMensagem
:
[]
analiseMetadado
:
[]
analisePorte
:
[]
analiseSociedade
:
[]
classificacaoInterna
:
null
cnpjMatriz
:
null
cnpjOuCpf
:
null
dataEdicao
:
“0001-01-01T00:00:00”
dataInclusao
:
“0001-01-01T00:00:00”
nomeAssessor
:
null
observacao
:
null
parecer
:
null
parecerComercial
:
null
status
:
null
statusId
:
“00000000-0000-0000-0000-000000000000”
tipoAnalise
:
0
usuarioEdicao
:
null
usuarioInclusao
:
null
valorSolicitado
:
0
[[Prototype]]
:
Object

How can I change my code to obtain simpler form and brighter blinking dots [closed]

I want to obtain blinking dots one after one in one direction and then blinking have to go back, and all have to happen in the loop. I have created CSS code with Javascript according to first five dots. What can I do to make shorter all reqired code in CSS? Also I want to make not visible black backgrounds of the dots during blinking.

for (let i = 0; i < 40; i++) {
  const dots = document.createElement("div");
  document.body.appendChild(dots).classList.add("dots");
}
body {
  background-color: #737373;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  min-height: 100vh;
  overflow: hidden;
}

.dots {
  background-color: rgb(0, 0, 0);
  width: 20px;
  height: 20px;
  border-radius: 50%;
}

.dots:nth-of-type(1)::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  animation: bright 0.3s linear;
  background-color: rgb(255, 255, 255);
  box-shadow: 0 0 3px 3px rgb(255, 255, 255),
    0 0 5px 5px rgb(255, 255, 255), 0 0 12px 12px rgb(255, 255, 255);
  opacity: 0;
  animation-delay: 0s;
}

.dots:nth-of-type(2)::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  animation: bright 0.3s linear;
  background-color: rgb(255, 255, 255);
  box-shadow: 0 0 3px 3px rgb(255, 255, 255),
    0 0 5px 5px rgb(255, 255, 255), 0 0 12px 12px rgb(255, 255, 255);
  opacity: 0;
  animation-delay: 0.25s;
}

.dots:nth-of-type(3)::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  animation: bright 0.3s linear;
  background-color: rgb(255, 255, 255);
  box-shadow: 0 0 3px 3px rgb(255, 255, 255),
    0 0 5px 5px rgb(255, 255, 255), 0 0 12px 12px rgb(255, 255, 255);
  opacity: 0;
  animation-delay: 0.5s;
}

.dots:nth-of-type(4)::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  animation: bright 0.3s linear;
  background-color: rgb(255, 255, 255);
  box-shadow: 0 0 3px 3px rgb(255, 255, 255),
    0 0 5px 5px rgb(255, 255, 255), 0 0 12px 12px rgb(255, 255, 255);
  opacity: 0;
  animation-delay: 0.75s;
}

.dots:nth-of-type(5)::after {
  content: "";
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  animation: bright 0.3s linear;
  background-color: rgb(255, 255, 255);
  box-shadow: 0 0 3px 3px rgb(255, 255, 255),
    0 0 5px 5px rgb(255, 255, 255), 0 0 12px 12px rgb(255, 255, 255);
  opacity: 0;
  animation-delay: 1s;
}

@keyframes bright {

  0%,
  100% {
    opacity: 0;
  }

  50% {
    opacity: 1;
  }
}

How to retrieve automatic values from the Y-axis in the recharts library?

The domain parameter is responsible for generating values on the Y-axis. If expenses are added, the axis generates 5 points (tickCount={5}) from 0 to the "auto" value, which returns an automatically generated value from the library.

Here is my question: how can I retrieve the highest value among the generated values on the Y-axis?

<YAxis
   dataKey="totalExpenses"
   orientation="right"
   type="number"
   tickCount={5}
   fontSize={10}
   fontWeight={500}
   width={40}
   domain={noExpenses || noData ? [0, 120] : [0, "auto"]}
/>

Example Chart

What I need to achieve is, for example, as shown in the sample image, I need to retrieve the value 600, which is the highest value from the Y-axis that was automatically generated in the YAxis component.

Review box disappears when slider to the left is clicked

I am very new to JavaScript.

My review slider works on the button to the right but when left is clicked and try to get from the first to the left last slide the review box disappears

const slides = document.querySelectorAll('.slide')
const leftBtn = document.getElementById('left')
const rightBtn = document.getElementById('right')

let activeSlide = 0

rightBtn.addEventListener('click', () => {
  activeSlide++

  if (activeSlide > slides.length - 1) {
    activeSlide = 0
  }

  setActiveSlide()
})

leftBtn.addEventListener('click', () => {
  activeSlide--

  if (activeSlide > slides.length - 1) {
    activeSlide = slides.length - 1
  }

  setActiveSlide()
})

function setActiveSlide() {
  slides.forEach(slide => {
    slide.classList.remove('active')
    slide.classList.add('hide')
  })

  slides [activeSlide].classList.add('active')
  slides [activeSlide].classList.remove('hide')
}
<section class="section-recensies">
    <div class="recensies container">
        <h2>Recensies</h2>
        <div class="slider-container">

            <div class="recensie-box slide active">
                <p class="recensie-tekst">Recensie</p>
                <img src="{{ asset('images/logo/grace-business-group-lijn-met-kroon.png') }}"
                     alt="Logo Grace Business Group met lijn">
                <p>
                    <span>Naam</span>
                    <span>Bedrijf</span>
                </p>
            </div>

            <div class="recensie-box slide hide">
                <p class="recensie-tekst">Recensie</p>
                <img src="{{ asset('images/logo/grace-business-group-lijn-met-kroon.png') }}"
                     alt="Logo Grace Business Group met lijn">
                <p>
                    <span>Naam</span>
                    <span>Bedrijf</span>
                </p>
            </div>

            <div class="recensie-box slide hide">
                <p class="recensie-tekst">Recensie</p>
                <img src="{{ asset('images/logo/grace-business-group-lijn-met-kroon.png') }}"
                     alt="Logo Grace Business Group met lijn">
                <p>
                    <span>Naam</span>
                    <span>Bedrijf</span>
                </p>
            </div>

            <button class="recensie-button recensie-button-links" id="left"><</button>
            <button class="recensie-button recensie-button-rechts" id="right">></button>

        </div>
    </div>
</section>

I can’t see why it is not working because it does go to the left. When I click the right button the review box appears again.

Angular Jasmine tests that continue upon previous tests

I have a service called items here is the spec file:

import { TestBed } from '@angular/core/testing';

import { ItemsService } from './items.service';

describe('ItemsListService', () => {
  let service: ItemsService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(ItemsService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should insert new item', () => {
    const item = {
      id: '123',
      name: 'item',
      baseUnit: 'unit',
      brand: 'brand',
      price: 100,
      quantity: 10,
      supplier: 'supplier',
      cost: 50,
      imageUrl: 'url',
    };
    service.insertItem(item);
    const items = service.getItems();
    expect(items.length).toBeGreaterThan(0);
    expect(items).toContain(item);
  });
});

And here is another service called orders that depends on items service to create orders from items stored.

import { TestBed } from '@angular/core/testing';

import { OrdersService } from './orders.service';

describe('OrdersService', () => {
  let service: OrdersService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(OrdersService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should insert new order', async () => {
    const newOrder = await service.insertOrder(itemsService.getItems()[0]);
  });
});

So in order for orders service to create a new order it needs to access the item stored inside items service here service.insertOrder(itemsService.getItems()[0]).

How do I access the same instance of items service created in the spec file?

Syncing two folders

I created custom script to sync data from 2 folders in Google Drive.
To be blunt this is due to me emulating gameboy/gameboy advance on both android device and PC.
The emulator on PC works fine for both GB/GBA games and it stores save files in specified location (does not diffrentiate ROM type, stores and load all .sav files from set location)
But on android I have two emulators (one for GB one for GBA) and each of them have option of syncing with google cloud, but no custom selecting target folder (these are by default named after emulators)
I of course selected one of these folder as save file location for PC emmulator, but it lacks saves from other.
I created google apps script that triggers based on timer (for testing set once every 5 minutes)

function syncFolders() {
  const gbcFolderId = <link to GB folder>; 
  const gbaFolderId = <link to GBA folder>; 

  const gbcFolder = DriveApp.getFolderById(gbcFolderId);
  const gbaFolder = DriveApp.getFolderById(gbaFolderId);

  syncFolderFiles(gbcFolder, gbaFolder); // Sync from GBC to GBA
  syncFolderFiles(gbaFolder, gbcFolder); // Sync from GBA to GBC
}

function syncFolderFiles(sourceFolder, targetFolder) {
  const sourceFiles = sourceFolder.getFiles();
  const targetFilesMap = createFileMap(targetFolder);

  while (sourceFiles.hasNext()) {
    const sourceFile = sourceFiles.next();
    const sourceName = sourceFile.getName();
    const sourceDate = sourceFile.getLastUpdated();

    if (
      targetFilesMap[sourceName] &&
      targetFilesMap[sourceName].date >= sourceDate
    ) {
      continue; // Skip if target file is newer or the same
    }

    // Copy or update the file in the target folder
    if (targetFilesMap[sourceName]) {
      targetFilesMap[sourceName].file.setTrashed(true); // Trash old version
    }
    sourceFile.makeCopy(sourceName, targetFolder);
  }
}

function createFileMap(folder) {
  const fileMap = {};
  const files = folder.getFiles();

  while (files.hasNext()) {
    const file = files.next();
    fileMap[file.getName()] = {
      file: file,
      date: file.getLastUpdated(),
    };
  }

  return fileMap;
}

The issue is that even though this script indeed syncs folders with each other allowing me to have all actual saves in both folders. but some of them are appended with incremented number (1) in the name, file copy style. This also lead to having save file for one of games in literally 5000 copies overnight.

And since old files are set to be trashed many of saves were unusable before renaming and removing index from the name.

Any idea how to fix the script to not append name/ trim it after copying or any other way to make the script work as intended?

How to fix “paths[0]” argument must be of type string. Received an instance of Array” error when introducing Mustache partials with ExpressJS?

index.js logging the view paths to ensure partials are visable

import express from 'express';
import bodyParser from 'body-parser';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import mustacheExpress from 'mustache-express';
import dotenv from 'dotenv';
import homeRouter from './home/router.js';
import jobsRouter from './jobs/router.js';
import errors from './errors/errors.js';

dotenv.config();

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const app = express();

// Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, '..', 'public')));

// Configure mustache
app.set('views', [
  path.join(__dirname, 'home'), // Home templates
  path.join(__dirname, 'jobs'), // Jobs templates
  path.join(__dirname, 'errors'), // Error templates
  path.join(__dirname, 'site/partial-views'),
]);
console.log('Views set to:', app.get('views'));

app.set('view engine', 'mustache');
app.engine('mustache', mustacheExpress());

// Routes
app.use('/', homeRouter);
app.use('/jobs', jobsRouter);

// Error handling middleware
app.use(errors.notFound);
app.use(errors.internalServerError);
app.use(errors.emailError);

export default app;

terminal with views log

[nodemon] starting `node ./app/server.js`
Views set to: [
  'C:\Users\Keegan\source\repos\my-website\app\home',
  'C:\Users\Keegan\source\repos\my-website\app\jobs',
  'C:\Users\Keegan\source\repos\my-website\app\errors',
  'C:\Users\Keegan\source\repos\my-website\app\site\partial-views'
]
Server running on port 3000
Connected to MySQL database

terminal with error

node:internal/errors:540
      throw error;
      ^

TypeError [ERR_INVALID_ARG_TYPE]: The "paths[0]" argument must be of type string. Received an instance of Array
    at Object.resolve (node:path:198:9)
    at C:UsersKeegansourcereposmy-websitenode_modulesmustache-expressmustache-express.js:99:24
    at C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:247:13
    at eachOfArrayLike (C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:507:13)
    at eachOf (C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:627:16)
    at awaitable (C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:212:32)
    at _asyncMap (C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:245:16)
    at Object.map (C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:750:16)
    at Object.awaitable [as map] (C:UsersKeegansourcereposmy-websitenode_modulesasyncdistasync.js:212:32)
    at loadAllPartials (C:UsersKeegansourcereposmy-websitenode_modulesmustache-expressmustache-express.js:94:8) {
  code: 'ERR_INVALID_ARG_TYPE'
}

Node.js v22.12.0
[nodemon] app crashed - waiting for file changes before starting...

directory

my-website/
----app/
--------index.js
--------server.js
--------db.js
--------config.js
--------jobs/router.js, jobs.mustache
--------home/router.js, home.mustache
--------errors/errors.js, erorrs.mustache
--------site/partial-views/head.mustache
----public/

home.mustache

<html lang="en" data-bs-theme="auto">
{{> head}}
</html>

home/router.js

import express from 'express';
import connection from '../db.js';

const router = express.Router();

router.get('/', (req, res, next) => {
  const query = 'SELECT id, title, location, salary, posted FROM jobs';

  connection.query(query, (err, results) => {
    if (err) {
      return next(err);
    }
    res.render('home', { jobs: results });
  });
});

export default router;

home.mustache is currently the only template attempting to use the partial. Removing the 1 line {{> head}} in home.mustache and manually copying the contents of head.mustache in its place removes the error, and thewebsite renders the ‘/’ root endpoint correctly.

Thank you for your time and feedback !

Auth.js/NextAuth – Application redirect not working after successful sign in

I am using Auth.js Google Oauth to login the user, then checking if that email exists in my database. If yes, then I route the user to “/dashboard/inventory”. Once sign in is successful, my app is not auto-routing the user to “/dashboard/inventory”. It’s stuck on “/”. I am able to manually go to “/dashboard/inventory” and verify the authentication is successful.

If the user signs out, then the app properly redirects to “/” as I have setup, but that’s probably because I also have a middleware.js in place which is set to redirect to “/” if no user session is found.

app/auth.js

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { supabase } from "@/lib/supabase"

export const { handlers, auth, signIn, signOut } = NextAuth({
    providers: [Google],
    callbacks: {
        async signIn({ user }) {
            const { data, error } = await supabase
                .from('stores')
                .select()
                .eq('email', user.email)
                .single()

            if (error || !data) {
                return '/error'
            }

            return true
        },
    },
    pages: {
        error: '/error'
    }
})

app/utils/auth-utils.js

"use server"
import { signIn, signOut } from '@/auth'

const SignInWithGoogle = async () => {
    await signIn('google', { callbackUrl: "/dashboard/inventory" })
}

const SignOut = async () => {
    await signOut({ callbackUrl: "/" })
}

export { SignInWithGoogle, SignOut }

app/components/AuthComponent.js

"use client"
import { useSearchParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { SignInWithGoogle, SignOut } from "@/lib/auth-utils"

const AuthComponent = () => {
    const searchParams = useSearchParams()
    const error = searchParams?.get('error')

    return (
        <div className="flex flex-col w-full h-full justify-center items-center">{error && (
            <p className="text-red-500 mb-4">
                {error}
            </p>
        )}
            <form action={SignInWithGoogle}>
                <Button className="gap-2 font-semibold m-4" variant="secondary">
                    sign in with google
                </Button>
            </form>
            <form action={SignOut}>
            <Button type="submit" variant="secondary">sign out</Button>
            </form>
            </div>
    )
}

export default AuthComponent

app/page.js

import AuthComponent from "@/components/app/AuthComponent"

const HomePage = () => {
    

    return (
        <div className="flex w-full justify-center items-center overflow-hidden flex-col h-dvh p-4">
            <AuthComponent/>
        </div>
    )
}

export default HomePage

string length issue when sent to Laravel server

When a string is appended to a FormData object and sent to Laravel server, who decides the encoding that will be used? How can we make sure that the string length reported on the client-side matches the one that we get on the server, and not changed due to control characters, especially new line (rn vs ‘n`)?

Detail

I’m POSTing my model object that includes, among other things, its description that is a string (max: 1000 chars), using axios from my SPA (Vue 3 + Vuetify + TS) to the backend Laravel server’s API endpoint. Validations are in place on both client and server sides. On that frontend, this description is being displayed in a v-textarea that reports that the content is exactly 1000 characters, including 8 newline characters. This description, along with other data (including images), is then POSTed to the server using a FormData object.

On the server-side, this string is received as 1008 characters long instead of 1000, which causes the validation rule to fail. Upon inspecting the client- and server-side versions of the string, the only difference is that those newlines have been converted from n to rn at some point. I’m just looking for how to avoid this conversion or at least make the reported length match on both client and server sides.