diff --git a/src/app.tsx b/src/app.tsx index 1c261c1..e5b5d73 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -2,16 +2,17 @@ import { Route, Routes } from 'react-router-dom' import { BrowserRouter as Router } from 'react-router-dom' import './app.css' import { DefaultLayout } from './layouts' -import routes from './routes' import "yet-another-react-lightbox/styles.css"; import { Login, NotFound, Submissions } from './pages' import { Provider } from 'react-redux' import { persistore, store } from './store/config' import { PersistGate } from 'redux-persist/integration/react' -import { ProtectedRoute } from './routes/ProtectedRoute' +import { AdminProtectedRoute, UserProtectedRoute } from './routes/ProtectedRoute' +import { getRoutes } from './routes'; export function App() { + const { routes } = getRoutes(); return ( <> @@ -20,20 +21,51 @@ export function App() { } /> }> - {routes.map(({ path, name, element }) => ( - <> - - - ))} - - - - } /> + {routes.map(({ path, name, element, protectedRoute }) => { + let Element = element as any + if (protectedRoute === "user") { + return ( + <> + + + + } + /> + + ) + } + + if (protectedRoute === "admin") { + return ( + <> + <> + + + + } + /> + + + ) + } + return ( + <> + + + ) + })} } /> diff --git a/src/constants/api.ts b/src/constants/api.ts index e6a15c4..e8a36ab 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -9,6 +9,9 @@ const GET_REGENCIES = `${BASE_URL}/region/regencies`; const GET_PROVINCES = `${BASE_URL}/region/provinces`; const GET_CURRENT_USER_STATS = `${BASE_URL}/user/profile`; +const PATCH_USER_AVATAR = `${BASE_URL}/user/avatar`; +const PATCH_USER_INFO = `${BASE_URL}/user`; +const DELETE_USER_AVATAR = `${BASE_URL}/user/avatar` const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`; const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings` @@ -24,9 +27,10 @@ const GET_CURRENT_USER_REVIEW_LOCATION_URI = `${BASE_URL}/user/review/location` export { BASE_URL, - SIGNUP_URI, LOGIN_URI, LOGOUT_URI, + SIGNUP_URI, + DELETE_USER_AVATAR, GET_REGIONS, GET_PROVINCES, GET_REGENCIES, @@ -34,10 +38,12 @@ export { GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_TOP_LOCATIONS, GET_LIST_LOCATIONS_URI, - POST_CREATE_LOCATION, GET_LOCATION_URI, GET_LOCATION_TAGS_URI, GET_IMAGES_BY_LOCATION_URI, - POST_REVIEW_LOCATION_URI, GET_CURRENT_USER_REVIEW_LOCATION_URI, + PATCH_USER_AVATAR, + PATCH_USER_INFO, + POST_REVIEW_LOCATION_URI, + POST_CREATE_LOCATION, } \ No newline at end of file diff --git a/src/domains/User.ts b/src/domains/User.ts index bd1aeda..423046a 100644 --- a/src/domains/User.ts +++ b/src/domains/User.ts @@ -1,9 +1,22 @@ import { NullValueRes } from "../types/common"; +export interface SocialMedia { + type: string, + value: string +} + +export interface UserInfo { + about: string, + website: string, + social_media: Array +} + export type User = { id: number, email: string, username: string, + about: string, + website: string, avatar_picture: string, banned_at: NullValueRes<"Time", string>, banned_until: NullValueRes<"Time", string>, @@ -12,7 +25,7 @@ export type User = { is_admin: boolean, is_critics: boolean, is_verfied: boolean, - social_media: NullValueRes<"RawMessage", any> + social_media: Array } @@ -20,6 +33,8 @@ export function emptyUser(): User { return { avatar_picture: '', ban_reason: '', + about: '', + website: '', banned_at: { Time: '', Valid: false}, banned_until: { Time: '', Valid: false}, email: '', @@ -28,7 +43,7 @@ export function emptyUser(): User { is_critics: false, is_permaban: false, is_verfied: false, - social_media: {RawMessage: '', Valid: false}, + social_media: Array(), username: '' } } \ No newline at end of file diff --git a/src/pages/UserSettings/index.tsx b/src/pages/UserSettings/index.tsx new file mode 100644 index 0000000..d62edec --- /dev/null +++ b/src/pages/UserSettings/index.tsx @@ -0,0 +1,286 @@ +import { useSelector } from "react-redux"; +import { DefaultButton, TitleSeparator, WarningButton } from "../../components"; +import { UserRootState } from "../../store/type"; +import { DEFAULT_AVATAR_IMG } from "../../constants/default"; +import { ChangeEvent, TargetedEvent, useRef } from "preact/compat"; +import { useState } from "react"; +import { enumKeys, useAutosizeTextArea } from "../../utils"; +import { SocialMediaEnum } from "../../types/common"; +import { SocialMedia, UserInfo } from "../../../src/domains/User"; +import './styles.css' +import { deleteUserAvatarService, patchUserAvatarService, patchUserInfoService } from "../../../src/services/users"; +import { AxiosError } from "axios"; +import { useDispatch } from "react-redux"; +import { authAdded } from "../../features/auth/authSlice/authSlice"; + +interface PageState { + avatar_file: File | null, + avatar: string, + upload_file_err_msg: string, + file_input_key: string +} + +function UserSettings() { + const user = useSelector((state: UserRootState) => state.auth) + const dispatch = useDispatch() + + const [pageState, setPageState] = useState({ + avatar_file: null, + avatar: user.avatar_picture, + upload_file_err_msg: '', + file_input_key: Math.random().toString(24) + }) + + const [isLoading, setIsLoading] = useState(false) + + const [form, setForm] = useState({ + about: user.about, + website: user.website, + social_media: user.social_media + }) + + + const textAreaRef = useRef(null); + useAutosizeTextArea(textAreaRef.current, form.about); + + function onChangeFormInput(e: ChangeEvent) { + let event = e.target as HTMLInputElement; + setForm({ ...form, [event.name]: event.value }) + } + + function handleTextAreaChange(e: ChangeEvent): void { + const val = e.target as HTMLTextAreaElement; + + setForm({ ...form, about: val.value }) + } + + function onAddSocialMediaClicked() { + let tempArr: Array = form.social_media + tempArr.push({ type: SocialMediaEnum.Facebook, value: '' }) + setForm({ ...form, social_media: tempArr }) + } + + function onChangeSocialMediaInput(e: ChangeEvent, idx: number) { + let event = e.target as HTMLInputElement; + let socmed = form.social_media; + socmed[idx].value = event.value; + setForm({ ...form, social_media: socmed }) + + } + + function onChangeSocialMediaType(e: ChangeEvent, idx: number) { + let event = e.target as HTMLOptionElement + let socmed = form.social_media; + socmed[idx].type = event.value; + setForm({ ...form, social_media: socmed }) + } + + function onDeleteSelectedSocial(idx: number) { + setForm({ ...form, social_media: form.social_media.filter((_, index) => index !== idx) }) + } + + async function onUploadAvatar(e: TargetedEvent) { + setIsLoading(true) + e.preventDefault(); + try { + const form = new FormData(); + form.append('file', pageState.avatar_file!) + const res = await patchUserAvatarService(form) + let userStore = { + ...user, + avatar_picture: res.data.image_url + } + dispatch(authAdded(userStore)); + setIsLoading(false) + } catch(err) { + let error = err as AxiosError; + console.log(error) + setIsLoading(false) + } + + setPageState({ ...pageState, file_input_key: Math.random().toString(24)}) + + } + + async function onRemoveAvatar(e: TargetedEvent) { + setIsLoading(true) + e.preventDefault(); + try { + await deleteUserAvatarService() + let userStore = { + ...user, + avatar_picture: '' + } + dispatch(authAdded(userStore)) + setPageState({ ...pageState, avatar: ''}) + setIsLoading(false) + } catch(err) { + setIsLoading(false) + const error = err as AxiosError + console.log(error) + } + } + + async function onUpdateUserInfo(e: TargetedEvent) { + setIsLoading(true) + e.preventDefault(); + try { + console.log(form) + const res = await patchUserInfoService(form) + let userData = { + ...user, + about: res.data.about, + website: res.data.website, + social_media: res.data.social_media + } + + dispatch(authAdded(userData)) + setIsLoading(false) + }catch(err) { + setIsLoading(false) + let error = err as AxiosError; + alert(error) + } + } + + function onChangeUploadImage(e: ChangeEvent) { + let event = e.target as HTMLInputElement; + const file = event.files![0] as File; + + // if cancel button pressed + if (!file) { + return + } + + if (file.type !== "image/png" && file.type !== "image/jpg" && file.type !== "image/jpeg" ) { + setPageState({ ...pageState, upload_file_err_msg: 'ONLY ACCEPT JPEG/PNG/JPG FILE!!!!' }) + return + } + + if (file.size > 10000000) { + setPageState({ ...pageState, upload_file_err_msg: 'file size too large, max file size 10mb' }) + return + } + + setPageState({ ...pageState, upload_file_err_msg: '', avatar_file: file, avatar: URL.createObjectURL(file)}) + } + + return ( +
+
+

User settings

+ +
+ +
+
+ Change avatar + + {pageState.upload_file_err_msg && +

{pageState.upload_file_err_msg}

+ } +
+

File extension must be .jpeg, .png, .jpg

+

File size max 10mb

+
+
+
+ + +
+
+
+ +
+ +
+

ABOUT YOU

+