add user settings, add protected route
This commit is contained in:
parent
68cc693e3f
commit
905ee3669c
50
src/app.tsx
50
src/app.tsx
@ -2,16 +2,17 @@ import { Route, Routes } from 'react-router-dom'
|
|||||||
import { BrowserRouter as Router } from 'react-router-dom'
|
import { BrowserRouter as Router } from 'react-router-dom'
|
||||||
import './app.css'
|
import './app.css'
|
||||||
import { DefaultLayout } from './layouts'
|
import { DefaultLayout } from './layouts'
|
||||||
import routes from './routes'
|
|
||||||
import "yet-another-react-lightbox/styles.css";
|
import "yet-another-react-lightbox/styles.css";
|
||||||
import { Login, NotFound, Submissions } from './pages'
|
import { Login, NotFound, Submissions } from './pages'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { persistore, store } from './store/config'
|
import { persistore, store } from './store/config'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
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() {
|
export function App() {
|
||||||
|
const { routes } = getRoutes();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
@ -20,7 +21,42 @@ export function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/login' element={<Login />} />
|
<Route path='/login' element={<Login />} />
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
{routes.map(({ path, name, element }) => (
|
{routes.map(({ path, name, element, protectedRoute }) => {
|
||||||
|
let Element = element as any
|
||||||
|
if (protectedRoute === "user") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Route
|
||||||
|
path={path}
|
||||||
|
id={name}
|
||||||
|
element={
|
||||||
|
<UserProtectedRoute>
|
||||||
|
<Element />
|
||||||
|
</UserProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protectedRoute === "admin") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
<Route
|
||||||
|
path={path}
|
||||||
|
id={name}
|
||||||
|
element={
|
||||||
|
<AdminProtectedRoute>
|
||||||
|
<Element />
|
||||||
|
</AdminProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<Route
|
<Route
|
||||||
path={path}
|
path={path}
|
||||||
@ -28,12 +64,8 @@ export function App() {
|
|||||||
element={element}
|
element={element}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
))}
|
)
|
||||||
<Route path='/submissions' id='submissions' element={
|
})}
|
||||||
<ProtectedRoute>
|
|
||||||
<Submissions />
|
|
||||||
</ProtectedRoute>
|
|
||||||
} />
|
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
@ -9,6 +9,9 @@ const GET_REGENCIES = `${BASE_URL}/region/regencies`;
|
|||||||
const GET_PROVINCES = `${BASE_URL}/region/provinces`;
|
const GET_PROVINCES = `${BASE_URL}/region/provinces`;
|
||||||
|
|
||||||
const GET_CURRENT_USER_STATS = `${BASE_URL}/user/profile`;
|
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_LOCATIONS_URI = `${BASE_URL}/locations`;
|
||||||
const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings`
|
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 {
|
export {
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
SIGNUP_URI,
|
|
||||||
LOGIN_URI,
|
LOGIN_URI,
|
||||||
LOGOUT_URI,
|
LOGOUT_URI,
|
||||||
|
SIGNUP_URI,
|
||||||
|
DELETE_USER_AVATAR,
|
||||||
GET_REGIONS,
|
GET_REGIONS,
|
||||||
GET_PROVINCES,
|
GET_PROVINCES,
|
||||||
GET_REGENCIES,
|
GET_REGENCIES,
|
||||||
@ -34,10 +38,12 @@ export {
|
|||||||
GET_LIST_RECENT_LOCATIONS_RATING_URI,
|
GET_LIST_RECENT_LOCATIONS_RATING_URI,
|
||||||
GET_LIST_TOP_LOCATIONS,
|
GET_LIST_TOP_LOCATIONS,
|
||||||
GET_LIST_LOCATIONS_URI,
|
GET_LIST_LOCATIONS_URI,
|
||||||
POST_CREATE_LOCATION,
|
|
||||||
GET_LOCATION_URI,
|
GET_LOCATION_URI,
|
||||||
GET_LOCATION_TAGS_URI,
|
GET_LOCATION_TAGS_URI,
|
||||||
GET_IMAGES_BY_LOCATION_URI,
|
GET_IMAGES_BY_LOCATION_URI,
|
||||||
POST_REVIEW_LOCATION_URI,
|
|
||||||
GET_CURRENT_USER_REVIEW_LOCATION_URI,
|
GET_CURRENT_USER_REVIEW_LOCATION_URI,
|
||||||
|
PATCH_USER_AVATAR,
|
||||||
|
PATCH_USER_INFO,
|
||||||
|
POST_REVIEW_LOCATION_URI,
|
||||||
|
POST_CREATE_LOCATION,
|
||||||
}
|
}
|
@ -1,9 +1,22 @@
|
|||||||
import { NullValueRes } from "../types/common";
|
import { NullValueRes } from "../types/common";
|
||||||
|
|
||||||
|
export interface SocialMedia {
|
||||||
|
type: string,
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
about: string,
|
||||||
|
website: string,
|
||||||
|
social_media: Array<SocialMedia>
|
||||||
|
}
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number,
|
id: number,
|
||||||
email: string,
|
email: string,
|
||||||
username: string,
|
username: string,
|
||||||
|
about: string,
|
||||||
|
website: string,
|
||||||
avatar_picture: string,
|
avatar_picture: string,
|
||||||
banned_at: NullValueRes<"Time", string>,
|
banned_at: NullValueRes<"Time", string>,
|
||||||
banned_until: NullValueRes<"Time", string>,
|
banned_until: NullValueRes<"Time", string>,
|
||||||
@ -12,7 +25,7 @@ export type User = {
|
|||||||
is_admin: boolean,
|
is_admin: boolean,
|
||||||
is_critics: boolean,
|
is_critics: boolean,
|
||||||
is_verfied: boolean,
|
is_verfied: boolean,
|
||||||
social_media: NullValueRes<"RawMessage", any>
|
social_media: Array<SocialMedia>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +33,8 @@ export function emptyUser(): User {
|
|||||||
return {
|
return {
|
||||||
avatar_picture: '',
|
avatar_picture: '',
|
||||||
ban_reason: '',
|
ban_reason: '',
|
||||||
|
about: '',
|
||||||
|
website: '',
|
||||||
banned_at: { Time: '', Valid: false},
|
banned_at: { Time: '', Valid: false},
|
||||||
banned_until: { Time: '', Valid: false},
|
banned_until: { Time: '', Valid: false},
|
||||||
email: '',
|
email: '',
|
||||||
@ -28,7 +43,7 @@ export function emptyUser(): User {
|
|||||||
is_critics: false,
|
is_critics: false,
|
||||||
is_permaban: false,
|
is_permaban: false,
|
||||||
is_verfied: false,
|
is_verfied: false,
|
||||||
social_media: {RawMessage: '', Valid: false},
|
social_media: Array<SocialMedia>(),
|
||||||
username: ''
|
username: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
286
src/pages/UserSettings/index.tsx
Normal file
286
src/pages/UserSettings/index.tsx
Normal file
@ -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<PageState>({
|
||||||
|
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<UserInfo>({
|
||||||
|
about: user.about,
|
||||||
|
website: user.website,
|
||||||
|
social_media: user.social_media
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const textAreaRef = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>): void {
|
||||||
|
const val = e.target as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
setForm({ ...form, about: val.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAddSocialMediaClicked() {
|
||||||
|
let tempArr: Array<SocialMedia> = 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 (
|
||||||
|
<div className={'main-content content'}>
|
||||||
|
<div className={'mb-4'}>
|
||||||
|
<h1 className={'text-2xl'}>User settings</h1>
|
||||||
|
<TitleSeparator titleClasses="mt-8 " pageName={'avatar'} titleStyles={{ letterSpacing: .9 }} />
|
||||||
|
<div style={{ padding: '0 15px 20px 0', float: 'left', maxWidth: '50%', width: 200 }}>
|
||||||
|
<img
|
||||||
|
src={pageState.avatar !== '' ? pageState.avatar : DEFAULT_AVATAR_IMG}
|
||||||
|
style={{ aspectRatio: '1/1', objectFit: 'cover'}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'text-sm mb-5'} style={{ overflow: 'hidden' }}>
|
||||||
|
<span className={'mr-3'}>Change avatar</span>
|
||||||
|
<input name="uploadImage" key={pageState.file_input_key} type={"file"} accept={'image/*'} onChange={onChangeUploadImage} />
|
||||||
|
{pageState.upload_file_err_msg &&
|
||||||
|
<p className={'text-xs text-error mt-2'}>{pageState.upload_file_err_msg}</p>
|
||||||
|
}
|
||||||
|
<div className={'mt-2 text-tertiary'}>
|
||||||
|
<p>File extension must be .jpeg, .png, .jpg</p>
|
||||||
|
<p>File size max 10mb</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'text-xs button-avatar-container'}>
|
||||||
|
<DefaultButton
|
||||||
|
label="update"
|
||||||
|
className={isLoading ? 'pointer-events-none' : ''}
|
||||||
|
style={{ paddingLeft: 36, paddingRight: 36, textTransform: 'uppercase'}}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={onUploadAvatar}
|
||||||
|
/>
|
||||||
|
<WarningButton
|
||||||
|
label="remove avatar"
|
||||||
|
containerClassName="mt-5"
|
||||||
|
style={{ textTransform: 'uppercase', paddingLeft: 8, paddingRight: 8 }}
|
||||||
|
className={isLoading ? 'pointer-events-none' : ''}
|
||||||
|
onClick={onRemoveAvatar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ clear: 'both' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section name="User Info">
|
||||||
|
<TitleSeparator pageName="INFO" />
|
||||||
|
<div className={'mb-3'} style={{ maxWidth: 600, minWidth: 280 }}>
|
||||||
|
<h2 className={'text-sm mb-1 font-bold'}>ABOUT YOU</h2>
|
||||||
|
<textarea
|
||||||
|
onChange={handleTextAreaChange}
|
||||||
|
maxLength={500}
|
||||||
|
ref={textAreaRef}
|
||||||
|
className={'p-2 text-sm bg-secondary'}
|
||||||
|
value={form.about}
|
||||||
|
style={{ border: 'none', overflow: 'auto', outline: 'none', boxShadow: 'none', width: '100%', minHeight: 100, overflowY: 'hidden' }}
|
||||||
|
/>
|
||||||
|
<div className={'mt-2 mb-1'}>
|
||||||
|
<h2 className={'font-bold text-sm mb-1'}>WEBSITE</h2>
|
||||||
|
<input className={'bg-secondary text-sm'} placeholder={'www.mywebsite.com'} style={{ width: '100%', borderRadius: 7, padding: '5px 10px' }} type={'text'} onChange={onChangeFormInput} name={'website'} value={form.website} />
|
||||||
|
</div>
|
||||||
|
<h2>Social</h2>
|
||||||
|
{form.social_media.map((val, index) => (
|
||||||
|
<div className={'mt-2'}>
|
||||||
|
<select
|
||||||
|
onChange={(e) => onChangeSocialMediaType(e, index)}
|
||||||
|
className={'bg-secondary inline-block text-sm'}
|
||||||
|
name="social_media_type"
|
||||||
|
id="social_media_type"
|
||||||
|
style={{ borderRadius: 7, padding: '5px 10px' }}
|
||||||
|
value={form.social_media[index].type.toLowerCase()}
|
||||||
|
>
|
||||||
|
{enumKeys(SocialMediaEnum).map(x => (
|
||||||
|
<option value={SocialMediaEnum[x]}>{SocialMediaEnum[x]}</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
className={'bg-secondary text-sm ml-1 mr-1 inline-block'}
|
||||||
|
style={{ width: '66%', borderRadius: 7, padding: '5px 10px' }}
|
||||||
|
type={'text'}
|
||||||
|
onChange={(e) => onChangeSocialMediaInput(e, index)}
|
||||||
|
name={'social_media'}
|
||||||
|
value={val.value} />
|
||||||
|
<span className={'text-xs text-gray mr-3'}>( username )</span>
|
||||||
|
<span
|
||||||
|
className={'text-sm bg-error'}
|
||||||
|
style={{ padding: '0 5px' }}
|
||||||
|
>
|
||||||
|
<a onClick={() => onDeleteSelectedSocial(index)} style={{ verticalAlign: 'text-bottom' }}>x</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<a onClick={onAddSocialMediaClicked}>
|
||||||
|
<div className={'text-center mt-2 bg-secondary'} style={{ padding: '5px 10px', borderRadius: 7 }}>
|
||||||
|
+ add social media
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className={'flex flex-row justify-between mt-7'}>
|
||||||
|
<DefaultButton
|
||||||
|
label="update"
|
||||||
|
containerClassName="text-xs"
|
||||||
|
onClick={onUpdateUserInfo}
|
||||||
|
style={{ paddingLeft: 36, paddingRight: 36, textTransform: 'uppercase', }}
|
||||||
|
className={isLoading ? 'pointer-events-none' : ''}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
<WarningButton
|
||||||
|
label="delete account"
|
||||||
|
containerClassName="text-xs"
|
||||||
|
style={{ textTransform: 'uppercase', paddingLeft: 8, paddingRight: 8 }}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className={isLoading ? 'pointer-events-none' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserSettings;
|
3
src/pages/UserSettings/styles.css
Normal file
3
src/pages/UserSettings/styles.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.button-avatar-container p a:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
@ -10,6 +10,7 @@ import AddLocation from "./AddLocation";
|
|||||||
import Submissions from "./Submissions";
|
import Submissions from "./Submissions";
|
||||||
import UserProfile from "./UserProfile";
|
import UserProfile from "./UserProfile";
|
||||||
import UserFeed from "./UserFeed";
|
import UserFeed from "./UserFeed";
|
||||||
|
import UserSettings from "./UserSettings";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Login,
|
Login,
|
||||||
@ -17,6 +18,7 @@ export {
|
|||||||
Home,
|
Home,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
UserFeed,
|
UserFeed,
|
||||||
|
UserSettings,
|
||||||
|
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { useSelector } from "react-redux"
|
|||||||
import { UserRootState } from "src/store/type"
|
import { UserRootState } from "src/store/type"
|
||||||
|
|
||||||
|
|
||||||
export const ProtectedRoute = ({children}: any) => {
|
export const AdminProtectedRoute = ({children}: any) => {
|
||||||
|
|
||||||
const user = useSelector((state: UserRootState) => state.auth)
|
const user = useSelector((state: UserRootState) => state.auth)
|
||||||
if(!user.is_admin) {
|
if(!user.is_admin) {
|
||||||
@ -12,3 +12,12 @@ export const ProtectedRoute = ({children}: any) => {
|
|||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UserProtectedRoute = ({ children }: any) => {
|
||||||
|
const user = useSelector((state: UserRootState) => state.auth)
|
||||||
|
if(Object.keys(user).length === 0) {
|
||||||
|
return <Navigate to={"/"} replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
@ -7,10 +7,24 @@ import {
|
|||||||
Story,
|
Story,
|
||||||
AddLocation,
|
AddLocation,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
UserFeed
|
UserFeed,
|
||||||
|
UserSettings,
|
||||||
|
Submissions
|
||||||
} from '../pages';
|
} from '../pages';
|
||||||
|
|
||||||
const routes = [
|
interface BaseRoutes {
|
||||||
|
path: string,
|
||||||
|
name: string,
|
||||||
|
protectedRoute?: string,
|
||||||
|
element: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoutes {
|
||||||
|
routes: Array<BaseRoutes>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRoutes = (): IRoutes => {
|
||||||
|
let groupRoutes: Array<BaseRoutes> = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
@ -41,21 +55,44 @@ const routes = [
|
|||||||
name: "LocationDetail",
|
name: "LocationDetail",
|
||||||
element: <LocationDetail />
|
element: <LocationDetail />
|
||||||
},
|
},
|
||||||
|
// PROTECTED USER ROUTES
|
||||||
{
|
{
|
||||||
path: "/add-location",
|
path: "/add-location",
|
||||||
name: "AddLocation",
|
name: "AddLocation",
|
||||||
element: <AddLocation />
|
protectedRoute: "user",
|
||||||
|
element: AddLocation
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user/profile",
|
path: "/user/profile",
|
||||||
name: "UserProfile",
|
name: "UserProfile",
|
||||||
element: <UserProfile />
|
protectedRoute: "user",
|
||||||
|
element: UserProfile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/user/settings",
|
||||||
|
name: "UserSettings",
|
||||||
|
protectedRoute: "user",
|
||||||
|
element: UserSettings
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user/feed",
|
path: "/user/feed",
|
||||||
name: "UserFeed",
|
name: "UserFeed",
|
||||||
element: <UserFeed />
|
protectedRoute: "user",
|
||||||
|
element: UserFeed
|
||||||
|
},
|
||||||
|
//PROTECTED ADMIN ROUTES
|
||||||
|
{
|
||||||
|
path: "/admin/submissions",
|
||||||
|
name: "Submissions",
|
||||||
|
protectedRoute: "admin",
|
||||||
|
element: Submissions
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default routes;
|
let routes: IRoutes = {
|
||||||
|
routes: groupRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes
|
||||||
|
|
||||||
|
}
|
@ -16,7 +16,7 @@ interface IAuthentication {
|
|||||||
async function createAccountService({ username, password }: IAuthentication) {
|
async function createAccountService({ username, password }: IAuthentication) {
|
||||||
const newState = { ...initialState };
|
const newState = { ...initialState };
|
||||||
try {
|
try {
|
||||||
const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password } })
|
const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password }, withCredentials: true })
|
||||||
newState.data = response.data
|
newState.data = response.data
|
||||||
newState.error = null
|
newState.error = null
|
||||||
return newState
|
return newState
|
||||||
@ -29,7 +29,7 @@ async function createAccountService({ username, password }: IAuthentication) {
|
|||||||
async function loginService({ username, password }: IAuthentication) {
|
async function loginService({ username, password }: IAuthentication) {
|
||||||
const newState = { ...initialState };
|
const newState = { ...initialState };
|
||||||
try {
|
try {
|
||||||
const response = await client({ method: 'POST', url: LOGIN_URI, data: { username, password } })
|
const response = await client({ method: 'POST', url: LOGIN_URI, data: { username, password }, withCredentials: true })
|
||||||
newState.data = response.data
|
newState.data = response.data
|
||||||
newState.error = null
|
newState.error = null
|
||||||
return newState
|
return newState
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { GET_CURRENT_USER_STATS } from "../constants/api";
|
import { DELETE_USER_AVATAR, GET_CURRENT_USER_STATS, PATCH_USER_AVATAR, PATCH_USER_INFO } from "../constants/api";
|
||||||
import { IHttpResponse } from "../types/common";
|
import { IHttpResponse } from "../types/common";
|
||||||
import { client } from "./config";
|
import { client } from "./config";
|
||||||
|
import { UserInfo } from "../../src/domains/User";
|
||||||
|
|
||||||
|
|
||||||
async function getUserStatsService(): Promise<IHttpResponse> {
|
async function getUserStatsService(): Promise<IHttpResponse> {
|
||||||
@ -19,6 +20,54 @@ async function getUserStatsService(): Promise<IHttpResponse> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function patchUserAvatarService(form: FormData): Promise<IHttpResponse> {
|
||||||
|
const newState: IHttpResponse = { data: null, error: null};
|
||||||
|
try {
|
||||||
|
const res = await client({ method: "PATCH", url: PATCH_USER_AVATAR, data: form, withCredentials: true})
|
||||||
|
newState.data = res.data;
|
||||||
|
newState.status = res.status;
|
||||||
|
return newState;
|
||||||
|
} catch(error) {
|
||||||
|
let err = error as AxiosError;
|
||||||
|
newState.error = err
|
||||||
|
newState.status = err.status
|
||||||
|
throw(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function patchUserInfoService(data: UserInfo): Promise<IHttpResponse> {
|
||||||
|
const newState: IHttpResponse = { data: null, error: null};
|
||||||
|
try {
|
||||||
|
const res = await client({ method: 'PATCH', url: PATCH_USER_INFO, data: data, withCredentials: true})
|
||||||
|
newState.data = res.data;
|
||||||
|
newState.status = res.status;
|
||||||
|
return newState;
|
||||||
|
} catch(error) {
|
||||||
|
let err = error as AxiosError;
|
||||||
|
newState.error = err;
|
||||||
|
newState.status = err.status;
|
||||||
|
throw(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUserAvatarService(): Promise<IHttpResponse> {
|
||||||
|
const newState: IHttpResponse = { data: null, error: null};
|
||||||
|
try {
|
||||||
|
const res = await client({ method: 'DELETE', url: DELETE_USER_AVATAR, withCredentials: true})
|
||||||
|
newState.data = res.data;
|
||||||
|
newState.status = res.status
|
||||||
|
return newState
|
||||||
|
} catch (error) {
|
||||||
|
let err = error as AxiosError;
|
||||||
|
newState.error = err;
|
||||||
|
newState.status = err.status;
|
||||||
|
throw(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getUserStatsService,
|
getUserStatsService,
|
||||||
|
patchUserAvatarService,
|
||||||
|
deleteUserAvatarService,
|
||||||
|
patchUserInfoService
|
||||||
}
|
}
|
@ -32,3 +32,13 @@ export enum LocationType {
|
|||||||
HikingCamping = "hiking / camping",
|
HikingCamping = "hiking / camping",
|
||||||
Other = "other"
|
Other = "other"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://www.similarweb.com/top-websites/indonesia/computers-electronics-and-technology/social-networks-and-online-communities/#:~:text=facebook.com%20ranked%20number%201,Media%20Networks%20websites%20in%20Indonesia.
|
||||||
|
// maybe should add ppgames.net too BASED
|
||||||
|
export enum SocialMediaEnum {
|
||||||
|
Facebook = "facebook",
|
||||||
|
Instagram = "instagram",
|
||||||
|
Tiktox = "tiktok",
|
||||||
|
X = "x",
|
||||||
|
Youtube = "youtube"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user