Compare commits

..

No commits in common. "946bed296abb7b918eea94dd43d31eee77ab8945" and "00c8831dc92b8215decee2863bcc2ad16c2ac277" have entirely different histories.

67 changed files with 234 additions and 2413 deletions

View File

@ -1,7 +1 @@
yik hiling gis bir gik cipik mikirin kihidipin ying simintiri ini
## RESOURCES
- https://react-select.com/home (MAYBE USE THIS ONE OVER MY OWN DOGSHIT SELECT)
- https://github.com/Upstatement/react-router-guards
yik hiling gis bir gik cipik mikirin kihidipin ying simintiri ini

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hilingin</title>
<title>Vite + Preact + TS</title>
</head>
<body>
<div id="app"></div>

View File

@ -21,7 +21,6 @@
"react": "^18.2.0",
"react-redux": "^8.1.2",
"react-router-dom": "^6.16.0",
"react-textarea-autosize": "^8.5.3",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.4.2",
"yet-another-react-lightbox": "^3.12.2"

View File

@ -10,17 +10,6 @@
border-color: #38444d;
}
.text-area {
border: none;
overflow: auto;
outline: none;
box-shadow: none;
background-color: #40444b;
width: 100%;
min-height: 100px;
overflow-y: hidden;
}
.content {
max-width: 1440px;
margin: 0 auto;

View File

@ -2,61 +2,24 @@ 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 { Login, NotFound } from './pages'
import { Provider } from 'react-redux'
import { persistore, store } from './store/config'
import { PersistGate } from 'redux-persist/integration/react'
import { AdminProtectedRoute, UserProtectedRoute } from './routes/ProtectedRoute'
import { getRoutes } from './routes';
export function App() {
const { routes } = getRoutes();
return (
<>
<Provider store={store}>
<PersistGate persistor={persistore}>
<Router>
<Routes>
<Route path='/login' element={<Login />} />
<Route element={<DefaultLayout />}>
{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 (
<Provider store={store}>
<PersistGate persistor={persistore}>
<Router>
<Routes>
<Route path='/login' element={<Login />} />
<Route element={<DefaultLayout />}>
{routes.map(({ path, name, element }) => (
<>
<Route
path={path}
@ -64,14 +27,13 @@ export function App() {
element={element}
/>
</>
)
})}
))}
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>
</PersistGate>
</Provider>
</Routes>
</Router>
</PersistGate>
</Provider>
</>
)
}

View File

@ -1,37 +0,0 @@
import { TargetedEvent, CSSProperties } from "preact/compat";
import './style.css';
import { SpinnerLoading } from "../..";
interface DefaultButtonProps {
label: string
containerClassName?: string,
containerStyle?: CSSProperties,
className?: string,
style?: CSSProperties
onClick?: (event: TargetedEvent) => any,
href?: string,
isLoading?: boolean
}
const DefaultButton = (props: DefaultButtonProps) => (
<div
className={props.containerClassName}
style={props.containerStyle}
>
<a
href={props.href}
onClick={props.onClick}
className={`bg-gray default-button ${props.className ? props.className : ''}`}
style={{ paddingTop: 6, paddingBottom: 6, letterSpacing: .9, borderRadius: 7, ...props.style }}
>
{ props.isLoading ?
<SpinnerLoading style={{ verticalAlign: 'middle'}}/>
:
props.label
}
</a>
</div>
)
export default DefaultButton;

View File

@ -1,4 +0,0 @@
div a.default-button:hover {
color: white;
background-color: #575757;
}

View File

@ -1,36 +0,0 @@
import { CSSProperties, TargetedEvent } from "preact/compat";
import './style.css';
import { SpinnerLoading } from "../..";
interface DefaultButtonProps {
label: string
containerClassName?: string,
containerStyle?: CSSProperties,
className?: string,
style?: CSSProperties,
isLoading?: boolean,
onClick?: (event: TargetedEvent) => any,
href?: string,
}
const WarningButton = (props: DefaultButtonProps) => (
<div
className={props.containerClassName}
style={props.containerStyle}
>
<a
href={props.href}
onClick={props.onClick}
className={`bg-error warning-button ${props.className ? props.className : ''}`}
style={{ paddingTop: 6, paddingBottom: 6, letterSpacing: .9, borderRadius: 7, ...props.style }}
>
{props.isLoading ?
<SpinnerLoading style={{ verticalAlign: 'middle'}}/>
:
props.label
}
</a>
</div>
)
export default WarningButton;

View File

@ -1,4 +0,0 @@
div a.warning-button:hover{
color: white;
background-color: #af3030;
}

View File

@ -1,128 +0,0 @@
import { ChangeEvent, useEffect, useRef, useState } from "react";
import "./style.css";
const Icon = () => {
return (
<svg height="20" width="20" viewBox="0 0 20 20" fill={"white"}>
<path d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"></path>
</svg>
);
};
interface dropdownProps {
placeholder: string,
options: Array<any>,
labelPropsName: string,
isSearchable: boolean,
onChange: Function
}
const DropdownInput = ({
placeholder,
options,
isSearchable,
labelPropsName,
onChange
}: dropdownProps) => {
const [showMenu, setShowMenu] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<any>();
const [searchValue, setSearchValue] = useState("");
const searchRef = useRef<any>();
const inputRef = useRef<any>();
useEffect(() => {
setSearchValue("");
if (showMenu && searchRef.current) {
searchRef.current.focus();
}
}, [showMenu]);
useEffect(() => {
const handler = (e: any) => {
if (inputRef.current && !inputRef.current.contains(e.target)) {
setShowMenu(false);
}
};
window.addEventListener("click", handler);
return () => {
window.removeEventListener("click", handler);
};
});
const handleInputClick = () => {
setShowMenu(!showMenu);
};
const getDisplay = () => {
if (!selectedValue) {
return placeholder;
}
return selectedValue[labelPropsName]
};
const onItemClick = (option: any) => {
setSelectedValue(option)
onChange(option);
};
const isSelected = (option: any) => {
if (!selectedValue) {
return false;
}
return selectedValue[labelPropsName] === option[labelPropsName];
};
const onSearch = (e: ChangeEvent) => {
const event = e.target as HTMLInputElement
setSearchValue(event.value);
};
const getIDropdownInputPropss = () => {
if (!searchValue) {
return options;
}
return options.filter(
(option: any) =>
option[labelPropsName].toLowerCase().indexOf(searchValue.toLowerCase()) >= 0
);
};
return (
<div className="dropdown-container">
<div ref={inputRef} onClick={handleInputClick} className="dropdown-input">
<div className="dropdown-selected-value text-sm">{getDisplay()}</div>
<div className="dropdown-tools">
<div className="dropdown-tool">
<Icon />
</div>
</div>
</div>
{showMenu && (
<div className="dropdown-item-menu">
{isSearchable && (
<div className="search-box bg-secondary">
<input className={'bg-primary text-sm'} onChange={onSearch} value={searchValue} ref={searchRef} />
</div>
)}
<div class={'dropdown-item-container'}>
{getIDropdownInputPropss().map((option: any) => (
<div
onClick={() => onItemClick(option)}
key={option[labelPropsName]}
className={`dropdown-item text-sm ${isSelected(option) && "selected"}`}
>
{option[labelPropsName]}
</div>
))}
</div>
</div>
)}
</div>
);
};
export default DropdownInput;

View File

@ -1,104 +0,0 @@
.dropdown-container {
text-align: left;
border: 1px solid #474747;
position: relative;
border-radius: 5px;
min-width: 325px;
max-width: 450px;
width: 100%;
}
.dropdown-input {
padding: 5px;
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
}
.dropdown-input-menu {
display: block;
position: absolute;
transform: translateY(4px);
width: 100%;
border: 1px solid #ccc;
border-radius: 5px;
overflow: auto;
max-height: 150px;
background-color: #fff;
}
.dropdown-item-container {
max-height: 200px;
overflow-y: scroll;
}
/* width */
.dropdown-item-container::-webkit-scrollbar {
width: 8px;
}
/* Track */
.dropdown-item-container::-webkit-scrollbar-track {
background: #212121;
}
/* Handle */
.dropdown-item-container::-webkit-scrollbar-thumb {
background: #888;
}
/* Handle on hover */
.dropdown-item-container::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dropdown-item {
padding: 5px 10px;
cursor: pointer;
}
.dropdown-item:hover {
background-color: #9fc3f870;
}
.dropdown-item.selected {
background-color: #a8adb3;
color: #fff;
}
.dropdown-selected-value {
padding: 5px 10px;
}
.dropdown-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.dropdown-tag-item {
background-color: #ddd;
padding: 2px 4px;
border-radius: 2px;
display: flex;
align-items: center;
}
.dropdown-tag-close {
display: flex;
align-items: center;
}
.search-box {
padding: 5px;
background-color: #eee;
}
.search-box input {
width: 100%;
box-sizing: border-box;
padding: 5px 10px;
border-radius: 5px;
}

View File

@ -1,4 +1,4 @@
import React from "preact/compat";
import React, { TargetedEvent } from "preact/compat";
import { useState } from "preact/hooks";
import { useSelector, useDispatch } from "react-redux";
import { UserRootState } from "../../store/type";
@ -22,7 +22,7 @@ function Header() {
setSearchVal(val.value)
}
const handleLogout = async (): Promise<void> => {
const handleLogout = async (): Promise<void> => {
try {
await logoutService()
dispatch(logout())
@ -60,19 +60,18 @@ function Header() {
src={'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
{user.username &&
<div className={'profile-dropdown-img bg-secondary text-left'} style={pageState.profileMenu ? { display: 'block' } : { display: 'none' }}>
<a href={'/user/profile'}><div className={'p-2'}>Profile</div></a>
<a href={'#'}><div className={'p-2'}>Feed</div></a>
<a href={'/add-location'}><div className={'p-2'}>Add location</div></a>
<a href={'/add-location'}><div className={'p-2'}>Settings</div></a>
<a href={'#'} onClick={handleLogout}><div className={'p-2'}>Logout</div></a>
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
</div>
{user.username &&
<div className={'profile-dropdown-img bg-secondary text-left'} style={pageState.profileMenu ? { display: 'block'} : { display: 'none'}}>
<a href={'#'}><div className={'p-2'}>Profile</div></a>
<a href={'#'}><div className={'p-2'}>Feed</div></a>
<a href={'#'}><div className={'p-2'}>Add location</div></a>
<a href={'#'} onClick={handleLogout}><div className={'p-2'}>Logout</div></a>
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
</div>
}
</div>
<form onSubmit={onSearchSubmit} className={`header-search-input ${dropdown ? "search-input-dropdown" : ""}`}>
<form onSubmit={onSearchSubmit} className={`search-input ${dropdown ? "search-input-dropdown" : ""}`}>
<label>
<input
type="text"
@ -99,14 +98,10 @@ function Header() {
<div className={'profile-container'}>
<a href={user.username ? '#' : '/login'} onClick={() => user.username ? setPageState({ ...pageState, profileMenu: !pageState.profileMenu }) : ''} className={`navLink ${!dropdown ? "navLink-disabled" : ""}`}>{user.username ? user.username : 'Sign in'}</a>
{user && screen.width > 600 &&
<div className={'profile-dropdown bg-secondary ml-6'} style={pageState.profileMenu ? { display: 'block' } : { display: 'none' }}>
<a href={'/add-location'}><div className={'p-1'}>Add location</div></a>
<a href={'/user/profile'}><div className={'p-1'}>Profile</div></a>
<div className={'profile-dropdown bg-secondary ml-6'} style={pageState.profileMenu ? { display: 'block' } : {display: 'none'}}>
<a href={'#'}><div className={'p-1'}>Profile</div></a>
<a href={'#'}><div className={'p-1'}>Feed</div></a>
<a href={'/user/settings'}><div className={'p-1'}>Settings</div></a>
{user.is_admin &&
<a href={'/submissions'} ><div className={'p-1'}>Submissions</div></a>
}
<a href={'#'}><div className={'p-1'}>Add location</div></a>
<a href={'#'} onClick={handleLogout}><div className={'p-1'}>Logout</div></a>
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}
{/* <div className={'p-2'}><a href={'#'}>Halo</a></div> */}

View File

@ -3,11 +3,11 @@
}
.header-search-input label {
label {
position: relative;
}
.header-search-input label:before {
label:before {
content: "";
position: absolute;
left: 10px;
@ -41,7 +41,6 @@
padding: 5px;
width: 135px;
font-size: 13px;
z-index: 9999;
}
.profile-dropdown a div:hover {
@ -77,7 +76,7 @@
text-align: 'center'; */
}
.header-search-input {
.search-input {
margin-left: auto;
}
@ -125,7 +124,7 @@
display: none;
}
form.header-search-input {
form.search-input {
display: none;
margin-left: 0;
}

View File

@ -1,13 +1,6 @@
import { CSSProperties } from 'preact/compat'
import './style.css'
interface SpinnerLoadingProps {
style?: CSSProperties
className?: string
}
export default function SpinnerLoading(props: SpinnerLoadingProps) {
export default function SpinnerLoading() {
return (
<div className={`spinner ${props.className ? props.className : ''}`} style={{ display: 'inline-block', ...props.style}}></div>
<div className={'spinner'} style={{ display: 'inline-block'}}></div>
)
}

View File

@ -1,37 +0,0 @@
import { useRef, useEffect } from "preact/compat";
interface RefOutsideClickInterface {
onOutsideClick: Function,
children: any
}
/**
* https://stackoverflow.com/questions/32553158/detect-click-outside-react-component
*/
function useRefOutsideClick(ref: any, onOutsideClick: Function) {
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target)) {
onOutsideClick()
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
function RefOutsideClick(props: RefOutsideClickInterface) {
const wrapperRef = useRef(null);
useRefOutsideClick(wrapperRef, props.onOutsideClick);
return <div ref={wrapperRef}>{props.children}</div>;
}
export default RefOutsideClick;

View File

@ -1,27 +0,0 @@
import { CSSProperties, TargetedEvent } from 'preact/compat';
import './style.css';
type NavigationSeparatorProps = {
pageNames: Array<string>,
onClick: (e: TargetedEvent, name: string) => void,
titleStyles?: any,
containerStyle?: CSSProperties
selectedValue?: string | number
}
function NavigationSeparator(props: NavigationSeparatorProps) {
return (
<div class={"divider mb-2 navigation-separator-container"} style={props.containerStyle}>
{props.pageNames.map(x => (
<a
className={`text-xs font-bold uppercase mr-5 ${props.selectedValue != x.toLowerCase() ? 'text-gray' : 'pointer-events-none'}`}
style={{...props.titleStyles, letterSpacing: .9}}
onClick={(e) => props.onClick(e, x)}>
{x}
</a>
))}
</div>
)
}
export default NavigationSeparator;

View File

@ -1,3 +0,0 @@
.navigation-separator-text:hover {
color: white
}

View File

@ -1,17 +0,0 @@
type SeparatorProps = {
pageName: string,
titleStyles?: any,
titleClasses?: string
}
function TitleSeparator(props: SeparatorProps) {
return (
<div class={"flex flex-row justify-between divider mb-2"}>
<h2 className={`text-sm font-bold uppercase ${props.titleClasses}`} style={props.titleStyles}>
{props.pageName}
</h2>
</div>
)
}
export default TitleSeparator;

View File

@ -4,13 +4,12 @@ type SeparatorProps = {
pageName: String,
pageLink: string,
secondLink?: string,
titleStyles?: any
}
function SeparatorWithAnchor(props: SeparatorProps) {
return (
<div class={"flex flex-row justify-between divider mb-2"}>
<h1 className="text-sm font-bold uppercase" style={props.titleStyles}>
<h1 className="text-sm font-bold" style={{ textTransform: 'uppercase' }}>
<a href={props.pageLink}>{props.pageName}</a>
</h1>
{props.secondLink &&

View File

@ -1,34 +1,17 @@
import Header from "./Header";
import RefOutsideClick from "./RefOutsideClick";
import WarningButton from "./Button/WarningButton";
import DefaultButton from "./Button/DefaultButton";
import SeparatorWithAnchor from "./Separator/WithAnchor";
import DefaultSeparator from "./Separator/Default";
import TitleSeparator from "./Separator/TitleSeparator";
import NavigationSeparator from "./Separator/NavigationSeparator";
import Footer from './Footer/';
import CustomInterweave from "./CustomInterweave";
import DropdownInput from "./DropdownInput";
import SpinnerLoading from "./Loading/Spinner";
export {
Header,
WarningButton,
DefaultButton,
RefOutsideClick,
SeparatorWithAnchor,
DefaultSeparator,
TitleSeparator,
NavigationSeparator,
Footer,
CustomInterweave,
DropdownInput,
SpinnerLoading,
}

View File

@ -1,54 +1,29 @@
const BASE_URL = "http://localhost:8888"
const SIGNUP_URI = `${BASE_URL}/user/signup`
const LOGIN_URI = `${BASE_URL}/user/login`
const LOGOUT_URI = `${BASE_URL}/user/logout`
const SIGNUP_URI = `${BASE_URL}/user/signup`
const LOGIN_URI = `${BASE_URL}/user/login`
const LOGOUT_URI = `${BASE_URL}/user/logout`
const GET_REGIONS = `${BASE_URL}/regions`;
const GET_REGENCIES = `${BASE_URL}/region/regencies`;
const GET_PROVINCES = `${BASE_URL}/region/provinces`;
const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`;
const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings`
const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`
const GET_LOCATION_URI = `${BASE_URL}/location`;
const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`
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_IMAGES_BY_LOCATION_URI = `${BASE_URL}/images/location`
const GET_NEWS_EVENTS_URI = `${BASE_URL}/news-events`;
const POST_NEWS_EVENTS_URI = GET_NEWS_EVENTS_URI
const GET_LIST_LOCATIONS_URI = `${BASE_URL}/locations`;
const GET_LIST_TOP_LOCATIONS = `${BASE_URL}/locations/top-ratings`
const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`
const GET_LOCATION_URI = `${BASE_URL}/location`;
const GET_LOCATION_TAGS_URI = `${BASE_URL}/location/tags`
const POST_CREATE_LOCATION = GET_LIST_LOCATIONS_URI;
const GET_IMAGES_BY_LOCATION_URI = `${BASE_URL}/images/location`
const POST_REVIEW_LOCATION_URI = `${BASE_URL}/review/location`
const GET_CURRENT_USER_REVIEW_LOCATION_URI = `${BASE_URL}/user/review/location`
const POST_REVIEW_LOCATION_URI = `${BASE_URL}/review/location`
export {
BASE_URL,
SIGNUP_URI,
LOGIN_URI,
LOGOUT_URI,
SIGNUP_URI,
DELETE_USER_AVATAR,
GET_NEWS_EVENTS_URI,
GET_REGIONS,
GET_PROVINCES,
GET_REGENCIES,
GET_CURRENT_USER_STATS,
GET_LIST_RECENT_LOCATIONS_RATING_URI,
GET_LIST_TOP_LOCATIONS,
GET_LIST_LOCATIONS_URI,
GET_LOCATION_URI,
GET_LOCATION_TAGS_URI,
GET_IMAGES_BY_LOCATION_URI,
GET_CURRENT_USER_REVIEW_LOCATION_URI,
PATCH_USER_AVATAR,
PATCH_USER_INFO,
POST_REVIEW_LOCATION_URI,
POST_CREATE_LOCATION,
POST_NEWS_EVENTS_URI,
POST_REVIEW_LOCATION_URI
}

View File

@ -1,3 +1,2 @@
export const DEFAULT_AVATAR_IMG = 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png';
export const DEFAULT_LOCATION_THUMBNAIL_IMG = 'https://i.ytimg.com/vi/0DY1WSk8B9o/maxresdefault.jpg';
export const DEFAULT_AVATAR_IMG = 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png';

View File

@ -1,13 +0,0 @@
import { NullValueRes } from '../types/common';
export type LocationInfo = {
id: Number,
name: string,
thumbnail: NullValueRes<'String', string>,
regency_name: String,
province_name: String,
critic_score: Number,
critic_count: Number,
user_score: Number,
user_count: Number
}

View File

@ -1,29 +0,0 @@
export type News = {
id: number,
title: string,
url: string
source: string
thumbnail: string,
description: string
is_deleted: boolean,
submitted_by: string,
approved_by: number,
created_at: string,
updated_at: string
}
export function emptyNewsEvent(): News {
return {
id: 0,
title: '',
url: '',
source: '',
thumbnail: '',
description: '',
is_deleted: false,
submitted_by: '',
approved_by: 0,
created_at: '',
updated_at: '',
}
}

View File

@ -1,13 +0,0 @@
export type Province = {
id: number,
province_name: string,
region_id: number
}
export function emptyProvince(): Province {
return {
id: 0,
province_name: '',
region_id: 0
}
}

View File

@ -1,13 +0,0 @@
export type Regency = {
id: number,
regency_name: string,
province_id: number
}
export function emptyRegency(): Regency {
return {
id: 0,
province_id: 0,
regency_name: ''
}
}

View File

@ -1,4 +0,0 @@
export type Region = {
id: number,
region_name: string
}

View File

@ -1,49 +0,0 @@
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 = {
id: number,
email: string,
username: string,
about: string,
website: string,
avatar_picture: string,
banned_at: NullValueRes<"Time", string>,
banned_until: NullValueRes<"Time", string>,
ban_reason: string,
is_permaban: boolean,
is_admin: boolean,
is_critics: boolean,
is_verfied: boolean,
social_media: Array<SocialMedia>
}
export function emptyUser(): User {
return {
avatar_picture: '',
ban_reason: '',
about: '',
website: '',
banned_at: { Time: '', Valid: false},
banned_until: { Time: '', Valid: false},
email: '',
id: 0,
is_admin: false,
is_critics: false,
is_permaban: false,
is_verfied: false,
social_media: Array<SocialMedia>(),
username: ''
}
}

View File

@ -1,18 +0,0 @@
import { LocationInfo } from "./LocationInfo"
import { Province, emptyProvince } from "./Province"
import { Regency, emptyRegency } from "./Regency"
import { User } from './User';
import { Region } from "./Region"
export type {
LocationInfo,
Province,
Regency,
Region,
User,
}
export {
emptyProvince,
emptyRegency
}

View File

@ -0,0 +1,14 @@
export interface IUser {
id: Number,
email: String,
username: String,
avatar_picture: String,
banned_at: NullValueRes<"Time", String>,
banned_until: NullValueRes<"Time", String>,
ban_reason: String,
is_permaban: boolean,
is_admin: boolean,
is_critics: boolean,
is_verfied: boolean,
social_media: NullValueRes<"RawMessage", any>
}

View File

@ -1,243 +0,0 @@
import { ChangeEvent, TargetedEvent, useEffect, useState } from "preact/compat";
import { getListRecentLocationsRatingsService, getProvincesService, getRegenciesService } from "../../services";
import { LocationInfo } from "../../domains/LocationInfo";
import { DropdownInput } from "../../components";
import { IndonesiaRegionsInfo, LocationType } from "../../types/common";
import { Regency, emptyRegency } from "../../domains";
import { Form } from './types';
import { enumKeys } from "../../utils";
import './style.css';
import { createLocationService } from "../../services/locations";
import { useSelector } from "react-redux";
import { UserRootState } from "../../store/type";
function AddLocation() {
const [recentLocations, setRecentLocations] = useState<Array<LocationInfo>>()
const [idnRegions, setIdnRegions] = useState<IndonesiaRegionsInfo>({
regencies: Array<Regency>(),
})
const [form, setForm] = useState<Form>({
name: '',
address: '',
google_maps_link: '',
location_type: LocationType.Beach,
regency: emptyRegency(),
thumbnails: [],
})
const user = useSelector((state: UserRootState) => state.auth)
const [pageState, setPageState] = useState({
regency_form_error: false,
})
async function getRecentLocations() {
try {
const locations = await getListRecentLocationsRatingsService(9)
setRecentLocations(locations.data)
// setIsLoading(false)
} catch (error) {
console.log(error)
}
}
async function getRegions() {
try {
const provinces = await getProvincesService();
const regencies = await getRegenciesService();
setIdnRegions({
provinces: provinces.data,
regencies: regencies.data
})
} catch (error) {
console.log(error)
}
}
async function onSubmitForm(e: TargetedEvent) {
e.preventDefault();
if(form.regency.regency_name === '') {
setPageState({ regency_form_error: true })
return
}
let tempThumbnailArr: Array<File> = [];
let formData = new FormData();
form.thumbnails.forEach(x => {
tempThumbnailArr.push(x.file)
})
formData.append("address", form.address);
formData.append("location_type", form.location_type);
formData.append("name", form.name);
formData.append("regency_id", form.regency.id.toString());
formData.append("submitted_by", user.id.toString());
formData.append("google_maps_link", form.google_maps_link);
for(let i = 0; i < tempThumbnailArr.length; i++) {
formData.append("thumbnail", tempThumbnailArr[i])
}
try {
await createLocationService(formData)
alert("Location Added")
} catch(error) {
console.log(error)
}
}
function onChangeUploadImage(e: TargetedEvent) {
e.preventDefault()
let event = e.target as HTMLInputElement;
const files = Array.from(event.files as ArrayLike<File>);
const result = files.filter((x) => {
if (x.type === "image/jpg" || x.type === "image/png" || x.type === "image/jpeg") {
return true
}
return false
}).map(v => {
let ret = {
file: v,
url: URL.createObjectURL(v)
}
return ret
})
setForm({ ...form, thumbnails: result })
}
function onDeleteSelectedThumbnail(e: TargetedEvent, idx: number) {
e.preventDefault();
// remove image from FileList
const dt = new DataTransfer();
const tempInput = document.getElementById('imageUpload')!;
const input = tempInput as HTMLInputElement;
const { files } = input;
for (let i = 0; i < files!.length; i++) {
const file = files![i]
if (idx !== i)
dt.items.add(file)
}
input.files = dt.files
let thumbnails = form.thumbnails.filter((_, index) => index != idx);
setForm({ ...form, thumbnails: thumbnails })
}
function onChangeFormInput(e: ChangeEvent) {
let event = e.target as HTMLInputElement;
setForm({ ...form, [event.name]: event.value })
}
function onDeleteAllThumbnails(e: TargetedEvent) {
e.preventDefault();
const dt = new DataTransfer();
const tempInput = document.getElementById('imageUpload');
const input = tempInput as HTMLInputElement;
dt.items.clear();
input.files = dt.files
setForm({ ...form, thumbnails: [] })
}
function onChangeRegencyDropdownInput(val: Regency) {
setPageState({ ...pageState, regency_form_error: false })
setForm({ ...form, regency: val })
}
useEffect(() => {
getRecentLocations()
getRegions()
}, [])
return (
<div className={'content main-content'}>
<div style={{ tableLayout: 'fixed', display: 'table', width: '100%' }}>
<div style={{ textAlign: 'left', padding: '20px 30px', maxWidth: 1096, minWidth: 680, display: 'table-cell', position: 'relative', verticalAlign: 'top', width: '100%' }}>
<h1 className={'text-3xl mb-5 font-bold'}>Add New Location</h1>
<div style={{ backgroundColor: '#2f3136', padding: 10, margin: '0 0 25px' }}>
<form onSubmit={onSubmitForm}>
<span className={'block mt-2 text-sm mb-2'}>Location Name <span className={'text-error'}>*</span></span>
<input className={'bg-primary text-sm input-text'} type={'text'} onChange={onChangeFormInput} name={'name'} value={form.name} required />
<span className={'block mt-2 text-sm mb-2'}>Address <span className={'text-error'}>*</span></span>
<input className={'bg-primary text-sm input-text'} type={'text'} onChange={onChangeFormInput} name={'address'} value={form.address} required />
<label className={'block text-sm mt-2 mb-2'} for="location_type">Location Category</label>
<select className={'bg-primary p-1'} name="location_type" id="location_type">
{ enumKeys(LocationType).map(x => (
<option value={LocationType[x]}>{LocationType[x]}</option>
))
}
</select>
<span className={'block mt-2 text-sm mb-2'}>Kota / Kabupaten <span className={'text-error'}>*</span> <span className={`text-xs text-error ${!pageState.regency_form_error && 'hidden'}`}> (regency mustn't be empty)</span></span>
<DropdownInput
isSearchable={true}
onChange={(val: Regency) => onChangeRegencyDropdownInput(val)}
labelPropsName={"regency_name"}
options={idnRegions.regencies!}
placeholder={''}
/>
<span className={'block mt-2 text-sm mb-2'}>Google Maps Link <span className={'text-error'}>*</span></span>
<input className={'bg-primary text-sm input-text'} type={'text'} onChange={onChangeFormInput} name={'google_maps_link'} value={form.google_maps_link} required />
<span className={'block mt-2 text-sm mb-2'}>Thumbnails</span>
<input id={'imageUpload'} className={'inputfile'} type="file" accept={'image/*'} multiple onChange={onChangeUploadImage} />
<label for={'imageUpload'}>Choose File(s)</label>
<div style={{ clear: 'both' }} />
{form.thumbnails.length > 0 &&
form.thumbnails.map((x, idx) => (
<div className={'mr-3 font-bold'} style={{ width: 150, display: 'inline-block', textAlign: 'right', fontSize: 20 }}>
<a onClick={(e) => onDeleteSelectedThumbnail(e, idx)}>
x
</a>
<img
loading={'lazy'}
src={x.url}
className={'mt-1'}
style={{ aspectRatio: '1/1', objectFit: 'cover' }}
/>
</div>
))
}
{form.thumbnails.length > 0 &&
<a className={'block mt-2'} onClick={onDeleteAllThumbnails}>Delete All Thumbnails</a>
}
<input type={'submit'} value={'Submit'} className={'block p-1 text-sm text-primary mt-4'} style={{ backgroundColor: '#a8adb3', letterSpacing: .5, width: 75 }} />
</form>
<span className={'mt-5 text-sm font-bold block'}>NOTE: LOCATION SUBMISSION MAY BE EDITED BY MODERATOR SO DON'T PUT STUPID ASS THUMBNAILS YOU 1 CENT DOORKNOB</span>
</div>
</div>
<div style={{ display: 'table-cell', position: 'relative', verticalAlign: 'top', width: 345, textAlign: 'left', padding: '15px 0' }}>
<p>Recently added locations</p>
{recentLocations?.map(x => (
<div style={{ width: '32%', display: 'inline-block', padding: '1px 1%' }}>
<a href={'#'} title={x.name}>
<img
loading={'lazy'}
src={x.thumbnail.String.toString()}
alt={x.name}
style={{ aspectRatio: '1/1' }}
/>
</a>
</div>
))}
</div>
</div>
</div>
)
}
export default AddLocation;

View File

@ -1,33 +0,0 @@
.input-text {
border-radius: 7px;
min-width: 325px;
max-width: 450px;
width: 100%;
padding: 5px 10px;
}
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.inputfile + label {
font-size: 14px;
color: white;
display: inline-block;
padding: 5px;
background-color: #555555;
border-radius: 10px;
}
.inputfile + label:hover {
background-color: #202225;
}
.inputfile + label {
cursor: pointer; /* "hand" cursor */
}

View File

@ -1,16 +0,0 @@
import { LocationType } from "src/types/common"
import { Regency } from "../../domains"
export interface Thumbnail {
file: File,
url: string
}
export interface Form {
name: string,
address: string,
regency: Regency,
location_type: LocationType,
google_maps_link: string,
thumbnails: Array<Thumbnail>
}

View File

@ -1,8 +1,7 @@
import { TargetedEvent } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import { getListTopLocationsService } from "../../services";
import { DefaultSeparator } from "../../components";
import { NullValueRes } from "../../types/common";
import { TargetedEvent } from "preact/compat";
import './style.css';
interface TopLocation {
@ -58,11 +57,7 @@ function BestLocation() {
async function getTopLocations() {
try {
const res = await getListTopLocationsService({
page: page, page_size: 20,
order_by: pageState.filterScoreTypeidx,
region_type: pageState.filterRegionType
})
const res = await getListTopLocationsService({ page: page, page_size: 20, order_by: pageState.filterScoreTypeidx, region_type: pageState.filterRegionType })
setTopLocations(res.data)
} catch (err) {

View File

@ -1,12 +1,24 @@
import { SeparatorWithAnchor } from '../../components';
import news from '../../datas/recent_news_event.json';
import popular from '../../datas/popular.json';
import critics_users_pick from '../../datas/critics_users_best_pick.json';
import popular_user_review from '../../datas/popular_user_reviews.json';
import './style.css';
import { useEffect, useState } from 'preact/hooks';
import { getListRecentLocationsRatingsService, getListTopLocationsService } from '../../services';
import { getListRecentLocationsRatingsService } from '../../services';
import { useNavigate } from 'react-router-dom';
import { LocationInfo } from '../../domains/LocationInfo';
type NewPlaces = {
id: Number,
name: string,
thumbnail: NullValueRes<'String', string>,
regency_name: String,
province_name: String,
critic_score: Number,
critic_count: Number,
user_score: Number,
user_count: Number
}
type News = {
header: string,
@ -17,9 +29,7 @@ type News = {
}
function Home() {
const [recentLocations, setRecentLocations] = useState<Array<LocationInfo>>([])
const [topCriticsLocations, setTopCriticsLocations] = useState<Array<LocationInfo>>([])
const [topUsersLocations, setTopUsersLocations] = useState<Array<LocationInfo>>([])
const [recentLocations, setRecentLocations] = useState<Array<NewPlaces>>([])
// const [isLoading, setIsLoading] = useState<boolean>(true)
const navigate = useNavigate()
@ -35,24 +45,6 @@ function Home() {
}
}
async function getCrititsBestLocations() {
try {
const res = await getListTopLocationsService({ page: 1, page_size: 6, order_by: 2, region_type: 0})
setTopCriticsLocations(res.data)
}catch(err) {
console.log(err)
}
}
async function getUsersBestLocations() {
try {
const res = await getListTopLocationsService({ page: 1, page_size: 6, order_by: 3, region_type: 0})
setTopUsersLocations(res.data)
}catch(err) {
console.log(err)
}
}
function onNavigateToDetail(
id: Number,
critic_count: Number,
@ -67,8 +59,6 @@ function Home() {
useEffect(() => {
getRecentLocations()
getCrititsBestLocations()
getUsersBestLocations()
},[])
return (
<>
@ -195,21 +185,17 @@ function Home() {
</div>
<div className={'col-span-2 lg:col-span-1'}>
<SeparatorWithAnchor pageLink='#' pageName={"Critic's Best"} />
{topCriticsLocations.map((x) => (
<div className={"pt-2 text-sm top-location-container"}>
{critics_users_pick.critics.map((x) => (
<div className={"pt-2 text-sm"}>
<div className={'mr-2 critics-users-image'}>
<img
src={x.thumbnail.Valid ? x.thumbnail.String.toString() : 'https://i.ytimg.com/vi/0DY1WSk8B9o/maxresdefault.jpg'}
loading={'lazy'}
style={{ height: '100%', width: '100%', borderRadius: 3}}
/>
<img src={x.thumbnail} loading={'lazy'} style={{ height: '100%', width: '100%', borderRadius: 3}} />
</div>
<p className={'location-title'}>{x.name}</p>
<p className={'text-xs location-province location-title'}>{x.regency_name}</p>
<p className={'text-xs location-province location-title'}>{x.location}</p>
<div className={'critics-users-rating-container'} style={{ display: 'inline-block' }}>
<p className={'text-xs ml-2'}>{x.critic_score} <span className={'text-gray'}>({x.critic_count})</span></p>
<p className={'text-xs ml-2'}>{x.critic_rating}</p>
<div style={{ height: 4, width: 30, backgroundColor: "#72767d" }}>
<div style={{ height: 4, width: `${x.critic_score}%`, backgroundColor: 'green' }} />
<div style={{ height: 4, width: `${x.critic_rating}%`, backgroundColor: 'green' }} />
</div>
</div>
<div style={{ clear: 'both'}} />
@ -219,20 +205,17 @@ function Home() {
</div>
<div className={'col-span-2 lg:col-span-1'}>
<SeparatorWithAnchor pageLink='#' pageName={"User's Best"} />
{topUsersLocations.map((x) => (
<div className={"pt-2 text-sm top-location-container"}>
{critics_users_pick.users.map((x) => (
<div className={"pt-2 text-sm"}>
<div className={'mr-2 critics-users-image'}>
<img
src={x.thumbnail.Valid ? x.thumbnail.String.toString() : 'https://i.ytimg.com/vi/0DY1WSk8B9o/maxresdefault.jpg'}
style={{ height: '100%', width: '100%', borderRadius: 3}}
/>
<img src={x.thumbnail} style={{ height: '100%', width: '100%', borderRadius: 3}} />
</div>
<p className={'location-title'}>{x.name}</p>
<p className={'text-xs location-province location-title'}>{x.regency_name}</p>
<p className={'text-xs location-province location-title'}>{x.location}</p>
<div className={'critics-users-rating-container'} style={{ display: 'inline-block' }}>
<p className={'text-xs ml-2'}>{x.user_score} <span className={'text-xs text-gray'}>({x.user_count})</span></p>
<p className={'text-xs ml-2'}>{x.user_rating}</p>
<div style={{ height: 4, width: 30, backgroundColor: "#72767d"}}>
<div style={{ height: 4, width: `${x.user_score}%`, backgroundColor: 'green' }} />
<div style={{ height: 4, width: `${x.user_rating}%`, backgroundColor: 'green' }} />
</div>
</div>
<div style={{ clear: 'both'}} />

View File

@ -152,6 +152,7 @@
.news-card {
width: 100%;
}
.news-link {
font-size: 0.75rem;
line-height: 1rem;
@ -177,8 +178,4 @@
height: 100px;
}
.top-location-container .location-title {
font-size: 0.685rem;
}
}

View File

@ -86,18 +86,6 @@ img {
cursor: pointer;
}
.review-box-content {
width: 75%;
margin: 0 auto;
}
@media screen and (max-width: 1024px) {
.review-box-content {
width: 100%;
}
}
@media screen and (max-width: 380px) {
.header-link {
white-space: nowrap;

View File

@ -10,16 +10,14 @@ import {
emptyLocationResponse,
CurrentUserLocationReviews,
} from './types';
import { AxiosError } from 'axios';
import { handleAxiosError, useAutosizeTextArea } from '../../utils';
import { getCurrentUserLocationReviewService, getImagesByLocationService, getLocationService, postReviewLocation } from "../../services";
import { getImagesByLocationService, getLocationService, postReviewLocation } from "../../services";
import { DefaultSeparator, SeparatorWithAnchor, CustomInterweave, SpinnerLoading } from '../../components';
import { useSelector } from 'react-redux';
import { UserRootState } from '../../store/type';
import { DEFAULT_AVATAR_IMG } from '../../constants/default';
import './index.css';
import { IHttpResponse } from '../../types/common';
import ReactTextareaAutosize from 'react-textarea-autosize';
import { AxiosError } from 'axios';
const SORT_TYPE = [
'highest rated',
@ -33,7 +31,6 @@ function LocationDetail() {
const [locationImages, setLocationImages] = useState<LocationResponse>(emptyLocationResponse())
const [currentUserReview, setCurrentUserReview] = useState<CurrentUserLocationReviews>()
const [lightboxOpen, setLightboxOpen] = useState<boolean>(false)
const[updatePage, setUpdatePage] = useState<boolean>(true)
const [pageState, setPageState] = useState({
critic_filter_name: 'highest rated',
critic_filter_type: 0,
@ -77,27 +74,12 @@ function LocationDetail() {
const res = await getImagesByLocationService({ page: 1, page_size: 15, location_id: Number(id) })
res.data.images.push({ src: thumbnail })
setLocationImages(res.data)
setUpdatePage(false)
} catch (error) {
console.log(error)
}
setIsLoading(false)
}
async function getCurrentUserLocationReview(): Promise<void> {
try {
const res = await getCurrentUserLocationReviewService(Number(id))
setCurrentUserReview(res.data)
setPageState({ ...pageState, enable_post: false})
} catch (error) {
let err = error as IHttpResponse;
if(err.status == 404 || err.status == 401 ) {
return
}
alert(err.error.response.data.message)
}
}
function handleTextAreaChange(e: ChangeEvent<HTMLTextAreaElement>): void {
const val = e.target as HTMLTextAreaElement;
@ -168,7 +150,6 @@ function LocationDetail() {
created_at: data.created_at,
updated_at: data.updated_at
})
setUpdatePage(true)
} catch (error) {
let err = error as AxiosError;
console.log(err)
@ -187,14 +168,8 @@ function LocationDetail() {
}
useEffect(() => {
getCurrentUserLocationReview()
getLocationDetail()
}, [])
useEffect(() => {
if(updatePage) {
getLocationDetail()
}
}, [updatePage])
return (
<>
@ -325,7 +300,7 @@ function LocationDetail() {
<div className={'bg-secondary text-center p-3'} style={{ width: '100%' }}><a href={'#'} onClick={handleSignInNavigation} style={{ borderBottom: '1px solid white' }}>SIGN IN</a> TO REVIEW</div>
:
<div name="REVIEW INPUT TEXTAREA" className={'reviewContainer p-4'} style={{ backgroundColor: '#2f3136' }}>
<div className={'review-box-content'}>
<div className={'reviewBoxContent'} style={{ width: '75%', margin: '0 auto' }}>
<div className={'userImage mr-3'} style={{ width: 55, float: 'left' }}>
<a href={'#'}>
@ -340,6 +315,7 @@ function LocationDetail() {
<div className={'ratingInput'} style={currentUserReview ? { margin: '0 0 10px' } : { margin: '5px 0 10px' }}>
{currentUserReview ?
<div style={{ display: 'inline-block' }}>
{console.log(currentUserReview)}
<p className={'ml-2'}>{currentUserReview.score}</p>
<div style={{ height: 4, width: 35, backgroundColor: "#72767d" }}>
<div style={{ height: 4, width: `${currentUserReview.score}%`, backgroundColor: 'green' }} />
@ -373,11 +349,12 @@ function LocationDetail() {
content={currentUserReview.comments}
/>
:
<ReactTextareaAutosize
<textarea
onChange={handleTextAreaChange}
ref={textAreaRef}
className={'p-2 text-area text-sm'}
className={'p-2'}
value={reviewValue.review_textArea}
style={{ border: 'none', overflow: 'auto', outline: 'none', boxShadow: 'none', backgroundColor: '#40444b', width: '100%', minHeight: 100, overflowY: 'hidden' }}
/>
}
</div>
@ -402,77 +379,67 @@ function LocationDetail() {
}
<div name={'CRTICITS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"critic's review"} pageLink='#' />
{locationDetail.critics_review.length > 0 ?
<>
<div className={'criticSortFilter'}>
<div className={'inline-block text-sm'}>Sort by: </div>
<a className={'dropdownLabel'} onClick={() => setPageState({ ...pageState, show_sort: !pageState.show_sort })}>
<p className={'ml-2 inline-block capitalize text-sm'}>{pageState.critic_filter_name}</p>
<svg style={{ display: 'inline-block' }} fill={"currentColor"} xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" /></svg>
</a>
<div className={'dropdown-content text-sm bg-secondary'} style={pageState.show_sort ? { display: 'block' } : ''}>
{SORT_TYPE.map((x, index) => (
<a onClick={(e) => onChangeCriticsSort(e, x, index)} className={'block pt-1 capitalize'}>{x}</a>
))}
<div className={'criticSortFilter'}>
<div className={'inline-block text-sm'}>Sort by: </div>
<a className={'dropdownLabel'} onClick={() => setPageState({ ...pageState, show_sort: !pageState.show_sort })}>
<p className={'ml-2 inline-block capitalize text-sm'}>{pageState.critic_filter_name}</p>
<svg style={{ display: 'inline-block' }} fill={"currentColor"} xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" /></svg>
</a>
<div className={'dropdown-content text-sm bg-secondary'} style={pageState.show_sort ? { display: 'block' } : ''}>
{SORT_TYPE.map((x, index) => (
<a onClick={(e) => onChangeCriticsSort(e, x, index)} className={'block pt-1 capitalize'}>{x}</a>
))}
</div>
</div>
<div style={{ clear: 'both' }} />
{locationDetail.critics_review.map(x => (
<div className={''} style={{ padding: '15px 0' }}>
<div style={{ float: 'left' }}>
<div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}>
{x.score}
</div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
</div>
</div>
<div style={{ clear: 'both' }} />
{locationDetail.critics_review.map(x => (
<div className={''} style={{ padding: '15px 0' }}>
<div style={{ float: 'left' }}>
<div style={{ fontSize: 20, marginRight: 20, textAlign: 'center', width: 55, marginBottom: 3 }}>
{x.score}
</div>
<div style={{ height: 4, width: 55, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
</div>
</div>
<div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={x.user_avatar.Valid ? x.user_avatar.String.toString() : 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>{x.username}</span>
</a>
</div>
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px' }}>
<CustomInterweave
content={x.comments}
/>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
<div className={'mr-3'} style={{ display: 'inline-block', width: 40 }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%' }}
src={x.user_avatar.Valid ? x.user_avatar.String.toString() : 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>{x.username}</span>
</a>
</div>
))}
</>
:
<>
<span className={'text-sm italic'}>No Critics review to display</span>
</>
}
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '5px 75px 1px' }}>
<CustomInterweave
content={x.comments}
/>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 72 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Video</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} fill={'currentColor'} xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
<div className={'inline-block'}>Instagram</div>
</a>
</div>
</div>
</div>
))}
</div>
<div name={'USERS REVIEW'} style={{ margin: '50px 0', textAlign: 'left' }}>
@ -638,8 +605,6 @@ function LocationDetail() {
</div>
{/* {screen.width >= 1024 &&
<div className={'bg-secondary'} style={{ display: 'table-cell', position: 'relative', verticalAlign: 'top', width: 330, textAlign: 'left', padding: 15, boxSizing: 'border-box', height: 1080 }}>
// ADD USER DISTRIBUTION SOMETHING LIKE THIS
// https://www.w3schools.com/howto/tryit.asp?filename=tryhow_css_user_rating
Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit cumque aliquam doloribus in reiciendis? Laborum, ea assumenda, tempora dolore placeat aspernatur, cumque totam sequi debitis dolor nam eligendi suscipit aliquid?
</div>
} */}

View File

@ -1,4 +1,3 @@
import { NullValueRes } from "../../types/common"
import { SlideImage } from "yet-another-react-lightbox"
export interface ILocationDetail {

View File

@ -1,4 +1,4 @@
td input {
input {
padding: 5px;
font-size: 14px;
}
}

View File

@ -24,14 +24,8 @@ function Login() {
e.preventDefault();
try {
const res = await loginService({ username: form.username, password: form.password })
if(res.error) {
if (res.error.response.status == 400) {
setErrorMsg(res.error.response.data.errors)
return
}
alert(res.error.response.data.message)
if (res.error) {
setErrorMsg(res.error.response.data.errors)
return
}
@ -78,6 +72,7 @@ function Login() {
function onChangeInput(e: ChangeEvent<HTMLInputElement>) {
const val = e.target as HTMLInputElement;
setFrom({ ...form, [val.placeholder]: val.value })
}
return (
@ -91,14 +86,11 @@ function Login() {
<td><input value={form.username} onChange={onChangeInput} placeholder={'username'} className={'bg-secondary'} /></td>
</tr>
<tr>
<td>password: </td>
<td>password: </td>
<td><input value={form.password} onChange={onChangeInput} type={'password'} placeholder={'password'} className={'bg-secondary'} /></td>
</tr>
</tbody>
</table>
{errorMsg.map(x => (
<p>{x.msg}</p>
))}
<a className={'block'}>Forgout account ?</a>
<input type={'submit'} value={'sign in'} className={'p-1 text-sm text-primary mt-4'} style={{ backgroundColor: '#a8adb3', borderRadius: 7 }} />
</form>

View File

@ -1,241 +0,0 @@
import { ChangeEvent, TargetedEvent, useEffect, useRef, useState } from "preact/compat";
import { DefaultButton, NavigationSeparator } from "../../components";
import { News } from "../../domains/NewsEvent";
import { getNewsServices, postNewsService } from "../../../src/services";
import { AxiosError } from "axios";
import { DEFAULT_LOCATION_THUMBNAIL_IMG } from "../../../src/constants/default";
import { isUrl, useAutosizeTextArea } from "../../../src/utils";
import ReactTextareaAutosize from "react-textarea-autosize";
import { IHttpResponse } from "../../../src/types/common";
import { useSelector } from "react-redux";
import { UserRootState } from "src/store/type";
import "./style.css"
import useCallbackState from "../../../src/types/state-callback";
function NewsEvent() {
const [news, setNews] = useState<Array<News>>([]);
const [isLoading, setIsLoading] = useState(false);
const [pageState, setPageState] = useState({
news_category_name: "hot",
page: 1,
})
const user = useSelector((state: UserRootState) => state.auth)
const [form, setForm] = useState({
description: '',
url: '',
title: '',
title_error_msg: '',
link_error_msg: '',
})
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useAutosizeTextArea(textAreaRef.current, form.description);
async function getNewsEvents(approved: number = 1) {
try {
const news = await getNewsServices({ page: pageState.page, page_size: 10, is_with_approval: approved})
if (news.status == 200) {
setNews(news.data)
}
console.log(news)
} catch (error) {
let err = error as AxiosError;
if (!err.status) {
alert('Server is in trouble, probably dead RIP');
}
console.log(err)
}
}
function handleTextAreaChange(e: ChangeEvent<HTMLTextAreaElement>): void {
const val = e.target as HTMLTextAreaElement;
setForm({
...form,
description: val.value
})
}
function handleInputChange(e: ChangeEvent): void {
const target = e.target as HTMLInputElement;
setForm({
...form,
[target.name]: target.value,
link_error_msg: '',
title_error_msg: ''
})
}
function isFormEmpty() {
const { url, title} = form;
let url_error = ''
let title_error = ''
if(url == '') url_error = "url mustn't empty"
if(title == '') title_error = "title mustn't empty"
return {url_error, title_error}
}
function handleOnChangeNewsCategory(e: TargetedEvent, name: string) {
e.preventDefault();
if(name.toLowerCase() === "fresh") {
setPageState({ ...pageState, news_category_name: "fresh" })
getNewsEvents(0)
return
}
setPageState({ ...pageState, news_category_name: "hot" })
getNewsEvents(1)
}
async function handleSumbitNews(e: TargetedEvent) {
e.preventDefault();
setIsLoading(true)
const {url_error, title_error } = isFormEmpty()
if(url_error !== "" || title_error !== "") {
setForm({ ...form, link_error_msg: url_error, title_error_msg: title_error})
setIsLoading(false)
return
}
if(!isUrl(form.url)) {
setForm({ ...form, link_error_msg: 'url is not correct(https://validurl.com")'})
setIsLoading(false)
return
}
try {
const response = await postNewsService({
description: form.description,
submitted_by: user.id,
title: form.title,
url: form.url
})
if(response.status == 201) {
alert("success")
setForm({
link_error_msg: '',
title_error_msg: '',
title: '',
description: '',
url: '',
})
}
setIsLoading(false)
} catch(error) {
setIsLoading(false)
let err = error as IHttpResponse;
console.log(err)
}
}
useEffect(() => {
getNewsEvents()
}, [])
return (
<div className={'content main-content'}>
<h1 className={'text-2xl mb-5 font-bold'}>News / Events</h1>
<div style={{ maxWidth: 1003 }}>
<NavigationSeparator
pageNames={['Hot', 'Fresh']}
onClick={handleOnChangeNewsCategory}
selectedValue={pageState.news_category_name}
/>
<section name="news content">
{news.map(x => (
<div style={{ marginBottom: 15, paddingBottom: 15, borderBottom: '1px solid #38444d', marginTop: 30 }}>
<a href={x.url} rel={"nofollow"} target={"_"}>
<div style={{ float: 'left', marginRight: 20, width: 200 }}>
<img
src={x.thumbnail != '' ? x.thumbnail : DEFAULT_LOCATION_THUMBNAIL_IMG}
style={{ width: '100%' }}
/>
</div>
</a>
<div style={{ position: 'relative', overflow: 'hidden' }}>
<div className={'text-2xl font-bold'}>
<a href={x.url} target={"_"} rel={"nofollow"}>{x.title}</a>
</div>
<div className={'text-xs mt-1'}>
<div className={'inline-block text-yellow font-bold'}>
{x.source}
</div>
<div className={'submitted-info'} style={{ display: 'inline-block' }}>
1d ago by
<a> {x.submitted_by}</a>
</div>
</div>
<div className={'text-xs text-gray mt-6'}>
<a className={'inline-block'}>
<svg className={'inline-block'} xmlns="http://www.w3.org/2000/svg" fill={"gray"} height="18" viewBox="0 -960 960 960" width="18"><path d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Zm0-273Z" /></svg>
<p className={"mr-3 ml-1 inline-block"}>10</p>
</a>
<a className={'inline-block'}>
<svg className={'inline-block'} style={{ marginTop: 2 }} stroke="currentColor" fill="white" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32zM128 272c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 0c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 0c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"></path></svg>
<p className={"ml-1 inline-block"}>12</p>
</a>
</div>
</div>
<div style={{ clear: 'both' }} />
</div>
))}
</section>
<section name={"news input"}>
<div className={'bg-secondary'} style={{ maxWidth: '100%', padding: '10px 10px'}}>
<h1>SUBMIT A NEWS / EVENT</h1>
<h2 className={'mt-3'}>Title</h2>
<input
className={'input-text'}
style={{ background: '#40444b'}}
maxLength={150}
name={'title'}
placeholder={'Konser besar akbar agung di bengkulu'}
onChange={handleInputChange}
value={form.title}
/>
<span className={'text-xs ml-3 text-error'}>{form.title_error_msg}</span>
<h2 className={'mt-3'}>Link</h2>
<input
className={'input-text'}
style={{ background: '#40444b'}}
name={'url'}
value={form.url}
placeholder={'https://tourismnews.com/12'}
onChange={handleInputChange}
/>
<span className={'text-xs ml-3 text-error'}>{form.link_error_msg}</span>
<h2 className={'mt-3'}>Description (optional) </h2>
<ReactTextareaAutosize
class="text-area p-2"
onChange={handleTextAreaChange}
value={form.description}
/>
<DefaultButton
label="submit"
containerClassName="mt-5"
containerStyle={{ float: 'right'}}
style={{ padding: '5px 10px', letterSpacing: 0}}
isLoading={isLoading}
onClick={handleSumbitNews}
/>
<div style={{ clear: 'both'}} />
</div>
</section>
</div>
</div>
)
}
export default NewsEvent;

View File

@ -1,4 +0,0 @@
.submitted-info::before {
content: " // ";
margin-left: 5px;
}

View File

@ -0,0 +1,10 @@
function NewsEvent() {
return(
<>
<h1>Best PLaces</h1>
</>
)
}
export default NewsEvent;

View File

@ -1,9 +0,0 @@
function Submissions() {
return (
<div className={'content main-content'}>
<h1>Submssions</h1>
</div>
)
}
export default Submissions;

View File

@ -1,9 +0,0 @@
function UserFeed() {
return (
<div>
<h1>User feed</h1>
</div>
)
}
export default UserFeed;

View File

@ -1,226 +0,0 @@
import { useSelector } from "react-redux";
import { DEFAULT_AVATAR_IMG } from "../../constants/default";
import { UserRootState } from "../../store/type";
import "./style.css"
import { useEffect, useState } from "preact/compat";
import { getUserStatsService } from "../../services";
import { CustomInterweave, SeparatorWithAnchor } from "../../components";
interface UserStats {
followers: number,
score_count: number
}
interface UserStatsReviews {
id: number,
comments: string,
name: string,
province_name: string,
score: number,
thumbnail: string
}
interface ScoresDistrbution {
score: number
}
interface UserStatsResponse {
reviews: Array<UserStatsReviews>,
user_stats: UserStats,
scores_distribution: Array<ScoresDistrbution>
}
const emptyUserStatsResponse: UserStatsResponse = {
reviews: Array<UserStatsReviews>(),
user_stats: {
followers: 0,
score_count: 0
},
scores_distribution: [
{
score: 0
}
]
}
function UserProfile() {
const [userStats, setUserStats] = useState<UserStatsResponse>(emptyUserStatsResponse);
const user = useSelector((state: UserRootState) => state.auth)
async function getUserStats() {
try {
const res = await getUserStatsService()
setUserStats(res.data)
} catch (err) {
console.log(err)
}
}
function scoreDistributionLabel(val: string) {
if (val === '0') {
return `${val}-9`
}
if (val === '99') {
return '100'
}
return `${val}0-${val}9`
}
useEffect(() => {
getUserStats()
}, [])
return (
<div className={'content main-content'}>
<div name={'profile_header'} className={'flex column items-end mb-4'}>
<div className={'flex-1'}>
<img
src={user.avatar_picture !== '' ? user.avatar_picture : DEFAULT_AVATAR_IMG}
style={{ width: 140, aspectRatio: '1/1', float: 'left' }}
className={'mr-4'}
/>
<p className={'text-lg'}>{user.username}</p>
{/* <div className={'mt-2'}>
<button className={'bg-tertiary text-xs mr-2'} style={{ padding: '5px 15px', letterSpacing: .8}}>
FOLLOW
</button>
<button className={'bg-error text-xs'} style={{ padding: '5px 15px', letterSpacing: .8}}>
REPORT
</button>
</div> */}
</div>
<div className={'inline-block mr-4 text-center'}>
<p className={'font-bold'} style={{ fontSize: 32 }}>{userStats?.user_stats.followers}</p>
<p className={'text-xs'}>Followers</p>
</div>
<div className={'inline-block text-center'}>
<p className={'font-bold'} style={{ fontSize: 32 }}>{userStats?.user_stats.score_count}</p>
<p className={'text-xs'}>Reviews</p>
</div>
</div>
<section name={"profile-navigation"}>
<div className={'bg-secondary profile-nav'}>
<a>
<div>
summary
</div>
</a>
<a>
<div>
reviews
</div>
</a>
<a>
<div>
likes
</div>
</a>
<a>
<div>
stories
</div>
</a>
<a>
<div>
tags
</div>
</a>
</div>
</section>
<section name={'REVIEWS SECTION'}>
<div className={'mt-4'} style={{ tableLayout: 'fixed', display: 'table', width: '100%' }}>
<div className={'wideLeft'} style={{ textAlign: 'left', paddingRight: 20, maxWidth: 1096, minWidth: 680, display: 'table-cell', position: 'relative', verticalAlign: 'top', width: '100%', boxSizing: 'border-box' }}>
<div name={'USERS REVIEW'} style={{ textAlign: 'left' }}>
<SeparatorWithAnchor pageName={"User's review"} pageLink='#' secondLink={userStats!.reviews.length > 0 ? '#' : ''} />
{userStats!.reviews.length > 0 ?
<>
{userStats!.reviews.map(x => (
<div style={{ padding: '15px 0' }}>
<div className={'mr-3'} style={{ width: 65, float: 'left' }}>
<a href="#">
<img
loading={'lazy'}
style={{ width: '100%', aspectRatio: '1/1', objectFit: 'cover' }}
src={x.thumbnail !== '' ? x.thumbnail : 'https://cdn.discordapp.com/attachments/743422487882104837/1153985664849805392/421-4212617_person-placeholder-image-transparent-hd-png-download.png'}
/>
</a>
</div>
<div>
<div style={{ fontWeight: 700, fontSize: 16, lineHeight: 'initial' }}>
<a>
<span>{x.name}</span>
</a>
</div>
</div>
<p className={'text-xs'}>{x.province_name}</p>
<div className={'inline-block'}>
<div className={'text-sm text-center'} >{x.score}</div>
<div style={{ height: 4, width: 25, position: 'relative', backgroundColor: '#d8d8d8' }}>
<div style={{ height: 4, backgroundColor: '#85ce73', width: `${x.score}%` }} />
</div>
</div>
<div style={{ fontSize: 15, lineHeight: '24px', margin: '10px 77px 1px', wordWrap: 'break-word' }}>
<CustomInterweave
content={x.comments}
/>
</div>
<div className={'reviewLinks'} style={{ marginLeft: 76 }}>
<div className={'mr-2'} style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle' }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} xmlns="http://www.w3.org/2000/svg" fill={"gray"} height="14" viewBox="0 -960 960 960" width="14"><path d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Zm0-273Z" /></svg>
<div className={'inline-block'}>24</div>
</a>
</div>
<div style={{ minWidth: 55, display: 'inline-block', verticalAlign: 'middle', }}>
<a className={'text-sm'} href={'#'}>
<svg className={'inline-block mr-1'} stroke="currentColor" fill="white" stroke-width="0" viewBox="0 0 512 512" height="14" width="14" xmlns="http://www.w3.org/2000/svg"><path d="M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32zM128 272c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 0c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 0c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"></path></svg>
<div className={'inline-block'}>25 </div>
</a>
</div>
</div>
</div>
))}
</>
:
<>
<span className={'text-sm italic'}>No users review to display</span>
</>
}
</div>
</div>
{screen.width >= 1024 &&
<div className={'bg-secondary'} style={{ display: 'table-cell', position: 'relative', verticalAlign: 'top', width: 330, textAlign: 'left', boxSizing: 'border-box' }}>
<div style={{ padding: '20px 10px' }}>
<div className={'bg-primary p-2'}>
<SeparatorWithAnchor
pageName={"Score Distribution"}
pageLink="#"
titleStyles={{ fontSize: 12, letterSpacing: .9 }}
/>
{Object.entries(userStats.scores_distribution[0]).map(x => (
<div style={{ height: 15, margin: '5px 0'}}>
<div style={{ float: 'left', marginRight: 5, width: 40, fontSize: 11, textAlign: 'center' }}>{scoreDistributionLabel(x[0])}</div>
<div style={{ borderTopRightRadius: 3, borderBottomRightRadius: 3, height: 13, float: 'left', marginRight: 5, minWidth: 1, backgroundColor: '#0be881', width: `calc(${x[1]/userStats.user_stats.score_count * 100}%/2)` }}></div>
<div style={{ float: 'left', fontSize: 11 }}>{x[1]}</div>
<br style={{ clear: 'both' }} />
</div>
))
}
</div>
</div>
</div>
}
</div>
<div style={{ clear: 'both' }} />
</section>
</div>
)
}
export default UserProfile;

View File

@ -1,13 +0,0 @@
.profile-nav {
white-space: nowrap;
overflow: hidden;
text-align: center;
}
.profile-nav div {
display: inline-block;
padding: 0 2%;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.75rem;
}

View File

@ -1,286 +0,0 @@
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;

View File

@ -1,3 +0,0 @@
.button-avatar-container p a:hover {
color: white;
}

View File

@ -2,30 +2,20 @@ import Home from "./Home";
import BestLocation from "./BestLocations";
import Discovery from "./Discovery";
import Story from "./Stories";
import NewsEvent from "./NewsEvent";
import NewsEvent from "./NewsEvents";
import LocationDetail from "./LocationDetail";
import Login from './Login';
import NotFound from "./NotFound";
import AddLocation from "./AddLocation";
import Submissions from "./Submissions";
import UserProfile from "./UserProfile";
import UserFeed from "./UserFeed";
import UserSettings from "./UserSettings";
export {
Login,
Home,
UserProfile,
UserFeed,
UserSettings,
NotFound,
BestLocation,
AddLocation,
LocationDetail,
Submissions,
Discovery,
Story,

View File

@ -1,23 +0,0 @@
import { Navigate } from "react-router-dom"
import { useSelector } from "react-redux"
import { UserRootState } from "src/store/type"
export const AdminProtectedRoute = ({children}: any) => {
const user = useSelector((state: UserRootState) => state.auth)
if(!user.is_admin) {
return <Navigate to={"/"} replace />;
}
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
}

View File

@ -5,94 +5,40 @@ import {
LocationDetail,
NewsEvent,
Story,
AddLocation,
UserProfile,
UserFeed,
UserSettings,
Submissions
Login
} from '../pages';
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: "/",
name: "Home",
element: <Home />
},
{
path: "/best-places",
name: "Home",
element: <BestLocation />
},
{
path: "/discover",
name: "Home",
element: <Discovery />
},
{
path: "/stories",
name: "Home",
element: <Story />
},
{
path: "/news-events",
name: "Home",
element: <NewsEvent />
},
{
path: "/location/:id",
name: "LocationDetail",
element: <LocationDetail />
},
// PROTECTED USER ROUTES
{
path: "/add-location",
name: "AddLocation",
protectedRoute: "user",
element: AddLocation
},
{
path: "/user/profile",
name: "UserProfile",
protectedRoute: "user",
element: UserProfile
},
{
path: "/user/settings",
name: "UserSettings",
protectedRoute: "user",
element: UserSettings
},
{
path: "/user/feed",
name: "UserFeed",
protectedRoute: "user",
element: UserFeed
},
//PROTECTED ADMIN ROUTES
{
path: "/admin/submissions",
name: "Submissions",
protectedRoute: "admin",
element: Submissions
},
]
let routes: IRoutes = {
routes: groupRoutes
const routes = [
{
path: "/",
name: "Home",
element: <Home />
},
{
path: "/best-places",
name: "Home",
element: <BestLocation />
},
{
path: "/discover",
name: "Home",
element: <Discovery />
},
{
path: "/stories",
name: "Home",
element: <Story />
},
{
path: "/news-events",
name: "Home",
element: <NewsEvent />
},
{
path: "/location/:id",
name: "LocationDetail",
element: <LocationDetail />
}
]
return routes
}
export default routes;

View File

@ -1,9 +1,8 @@
import { AxiosError } from "axios";
import { LOGIN_URI, SIGNUP_URI } from "../constants/api";
import { client } from "./config";
import { IHttpResponse } from "../types/common";
const initialState: IHttpResponse = {
const initialState: IEmptyResponseState = {
data: null,
error: AxiosError
}
@ -16,7 +15,7 @@ interface IAuthentication {
async function createAccountService({ username, password }: IAuthentication) {
const newState = { ...initialState };
try {
const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password }, withCredentials: true })
const response = await client({ method: 'POST', url: SIGNUP_URI, data: { username, password } })
newState.data = response.data
newState.error = null
return newState

View File

@ -1,4 +1,3 @@
import { GetRequestPagination } from "../types/common"
import { GET_IMAGES_BY_LOCATION_URI } from "../constants/api"
import { client } from "./config"
import statusCode from "./status-code"

View File

@ -7,22 +7,13 @@ import {
} from "./locations";
import { getImagesByLocationService } from "./images"
import { createAccountService, loginService, logoutService } from "./auth";
import { postReviewLocation, getCurrentUserLocationReviewService } from "./review";
import { getRegionsService, getProvincesService, getRegenciesService} from "./regions";
import { getUserStatsService } from "./users";
import { getNewsServices, postNewsService} from "./news";
import { postReviewLocation } from "./review";
export {
createAccountService,
loginService,
logoutService,
getRegionsService,
getProvincesService,
getRegenciesService,
getUserStatsService,
getListLocationsService,
getListRecentLocationsRatingsService,
getListTopLocationsService,
@ -30,9 +21,5 @@ export {
getLocationTagsService,
getImagesByLocationService,
postReviewLocation,
getCurrentUserLocationReviewService,
getNewsServices,
postNewsService
postReviewLocation
}

View File

@ -1,27 +1,18 @@
import { GetRequestPagination, IHttpResponse } from "../types/common";
import {
GET_LIST_LOCATIONS_URI,
GET_LIST_RECENT_LOCATIONS_RATING_URI,
GET_LIST_TOP_LOCATIONS,
GET_LOCATION_TAGS_URI,
GET_LOCATION_URI,
POST_CREATE_LOCATION
} from "../constants/api";
import { GET_LIST_LOCATIONS_URI, GET_LIST_RECENT_LOCATIONS_RATING_URI, GET_LIST_TOP_LOCATIONS, GET_LOCATION_TAGS_URI, GET_LOCATION_URI } from "../constants/api";
import { client } from "./config";
import statusCode from "./status-code";
import { AxiosError } from "axios";
const initialState: any = {
data: null,
error: null
}
interface GetListLocationsArg extends GetRequestPagination {
interface getListLocationsArg extends GetRequestPagination {
order_by?: number,
region_type?: number
}
async function getListLocationsService({ page, page_size }: GetListLocationsArg) {
async function getListLocationsService({ page, page_size }: getListLocationsArg) {
const newState = { ...initialState };
const url = `${GET_LIST_LOCATIONS_URI}?page=${page}&page_size=${page_size}`
try {
@ -57,7 +48,7 @@ async function getListRecentLocationsRatingsService(page_size: Number) {
}
}
async function getListTopLocationsService({ page, page_size, order_by, region_type }: GetListLocationsArg) {
async function getListTopLocationsService({ page, page_size, order_by, region_type }: getListLocationsArg) {
const newState = { ...initialState };
const url = `${GET_LIST_TOP_LOCATIONS}?page=${page}&page_size=${page_size}&order_by=${order_by}&region_type=${region_type}`
try {
@ -111,26 +102,10 @@ async function getLocationTagsService(id: Number) {
}
}
async function createLocationService(data: FormData): Promise<IHttpResponse> {
const newState: IHttpResponse = { data: null, error: null};
try {
const response = await client({ method: 'POST', url: POST_CREATE_LOCATION, data: data, withCredentials: true})
newState.data = response.data;
newState.status = response.status
return newState;
} catch (error) {
let err = error as AxiosError;
newState.error = err;
newState.status = err.status;
return newState;
}
}
export {
getListLocationsService,
getListRecentLocationsRatingsService,
getListTopLocationsService,
getLocationTagsService,
getLocationService,
createLocationService,
getLocationService
}

View File

@ -1,50 +0,0 @@
import { AxiosError } from "axios";
import { GetRequestPagination, IHttpResponse } from "..//types/common";
import { client } from "./config";
import { GET_NEWS_EVENTS_URI, POST_NEWS_EVENTS_URI } from "../../src/constants/api";
interface GetNewsSevice extends GetRequestPagination {
is_with_approval: number
}
async function getNewsServices({ page, page_size, is_with_approval}: GetNewsSevice): Promise<IHttpResponse> {
const newState: IHttpResponse = { data: null, error: null};
try {
const response = await client({ method: 'GET', url: `${GET_NEWS_EVENTS_URI}?page=${page}&page_size=${page_size}&is_with_approval=${is_with_approval}`});
newState.data = response.data;
newState.status = response.status;
return newState;
} catch (error) {
let err = error as AxiosError;
newState.error = err;
newState.status = err.status
throw(newState)
}
}
interface PostNewsServiceBody {
title: string,
url: string,
description: string,
submitted_by: number
}
async function postNewsService(req: PostNewsServiceBody): Promise<IHttpResponse> {
const newState: IHttpResponse = { data: null, error: null}
try {
const response = await client({ method: 'POST', url: POST_NEWS_EVENTS_URI, data: req, withCredentials: true})
newState.data = response.data
newState.status = response.status
return newState
} catch (error) {
let err = error as AxiosError;
newState.error = err;
newState.status = err.status
throw(newState)
}
}
export {
getNewsServices,
postNewsService
}

View File

@ -1,46 +0,0 @@
import { client } from "./config";
import { GET_PROVINCES, GET_REGENCIES, GET_REGIONS } from "../constants/api";
import { IHttpResponse } from "src/types/common";
async function getRegionsService(): Promise<IHttpResponse> {
const newState: IHttpResponse = {data: null, error: null}
try {
const response = await client({ method: 'GET', url: GET_REGIONS})
newState.data = response.data;
return newState
} catch(err) {
newState.error = err
throw (newState)
}
}
async function getProvincesService(): Promise<IHttpResponse> {
const newState: IHttpResponse = { data: null, error: null}
try {
const response = await client({ method: 'GET', url: GET_PROVINCES})
newState.data = response.data;
return newState
} catch(err) {
newState.error = err
throw (newState)
}
}
async function getRegenciesService(): Promise<IHttpResponse> {
const newState: IHttpResponse = { data: null, error: null};
try {
const response = await client({ method: 'GET', url: GET_REGENCIES})
newState.data = response.data;
newState.status = response.status
return newState
} catch(err) {
newState.error = err
throw (newState)
}
}
export {
getRegionsService,
getProvincesService,
getRegenciesService,
}

View File

@ -1,11 +1,10 @@
import { AxiosError } from "axios"
import { client } from "./config";
import { GET_CURRENT_USER_REVIEW_LOCATION_URI, POST_REVIEW_LOCATION_URI } from "../constants/api";
import { POST_REVIEW_LOCATION_URI } from "../constants/api";
const initialState: IHttpResponse = {
const initialState: IEmptyResponseState = {
data: null,
error: AxiosError,
status: 0,
error: AxiosError
}
interface postReviewLocationReq {
@ -30,22 +29,6 @@ async function postReviewLocation(req: postReviewLocationReq) {
}
}
async function getCurrentUserLocationReviewService(location_id: number): Promise<IHttpResponse> {
const newState = { ...initialState };
try {
const response = await client({ method: 'GET', url: `${GET_CURRENT_USER_REVIEW_LOCATION_URI}/${location_id}`, withCredentials: true})
newState.data = response.data
newState.error = null
return newState
} catch (err) {
let error = err as AxiosError;
newState.error = error
newState.status = error.response?.status;
throw(newState)
}
}
export {
postReviewLocation,
getCurrentUserLocationReviewService,
postReviewLocation
}

View File

@ -1,73 +0,0 @@
import { AxiosError } from "axios";
import { DELETE_USER_AVATAR, GET_CURRENT_USER_STATS, PATCH_USER_AVATAR, PATCH_USER_INFO } from "../constants/api";
import { IHttpResponse } from "../types/common";
import { client } from "./config";
import { UserInfo } from "../../src/domains/User";
async function getUserStatsService(): Promise<IHttpResponse> {
const newState: IHttpResponse = { data: null, error: null };
try {
const res = await client({ method: 'GET', url: GET_CURRENT_USER_STATS, 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 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 {
getUserStatsService,
patchUserAvatarService,
deleteUserAvatarService,
patchUserInfoService
}

View File

@ -1,6 +1,6 @@
import { User } from "../domains";
import { IUser } from "../features/auth/authSlice/type";
import { RootState } from "./config";
export interface UserRootState extends RootState {
auth: User
auth: IUser
}

View File

@ -1,44 +1,13 @@
import { Province, Regency, Region } from "../domains";
type BaseNullValueRes = { Valid: boolean };
export type NullValueRes<Key extends string, _> = BaseNullValueRes & Record<Key, string | number>
type NullValueRes<Key extends string, _> = BaseNullValueRes & Record<Key, string | number>
export interface GetRequestPagination {
interface GetRequestPagination {
page: number,
page_size: number,
}
export interface IHttpResponse {
interface IEmptyResponseState {
data: any,
error: any,
status?: number,
};
export interface IDropdownInputProps {
label: string,
value: string
}
export interface IndonesiaRegionsInfo {
regions?: Array<Region>,
provinces?: Array<Province>,
regencies?: Array<Regency>,
}
export enum LocationType {
Beach = "beach",
AmusementPark = "amusement park",
Culinary = "culinary",
HikingCamping = "hiking / camping",
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"
}
};

View File

@ -2,13 +2,4 @@ import { AxiosError } from "axios";
export function handleAxiosError(error: AxiosError) {
return error.response?.data
}
export function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
return Object.keys(obj).filter(k => Number.isNaN(+k)) as K[];
}
export function isUrl(val: string): boolean {
var urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
return urlPattern.test(val);
}

View File

@ -1,9 +1,7 @@
import useAutosizeTextArea from "./useAutosizeTextArea";
import { handleAxiosError, enumKeys, isUrl } from "./common";
import { handleAxiosError } from "./common";
export {
useAutosizeTextArea,
handleAxiosError,
enumKeys,
isUrl
handleAxiosError
}

View File

@ -9,8 +9,6 @@ export default {
secondary: '#2f3136',
tertiary: '#a8adb3',
error: '#ff5454',
gray: '#797979',
yellow: '#e5c453'
},
borderColor: {
primary: '#38444d',

View File

@ -204,13 +204,6 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.20.13":
version "7.23.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d"
integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
@ -1227,15 +1220,6 @@ react-router@6.16.0:
dependencies:
"@remix-run/router" "1.9.0"
react-textarea-autosize@^8.5.3:
version "8.5.3"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz#d1e9fe760178413891484847d3378706052dd409"
integrity sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==
dependencies:
"@babel/runtime" "^7.20.13"
use-composed-ref "^1.3.0"
use-latest "^1.2.1"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@ -1419,23 +1403,6 @@ update-browserslist-db@^1.0.11:
escalade "^3.1.1"
picocolors "^1.0.0"
use-composed-ref@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
use-isomorphic-layout-effect@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
use-latest@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2"
integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==
dependencies:
use-isomorphic-layout-effect "^1.1.1"
use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"