Tạo trang Profile

This commit is contained in:
Truong Vo 2024-09-16 00:52:53 +07:00
parent 1b84528dec
commit 69806fda4b
4 changed files with 373 additions and 53 deletions

View File

@ -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 },

View File

@ -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);
}

View File

@ -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>
)

View File

@ -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': {