From ebb2364a6790967ba3650914e8033a09aaa0d564 Mon Sep 17 00:00:00 2001
From: Oleksandr Honcharov <0976053529@ukr.net>
Date: Fri, 28 Mar 2025 17:03:23 +0200
Subject: [PATCH] login/register page ui
---
apps/browser/package.json | 5 +-
apps/browser/src/App.tsx | 42 +++---
apps/browser/src/components/ui/Button.tsx | 62 ++++++++
apps/browser/src/components/ui/Checkbox.tsx | 29 ++++
apps/browser/src/components/ui/Input.tsx | 49 ++++++
apps/browser/src/hooks/useLoginForm.ts | 139 ++++++++++++++++++
apps/browser/src/hooks/useRegisterForm.ts | 121 +++++++++++++++
apps/browser/src/index.css | 2 +
.../src/pages/Authorization/LoginPage.tsx | 126 ++++++++++++++++
.../src/pages/Authorization/RegisterPage.tsx | 134 +++++++++++++++++
apps/browser/src/validation/index.ts | 6 +
apps/browser/src/validation/loginSchema.ts | 10 ++
apps/browser/src/validation/registerSchema.ts | 28 ++++
apps/browser/vite.config.ts | 11 +-
14 files changed, 739 insertions(+), 25 deletions(-)
create mode 100644 apps/browser/src/components/ui/Button.tsx
create mode 100644 apps/browser/src/components/ui/Checkbox.tsx
create mode 100644 apps/browser/src/components/ui/Input.tsx
create mode 100644 apps/browser/src/hooks/useLoginForm.ts
create mode 100644 apps/browser/src/hooks/useRegisterForm.ts
create mode 100644 apps/browser/src/pages/Authorization/LoginPage.tsx
create mode 100644 apps/browser/src/pages/Authorization/RegisterPage.tsx
create mode 100644 apps/browser/src/validation/index.ts
create mode 100644 apps/browser/src/validation/loginSchema.ts
create mode 100644 apps/browser/src/validation/registerSchema.ts
diff --git a/apps/browser/package.json b/apps/browser/package.json
index 63b2b89..fffc8d1 100644
--- a/apps/browser/package.json
+++ b/apps/browser/package.json
@@ -13,8 +13,11 @@
},
"dependencies": {
"@entityseven/rage-fw-browser": "0.2.0",
+ "@tailwindcss/vite": "^4.0.17",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "tailwindcss": "^4.0.17",
+ "zod": "^3.24.2"
},
"devDependencies": {
"@types/react": "^18.2.66",
diff --git a/apps/browser/src/App.tsx b/apps/browser/src/App.tsx
index 0d9e5de..1d69fc1 100644
--- a/apps/browser/src/App.tsx
+++ b/apps/browser/src/App.tsx
@@ -1,26 +1,30 @@
-import { fw } from '@entityseven/rage-fw-browser'
-import { useEffect, useState } from 'react'
+import React from 'react'
+import LoginPage from './pages/Authorization/LoginPage'
+import RegisterPage from './pages/Authorization/RegisterPage'
+
+import './index.css'
function App() {
- const [data, setData] = useState('initial')
-
- useEffect(() => {
- fw.event.register('customBrowserEvent', async message => {
- setData(p => p + ' | ' + message)
-
- const response = await fw.event.triggerServer('customServerEvent', [
- 'hello from browser',
- ])
- setData(p => p + ' | ' + response)
-
- return 'response from browser'
- })
- }, [])
+ const [showLogin, setShowLogin] = React.useState(true)
return (
-
-
Hello World!
-
{data}
+
+
+
+
+
+
+ {showLogin ?
:
}
)
}
diff --git a/apps/browser/src/components/ui/Button.tsx b/apps/browser/src/components/ui/Button.tsx
new file mode 100644
index 0000000..b71705a
--- /dev/null
+++ b/apps/browser/src/components/ui/Button.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+
+interface ButtonProps extends React.ButtonHTMLAttributes
{
+ isLoading?: boolean
+}
+
+const Button: React.FC = ({
+ children,
+ className,
+ isLoading = false,
+ disabled,
+ ...props
+}) => {
+ const baseClasses =
+ 'w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2'
+ const activeClasses = 'bg-sky-600 hover:bg-sky-700 focus:ring-sky-500'
+ const loadingClasses = 'bg-sky-400 cursor-not-allowed'
+ const disabledClasses = 'bg-gray-400 cursor-not-allowed'
+
+ const getButtonClasses = () => {
+ if (isLoading)
+ return `${baseClasses} ${loadingClasses} ${className || ''}`
+ if (disabled)
+ return `${baseClasses} ${disabledClasses} ${className || ''}`
+ return `${baseClasses} ${activeClasses} ${className || ''}`
+ }
+
+ return (
+
+ )
+}
+
+export default Button
diff --git a/apps/browser/src/components/ui/Checkbox.tsx b/apps/browser/src/components/ui/Checkbox.tsx
new file mode 100644
index 0000000..bf46495
--- /dev/null
+++ b/apps/browser/src/components/ui/Checkbox.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+
+interface CheckboxProps extends React.InputHTMLAttributes {
+ label: string
+ id: string
+}
+
+const Checkbox: React.FC = ({
+ label,
+ id,
+ className,
+ ...props
+}) => {
+ return (
+
+
+
+
+ )
+}
+
+export default Checkbox
diff --git a/apps/browser/src/components/ui/Input.tsx b/apps/browser/src/components/ui/Input.tsx
new file mode 100644
index 0000000..b696c19
--- /dev/null
+++ b/apps/browser/src/components/ui/Input.tsx
@@ -0,0 +1,49 @@
+import React from 'react'
+
+interface InputProps extends React.InputHTMLAttributes {
+ label: string
+ id: string
+ error?: string
+}
+
+const Input: React.FC = ({
+ label,
+ id,
+ error,
+ className,
+ ...props
+}) => {
+ const baseClasses =
+ 'mt-1 block w-full px-3 py-2 bg-white border border-slate-300 rounded-md text-sm shadow-sm placeholder-slate-400 focus:outline-none focus:border-sky-500 focus:ring-1 focus:ring-sky-500 disabled:bg-slate-50 disabled:text-slate-500 disabled:border-slate-200 disabled:shadow-none'
+ const errorClasses =
+ 'border-red-500 text-red-600 focus:border-red-500 focus:ring-red-500'
+
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ )
+}
+
+export default Input
diff --git a/apps/browser/src/hooks/useLoginForm.ts b/apps/browser/src/hooks/useLoginForm.ts
new file mode 100644
index 0000000..2dc6078
--- /dev/null
+++ b/apps/browser/src/hooks/useLoginForm.ts
@@ -0,0 +1,139 @@
+import React, { useState, useCallback } from 'react'
+import { loginSchema, LoginFormData, FieldErrors } from '../validation'
+import { ZodError } from 'zod'
+
+interface UseLoginFormProps {
+ onSubmitSuccess?: (data: LoginFormData) => void // Optional callback for successful submission
+}
+
+export function useLoginForm({ onSubmitSuccess }: UseLoginFormProps = {}) {
+ const [formData, setFormData] = useState({
+ identifier: '', // Can hold email or username
+ password: '',
+ saveLogin: false,
+ savePassword: false,
+ })
+ const [errors, setErrors] = useState>({})
+ const [isLoading, setIsLoading] = useState(false)
+ const [submitError, setSubmitError] = useState(null)
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const { name, value, type, checked } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value,
+ }))
+ // Clear error for the field being edited
+ if (errors[name as keyof LoginFormData]) {
+ setErrors(prev => ({ ...prev, [name]: undefined }))
+ }
+ // Clear general submission error
+ if (submitError) {
+ setSubmitError(null)
+ }
+ },
+ [errors, submitError],
+ )
+
+ const validateForm = useCallback(() => {
+ try {
+ loginSchema.parse(formData)
+ setErrors({})
+ return true
+ } catch (error) {
+ if (error instanceof ZodError) {
+ const fieldErrors: FieldErrors = {}
+ error.errors.forEach(err => {
+ if (err.path.length > 0) {
+ fieldErrors[err.path[0] as keyof LoginFormData] =
+ err.message
+ }
+ })
+ setErrors(fieldErrors)
+ } else {
+ console.error(
+ 'An unexpected error occurred during validation:',
+ error,
+ )
+ setSubmitError('An unexpected validation error occurred.')
+ }
+ return false
+ }
+ }, [formData])
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault()
+ setSubmitError(null)
+
+ if (!validateForm()) {
+ return
+ }
+
+ setIsLoading(true)
+ console.log('Submitting login data:', formData)
+
+ // --- TODO: Replace with actual API call to your RageMP server ---
+ try {
+ // Example: Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ // Assuming API returns success:
+ console.log('Login successful!')
+ // Here you would typically receive a token or session info
+ // Handle 'saveLogin' and 'savePassword' (e.g., using localStorage/sessionStorage)
+ if (formData.saveLogin) {
+ localStorage.setItem('savedUsername', formData.identifier) // Example
+ } else {
+ localStorage.removeItem('savedUsername') // Example
+ }
+ // Password saving is generally discouraged for security reasons,
+ // but if required:
+ if (formData.savePassword) {
+ // Be VERY careful with storing passwords. Consider secure storage or tokens.
+ // localStorage.setItem('savedPassword', formData.password); // **Highly discouraged**
+ console.warn(
+ 'Password saving enabled - ensure secure storage mechanism.',
+ )
+ }
+
+ setErrors({}) // Clear errors on success
+ if (onSubmitSuccess) {
+ onSubmitSuccess(formData) // Call success callback
+ }
+ // You might redirect the user or update application state here
+
+ // --- Mock Error Handling (remove in real implementation) ---
+ // if (formData.identifier === 'wrong') {
+ // throw new Error("Invalid credentials.");
+ // }
+ // --- End Mock Error Handling ---
+ } catch (apiError: unknown) {
+ console.error('Login API error:', apiError)
+ setSubmitError('Login failed. Please check your credentials.')
+ } finally {
+ setIsLoading(false)
+ }
+ // --- End API call section ---
+ },
+ [formData, validateForm, onSubmitSuccess],
+ )
+
+ // Effect to potentially load saved username on initial render (example)
+ // useEffect(() => {
+ // const savedUser = localStorage.getItem('savedUsername');
+ // if (savedUser) {
+ // setFormData(prev => ({ ...prev, identifier: savedUser, saveLogin: true }));
+ // }
+ // }, []);
+
+ return {
+ formData,
+ errors,
+ isLoading,
+ submitError,
+ handleChange,
+ handleSubmit,
+ }
+}
diff --git a/apps/browser/src/hooks/useRegisterForm.ts b/apps/browser/src/hooks/useRegisterForm.ts
new file mode 100644
index 0000000..77b83ad
--- /dev/null
+++ b/apps/browser/src/hooks/useRegisterForm.ts
@@ -0,0 +1,121 @@
+import React, { useState, useCallback } from 'react'
+import { registerSchema, RegisterFormData, FieldErrors } from '../validation'
+import { ZodError } from 'zod'
+
+interface UseRegisterFormProps {
+ onSubmitSuccess?: (data: RegisterFormData) => void // Optional callback for successful submission
+}
+
+export function useRegisterForm({
+ onSubmitSuccess,
+}: UseRegisterFormProps = {}) {
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ password: '',
+ passwordRepeat: '',
+ promocode: '',
+ })
+ const [errors, setErrors] = useState>({})
+ const [isLoading, setIsLoading] = useState(false)
+ const [submitError, setSubmitError] = useState(null) // For general API errors
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const { name, value } = e.target
+ setFormData(prev => ({ ...prev, [name]: value }))
+ // Clear error for the field being edited
+ if (errors[name as keyof RegisterFormData]) {
+ setErrors(prev => ({ ...prev, [name]: undefined }))
+ }
+ // Clear general submission error when user starts typing again
+ if (submitError) {
+ setSubmitError(null)
+ }
+ },
+ [errors, submitError],
+ )
+
+ const validateForm = useCallback(() => {
+ try {
+ registerSchema.parse(formData)
+ setErrors({}) // Clear errors if validation passes
+ return true
+ } catch (error) {
+ if (error instanceof ZodError) {
+ const fieldErrors: FieldErrors = {}
+ error.errors.forEach(err => {
+ if (err.path.length > 0) {
+ fieldErrors[err.path[0] as keyof RegisterFormData] =
+ err.message
+ }
+ })
+ setErrors(fieldErrors)
+ } else {
+ console.error(
+ 'An unexpected error occurred during validation:',
+ error,
+ )
+ setSubmitError('An unexpected validation error occurred.') // Generic error for non-Zod issues
+ }
+ return false
+ }
+ }, [formData])
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault()
+ setSubmitError(null) // Clear previous submit errors
+
+ if (!validateForm()) {
+ return // Stop submission if validation fails
+ }
+
+ setIsLoading(true)
+ console.log('Submitting registration data:', formData)
+
+ // --- TODO: Replace with actual API call to your RageMP server ---
+ try {
+ // Example: Simulate API call
+ await new Promise(resolve => setTimeout(resolve, 1500))
+
+ // Assuming API returns success:
+ console.log('Registration successful!')
+ setFormData({
+ username: '',
+ email: '',
+ password: '',
+ passwordRepeat: '',
+ promocode: '',
+ }) // Reset form
+ setErrors({})
+ if (onSubmitSuccess) {
+ onSubmitSuccess(formData) // Call success callback if provided
+ }
+
+ // --- Mock Error Handling (remove in real implementation) ---
+ // if (formData.email.includes('fail')) {
+ // throw new Error("Registration failed: Email already exists.");
+ // }
+ // --- End Mock Error Handling ---
+ } catch (apiError: unknown) {
+ console.error('Registration API error:', apiError)
+ // Try to set a user-friendly error message
+ setSubmitError('Registration failed. Please try again.')
+ } finally {
+ setIsLoading(false)
+ }
+ // --- End API call section ---
+ },
+ [formData, validateForm, onSubmitSuccess],
+ )
+
+ return {
+ formData,
+ errors,
+ isLoading,
+ submitError,
+ handleChange,
+ handleSubmit,
+ }
+}
diff --git a/apps/browser/src/index.css b/apps/browser/src/index.css
index 293d3b1..03d224a 100644
--- a/apps/browser/src/index.css
+++ b/apps/browser/src/index.css
@@ -1,3 +1,5 @@
+@import "tailwindcss";
+
body {
margin: 0;
}
diff --git a/apps/browser/src/pages/Authorization/LoginPage.tsx b/apps/browser/src/pages/Authorization/LoginPage.tsx
new file mode 100644
index 0000000..b7961ea
--- /dev/null
+++ b/apps/browser/src/pages/Authorization/LoginPage.tsx
@@ -0,0 +1,126 @@
+import React from 'react'
+import Input from '../../components/ui/Input'
+import Checkbox from '../../components/ui/Checkbox'
+import Button from '../../components/ui/Button'
+import { useLoginForm } from '../../hooks/useLoginForm'
+import { LoginFormData } from '../../validation'
+
+const LoginPage: React.FC = () => {
+ const handleLoginSuccess = (data: LoginFormData) => {
+ console.log('Login success callback triggered:', data)
+ // Redirect user or update app state
+ alert(`Login successful for ${data.identifier}!`)
+ }
+
+ const {
+ formData,
+ errors,
+ isLoading,
+ submitError,
+ handleChange,
+ handleSubmit,
+ } = useLoginForm({ onSubmitSuccess: handleLoginSuccess })
+
+ return (
+
+
+
+
+ Sign in to your account
+
+ {/* Optional: Link to registration page */}
+ {/*
+ Or{' '}
+
+ create a new account
+
+
*/}
+
+
+
+
+ )
+}
+
+export default LoginPage
diff --git a/apps/browser/src/pages/Authorization/RegisterPage.tsx b/apps/browser/src/pages/Authorization/RegisterPage.tsx
new file mode 100644
index 0000000..63c3d36
--- /dev/null
+++ b/apps/browser/src/pages/Authorization/RegisterPage.tsx
@@ -0,0 +1,134 @@
+import React from 'react'
+import Input from '../../components/ui/Input'
+import Button from '../../components/ui/Button'
+import { useRegisterForm } from '../../hooks/useRegisterForm'
+import { RegisterFormData } from '../../validation'
+
+const RegisterPage: React.FC = () => {
+ const handleRegistrationSuccess = (data: RegisterFormData) => {
+ console.log('Registration success callback triggered:', data)
+ // Maybe show a success message or redirect
+ alert(`Registration successful for ${data.username}!`)
+ }
+
+ const {
+ formData,
+ errors,
+ isLoading,
+ submitError,
+ handleChange,
+ handleSubmit,
+ } = useRegisterForm({ onSubmitSuccess: handleRegistrationSuccess })
+
+ return (
+
+ )
+}
+
+export default RegisterPage
diff --git a/apps/browser/src/validation/index.ts b/apps/browser/src/validation/index.ts
new file mode 100644
index 0000000..0554432
--- /dev/null
+++ b/apps/browser/src/validation/index.ts
@@ -0,0 +1,6 @@
+export * from './loginSchema.ts'
+export * from './registerSchema.ts'
+
+export type FieldErrors = {
+ [K in keyof T]?: string | undefined
+}
diff --git a/apps/browser/src/validation/loginSchema.ts b/apps/browser/src/validation/loginSchema.ts
new file mode 100644
index 0000000..23e5281
--- /dev/null
+++ b/apps/browser/src/validation/loginSchema.ts
@@ -0,0 +1,10 @@
+import { z } from 'zod'
+
+export const loginSchema = z.object({
+ identifier: z.string().min(1, { message: 'Email or Username is required' }), // Can be email or username
+ password: z.string().min(1, { message: 'Password is required' }),
+ saveLogin: z.boolean().optional().default(false),
+ savePassword: z.boolean().optional().default(false),
+})
+
+export type LoginFormData = z.infer
diff --git a/apps/browser/src/validation/registerSchema.ts b/apps/browser/src/validation/registerSchema.ts
new file mode 100644
index 0000000..0f9121c
--- /dev/null
+++ b/apps/browser/src/validation/registerSchema.ts
@@ -0,0 +1,28 @@
+import { z } from 'zod'
+
+// --- Registration Schema ---
+export const registerSchema = z
+ .object({
+ username: z
+ .string()
+ .min(3, { message: 'Username must be at least 3 characters long' })
+ .max(20, { message: 'Username cannot exceed 20 characters' })
+ .regex(/^[a-zA-Z0-9_]+$/, {
+ message:
+ 'Username can only contain letters, numbers, and underscores',
+ }),
+ email: z.string().email({ message: 'Invalid email address' }),
+ password: z
+ .string()
+ .min(8, { message: 'Password must be at least 8 characters long' }),
+ // Optional: Add more complexity requirements if needed
+ // .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/, { message: "Password must contain uppercase, lowercase, and number" })
+ passwordRepeat: z.string(),
+ promocode: z.string().optional(), // Promocode is optional
+ })
+ .refine(data => data.password === data.passwordRepeat, {
+ message: "Passwords don't match",
+ path: ['passwordRepeat'], // Set error path to passwordRepeat field
+ })
+
+export type RegisterFormData = z.infer
diff --git a/apps/browser/vite.config.ts b/apps/browser/vite.config.ts
index e84ee6e..47fe383 100644
--- a/apps/browser/vite.config.ts
+++ b/apps/browser/vite.config.ts
@@ -1,11 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
- build: {
- outDir: '../../server/client_packages/cef',
- emptyOutDir: true
- }
+ plugins: [react(), tailwindcss()],
+ build: {
+ outDir: '../../server/client_packages/cef',
+ emptyOutDir: true,
+ },
})