kvm/ui/src/routes/welcome-local.password.tsx
Marc Brooks 7ccb8e617c
chore: Upgrade UI vite and tailwind packages (#443)
* chore: Upgrade UI vite and tailwind packages

Vite 5.2.0 -> 6.3.5
@vitejs/plugin-basic-ssl 1.2.0 -> 2.0.0
cva: 1.0.0-beta.1 -> 1.0.0-beta.3
focus-trap-react 10.2.3 -> 11.0.3
framer-motion 11.15.0 -> 12.11.0
@tailwindcss/postcss 4.1.6
@tailwindcss/vite 4.1.6
tailwind 3.4.17 -> 4.1.6
tailwind-merge 2.5.5 -> 3.3.0

Minor updates:
@headlessui/react 2.2.2 -> 2.2.3
@types/react 19.1.3 -> 19.1.4
@types/react-dom 19.1.3 -> 19.1.5
@typescript-eslint/eslint-plugin 8.32.0 -> 8.32.1
@typescript-eslint/parser 8.32.0 -> 8.32.1
react-simple-keyboard 3.8.71 -> 3.8.72

The new version of vite required an Node 22.15 (since that's current LTS and node 21.x is EOL)

The changes to css due to the tailwind 3 to 4 upgrade were done following [the upgrade guide](https://tailwindcss.com/docs/upgrade-guide#changes-from-v3)

Done in this order (important):
`shadow-sm` -> `shadow-xs`
`shadow` -> `shadown-sm`
`rounded` -> `rounded-sm`
`outline-none` -> `outline-hidden`
`32rem_32rem_at_center` -> `center_at_32rem_32rem` (revised order of gradient props)
`ring-1 ring-black ring-opacity-5` -> `ring-1 ring-black/50`
`flex-shrink-0` -> `shrink-0`
`flex-grow-0` -> `grow-0`
`outline outline-1` -> `outline-1`

ALSO removed the **extra** `opacity-0` on the video element (trips up latest tailwind causing the video to be invisible)

FocusTrap is now not exported as the default, so change those imports

headlessui's Menu completely changed, so upgrade to the new syntax which necessitated a reorganization of the Header.tsx to enable the "menu" to still work

* Update eslint config and fix errors
2025-05-15 14:21:03 +02:00

173 lines
6.2 KiB
TypeScript

import { ActionFunctionArgs, Form, redirect, useActionData } from "react-router-dom";
import { useState, useRef, useEffect } from "react";
import { LuEye, LuEyeOff } from "react-icons/lu";
import GridBackground from "@components/GridBackground";
import Container from "@components/Container";
import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel } from "@components/InputField";
import { Button } from "@components/Button";
import LogoBlueIcon from "@/assets/logo-blue.png";
import LogoWhiteIcon from "@/assets/logo-white.svg";
import { DEVICE_API } from "@/ui.config";
import api from "../api";
import { DeviceStatus } from "./welcome-local";
const loader = async () => {
const res = await api
.GET(`${DEVICE_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>);
if (res.isSetup) return redirect("/login-local");
return null;
};
const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const password = formData.get("password");
const confirmPassword = formData.get("confirmPassword");
if (password !== confirmPassword) {
return { error: "Passwords do not match" };
}
try {
const response = await api.POST(`${DEVICE_API}/device/setup`, {
localAuthMode: "password",
password,
});
if (response.ok) {
return redirect("/");
} else {
return { error: "Failed to set password" };
}
} catch (error) {
console.error("Error setting password:", error);
return { error: "An error occurred while setting the password" };
}
};
export default function WelcomeLocalPasswordRoute() {
const actionData = useActionData() as { error?: string };
const [showPassword, setShowPassword] = useState(false);
const passwordInputRef = useRef<HTMLInputElement>(null);
// Don't focus immediately, let the animation finish
useEffect(() => {
const timer = setTimeout(() => {
passwordInputRef.current?.focus();
}, 1000); // 1 second delay
return () => clearTimeout(timer);
}, []);
return (
<>
<GridBackground />
<div className="grid min-h-screen">
<Container>
<div className="flex items-center justify-center w-full h-full isolate">
<div className="max-w-2xl space-y-8">
<div className="flex items-center justify-center animate-fadeIn">
<img src={LogoWhiteIcon} alt="" className="-ml-4 h-[32px] hidden dark:block" />
<img src={LogoBlueIcon} alt="" className="-ml-4 h-[32px] dark:hidden" />
</div>
<div
className="space-y-2 text-center animate-fadeIn"
style={{ animationDelay: "200ms" }}
>
<h1 className="text-4xl font-semibold text-black dark:text-white">Set a Password</h1>
<p className="font-medium text-slate-600 dark:text-slate-400">
Create a strong password to secure your JetKVM device locally.
</p>
</div>
<Fieldset className="space-y-12">
<Form method="POST" className="max-w-sm mx-auto space-y-4">
<div className="space-y-4">
<div
className="animate-fadeIn"
style={{ animationDelay: "400ms" }}
>
<InputFieldWithLabel
label="Password"
type={showPassword ? "text" : "password"}
name="password"
placeholder="Enter a password"
autoComplete="new-password"
ref={passwordInputRef}
TrailingElm={
showPassword ? (
<div
onClick={() => setShowPassword(false)}
className="pointer-events-auto"
>
<LuEye className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>
) : (
<div
onClick={() => setShowPassword(true)}
className="pointer-events-auto"
>
<LuEyeOff className="w-4 h-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>
)
}
/>
</div>
<div
className="animate-fadeIn"
style={{ animationDelay: "400ms" }}
>
<InputFieldWithLabel
label="Confirm Password"
autoComplete="new-password"
type={showPassword ? "text" : "password"}
name="confirmPassword"
placeholder="Confirm your password"
error={actionData?.error}
/>
</div>
</div>
{actionData?.error && <p className="text-sm text-red-600">{}</p>}
<div
className="animate-fadeIn"
style={{ animationDelay: "600ms" }}
>
<Button
size="LG"
theme="primary"
fullWidth
type="submit"
text="Set Password"
textAlign="center"
/>
</div>
</Form>
</Fieldset>
<p
className="max-w-md text-xs text-center animate-fadeIn text-slate-500 dark:text-slate-400"
style={{ animationDelay: "800ms" }}
>
This password will be used to secure your device data and protect against
unauthorized access.{" "}
<span className="font-bold">All data remains on your local device.</span>
</p>
</div>
</div>
</Container>
</div>
</>
);
}
WelcomeLocalPasswordRoute.action = action;
WelcomeLocalPasswordRoute.loader = loader;