This commit is contained in:
lumijiez
2025-06-24 16:31:24 +03:00
parent 50adfd8d6b
commit 850e8ae852
3 changed files with 482 additions and 2 deletions

104
webui/package-lock.json generated
View File

@@ -21,13 +21,15 @@
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"axios": "^1.9.0",
"formik": "^2.4.6",
"i18next": "^25.2.1",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"next-i18next": "^15.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^15.5.3"
"react-i18next": "^15.5.3",
"yup": "^1.6.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -1954,6 +1956,15 @@
}
}
},
"node_modules/deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2134,6 +2145,31 @@
"node": ">= 6"
}
},
"node_modules/formik": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
"integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
"funding": [
{
"type": "individual",
"url": "https://opencollective.com/formik"
}
],
"license": "Apache-2.0",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.1",
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -2634,6 +2670,18 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -2991,6 +3039,12 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3018,6 +3072,12 @@
"react": "^19.1.0"
}
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
"license": "MIT"
},
"node_modules/react-i18next": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.3.tgz",
@@ -3287,12 +3347,42 @@
"node": ">=18"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
@@ -3329,6 +3419,18 @@
"engines": {
"node": ">= 6"
}
},
"node_modules/yup": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
}
}
}

View File

@@ -22,13 +22,15 @@
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"axios": "^1.9.0",
"formik": "^2.4.6",
"i18next": "^25.2.1",
"lucide-react": "^0.516.0",
"next": "15.3.3",
"next-i18next": "^15.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^15.5.3"
"react-i18next": "^15.5.3",
"yup": "^1.6.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

376
webui/src/app/form/page.js Normal file
View File

@@ -0,0 +1,376 @@
'use client';
import React from 'react';
import {
Box,
Card,
CardContent,
TextField,
Button,
Typography,
Grid,
MenuItem,
FormControl,
InputLabel,
Select,
FormHelperText,
Chip,
Avatar,
Container,
} from '@mui/material';
import { useFormik } from 'formik';
import * as yup from 'yup';
import PersonIcon from '@mui/icons-material/Person';
import EmailIcon from '@mui/icons-material/Email';
import PhoneIcon from '@mui/icons-material/Phone';
import WorkIcon from '@mui/icons-material/Work';
import SendIcon from '@mui/icons-material/Send';
const validationSchema = yup.object({
firstName: yup
.string('Enter your first name')
.min(2, 'First name should be at least 2 characters')
.required('First name is required'),
lastName: yup
.string('Enter your last name')
.min(2, 'Last name should be at least 2 characters')
.required('Last name is required'),
email: yup
.string('Enter your email')
.email('Enter a valid email')
.required('Email is required'),
phone: yup
.string('Enter your phone number')
.matches(/^[\+]?[1-9][\d]{0,15}$/, 'Enter a valid phone number')
.required('Phone number is required'),
age: yup
.number('Enter your age')
.min(18, 'Must be at least 18 years old')
.max(120, 'Must be less than 120 years old')
.required('Age is required'),
department: yup
.string('Select a department')
.required('Department is required'),
skills: yup
.array()
.min(1, 'Select at least one skill')
.required('Skills are required'),
bio: yup
.string('Enter your bio')
.min(10, 'Bio should be at least 10 characters')
.max(500, 'Bio should not exceed 500 characters')
.required('Bio is required'),
});
const departments = [
'Engineering',
'Marketing',
'Sales',
'HR',
'Finance',
'Operations',
'Design',
];
const skillOptions = [
'JavaScript',
'React',
'Node.js',
'Python',
'UI/UX Design',
'Project Management',
'Data Analysis',
'Marketing',
'Sales',
'Communication',
];
export default function FormPage() {
const formik = useFormik({
initialValues: {
firstName: '',
lastName: '',
email: '',
phone: '',
age: '',
department: '',
skills: [],
bio: '',
},
validationSchema: validationSchema,
onSubmit: (values, { setSubmitting, resetForm }) => {
setTimeout(() => {
console.log('Form submitted:', values);
alert('Form submitted successfully!');
setSubmitting(false);
resetForm();
}, 1000);
},
});
const handleSkillChange = (event) => {
const value = event.target.value;
formik.setFieldValue('skills', typeof value === 'string' ? value.split(',') : value);
};
return (
<Box
sx={{
minHeight: '100vh',
background: (theme) =>
theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
py: 4,
}}
>
<Container maxWidth="sm">
<Card
elevation={8}
sx={{
borderRadius: 3,
background: (theme) =>
theme.palette.mode === 'dark'
? 'rgba(26, 26, 46, 0.95)'
: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
border: (theme) =>
theme.palette.mode === 'dark'
? '1px solid rgba(99, 102, 241, 0.2)'
: '1px solid rgba(255, 255, 255, 0.3)',
}}
>
<CardContent sx={{ p: 4 }}>
<Box
display="flex"
flexDirection="column"
alignItems="center"
mb={4}
>
<Avatar
sx={{
bgcolor: 'primary.main',
width: 56,
height: 56,
mb: 2,
}}
>
<PersonIcon fontSize="large" />
</Avatar>
<Typography
variant="h4"
component="h1"
fontWeight="bold"
color="primary"
textAlign="center"
>
User Registration
</Typography>
<Typography variant="subtitle1" color="text.secondary" textAlign="center">
Please fill out all required fields
</Typography>
</Box>
<form onSubmit={formik.handleSubmit}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Typography variant="h6" color="primary" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PersonIcon /> Personal Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<TextField
fullWidth
id="firstName"
name="firstName"
label="First Name"
value={formik.values.firstName}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.firstName && Boolean(formik.errors.firstName)}
helperText={formik.touched.firstName && formik.errors.firstName}
variant="outlined"
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
id="lastName"
name="lastName"
label="Last Name"
value={formik.values.lastName}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.lastName && Boolean(formik.errors.lastName)}
helperText={formik.touched.lastName && formik.errors.lastName}
variant="outlined"
/>
</Grid>
</Grid>
<TextField
fullWidth
id="email"
name="email"
label="Email Address"
type="email"
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
variant="outlined"
InputProps={{
startAdornment: <EmailIcon sx={{ color: 'action.active', mr: 1 }} />,
}}
/>
<TextField
fullWidth
id="phone"
name="phone"
label="Phone Number"
value={formik.values.phone}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.phone && Boolean(formik.errors.phone)}
helperText={formik.touched.phone && formik.errors.phone}
variant="outlined"
InputProps={{
startAdornment: <PhoneIcon sx={{ color: 'action.active', mr: 1 }} />,
}}
/>
<TextField
fullWidth
id="age"
name="age"
label="Age"
type="number"
value={formik.values.age}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.age && Boolean(formik.errors.age)}
helperText={formik.touched.age && formik.errors.age}
variant="outlined"
/>
<Typography variant="h6" color="primary" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
<WorkIcon /> Professional Information
</Typography>
<FormControl
fullWidth
error={formik.touched.department && Boolean(formik.errors.department)}
>
<InputLabel id="department-label">Department</InputLabel>
<Select
labelId="department-label"
id="department"
name="department"
value={formik.values.department}
label="Department"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
>
{departments.map((dept) => (
<MenuItem key={dept} value={dept}>
{dept}
</MenuItem>
))}
</Select>
{formik.touched.department && formik.errors.department && (
<FormHelperText>{formik.errors.department}</FormHelperText>
)}
</FormControl>
<FormControl
fullWidth
error={formik.touched.skills && Boolean(formik.errors.skills)}
>
<InputLabel id="skills-label">Skills</InputLabel>
<Select
labelId="skills-label"
id="skills"
multiple
value={formik.values.skills}
onChange={handleSkillChange}
onBlur={formik.handleBlur}
label="Skills"
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{skillOptions.map((skill) => (
<MenuItem key={skill} value={skill}>
{skill}
</MenuItem>
))}
</Select>
{formik.touched.skills && formik.errors.skills && (
<FormHelperText>{formik.errors.skills}</FormHelperText>
)}
</FormControl>
<TextField
fullWidth
id="bio"
name="bio"
label="Bio"
multiline
rows={4}
value={formik.values.bio}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error={formik.touched.bio && Boolean(formik.errors.bio)}
helperText={
formik.touched.bio && formik.errors.bio
? formik.errors.bio
: `${formik.values.bio.length}/500 characters`
}
variant="outlined"
placeholder="Tell us about yourself..."
/>
<Button
color="primary"
variant="contained"
fullWidth
size="large"
type="submit"
disabled={formik.isSubmitting}
startIcon={<SendIcon />}
sx={{
mt: 2,
py: 1.5,
borderRadius: 3,
background: (theme) =>
theme.palette.mode === 'dark'
? 'linear-gradient(45deg, #6366f1 30%, #8b5cf6 90%)'
: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
boxShadow: (theme) =>
theme.palette.mode === 'dark'
? '0 3px 5px 2px rgba(99, 102, 241, .3)'
: '0 3px 5px 2px rgba(255, 105, 135, .3)',
'&:hover': {
background: (theme) =>
theme.palette.mode === 'dark'
? 'linear-gradient(45deg, #5b21b6 60%, #7c3aed 100%)'
: 'linear-gradient(45deg, #FE6B8B 60%, #FF8E53 100%)',
},
}}
>
{formik.isSubmitting ? 'Submitting...' : 'Submit Registration'}
</Button>
</Box>
</form>
</CardContent>
</Card>
</Container>
</Box>
);
}