diff --git a/src/@types/types.ts b/src/@types/types.ts index a9082b6..900d074 100644 --- a/src/@types/types.ts +++ b/src/@types/types.ts @@ -31,6 +31,7 @@ export type BlogPost = { export type UserInfo = { avatar: Blob | null; about: string; + headerImage: Blob | null; } export type ToastNotification = { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9d12200..1cc0b62 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,41 +1,44 @@ import {useNavigate} from 'react-router-dom'; import {RoutesEnum} from '../@types/enums'; import {OutlinedButton, PrimaryButton} from './Button'; -import {FunctionComponent, useContext} from 'react'; +import {FunctionComponent, useContext, PropsWithChildren} from 'react'; import {UserContext} from '../context/UserContext'; import {ThemeContext} from '../context/ThemeContext'; -const Header: FunctionComponent = () => { +const Header: FunctionComponent = ({children}) => { const {isOwner} = useContext(UserContext); const {theme} = useContext(ThemeContext); - const navigate = useNavigate(); return ( -
-
+ <> +
navigate(RoutesEnum.home)} + className='mx-auto flex items-center justify-between' + style={{maxWidth: '1000px'}} > - {/* Logo will go here */} - BlogSoftware -
- {isOwner ? ( -
- navigate(RoutesEnum.customize)}> - Customize - - navigate(RoutesEnum.admin)}> - Manage Your Blog - +
navigate(RoutesEnum.home)} + > + {/* Logo will go here */} + BlogSoftware
- ) : null} -
-
+ {isOwner && ( +
+ navigate(RoutesEnum.customize)}> + Customize + + navigate(RoutesEnum.admin)}> + Manage Your Blog + +
+ )} +
+ +
+ {children} + ); }; diff --git a/src/components/HeaderImage.tsx b/src/components/HeaderImage.tsx new file mode 100644 index 0000000..3ad7ad2 --- /dev/null +++ b/src/components/HeaderImage.tsx @@ -0,0 +1,69 @@ +import { + ChangeEvent, + useContext, + FunctionComponent, + Dispatch, + SetStateAction +} from 'react'; +import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'; +import {ThemeContext} from '../context/ThemeContext'; + +const HeaderImage: FunctionComponent<{ + headerImage: Blob | null; + setHeaderImage?: Dispatch>; +}> = ({headerImage, setHeaderImage}) => { + const {theme} = useContext(ThemeContext); + + const handleFileInput = (e: ChangeEvent) => { + setHeaderImage!(e.target.files ? e.target.files[0] : null); + }; + + return ( + <> + {headerImage ? ( + profile + + ) : ( +
+ + {setHeaderImage && ( + <> +

Upload a Cover Photo

+ + + )} +
+ )} + {headerImage && setHeaderImage && ( +

+ + Change Cover + + +

+ )} + + ); +}; + +export default HeaderImage; diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx index a8d5901..5d566c4 100644 --- a/src/context/UserContext.tsx +++ b/src/context/UserContext.tsx @@ -22,7 +22,7 @@ type UserContext = { ownerAddress: string; ownerIdentity: string; isOwner: boolean; - saveUserInfo: (info: Pick) => Promise; + saveUserInfo: (info: Pick) => Promise; } export const UserContext = createContext({} as unknown as UserContext); @@ -36,7 +36,8 @@ export const ProvideUserContext: FunctionComponent = ({childr const [userError, setUserError] = useState(false); const [userInfo, setUserInfo] = useState({ about: '', - avatar: null + avatar: null, + headerImage: null }); const [visitorAddress, setVisitorAddress] = useState(''); const [visitorIdentity, setVisitorIdentity] = useState(''); @@ -77,10 +78,19 @@ export const ProvideUserContext: FunctionComponent = ({childr setVisitorIdentity(visitorId); if (dataStorageHash) { - const {about, avatar} = await utils.getDataFromStorage(dataStorageHash); + const { + about, + avatar, + headerImage + } = await utils.getDataFromStorage(dataStorageHash); setUserInfo({ about, - avatar: avatar ? await window.point.storage.getFile({id: avatar}) : null + avatar: avatar + ? await window.point.storage.getFile({id: avatar}) + : null, + headerImage: headerImage + ? await window.point.storage.getFile({id: headerImage}) + : null }); } else if (_isOwner) { navigate(RoutesEnum.profile, {replace: true}); @@ -95,7 +105,7 @@ export const ProvideUserContext: FunctionComponent = ({childr getData(); }, []); - const saveUserInfo = async (info: Pick) => { + const saveUserInfo = async (info: Pick) => { setUserSaving(true); try { let avatarImage = ''; @@ -105,8 +115,16 @@ export const ProvideUserContext: FunctionComponent = ({childr const {data} = await window.point.storage.postFile(avatarFormData); avatarImage = data; } + let headerImage = ''; + if (info.headerImage) { + const headerImageFormData = new FormData(); + headerImageFormData.append('files', info.headerImage); + const {data} = await window.point.storage.postFile(headerImageFormData); + headerImage = data; + } const form = JSON.stringify({ avatar: avatarImage, + headerImage, about: info.about }); const file = new File([form], 'user.json', {type: 'application/json'}); diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 037a48e..e3386af 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -18,6 +18,7 @@ import SearchBar from '../components/SearchBar'; import {ThemeContext} from '../context/ThemeContext'; import {UserContext} from '../context/UserContext'; import {PostsContext} from '../context/PostsContext'; +import HeaderImage from '../components/HeaderImage'; enum BlogFilterOptions { published = 'published', @@ -58,7 +59,7 @@ const FilterOption = ({ const Admin: FunctionComponent = () => { const navigate = useNavigate(); - const {isOwner, userLoading} = useContext(UserContext); + const {isOwner, userLoading, userInfo} = useContext(UserContext); const {posts, deletedPosts, postsLoading} = useContext(PostsContext); const [searchTerm, setSearchTerm] = useState(''); @@ -98,7 +99,7 @@ const Admin: FunctionComponent = () => { return (
-
+
= ({deleted}) => { const navigate = useNavigate(); @@ -22,7 +23,7 @@ const BlogPage: FunctionComponent<{deleted?: boolean}> = ({deleted}) => { const id = Number(query.get('id')); const {theme} = useContext(ThemeContext); - const {visitorAddress, isOwner, visitorIdentity} = useContext(UserContext); + const {visitorAddress, isOwner, visitorIdentity, userInfo} = useContext(UserContext); const {setToast} = useContext(ToastContext); const {posts, deletedPosts, postsError, postsLoading} = useContext(PostsContext); const post = useMemo( @@ -245,7 +246,7 @@ const BlogPage: FunctionComponent<{deleted?: boolean}> = ({deleted}) => { return ( -
+
= ({edit}) => { const {userInfo, userSaving, saveUserInfo, userLoading, userError} = useContext(UserContext); const {theme} = useContext(ThemeContext); + const [showModal, setShowModal] = useState(false); const [avatar, setAvatar] = useState(userInfo.avatar); + const [headerImage, setHeaderImage] = useState(null); const [about, setAbout] = useState(userInfo.about); useEffect(() => { if (userInfo.about) { @@ -23,6 +34,12 @@ const CreateOrEditProfile: FunctionComponent<{ edit?: boolean }> = ({edit}) => { const navigate = useNavigate(); + const hiddenFileInput = useRef(null); + + const handleFileButtonClick = () => { + hiddenFileInput?.current?.click(); + }; + const handleFileInput = (e: ChangeEvent) => { setAvatar(e.target.files ? e.target.files[0] : null); }; @@ -32,12 +49,9 @@ const CreateOrEditProfile: FunctionComponent<{ edit?: boolean }> = ({edit}) => { return ( -
-
- {/* Logo will go here */} - BlogSoftware -
-
+
+ +

{edit ? 'Update' : 'Complete'} Your Profile @@ -68,19 +82,35 @@ const CreateOrEditProfile: FunctionComponent<{ edit?: boolean }> = ({edit}) => { /> )} {avatar && ( -

- - Change - - -

+
+

+ + Change + + +

+

+ setShowModal(true)} + > + Remove + +

+
)}

@@ -100,8 +130,8 @@ const CreateOrEditProfile: FunctionComponent<{ edit?: boolean }> = ({edit}) => {
{saveUserInfo({avatar, about});}} + disabled={userSaving} + onClick={() => {saveUserInfo({avatar, about, headerImage});}} > {userSaving ? 'Please Wait' : edit ? 'Update Profile' : 'Finish'} @@ -113,6 +143,34 @@ const CreateOrEditProfile: FunctionComponent<{ edit?: boolean }> = ({edit}) => {
+ {showModal && ( +
+
+
+
+

+ Are you sure you want to remove your profile image? +

+
+ setShowModal(false)}> + Cancel + + { + setAvatar(null); + setShowModal(false); + }} + > + Remove + +
+
+
+
+ )}
);