add news evenst page

This commit is contained in:
NCanggoro 2023-10-11 16:31:25 +07:00
parent 1c65b5b237
commit 946bed296a
12 changed files with 368 additions and 12 deletions

View File

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

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

View File

@ -8,6 +8,7 @@ import DefaultButton from "./Button/DefaultButton";
import SeparatorWithAnchor from "./Separator/WithAnchor"; import SeparatorWithAnchor from "./Separator/WithAnchor";
import DefaultSeparator from "./Separator/Default"; import DefaultSeparator from "./Separator/Default";
import TitleSeparator from "./Separator/TitleSeparator"; import TitleSeparator from "./Separator/TitleSeparator";
import NavigationSeparator from "./Separator/NavigationSeparator";
import Footer from './Footer/'; import Footer from './Footer/';
import CustomInterweave from "./CustomInterweave"; import CustomInterweave from "./CustomInterweave";
import DropdownInput from "./DropdownInput"; import DropdownInput from "./DropdownInput";
@ -24,6 +25,7 @@ export {
SeparatorWithAnchor, SeparatorWithAnchor,
DefaultSeparator, DefaultSeparator,
TitleSeparator, TitleSeparator,
NavigationSeparator,
Footer, Footer,
CustomInterweave, CustomInterweave,
DropdownInput, DropdownInput,

View File

@ -13,6 +13,9 @@ const PATCH_USER_AVATAR = `${BASE_URL}/user/avatar`;
const PATCH_USER_INFO = `${BASE_URL}/user`; const PATCH_USER_INFO = `${BASE_URL}/user`;
const DELETE_USER_AVATAR = `${BASE_URL}/user/avatar` const DELETE_USER_AVATAR = `${BASE_URL}/user/avatar`
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_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`
const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent` const GET_LIST_RECENT_LOCATIONS_RATING_URI = `${BASE_URL}/locations/recent`
@ -31,6 +34,7 @@ export {
LOGOUT_URI, LOGOUT_URI,
SIGNUP_URI, SIGNUP_URI,
DELETE_USER_AVATAR, DELETE_USER_AVATAR,
GET_NEWS_EVENTS_URI,
GET_REGIONS, GET_REGIONS,
GET_PROVINCES, GET_PROVINCES,
GET_REGENCIES, GET_REGENCIES,
@ -46,4 +50,5 @@ export {
PATCH_USER_INFO, PATCH_USER_INFO,
POST_REVIEW_LOCATION_URI, POST_REVIEW_LOCATION_URI,
POST_CREATE_LOCATION, POST_CREATE_LOCATION,
POST_NEWS_EVENTS_URI,
} }

View File

@ -1,2 +1,3 @@
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_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';

29
src/domains/NewsEvent.ts Normal file
View File

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

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

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

View File

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

View File

@ -2,7 +2,7 @@ import Home from "./Home";
import BestLocation from "./BestLocations"; import BestLocation from "./BestLocations";
import Discovery from "./Discovery"; import Discovery from "./Discovery";
import Story from "./Stories"; import Story from "./Stories";
import NewsEvent from "./NewsEvents"; import NewsEvent from "./NewsEvent";
import LocationDetail from "./LocationDetail"; import LocationDetail from "./LocationDetail";
import Login from './Login'; import Login from './Login';
import NotFound from "./NotFound"; import NotFound from "./NotFound";

View File

@ -10,6 +10,7 @@ import { createAccountService, loginService, logoutService } from "./auth";
import { postReviewLocation, getCurrentUserLocationReviewService } from "./review"; import { postReviewLocation, getCurrentUserLocationReviewService } from "./review";
import { getRegionsService, getProvincesService, getRegenciesService} from "./regions"; import { getRegionsService, getProvincesService, getRegenciesService} from "./regions";
import { getUserStatsService } from "./users"; import { getUserStatsService } from "./users";
import { getNewsServices, postNewsService} from "./news";
export { export {
createAccountService, createAccountService,
@ -31,4 +32,7 @@ export {
postReviewLocation, postReviewLocation,
getCurrentUserLocationReviewService, getCurrentUserLocationReviewService,
getNewsServices,
postNewsService
} }

50
src/services/news.ts Normal file
View File

@ -0,0 +1,50 @@
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
}