DEV Community

Cover image for Building an AI-powered Financial Behavior Analyzer with NodeJS, Python, SvelteKit, and TailwindCSS - Part 4
John Owolabi Idogun
John Owolabi Idogun

Posted on • Originally published at johnowolabiidogun.dev

Building an AI-powered Financial Behavior Analyzer with NodeJS, Python, SvelteKit, and TailwindCSS - Part 4

Introduction

Part 5 is already out on: https://johnowolabiidogun.dev/blog/asynchronous-server-building-and-rigorously-testing-a-websocket-and-http-server-3918df/67b0ab3c7a900ac23e502c51

In this part of the series, we will implement our backend service's WebSocket handler and begin building the frontend. This will enable real-time communication between the backend and frontend, allowing us to display analysis results as they become available.

Prerequisite

The main prerequisite is that you have gone through the previous articles in this series. This ensures you have the necessary context and environment set up.

Source code

GitHub logo Sirneij / finance-analyzer

An AI-powered financial behavior analyzer and advisor written in Python (aiohttp) and TypeScript (ExpressJS & SvelteKit with Svelte 5)




Implementation

Step: WebSocket handler

To provide real-time updates for financial data analysis and summaries, we use WebSockets, a bidirectional communication protocol, instead of traditional HTTP requests that require constant refreshing. The TransactionWebSocketHandler function manages WebSocket connections:

import { WebSocket } from "ws";
import { TransactionService } from "$services/transaction.service.js";
import { baseConfig } from "$config/base.config.js";
import mongoose from "mongoose";
import { sendError } from "$utils/error.utils.js";

export function TransactionWebSocketHandler(ws: WebSocket): void {
  ws.on("message", async (message: string) => {
    try {
      const actions = JSON.parse(message);

      if (!Array.isArray(actions)) {
        sendError(ws, "Invalid message format. Expected an array.");
        return;
      }

      for (const actionObj of actions) {
        if (!actionObj.action || !actionObj.userId) {
          sendError(
            ws,
            "Invalid action format. Each action requires 'action' and 'userId'."
          );
          return;
        }

        const { action, userId } = actionObj;

        if (!mongoose.Types.ObjectId.isValid(userId)) {
          sendError(ws, "Invalid userId format.");
          return;
        }

        switch (action) {
          case "analyze":
          case "summary":
            await handleAction(ws, new mongoose.Types.ObjectId(userId), action);
            break;
          default:
            sendError(ws, `Unknown action: ${action}`);
        }
      }
    } catch (error) {
      baseConfig.logger.error(
        `Error processing message: ${
          error instanceof Error ? error.message : error
        }`
      );
      sendError(
        ws,
        `Failed to process message: ${
          error instanceof Error ? error.message : error
        }`
      );
    }
  });

  ws.on("close", () => {
    baseConfig.logger.info("Frontend WebSocket connection closed");
  });

  ws.on("error", (error) => {
    baseConfig.logger.error(`WebSocket error: ${error.message}`);
  });
}

async function handleAction(
  frontendWs: WebSocket,
  userId: mongoose.Types.ObjectId,
  action: string
) {
  try {
    const transactions = await TransactionService.findTransactionsByUserId(
      userId,
      -1,
      -1
    );

    if (!transactions) {
      sendError(
        frontendWs,
        `No transactions found for userId: ${userId}`,
        action
      );
      return;
    }

    await TransactionService.connectToUtilityServer(
      action,
      transactions.transactions,
      frontendWs
    );
  } catch (error) {
    baseConfig.logger.error(
      `Error handling action: ${error instanceof Error ? error.message : error}`
    );
    sendError(
      frontendWs,
      `Failed to handle action: ${
        error instanceof Error ? error.message : error
      }`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the ws module (installed in the previous article), this function sets up the WebSocket connection and defines handlers for incoming messages, connection close, and errors. As soon as it receives a message, the callback for the message event listener gets fired and it works as follows:

  • Parses incoming messages as JSON, expecting an array of actions.
  • Validates the format of each action, ensuring it contains action and userId.
  • Validates the userId to ensure it is a valid Mongoose ObjectId.
  • Uses a switch statement to handle different actions (analyze and summary), and
  • Calls the handleAction function to process each valid action.

The handleAction processes specific actions requested by the client by retrieving transactions for the specified userId using TransactionService.findTransactionsByUserId static method discussed in the previous article. Any transactions retrieved are sent via the TransactionService.connectToUtilityServer method to the utility server for analysis or summary.

Let's register this handler with our app:

...
import { WebSocketServer } from "ws";
...
import { TransactionWebSocketHandler } from "$websockets/transaction.websocket.js";
...
const startServer = async () => {
  try {
    const server: HttpServer = createServer(app);
    const wss = new WebSocketServer({ server, path: "/ws" });

    // 5. Setup WebSocket handlers
    wss.on("connection", (ws) => {
      TransactionWebSocketHandler(ws);
    });

    // 6. Connect to MongoDB
    baseConfig.logger.info("Connecting to MongoDB cluster...");
    const db = await connectToCluster();

    ...
  } catch (error) {
    baseConfig.logger.error("Error starting server:", error);
    process.exit(1);
  }
};
startServer();
Enter fullscreen mode Exit fullscreen mode

With that, we conclude the backend service. Now it's time to set SvelteKit up with TailwindCSS.

Step 2: SvelteKit with TailwindCSS

To setup a SvelteKit project with TailwindCSS, we will refer to the official TailwindCSS guide with some modifications from the migration guide I previously wrote.

First, create a new SvelteKit project via the Svelte 5 sv CLI:

$ npx sv create interface # modify the name as you please
Enter fullscreen mode Exit fullscreen mode

You will be prompted to install sv which you should accent to. Your interaction with the CLI should look like this:

projects npx sv create interface
Need to install the following packages:
sv@0.6.18
Ok to proceed? (y) y

┌  Welcome to the Svelte CLI! (v0.6.18)
│
◇  Which template would you like?
│  SvelteKit minimal
│
◇  Add type checking with Typescript?
│  Yes, using Typescript syntax
│
◆  Project created
│
◇  What would you like to add to your project? (use arrow keys / space bar)
│  prettier, eslint, vitest, tailwindcss, sveltekit-adapter
│
◇  tailwindcss: Which plugins would you like to add?
│  typography, forms
│
◇  sveltekit-adapter: Which SvelteKit adapter would you like to use?
│  node
│
◇  Which package manager do you want to install dependencies with?
│  npm
│
◆  Successfully setup add-ons
│
◆  Successfully installed dependencies
│
◇  Successfully formatted modified files
│
◇  Project next steps ─────────────────────────────────────────────────────╮
│                                                                          │
│  1: cd interface                                                         │
│  2: git init && git add -A && git commit -m "Initial commit" (optional)  │
│  3: npm run dev -- --open                                                │
│                                                                          │
│  To close the dev server, hit Ctrl-C                                     │
│                                                                          │
│  Stuck? Visit us at https://svelte.dev/chat                              │
│                                                                          │
├──────────────────────────────────────────────────────────────────────────╯
│
└  You're all set!
Enter fullscreen mode Exit fullscreen mode

Feel free to modify any of the steps as you like. You can change directory into the newly created project and install the dependencies.

For some reasons, the tailwindcss installed by sv was version 3.4.17. However, at the time of writting this, TailwindCSS is already at version 4.0.2. So we need to migrate. To incept the migration steps, run this command:

interface$ npx @tailwindcss/upgrade@next
Enter fullscreen mode Exit fullscreen mode

You should get something like this:

interface$ npx @tailwindcss/upgrade@next
Need to install the following packages:
@tailwindcss/upgrade@4.0.2
Ok to proceed? (y) y

≈ tailwindcss v4.0.2

fatal: not a git repository (or any of the parent directories): .git
│ Searching for CSS files in the current
│ directory and its subdirectories…

│ ↳ Linked `./tailwind.config.ts` to
│   `./src/app.css`

│ Migrating JavaScript configuration files…

│ ↳ Migrated configuration file:
│   `./tailwind.config.ts`

│ Migrating templates…

│ ↳ Migrated templates for configuration file:
│   `./tailwind.config.ts`

│ Migrating stylesheets…

│ ↳ Migrated stylesheet: `./src/app.css`

│ Migrating PostCSS configuration…

│ ↳ Installed package: `@tailwindcss/postcss`

│ ↳ Removed package: `autoprefixer`

│ ↳ Migrated PostCSS configuration:
│   `./postcss.config.js`

│ Updating dependencies…

│ ↳ Updated package:
│   `prettier-plugin-tailwindcss`

│ ↳ Updated package: `tailwindcss`

fatal: not a git repository (or any of the parent directories): .git
│ No changes were made to your repository.

Enter fullscreen mode Exit fullscreen mode

It will magically modify your src/app.css to look like:

@import "tailwindcss";

@plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms';

/*
  The default border color has changed to `currentColor` in Tailwind CSS v4,
  so we've added these compatibility styles to make sure everything still
  looks the same as it did with Tailwind CSS v3.

  If we ever want to remove these styles, we need to add an explicit border
  color utility to any element that depends on these defaults.
*/
@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }
}
Enter fullscreen mode Exit fullscreen mode

tailwind.config.ts will be removed and postcss.config.js will be modified. We need to make a slight change to src/app.css and vite.config.js:

@import "tailwindcss";

@plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms';

@custom-variant dark (&:where(.dark, .dark *));

@layer base {
  *,
  ::after,
  ::before,
  ::backdrop,
  ::file-selector-button {
    border-color: var(--color-gray-200, currentColor);
  }
}
Enter fullscreen mode Exit fullscreen mode

We need line 6 so that we can leverage classes (and later, prefers-color-scheme) to dynamically switch themes.

Next is vite.config.js:

import { defineConfig } from "vitest/config";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss(), sveltekit()],

  test: {
    include: ["src/**/*.{test,spec}.{js,ts}"],
  },
});
Enter fullscreen mode Exit fullscreen mode

That concludes the initial setup.

Step 3: Leverage prefers-color-scheme in theme toggling

Many modern operating systems (OS) have made dark mode a first-class feature, and tuning your web application to honor the user's OS theme preference provides a better user experience. The CSS prefers-color-scheme media feature makes this easy to implement. Here's how to leverage it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="language" content="en" />
    <!-- Theme -->
    <meta
      name="theme-color"
      content="#ffffff"
      media="(prefers-color-scheme: light)"
    />
    <meta
      name="theme-color"
      content="#111827"
      media="(prefers-color-scheme: dark)"
    />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
      rel="stylesheet"
    />
    ... %sveltekit.head%
    <script>
      // Get user's explicit preference
      const userTheme = localStorage.getItem("theme");

      // Get system preference
      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");

      // Use user preference if set, otherwise follow system
      const theme = userTheme || (systemTheme.matches ? "dark" : "light");

      // Apply theme
      document.documentElement.classList.toggle("dark", theme === "dark");

      // Listen for system changes
      systemTheme.addEventListener("change", (e) => {
        // Only follow system if user hasn't set preference
        if (!localStorage.getItem("theme")) {
          document.documentElement.classList.toggle("dark", e.matches);
        }
      });
    </script>
  </head>
  <body
    data-sveltekit-preload-data="hover"
    class="bg-white text-black dark:bg-gray-900 dark:text-white"
  >
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This short HTML code does a lot of things. First, the theme-color meta tags adapt the browser's UI (e.g., address bar) to match the theme. prefers-color-scheme is used to set different colors for light and dark modes. Then, in the script, we first checks if the user has a saved theme preference in localStorage. If not, it checks the OS-level dark mode setting using window.matchMedia('(prefers-color-scheme: dark)'). It then applies the appropriate theme by adding or removing the dark class to the <html> element. This class is used to toggle CSS styles (e.g., using dark:bg-gray-900 in the <body> class). Finally, it listens for changes in the OS-level dark mode setting and updates the theme accordingly (but only if the user hasn't explicitly set a preference). We also used the <body> element to set the background and text colors based on the presence of the dark class using Tailwind CSS classes.

Step 4: Theme switching logic with icons

Before we proceed to creating the authentication/login page, let's make a simple ThemeSwitcher.svelte component:

<script lang="ts">
  import Moon from "$lib/components/icons/Moon.svelte";
  import Sun from "$lib/components/icons/Sun.svelte";

  let { ...props } = $props();

  let isDark = $state(false);

  $effect(() => {
    isDark = document.documentElement.classList.contains("dark");
  });

  function toggleTheme() {
    isDark = !isDark;
    localStorage.setItem("theme", isDark ? "dark" : "light");
    document.documentElement.classList.toggle("dark", isDark);
  }
</script>

<button onclick="{toggleTheme}" {...props}>
  {#if isDark}
  <Sun />
  {:else}
  <Moon />
  {/if}
</button>
Enter fullscreen mode Exit fullscreen mode

Since this app will be fully powered by Svelte 5, we are using the $props() rune to accept any attributes passed to the component as props, and the spread operator helps expand these attributes. We also declared a reactive variable with the $state rune, and it gets updated in the $effect rune and in the toggleTheme function. The $effect rune runs once on component initialization and whenever its dependencies change. In this case, it checks if the dark class is present on the <html> element and updates the isDark state accordingly. This ensures the component's initial state matches the current theme. As for the toggleTheme function, it gets called when the button is clicked and toggles the isDark state, saves the selected theme ("dark" or "light") to localStorage, and toggles the dark class on the <html> element. The <button> element calls the toggleTheme function on click and passes any additional props to the button. Inside the button, the {#if isDark}...{:else}...{/if} block conditionally renders either the Sun or Moon component based on the isDark state.

Note: Events are properties in Svelte 5

The on: directive was used in Svelte 4 to attach events to HTML elements. However, Svelte 5 changed that narrative by being more natural with HTML, which sees events as simply properties.

The sun and moon icons are simply svgs that have become svelte components:

<script lang="ts">
  let { ...props } = $props();
</script>

<svg
  class="h-6 w-6 text-yellow-400"
  fill="none"
  viewBox="0 0 24 24"
  stroke="currentColor"
  {...props}
>
  <path
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="2"
    d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
  />
</svg>
Enter fullscreen mode Exit fullscreen mode
<script lang="ts">
  let { ...props } = $props();
</script>

<svg
  class="h-6 w-6 text-gray-700 dark:text-gray-200"
  fill="none"
  viewBox="0 0 24 24"
  stroke="currentColor"
  {...props}
>
  <path
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-width="2"
    d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
  />
</svg>
Enter fullscreen mode Exit fullscreen mode

This is a nifty way to make svgs flexible as you can pass styles are other attributes and they will be reflected.

Step 5: Authentication or login page

Now, let's see what the login page will look like:

Authentication page dark

Authentication page light

<script lang="ts">
    import { page } from '$app/state';
    import AiNode from '$lib/components/icons/AINode.svelte';
    import Calculator from '$lib/components/icons/Calculator.svelte';
    import FinChart from '$lib/components/icons/FinChart.svelte';
    import GitHub from '$lib/components/icons/GitHub.svelte';
    import Google from '$lib/components/icons/Google.svelte';
    import Logo from '$lib/components/logos/Logo.svelte';
    import ThemeSwitcher from '$lib/components/reusables/ThemeSwitcher.svelte';
    import { BASE_API_URI } from '$lib/utils/contants';
    import { fade } from 'svelte/transition';

    const next = page.url.searchParams.get('next') || '/';
</script>

<div
    class="relative min-h-screen bg-linear-to-br from-gray-100 to-gray-200 transition-colors duration-300 dark:from-gray-900 dark:to-gray-800"
>
    <!-- Theme Toggle -->
    <ThemeSwitcher
        class="dark:ring-black-500/50 absolute top-4 right-4 z-50 cursor-pointer rounded-full bg-white p-2 shadow-lg transition-all duration-300 hover:shadow-xl dark:bg-gray-700 dark:ring-2"
    />

    <!-- Decorative background elements -->
    <div class="absolute inset-0 z-0 overflow-hidden">
        <!-- AI Network Nodes -->
        <div class="floating-icons absolute top-10 left-10 opacity-20 dark:opacity-30">
            <AiNode />
        </div>
        <!-- Financial Chart -->
        <div class="floating-icons absolute right-20 bottom-32 opacity-20 dark:opacity-30">
            <FinChart />
        </div>
        <!-- Calculator Icon -->
        <div class="floating-icons absolute top-20 right-10 opacity-20 dark:opacity-30">
            <Calculator />
        </div>
    </div>

    <!-- Main content -->
    <div class="relative z-10 flex min-h-screen items-center justify-center">
        <div
            in:fade={{ duration: 300 }}
            class="w-full max-w-md space-y-8 rounded-xl bg-white/80 p-8 shadow-lg backdrop-blur-xs transition-all duration-300 dark:bg-gray-800/90 dark:shadow-gray-900/30"
        >
            <!-- Logo -->
            <div class="logo-container flex justify-center">
                <Logo isSmall={false} class="h-12 w-auto" />
            </div>
            <!-- Header -->
            <div class="text-center">
                <h2 class="mt-6 text-3xl font-extrabold text-gray-900 dark:text-white">Welcome back</h2>
                <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
                    Sign in to continue to your account
                </p>
                {#if page.url.searchParams.get('error')}
                    <p class="mt-2 text-sm text-red-500 dark:text-red-400">
                        Log in failed. Please try again.
                    </p>
                {/if}
            </div>

            <!-- Social Login Buttons -->
            <div class="mt-8 space-y-4">
                <!-- GitHub Login Button -->
                <a
                    href={`${BASE_API_URI}/v1/auth/github?next=${next}`}
                    class="flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-xs transition-all duration-300 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
                >
                    <GitHub />
                    Continue with GitHub
                </a>
                <!-- Google Login Button -->
                <a
                    href="/auth/google"
                    class="pointer-events-none flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 opacity-50 shadow-xs transition-all duration-300 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
                    aria-disabled="true"
                >
                    <Google />
                    Continue with Google
                </a>
            </div>

            <!-- Divider -->
            <div class="mt-6">
                <div class="relative">
                    <div class="absolute inset-0 flex items-center">
                        <div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
                    </div>
                    <div class="relative flex justify-center text-sm">
                        <span class="bg-white px-2 text-gray-500 dark:bg-gray-800 dark:text-gray-400">
                            Policy Agreement
                        </span>
                    </div>
                </div>
            </div>
            <!-- Additional Info -->
            <div class="mt-6 text-center text-sm">
                <p class="text-gray-600 dark:text-gray-400">
                    By continuing, you agree to our
                    <a
                        href="/terms"
                        class="font-medium text-indigo-600 transition-colors duration-300 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
                    >
                        Terms of Service
                    </a>
                    and
                    <a
                        href="/privacy"
                        class="font-medium text-indigo-600 transition-colors duration-300 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
                    >
                        Privacy Policy
                    </a>
                </p>
            </div>
        </div>
    </div>
</div>

<style>
    .floating-icons {
        animation: float 6s ease-in-out infinite;
    }
    .floating-icons:nth-child(2) {
        animation-delay: 2s;
    }
    .floating-icons:nth-child(3) {
        animation-delay: 4s;
    }
    @keyframes float {
        0% {
            transform: translateY(0px) rotate(0deg);
        }
        50% {
            transform: translateY(-20px) rotate(360deg);
        }
        100% {
            transform: translateY(0px) rotate(0deg);
        }
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Though this code seems long, the only really important part is:

<script lang="ts">
    import { page } from '$app/state';
    ...
    import { BASE_API_URI } from '$lib/utils/contants';
    ...

    const next = page.url.searchParams.get('next') || '/';
</script>
...
<!-- Social Login Buttons -->
<div class="mt-8 space-y-4">
    <!-- GitHub Login Button -->
    <a
        href={`${BASE_API_URI}/v1/auth/github?next=${next}`}
        class="flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-xs transition-all duration-300 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
    >
        <GitHub />
        Continue with GitHub
    </a>
    <!-- Google Login Button -->
    <a
        href="/auth/google"
        class="pointer-events-none flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 opacity-50 shadow-xs transition-all duration-300 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
        aria-disabled="true"
    >
        <Google />
        Continue with Google
    </a>
</div>
...
Enter fullscreen mode Exit fullscreen mode

These are just <a> elements whose URLs link directly to the API endpoints (Google yet to be implemented). The BASE_API_URI is exported by src/lib/utils/constants.ts:

export const BASE_API_URI = import.meta.env.DEV
  ? import.meta.env.VITE_BASE_API_URI_DEV
  : import.meta.env.VITE_BASE_API_URI_PROD;
Enter fullscreen mode Exit fullscreen mode

It necessitates that you set either VITE_BASE_API_URI_DEV or VITE_BASE_API_URI_PRODas environment variables pointing to your server's base URL. In development, I set VITE_BASE_API_URI_DEV=http://localhost:3030/api in a .env file at the root of my SvelteKit project.

Tip: Environment Variables in Production

When deploying to a production server, configure the environment variable VITE_BASE_API_URI_PROD to point to your backend's production URL, including the /api path (e.g., https://your-production-url.com/api). Most deployment platforms offer a way to set these variables.

Back in our +page.svelte, we also retrieve the next page, which specifies where a user should be redirected after successful authentication. This is configured on a per-route basis. In addition to these core features, the page includes visual elements such as infinitely animating floating icons. These icons—AiNode (AI integration), FinChart (finance), and Calculator (analysis)—represent the application's key themes. Custom styles are applied to create a continuous vertical translation from 0px to -20px and back, combined with a simultaneous rotation from 0deg to 360deg. We could have used tailwind styles directly.

This article is getting long, so we will defer the user dashboard to the next article. However, there's a little safekeeping that needs to be done. Authenticated users should not be allowed to access this login page since they're already authenticated. To achieve this, we'll have a +page.server.ts file whose aim is to redirect any authenticated user from coming here:

import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ locals }) => {
  if (locals.user) {
    throw redirect(302, "/finanalyzer");
  }
  return {};
};
Enter fullscreen mode Exit fullscreen mode

See you in the next one!

Outro

Enjoyed this article? I'm a Software Engineer, Technical Writer and Technical Support Engineer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and X. I am also an email away.

If you found this article valuable, consider sharing it with your network to help spread the knowledge!

Top comments (0)