Tạo trang Profile
This commit is contained in:
parent
1b84528dec
commit
69806fda4b
|
|
@ -43,7 +43,6 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||
import classes from './NavbarSimpleColored.module.css'
|
||||
import { IconListCheck } from '@tabler/icons-react'
|
||||
import { label } from '../LanguagePicker/LanguagePicker.module.css'
|
||||
|
||||
const data = [
|
||||
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
display: flex;
|
||||
margin-top: 20px;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Sidebar section for user information */
|
||||
|
|
@ -150,5 +151,46 @@
|
|||
|
||||
.criteriaTable {
|
||||
padding-left: 55px; /* Further indentation for the criteria table */
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: rgb(255, 255, 255);
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
margin: var(--mantine-spacing-xs);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background-color: #00a164;
|
||||
|
||||
@mixin hover {
|
||||
/* background-color: #035737; */
|
||||
color: var(--mantine-color-white);
|
||||
|
||||
.linkIcon {
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
&,
|
||||
&:hover {
|
||||
box-shadow: var(--mantine-shadow-sm);
|
||||
background-color: #6089afb7;
|
||||
color: var(--mantine-color-white);
|
||||
|
||||
.linkIcon {
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
color: rgb(255, 255, 255);
|
||||
width: rem(20px);
|
||||
height: rem(22px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +1,56 @@
|
|||
import { changePassword } from '@/api/Auth'
|
||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
|
||||
import { logout } from '@/rtk/dispatches/auth'
|
||||
import { post } from '@/rtk/helpers/apiService'
|
||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
Box,
|
||||
Group,
|
||||
Button,
|
||||
Modal,
|
||||
PasswordInput,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Title
|
||||
} from '@mantine/core'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
IconCornerDownRight,
|
||||
IconChevronRight,
|
||||
IconChevronDown,
|
||||
} from '@tabler/icons-react'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { IconCornerDownRight, IconPasswordUser } from '@tabler/icons-react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import classes from './Profile.module.css'
|
||||
interface TableRow {
|
||||
// Add properties for each column in the table
|
||||
criteria: string
|
||||
note: string
|
||||
createdBy: string
|
||||
point: string
|
||||
}
|
||||
|
||||
const CriteriaTable = ({ data }) => (
|
||||
interface ExpandedProjects {
|
||||
[projectName: string]: boolean
|
||||
}
|
||||
|
||||
interface ExpandedSprints {
|
||||
[projectName: string]: {
|
||||
[sprintName: string]: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const CriteriaTable = ({ data }: { data: TableRow[] }) => (
|
||||
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr bg="#228be66b" style={{ textAlign: 'center' }}>
|
||||
<Table.Th>Criteria</Table.Th>
|
||||
<Table.Th>Note</Table.Th>
|
||||
<Table.Th>Created by</Table.Th>
|
||||
<Table.Th>Point</Table.Th>
|
||||
<Table.Tr bg="#228be66b">
|
||||
<Table.Th style={{ textAlign: 'center' }}>Criteria</Table.Th>
|
||||
<Table.Th style={{ textAlign: 'center' }}>Note</Table.Th>
|
||||
<Table.Th style={{ textAlign: 'center' }}>Created by</Table.Th>
|
||||
<Table.Th style={{ textAlign: 'center' }}>Point</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.map((row, index) => (
|
||||
<Table.Tr key={index} style={{ textAlign: 'center' }}>
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{row.criteria}</Table.Td>
|
||||
<Table.Td>{row.note}</Table.Td>
|
||||
<Table.Td>{row.createdBy}</Table.Td>
|
||||
|
|
@ -44,7 +67,7 @@ const projectsData = [
|
|||
sprints: [
|
||||
{
|
||||
name: 'Sprint 1',
|
||||
criteria: [
|
||||
criterias: [
|
||||
{
|
||||
criteria: 'Criteria 1',
|
||||
note: 'String (default when selecting point)',
|
||||
|
|
@ -61,7 +84,7 @@ const projectsData = [
|
|||
},
|
||||
{
|
||||
name: 'Sprint 2',
|
||||
criteria: [
|
||||
criterias: [
|
||||
{
|
||||
criteria: 'Criteria 3',
|
||||
note: 'String (default when selecting point)',
|
||||
|
|
@ -83,7 +106,7 @@ const projectsData = [
|
|||
sprints: [
|
||||
{
|
||||
name: 'Sprint 1',
|
||||
criteria: [
|
||||
criterias: [
|
||||
{
|
||||
criteria: 'Criteria 7',
|
||||
note: 'String (default when selecting point)',
|
||||
|
|
@ -95,19 +118,116 @@ const projectsData = [
|
|||
],
|
||||
},
|
||||
]
|
||||
const isCompactMenu = false
|
||||
const Profile = () => {
|
||||
const [projects, setProjects] = useState(projectsData)
|
||||
const [expandedProject, setExpandedProject] = useState(null)
|
||||
const [expandedSprint, setExpandedSprint] = useState({})
|
||||
const user = useAppSelector((state) => state.authentication.user)
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [dataChange, setDataChange] = useState({
|
||||
password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleProjectToggle = (projectName) => {
|
||||
setExpandedProject((prev) => (prev === projectName ? null : projectName))
|
||||
// const [projects, setProjects] = useState(projectsData)
|
||||
const [expandedProjects, setExpandedProjects] = useState<ExpandedProjects>({})
|
||||
const [expandedSprints, setExpandedSprints] = useState<ExpandedSprints>({})
|
||||
|
||||
const [countSpam, setCountSpam] = useState(0)
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
const passwordRegex =
|
||||
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
|
||||
|
||||
// const { setColorScheme } = useMantineColorScheme()
|
||||
// const computedColorScheme = useComputedColorScheme('light', {
|
||||
// getInitialValueInEffect: true,
|
||||
// })
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
try {
|
||||
if (countSpam > 5) {
|
||||
setLoading(true)
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Password error more than 5 times. Logout after 3s',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
localStorage.clear()
|
||||
window.location.reload()
|
||||
}, 3000)
|
||||
} else {
|
||||
setLoading(true)
|
||||
const res = await post(
|
||||
changePassword,
|
||||
{
|
||||
email: user.user.email,
|
||||
password: dataChange.password,
|
||||
new_password: dataChange.new_password,
|
||||
confirm_password: dataChange.confirm_password,
|
||||
},
|
||||
undefined,
|
||||
)
|
||||
|
||||
if (res.status) {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Reset password success',
|
||||
color: 'green',
|
||||
})
|
||||
|
||||
setOpened(false)
|
||||
setDataChange({
|
||||
password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
handleLogout()
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: res.errors.password,
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
setCountSpam(countSpam + 1)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
if (error.response.status === 422) {
|
||||
const errorMess = error.response.data.message
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: errorMess,
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSprintToggle = (projectName, sprintName) => {
|
||||
setExpandedSprint((prev) => ({
|
||||
const handleLogout = useCallback(() => {
|
||||
dispatch(logout(navigate))
|
||||
}, [dispatch, navigate])
|
||||
const handleProjectToggle = (projectName: string) => {
|
||||
setExpandedProjects((prev) => ({
|
||||
...prev,
|
||||
[projectName]: prev[projectName] === sprintName ? null : sprintName,
|
||||
[projectName]: !prev[projectName], // Toggle state for this project
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSprintToggle = (projectName: string, sprintName: string) => {
|
||||
setExpandedSprints((prev) => ({
|
||||
...prev,
|
||||
[projectName]: {
|
||||
...prev[projectName],
|
||||
[sprintName]: !prev[projectName]?.[sprintName], // Toggle state for this sprint
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -124,46 +244,103 @@ const Profile = () => {
|
|||
<Box className={classes.userInfoSection} display="flex">
|
||||
<Box className={classes.sidebar}>
|
||||
<Title order={3}>User Information</Title>
|
||||
<Box
|
||||
w={'100%'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={null || '/public/backgroundLogin.png'}
|
||||
alt="User Avatar"
|
||||
size={150}
|
||||
// radius="xl"
|
||||
mb={5}
|
||||
mt={5}
|
||||
onClick={() => {
|
||||
console.log('update avatar')
|
||||
}}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
// border: '2px solid black',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={classes.userInfo} mt="md">
|
||||
<TextInput label="Name" placeholder="Name" withAsterisk mb="md" />
|
||||
<TextInput
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
mb="md"
|
||||
readOnly
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
withAsterisk
|
||||
mb="md"
|
||||
readOnly
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
<TextInput
|
||||
label="Join Date"
|
||||
type="date"
|
||||
mb="md"
|
||||
readOnly
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
<TextInput label="Join Date" type="date" withAsterisk mb="md" />
|
||||
<TextInput
|
||||
label="Permission(s)"
|
||||
placeholder="Permissions"
|
||||
withAsterisk
|
||||
readOnly
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
w={'100%'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
mt={10}
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
className={classes.link}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
<IconPasswordUser className={classes.linkIcon} stroke={1.5} />
|
||||
<span
|
||||
className={`${classes.itemLabel} ${classes.labelCompactMenu}`}
|
||||
>
|
||||
Change password
|
||||
</span>
|
||||
</a>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className={classes.projectInvolvement}>
|
||||
<Title order={3} align="center">
|
||||
Project Involved
|
||||
</Title>
|
||||
<Title order={3}>Project Involved</Title>
|
||||
<Box className={classes.project} mt="lg">
|
||||
{projects.map((project) => (
|
||||
{projectsData.map((project) => (
|
||||
<div key={project.name}>
|
||||
{/* Project Header */}
|
||||
<Box
|
||||
className={classes.projectHeader}
|
||||
onClick={() => handleProjectToggle(project.name)}
|
||||
>
|
||||
{/* {expandedProject === project.name ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)} */}
|
||||
<Text ml="sm">{project.name}</Text>
|
||||
<Text ml="sm" fw={600}>
|
||||
{project.name}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Project's Sprints */}
|
||||
{expandedProject === project.name && (
|
||||
{expandedProjects[project.name] && (
|
||||
<Box className={classes.sprintList}>
|
||||
{project.sprints.map((sprint) => (
|
||||
<div key={sprint.name}>
|
||||
|
|
@ -174,18 +351,24 @@ const Profile = () => {
|
|||
}
|
||||
>
|
||||
<IconCornerDownRight size={20} />
|
||||
{/* {expandedSprint[project.name] === sprint.name ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)} */}
|
||||
<Text ml="xs" className={classes.sprintHeader2}>{sprint.name}</Text>
|
||||
<Text
|
||||
ml="xs"
|
||||
className={classes.sprintHeader2}
|
||||
fw={600}
|
||||
>
|
||||
{sprint.name}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Criteria Table */}
|
||||
{expandedSprint[project.name] === sprint.name && (
|
||||
{expandedSprints[project.name]?.[sprint.name] && (
|
||||
<Box className={classes.criteriaTable}>
|
||||
<CriteriaTable data={sprint.criteria} />
|
||||
{/* <CriteriaTable data={sprint.criteria} /> */}
|
||||
<CriteriaTable
|
||||
data={sprint.criterias.map((criteria) => ({
|
||||
...criteria,
|
||||
}))}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -197,6 +380,102 @@ const Profile = () => {
|
|||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
setOpened(false)
|
||||
setDataChange({
|
||||
password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
})
|
||||
}}
|
||||
title={
|
||||
<Text
|
||||
fw={700}
|
||||
fz={'1.2rem'}
|
||||
className={`${classes.itemLabel} ${
|
||||
isCompactMenu ? classes.labelCompactMenu : ''
|
||||
}`}
|
||||
>
|
||||
Change password
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Box p="sm">
|
||||
<TextInput
|
||||
label="E-mail"
|
||||
value={user.user.email}
|
||||
disabled
|
||||
mb={'md'}
|
||||
></TextInput>
|
||||
|
||||
<PasswordInput
|
||||
label="Current password"
|
||||
required
|
||||
placeholder="Current password"
|
||||
maxLength={32}
|
||||
value={dataChange.password}
|
||||
onChange={(e) => {
|
||||
setDataChange({ ...dataChange, password: e.target.value })
|
||||
}}
|
||||
error={
|
||||
dataChange.password.length < 8 &&
|
||||
dataChange.password !== '' &&
|
||||
'Length 8 characters or more'
|
||||
}
|
||||
/>
|
||||
|
||||
<PasswordRequirementInput
|
||||
requirements={requirementsPassword}
|
||||
value={dataChange}
|
||||
setValue={setDataChange}
|
||||
label="New password"
|
||||
placeholder="New password"
|
||||
name="new_password"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Confirm password"
|
||||
required
|
||||
placeholder="Confirm password"
|
||||
maxLength={32}
|
||||
value={dataChange.confirm_password}
|
||||
onChange={(e) => {
|
||||
setDataChange({
|
||||
...dataChange,
|
||||
confirm_password: e.target.value,
|
||||
})
|
||||
}}
|
||||
error={
|
||||
dataChange.new_password !== dataChange.confirm_password &&
|
||||
dataChange.confirm_password !== '' &&
|
||||
'Password do not match'
|
||||
}
|
||||
></PasswordInput>
|
||||
|
||||
<Button
|
||||
style={{ float: 'right' }}
|
||||
mb={'lg'}
|
||||
mt={'lg'}
|
||||
loading={loading}
|
||||
disabled={
|
||||
loading === false &&
|
||||
dataChange.password !== '' &&
|
||||
passwordRegex.test(dataChange.new_password) &&
|
||||
dataChange.new_password === dataChange.confirm_password &&
|
||||
dataChange.new_password !== ''
|
||||
? false
|
||||
: true
|
||||
}
|
||||
onClick={() => handleChangePassword()}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ const SprintReview = () => {
|
|||
// fontWeight: '700',
|
||||
// fontSize: 'initial',
|
||||
// }}
|
||||
styles={(theme) => ({
|
||||
styles={() => ({
|
||||
input: {
|
||||
// borderColor: theme.colors.blue[6], // Border color for the input
|
||||
// '&:hover': {
|
||||
|
|
|
|||
Loading…
Reference in New Issue