Обзор: Все представления и функциональность, связанные с профилем, все вызываемые функции исходят от profileReducer.
Страница маршрута профиля
внутри routes > profile > profile-page.tsx
импортируются функции, которые помогают вызывать функции.
При монтировании я получаю профиль по id
, который я получаю из useParams
. Если для параметра редактирования установлено значение true, я отображаю компонент редактирования профиля. Если нет, то отображается компонент фактического профиля.
import { useEffect } from 'react';
import { useParams } from 'react-router';
import { getProfileById } from '../../app/features/profile/profileSlice';
import BeatLoader from 'react-spinners/BeatLoader';
import { useAppSelector, useAppDispatch } from '../../app/hooks';
import Profile from '../../components/profile/profile.component';
import EditProfile from '../../components/profile/edit-profile.component';
const ProfilePage = () => {
const { id } = useParams();
const dispatch = useAppDispatch();
const { isLoading, isEditting } = useAppSelector((state) => state.profile);
useEffect(() => {
dispatch(getProfileById(id));
}, [id, dispatch]);
return (
<>
{isLoading ? (
<div className="text-center p-20">
<BeatLoader color="#ffffff" />
</div>
) : (
<>{isEditting ? <EditProfile /> : <Profile />}</>
)}
</>
);
};
export default ProfilePage;
Компонент профиля
внутри components > profile > profile.component.tsx
импортирует функции для вызова.
Функциональность
settingEditView
переключает isEditting
, чтобы определить, должен ли компонент профиля редактирования отображаться или нет. removeItem
удаляет проекты (по фильтру) и устанавливает новый результат в состояние project
. Я использую currentUser
для отображения информации в UI.
import { setEditView, setProjects } from '../../app/features/profile/profileSlice';
import { useAppDispatch, useAppSelector } from '../../app/hooks';
import Experience from '../experience/experience-component';
import Project from '../project/project-component';
const Profile = () => {
const dispatch = useAppDispatch();
const { profile, isEditting } = useAppSelector((state) => state.profile);
const { currentUser } = useAppSelector((state) => state.auth);
const settingEditView = () => {
dispatch(setEditView(!isEditting));
};
const removeItem = (id: number) => {
const newProjects = profile.projects.filter((_, i) => i !== id);
dispatch(setProjects(newProjects));
};
return ( {/* removed for simplicity */} )
}
UI
Отображает информацию о профиле (проекты, опыт, навыки, резюме, URL сайта и т.д.). Не будет отображаться кнопка редактирования профиля, если идентификаторы текущего пользователя id
и профиля не совпадают. Projects
и Experiences
являются отдельными компонентами. По умолчанию не отображается никаких данных, пока вы не отредактируете профиль.
const Profile = () => {
{/* removed for simplicity */}
return (
<>
{profile && (
<>
<section style={{ backgroundColor: '#252731' }}>
<div className="container mx-auto py-5 px-5 md:px-0 text-right text-white flex justify-between">
<div className="flex justify-center text-md text-slate-200">
{profile.isForHire ? (
<p>Actively looking for work</p>
) : (
<p>Not currently looking for work</p>
)}
</div>
{currentUser.uid !== profile.id ? null : (
<button
onClick={settingEditView}
className="underline text-md text-indigo-500"
>
Edit Profile
</button>
)}
</div>
<div className="md:px-12 lg:px-24 max-w-7xl relative items-center w-full px-5 py-5 mx-auto">
<div className="mx-auto flex flex-col w-full max-w-lg mb-12 text-center">
<p className="mb-5 font-medium text-2xl text-white">
{currentUser.displayName}
</p>
<img
alt="testimonial"
className="inline-block object-cover object-center w-20 h-20 mx-auto mb-8 rounded-full"
src="https://picsum.photos/200"
/>
<div className="flex justify-center">
{profile.headline ? (
<p className="text-base leading-relaxed font-color pr-2">
{profile.headline}
</p>
) : null}
{profile.websiteURL ? (
<>
<a
href={profile.websiteURL}
className="text-base leading-relaxed text-indigo-500 border-l-2 border-gray-500 pl-2"
>
Live Website
</a>
</>
) : (
<p className="font-color">No website to show</p>
)}
</div>
</div>
</div>
</section>
<div className="divide-y divide-gray-700" key={profile.id}>
<section className="text-gray-600 body-font mt-2">
<div className="container px-10 py-20 mx-auto">
<div className="flex flex-col w-full mx-auto">
<div className="w-full mx-auto">
<h2 className="sm:text-3xl text-2xl my-5 font-bold">
About Me
</h2>
<p className="lg:w-3/4 about-me font-color">
{profile.summary ? profile.summary : 'No info to show'}
</p>
</div>
</div>
</div>
</section>
<section className="text-gray-600 body-font overflow-hidden">
<div className="container px-10 py-20 mx-auto">
<div className="flex flex-col w-full mx-auto">
<div className="w-full mx-auto">
<h2 className="sm:text-3xl text-2xl mb-5 font-bold">
Skills
</h2>
{profile.skills.length ? (
<ul className="flex flex-wrap">
{profile.skills.map((skill, id) => {
return (
<li
className="mr-2 my-2 text-white px-4 py-2 rounded-3xl bg-indigo-700 cursor-pointer["
key={id}
>
{skill}
</li>
);
})}
</ul>
) : (
<p className="font-color">No skills to show</p>
)}
</div>
</div>
</div>
</section>
{/* Experience starts */}
<section className="text-gray-600 body-font overflow-hidden">
<div className="container px-10 py-20 mx-auto">
<div className="-my-8 mx-auto">
<h2 className="text-3xl my-8 mb-5 font-bold">Experience</h2>
{profile.experience.length ? (
<ol className="border-l-2 border-indigo-700 mt-10">
{profile.experience.map((exp, index) => {
return (
<Experience
experienceData={exp}
key={index}
itemIndex={index}
/>
);
})}
</ol>
) : (
<p className="font-color">No experiences to show</p>
)}
</div>
</div>
</section>
{/* Projects starts */}
<section className="text-gray-600 body-font">
<div className="container px-10 py-24 mx-auto">
<h2 className="sm:text-3xl text-2xl font-bold title-font mb-5">
Projects
</h2>
{profile.projects.length ? (
<div className="container py-5 mx-auto">
<div className="flex flex-wrap -m-4">
{profile.projects.map((proj, index) => {
return (
<Project
project={proj}
key={index}
itemIndex={index}
removeItem={removeItem}
/>
);
})}
</div>
</div>
) : (
<p className="font-color">No projects to show</p>
)}
</div>
</section>
</div>
</>
)}
</>
);
};
export default Profile;
Скриншоты
Компонент редактирования профиля
внутри components > profile > edit-profile.component.tsx
Импортирует: TagsInput
react пакет для добавления тегов, я использую его для добавления навыков, ExperiencePopupModal
pop-up modal для добавления опыта работы в профиль, ProjectPopupModal
pop-up modal для добавления проектов. Остальное — функции из profileReducer
для вызова, когда это необходимо.
import { ChangeEvent, useState } from 'react';
import { useNavigate } from 'react-router';
import { TagsInput } from 'react-tag-input-component';
import {
setEditView,
setProjects,
updateProfileById,
} from '../../app/features/profile/profileSlice';
import { useAppDispatch, useAppSelector } from '../../app/hooks';
import Experience from '../experience/experience-component';
import ExperiencePopupModal from '../modal/experience-modal.component';
import ProjectPopupModal from '../modal/project-modal.component';
import Project from '../project/project-component';
Функциональность
Я обрабатываю все поля, которые должны быть обновлены formFields
, skills
, и checked
для переключения состояния найма. isOpen
для открытия/закрытия модала опыта. isProjOpen
для модала открытия/закрытия проекта.
handleCheckedChange
— переключение состояния isForHire, определяет.
ищет ли пользователь работу.
settingEditView
— переключение вида редактирования.
onHandleChange
— обработка изменений для состояния formFields
.
onTextAreaChange
— обработка изменений для текстовой области
updateProfile
— обновление профиля, установка вида редактирования на false, перенаправление на главную страницу.
removeItem
— удаляет проекты по id
(используя фильтр).
closeModal
— закрывает модальную страницу опыта.
closeProjModal
— закрывает модальное окно проекта
const EditProfile = () => {
const { profile, isEditting } = useAppSelector((state) => state.profile);
const { currentUser } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [formFields, setFormFields] = useState({
headline: profile.headline ? profile.headline : '',
summary: profile.summary ? profile.summary : '',
websiteURL: profile.websiteURL ? profile.websiteURL : '',
});
const [skills, setSkills] = useState<string[]>(
profile.skills ? profile.skills : []
);
const [checked, setChecked] = useState<boolean>(profile.isForHire);
const [isOpen, setIsOpen] = useState(false);
const [isProjOpen, setIsProjOpen] = useState(false);
const handleCheckedChange = () => {
setChecked(!checked);
};
const settingEditView = () => {
dispatch(setEditView(!isEditting));
};
const onHandleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormFields({ ...formFields, [name]: value });
};
const onTextAreaChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setFormFields({ ...formFields, summary: event.target.value });
};
const updateProfile = async () => {
dispatch(
updateProfileById({
id: profile.id,
headline: formFields.headline,
summary: formFields.summary,
isForHire: checked,
websiteURL: formFields.websiteURL,
skills: skills,
experience: profile.experience,
projects: profile.projects,
})
);
dispatch(setEditView(false));
navigate('/app');
};
const removeItem = (id: number) => {
const newProjects = profile.projects.filter((_, i) => i !== id);
dispatch(setProjects(newProjects));
};
const closeModal = () => {
setIsOpen(false);
};
const closeProjModal = () => {
setIsProjOpen(false);
};
return ( {/* removed for simplicity */} );
};
export default EditProfile;
UI
Отображает все поля, которые вы можете редактировать. Вместе с кнопками для открытия модального окна опыта или модального окна проекта.
const EditProfile = () => {
{/* removed for simplicity */}
return (
<>
<section style={{ backgroundColor: '#252731' }}>
<div className="container mx-auto py-5 text-right text-white flex justify-between">
<div>
<label
htmlFor="toggle-example"
className="flex items-center cursor-pointer relative mb-4"
>
<input
onChange={handleCheckedChange}
checked={checked}
type="checkbox"
id="toggle-example"
className="sr-only"
/>
<div className="toggle-bg bg-gray-200 border-2 border-gray-200 h-6 w-11 rounded-full"></div>
<span className="ml-3 text-slate-200 text-md font-medium">
Are you looking for work?
</span>
</label>
</div>
<div>
<button
onClick={updateProfile}
className="mr-2 text-lg text-indigo-500"
>
Update
</button>
<button onClick={settingEditView}>Go Back</button>
</div>
</div>
<div className="md:px-12 lg:px-24 max-w-7xl relative items-center w-full px-5 py-5 mx-auto">
<div className="mx-auto flex flex-col w-full max-w-lg mb-12 text-center">
<p className="mb-5 font-medium text-2xl text-white">
{currentUser.displayName}
</p>
<img
alt="testimonial"
className="inline-block object-cover object-center w-20 h-20 mx-auto mb-8 rounded-full"
src="https://picsum.photos/200"
/>
<div className="flex justify-center">
<input
className="font-color mr-3 primary-bg-color input-border-color block w-full px-5 py-3 mt-2 text-base text-neutral-600 placeholder-gray-500 transition duration-500 ease-in-out transform border border-transparent rounded-lg focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 apearance-none"
id="headline"
value={formFields.headline}
onChange={onHandleChange}
name="headline"
placeholder="e.g. Front-end Developer"
/>
<input
className="font-color primary-bg-color input-border-color block w-full px-5 py-3 mt-2 text-base text-neutral-600 placeholder-gray-500 transition duration-500 ease-in-out transform border border-transparent rounded-lg focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 apearance-none"
id="websiteURL"
value={formFields.websiteURL}
onChange={onHandleChange}
name="websiteURL"
placeholder="Add website url..."
/>
</div>
</div>
</div>
</section>
<section>
<div className="divide-y divide-gray-700">
<section className="text-gray-600 body-font mt-2">
<div className="container px-5 py-20 mx-auto">
<div className="flex flex-col w-full mx-auto">
<div className="w-full mx-auto">
<h2 className="sm:text-3xl text-2xl my-5 font-bold">
About Me
</h2>
<div>
<textarea
maxLength={4000}
rows={5}
className="font-color input-border-color secondary-bg-color block w-full px-5 py-3 mt-2 text-base text-neutral-600 placeholder-gray-500 transition duration-500 ease-in-out transform border border-transparent rounded-lg focus:outline-none focus:border-transparent focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-300 apearance-none autoexpand"
id="summary"
name="summary"
value={formFields.summary}
onChange={onTextAreaChange}
placeholder="Message..."
></textarea>
</div>
</div>
</div>
</div>
</section>
<section className="text-gray-600 body-font overflow-hidden">
<div className="container px-5 py-20 mx-auto">
<div className="flex flex-col w-full mx-auto">
<div className="w-full mx-auto">
<h2 className="sm:text-3xl text-2xl mb-5 font-bold">
Skills
</h2>
<TagsInput
value={skills}
onChange={setSkills}
name="skills"
placeHolder="Add skills here..."
/>
</div>
</div>
</div>
</section>
{/* Experience starts */}
<section className="text-gray-600 body-font overflow-hidden">
<div className="container px-5 py-20 mx-auto">
<div className="-my-8 mx-auto">
<h2 className="sm:text-3xl text-2xl my-5 font-bold">
Experience
</h2>
<button
onClick={() => setIsOpen(true)}
className="block mb-5 text-white bg-indigo-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-indigo-600 dark:hover:bg-indigo-700 dark:focus:ring-indigo-800"
type="button"
data-modal-toggle="defaultModal"
>
Add Experience
</button>
<ExperiencePopupModal isOpen={isOpen} closeModal={closeModal} />
<p className="mb-5 font-color">
Be sure to <b>'Update'</b> for these changes to take effect :)
</p>
{profile.experience.length ? (
<ol className="border-l-2 border-indigo-700">
{profile.experience.map((exp, index) => {
return (
<Experience
experienceData={exp}
key={index}
itemIndex={index}
/>
);
})}
</ol>
) : null}
</div>
</div>
</section>
{/* Projects starts */}
<section className="text-gray-600 body-font">
<div className="container px-5 py-24 mx-auto">
<div className="text-left mb-5">
<h2 className="sm:text-3xl text-2xl font-bold title-font mb-5">
Projects
</h2>
<button
onClick={() => setIsProjOpen(true)}
className="block text-white bg-indigo-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-indigo-600 dark:hover:bg-indigo-700 dark:focus:ring-indigo-800"
type="button"
data-modal-toggle="defaultModal"
>
Add Project
</button>
<ProjectPopupModal
isProjOpen={isProjOpen}
closeProjModal={closeProjModal}
/>
</div>
<p className="mb-5 font-color">
Be sure to <b>'Update'</b> for these changes to take effect :)
</p>
<div className="flex flex-wrap -m-4">
{profile.projects.length
? profile.projects.map((project, index) => {
return (
<Project
project={project}
key={index}
itemIndex={index}
removeItem={removeItem}
/>
);
})
: null}
</div>
</div>
</section>
</div>
</section>
</>
);
};
export default EditProfile;
Скриншоты
Состояние редактирования
Состояние без редактирования
Это все для части проекта, посвященной UI/Profile, оставайтесь с нами!