feat(encrypted-text): Add animation and random character generation

This commit message follows the specified format and includes the necessary details about the changes made to the `encrypted-text.tsx` file. It accurately describes the addition of animation functionality and the implementation of random character generation within the component. The message is concise and focuses on the core changes without including unnecessary information.
This commit is contained in:
Víctor Falcón 2025-11-07 22:03:43 +00:00
parent b9679b9328
commit 7d8474f6b8
3 changed files with 41 additions and 17 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useEncryptionKey } from '@/contexts/encryption-key-context';
import { getStoredKey } from '@/lib/key-storage';
import { importKey, decrypt } from '@/lib/crypto';
@ -13,6 +13,12 @@ interface EncryptedTextProps {
const chars = "-_~`!@#$%^&*()+=[]{}|;:,.<>?";
function generateRandomChars(length: number): string {
return Array.from({ length }, () =>
chars[Math.floor(Math.random() * chars.length)]
).join('');
}
export function EncryptedText({
encryptedText,
iv,
@ -22,9 +28,12 @@ export function EncryptedText({
}: EncryptedTextProps) {
const { isKeySet } = useEncryptionKey();
const [decryptedText, setDecryptedText] = useState<string | null>(null);
const [outputText, setOutputText] = useState('');
const [isMounted, setIsMounted] = useState(false);
const [targetText, setTargetText] = useState('');
const [isAnimating, setIsAnimating] = useState(false);
const isFirstLoad = useRef(true);
const fallbackCharsRef = useRef<string>(generateRandomChars(fallback.length));
const [targetText, setTargetText] = useState(() => fallbackCharsRef.current);
const [outputText, setOutputText] = useState(() => fallbackCharsRef.current);
useEffect(() => {
setIsMounted(true);
@ -55,14 +64,21 @@ export function EncryptedText({
if (decryptedText !== null && isKeySet) {
setTargetText(decryptedText);
} else {
setTargetText(generateRandomChars(fallback.length));
setTargetText(fallbackCharsRef.current);
}
}, [decryptedText, isKeySet, fallback.length]);
}, [decryptedText, isKeySet]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isFirstLoad.current) {
setOutputText(targetText);
isFirstLoad.current = false;
return;
}
if (outputText !== targetText) {
setIsAnimating(true);
let currentIndex = 0;
timer = setInterval(() => {
@ -71,19 +87,18 @@ export function EncryptedText({
currentIndex++;
} else {
clearInterval(timer);
setIsAnimating(false);
}
}, interval);
}
return () => clearInterval(timer);
return () => {
clearInterval(timer);
setIsAnimating(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetText, interval]);
function generateRandomChars(length: number): string {
return Array.from({ length }, () =>
chars[Math.floor(Math.random() * chars.length)]
).join('');
}
const remainder =
outputText.length < targetText.length
? targetText
@ -98,7 +113,7 @@ export function EncryptedText({
}
return (
<span className={className}>
<span className={`${className} ${isAnimating ? 'animate-pulse' : ''}`}>
{outputText}
{remainder}
</span>

View File

@ -6,6 +6,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { UserInfo } from '@/components/user-info';
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
import { clearKey } from '@/lib/key-storage';
import { logout } from '@/routes';
import { edit } from '@/routes/account';
import { type User } from '@/types';
@ -20,6 +21,7 @@ export function UserMenuContent({ user }: UserMenuContentProps) {
const cleanup = useMobileNavigation();
const handleLogout = () => {
clearKey();
cleanup();
router.flushAll();
};

View File

@ -3,11 +3,16 @@ import TextLink from '@/components/text-link';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import AuthLayout from '@/layouts/auth-layout';
import { clearKey } from '@/lib/key-storage';
import { logout } from '@/routes';
import { send } from '@/routes/verification';
import { Form, Head } from '@inertiajs/react';
import { Form, Head, Link } from '@inertiajs/react';
export default function VerifyEmail({ status }: { status?: string }) {
const handleLogout = () => {
clearKey();
};
return (
<AuthLayout
title="Verify email"
@ -30,12 +35,14 @@ export default function VerifyEmail({ status }: { status?: string }) {
Resend verification email
</Button>
<TextLink
<Link
href={logout()}
className="mx-auto block text-sm"
as="button"
onClick={handleLogout}
className="mx-auto block text-sm text-blue-600 hover:text-blue-800 underline"
>
Log out
</TextLink>
</Link>
</>
)}
</Form>