VIEWS & ROUTING

In this step, we create the user interface for login and registration, and then register these components in the application routing system so that they are accessible at the appropriate URLs.

1

Creating views

Create a file web/src/views/Auth.tsx with the following code. It contains two main UI components: AuthLogin and AuthRegister.

  • Both components use local state (useState) to manage form data (username, password) and error messages (error).

  • The useLanguage hook is used for text internationalization and the useNavigateWithApps hook is used for navigation after successful login/registration.

  • Important note: The handleLogin and handleRegister functions are defined globally and do not need to be imported.

import { useLanguage } from "@/hooks/useLanguage";
import { useNavigateWithApps } from "@/hooks/useNavigateWithApps";
import { useState } from "react";

const AuthLogin = () => {
    const { getLang } = useLanguage();
    const navigate = useNavigateWithApps();
    
    const [username, setUsername] = useState<string>('');
    const [password, setPassword] = useState<string>('');
    const [error, setError] = useState<string | null>(null);

    const handleLoginButton = async () => {
        if (!username || !password) {
            setError(getLang('Auth:Messages.FillAllFields'));
            return;
        }

        const response = await handleLogin(username, password);

        if (response.success) {
            navigate('/');
        } else {
            setError(response.message || getLang('Auth:Errors.Error'));
        }
    }

    return (
        <div className='px-4 pt-10 size-full flex flex-col gap-6 justify-end'>
            <h2 className='text-black dark:text-white text-2xl font-bold text-center'>{getLang('Auth:Pages.Login.Title')}</h2>
            <div className='flex flex-col gap-4'>
                <div className='flex flex-col gap-2'>
                    <div className='flex flex-col gap-1'>
                        <label className='text-sm font-medium text-black dark:text-white'>{getLang('Auth:Pages.Login.Form.Username')}</label>
                        <input
                            type='text'
                            className='h-10 text-sm px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-[#121318] text-black dark:text-white transition duration-300 focus:!border-blue-700 focus:ring-4 focus:ring-blue-700/15 focus:outline-none'
                            value={username}
                            onChange={(e) => {
                                setUsername(e.target.value);
                                if (error) setError(null);
                            }}
                        />
                    </div>
                    <div className='flex flex-col gap-1'>
                        <label className='text-sm font-medium text-black dark:text-white'>{getLang('Auth:Pages.Login.Form.Password')}</label>
                        <input
                            type='password'
                            className='h-10 text-sm px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-[#121318] text-black dark:text-white transition duration-300 focus:!border-blue-700 focus:ring-4 focus:ring-blue-700/15 focus:outline-none'
                            value={password}
                            onChange={(e) => {
                                setPassword(e.target.value);
                                if (error) setError(null);
                            }}
                        />
                    </div>
                </div>
                <div className='flex flex-col gap-2'>
                    {(error && error?.trim() !== '') && (
                        <p className='text-xs text-center text-red-500'>{getLang(error)}</p>
                    )}
                    <button
                        type='button'
                        className='w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition duration-300 disabled:opacity-30'
                        disabled={!username || !password}
                        onClick={handleLoginButton}
                    >
                        {getLang('Auth:Pages.Login.Form.Button')}
                    </button>
                    <p className='text-sm text-center text-gray-500 dark:text-gray-400'>
                        {getLang('Auth:Pages.Login.Form.NoAccount')}{' '}
                        <button 
                            type='button' 
                            className='text-blue-600 hover:underline'
                            onClick={() => navigate('/auth/register')}
                        >
                            {getLang('Auth:Pages.Login.Form.NoAccount.Register')}
                        </button>
                    </p>
                </div>
            </div>
        </div>
    );
};

const AuthRegister = () => {
    const { getLang } = useLanguage();
    const navigate = useNavigateWithApps();
    
    const [username, setUsername] = useState<string>('');
    const [password, setPassword] = useState<string>('');
    const [repeatPassword, setRepeatPassword] = useState<string>('');
    const [error, setError] = useState<string | null>(null);

    const handleRegisterButton = async () => {
        if (!username || !password || !repeatPassword) {
            setError(getLang('Auth:Messages.FillAllFields'));
            return;
        }

        if (password !== repeatPassword) {
            setError(getLang('Auth:Messages.InvalidUsernameOrPassword'));
            return;
        }
        const response = await handleRegister(username, password, null);

        if (response.success) {
            navigate('/');
        } else {
            setError(response.message || getLang('Auth:Errors.Error'));
        }
    }

    return (
        <div className='px-4 pt-10 size-full flex flex-col gap-6 justify-end'>
            <h2 className='text-black dark:text-white text-2xl font-bold text-center'>{getLang('Auth:Pages.Register.Title')}</h2>
            <div className='flex flex-col gap-4'>
                <div className='flex flex-col gap-2'>
                    <div className='flex flex-col gap-1'>
                        <label className='text-sm font-medium text-black dark:text-white'>{getLang("Auth:Pages.Register.Form.Username")}</label>
                        <input
                            type='text'
                            className='h-10 text-sm px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-[#121318] text-black dark:text-white transition duration-300 focus:!border-blue-700 focus:ring-4 focus:ring-blue-700/15 focus:outline-none'
                            value={username}
                            onChange={(e) => {
                                setUsername(e.target.value);
                                if (error) setError(null);
                            }}
                        />
                    </div>
                    <div className='flex flex-col gap-1'>
                        <label className='text-sm font-medium text-black dark:text-white'>{getLang("Auth:Pages.Register.Form.Password")}</label>
                        <input
                            type='password'
                            className='h-10 text-sm px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-[#121318] text-black dark:text-white transition duration-300 focus:!border-blue-700 focus:ring-4 focus:ring-blue-700/15 focus:outline-none'
                            value={password}
                            onChange={(e) => {
                                setPassword(e.target.value);
                                if (error) setError(null);
                            }}
                        />
                    </div>
                    <div className='flex flex-col gap-1'>
                        <label className='text-sm font-medium text-black dark:text-white'>{getLang("Auth:Pages.Register.Form.RepeatPassword")}</label>
                        <input
                            type='password'
                            className='h-10 text-sm px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-[#121318] text-black dark:text-white transition duration-300 focus:!border-blue-700 focus:ring-4 focus:ring-blue-700/15 focus:outline-none'
                            value={repeatPassword}
                            onChange={(e) => {
                                setRepeatPassword(e.target.value);
                                if (error) setError(null);
                            }}
                        />
                    </div>
                </div>
                <div className='flex flex-col gap-2'>
                    {(error && error?.trim() !== '') && (
                        <p className='text-xs text-center text-red-500'>{getLang(error)}{error}</p>
                    )}
                    <button
                        type='button'
                        className='w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition duration-300 disabled:opacity-30'
                        disabled={!username || !password || !repeatPassword}
                        onClick={handleRegisterButton}
                    >
                        {getLang("Auth:Pages.Register.Form.Button")}
                    </button>
                    <p className='text-sm text-center text-gray-500 dark:text-gray-400'>
                        {getLang("Auth:Pages.Register.Form.HasAccount")}{' '}
                        <button 
                            type='button' 
                            className='text-blue-600 hover:underline'
                            onClick={() => navigate('/auth')}
                        >
                            {getLang("Auth:Pages.Register.Form.HasAccount.Login")}
                        </button>
                    </p>
                </div>
            </div>
        </div>
    );
};

export { AuthLogin, AuthRegister };
2

Route Definition

Add new Route objects to the routes array, defining the paths for login and registration.

  • The path for Login is /auth (this is the main path configured in the ExternalAuthProvider).

  • The path for Registration is /auth/register.

const routes: Route[] = [
    {
        path: '/',
        component: <Homepage />,
        className: '',
    },
    // โฌ…๏ธ ADD THE ROUTES BELOW
    { 
        path: '/auth', 
        component: <AuthLogin />, 
    },
    { 
        path: '/auth/register', 
        component: <AuthRegister />,
    },
    // ... other Routes
];

Your file should look like this:

circle-info

Once these changes are implemented, the application will correctly render the login and registration forms at the /auth and /auth/register paths.

3

Integrating User Info and Logout on Homepage

Now that the authentication context is set up, we can utilize the user data and the logout function within the main application view (Homepage). We'll add a simple user bar displaying the current username and a dedicated Log out button.

Importing Hooks

Inside the Homepage.tsx file, ensure you have imported the necessary components and hooks:

  1. useCurrentUser: A global function to retrieve the currently logged-in user object.

  2. useExternalAuth: The hook to get the context value, specifically the logoutUser function.

  3. Power: The icon used for the logout button (already included in the original component imports).

// web/src/views/Homepage.tsx

import { CardSim, ChevronRight, Hammer, Power, SunMoon, UserRound } from 'lucide-react';

import { useNavigateWithApps } from '@/hooks/useNavigateWithApps';
import { useLanguage } from '@/hooks/useLanguage';
import { useExternalAuth } from '@/contexts/ExternalAuthContext'; // โฌ…๏ธ IMPORT THIS HOOK
// ... other imports

Accessing User Data and Logout Function

Inside the Homepage component function, access the user data and the logout function:

const Homepage = () => {
    const { getLang } = useLanguage();
    const navigate = useNavigateWithApps();
    const settings = useSettings();
    
    const currentUser = useCurrentUser(); // โฌ…๏ธ GET CURRENT USER DATA
    const { logoutUser } = useExternalAuth(); // โฌ…๏ธ GET LOGOUT FUNCTION

    // ... Callbacks and Handlers below

Adding the User Info UI Block

Place the following JSX block inside the main return statement of Homepage, right after the opening <div> with className='px-4 pt-10 ...', to display the user bar at the top of the homepage if the user is logged in.

This block conditionally renders the user's name and a logout button using the retrieved currentUser object and the logoutUser function.

// web/src/views/Homepage.tsx - Inside the return statement:

return (
    <div className='px-4 pt-10 bg-white dark:bg-[#03050B] w-full flex flex-col gap-9'>
        
        {/* โฌ…๏ธ ADD THIS BLOCK */}
        {currentUser && (
            <div className='flex items-center justify-between'>
                <div className='flex items-center gap-2.5'>
                    <div className='size-10 rounded-[10px] bg-gradient-to-br from-[#7DA6FF] to-[#1A63FF] flex items-center justify-center text-white'>
                        <UserRound className='size-5' />
                    </div>
                    <div className='flex flex-col'>
                        <h3 className='text-[10px] font-bold text-[#7A7E96]'>{getLang('Userbar:Title')}</h3>
                        <p className='text-sm text-black dark:text-white font-bold'>{currentUser.username}</p>
                    </div>
                </div>
                <button 
                    type='button' 
                    className='size-8 rounded-full bg-[#7A7E96]/15 text-[#7A7E96] flex items-center justify-center transition duration-300 hover:bg-[#7A7E96] hover:text-white' 
                    onClick={logoutUser} // โฌ…๏ธ LOGOUT ON CLICK
                >
                    <Power className='size-3' />
                </button>
            </div>
        )}
        {/* โฌ…๏ธ END OF USER BAR BLOCK */}
        
        <div className='flex flex-col items-center justify-center gap-4'>
        {/* ... Rest of the component content ... */}

Last updated