diff options
Diffstat (limited to 'vnext/src/ui')
-rw-r--r-- | vnext/src/ui/Feeds.js | 137 | ||||
-rw-r--r-- | vnext/src/ui/Header.js | 134 |
2 files changed, 140 insertions, 131 deletions
diff --git a/vnext/src/ui/Feeds.js b/vnext/src/ui/Feeds.js index 8c79f779..e687f1e2 100644 --- a/vnext/src/ui/Feeds.js +++ b/vnext/src/ui/Feeds.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import qs from 'qs'; @@ -23,9 +23,9 @@ import { getMessages } from '../api'; * @property {import('history').History} history * @property {import('history').Location} location * @property {import('react-router').match} match - * @property {string} search + * @property {string=} search * @property {import('../api').SecureUser} visitor - * @property {import('../api').Message[]} msgs + * @property {import('../api').Message[]=} msgs */ /** @@ -101,84 +101,95 @@ export function Home(props) { } /** - * @param {{ - authRequired?: boolean, - visitor: import('../api').SecureUser, - history: import('history').History, - location: import('history').Location, - msgs: import('../api').Message[], - query: Query -}} props + * @typedef {Object} FeedState + * @property authRequired?: boolean + * @property visitor: import('../api').SecureUser + * @property history: import('history').History + * @property location: import('history').Location + * @property msgs: import('../api').Message[] + * @property query: Query + */ + +/** + * @param {FeedState} props */ function Feed(props) { - const [msgs, setMsgs] = useState([]); - const [loading, setLoading] = useState(true); - const [nextpage, setNextpage] = useState(null); - const [error, setError] = useState(false); + const [state, setState] = useState({ + history: props.history, + authRequired: props.authRequired, + query: props.query, + hash: props.visitor.hash, + filter: props.location.search.substring(1), + msgs: [], + loading: true, + nextpage: null, + error: false, + tag: '' + }); + + const stateRef = useRef(state); useEffect(() => { - let loadMessages = (hash = '', filter = '') => { - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - setMsgs([]); - setLoading(true); - const filterParams = qs.parse(filter); - let params = Object.assign({}, filterParams || {}, props.query.search || {}); - let url = props.query.baseUrl; - if (hash) { - params.hash = hash; - } - if (!params.hash && props.authRequired) { - props.history.push('/'); - } - getMessages(url, params) - .then(response => { - const { data } = response; - const { pageParam } = props.query; - const lastMessage = data.slice(-1)[0] || {}; - const nextpage = getPageParam(pageParam, lastMessage, filterParams); - setMsgs(data); - setLoading(false); - setNextpage(nextpage); - }).catch(ex => { - setError(true); - }); + let getPageParam = (pageParam, lastMessage, filterParams) => { + const pageValue = pageParam === 'before_mid' ? lastMessage.mid : pageParam === 'page' ? (Number(filterParams.page) || 0) + 1 : moment.utc(lastMessage.updated).valueOf(); + let newFilter = { ...filterParams }; + newFilter[pageParam] = pageValue; + return `?${qs.stringify(newFilter)}`; }; - loadMessages(props.visitor.hash, props.location.search.substring(1)); - }, [props]); - - let getPageParam = (pageParam, lastMessage, filterParams) => { - const pageValue = pageParam === 'before_mid' ? lastMessage.mid : pageParam === 'page' ? (Number(filterParams.page) || 0) + 1 : moment.utc(lastMessage.updated).valueOf(); - let newFilter = { ...filterParams }; - newFilter[pageParam] = pageValue; - return `?${qs.stringify(newFilter)}`; - }; - const { tag } = qs.parse(location.search.substring(1)) || {}; - const nodes = ( - <> + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + const filterParams = qs.parse(stateRef.current.filter); + let params = Object.assign({}, filterParams || {}, stateRef.current.query.search || {}); + let url = stateRef.current.query.baseUrl; + if (stateRef.current.hash) { + params.hash = stateRef.current.hash; + } + if (!params.hash && stateRef.current.authRequired) { + stateRef.current.history.push('/'); + } + getMessages(url, params) + .then(response => { + const { data } = response; + const { pageParam } = stateRef.current.query; + const lastMessage = data.slice(-1)[0] || {}; + const nextpage = getPageParam(pageParam, lastMessage, filterParams); + setState({ + ...stateRef.current, + msgs: data, + loading: false, + nextpage: nextpage, + tag: qs.parse(location.search.substring(1))['tag'] || '' + }); + }).catch(ex => { + setState({ + ...stateRef.current, + error: true + }); + }); + }, []); + return (state.msgs.length > 0 ? ( + <div className="msgs"> { - tag && ( + state.tag && ( <p className="page"> - <Link to={{ pathname: `/tag/${tag}` }}> - <span>← All posts with tag </span><b>{tag}</b> + <Link to={{ pathname: `/tag/${state.tag}` }}> + <span>← All posts with tag </span><b>{state.tag}</b> </Link> </p> ) } { - msgs.map(msg => + state.msgs.map(msg => <Message key={msg.mid} data={msg} visitor={props.visitor} />) } { - msgs.length >= 20 && ( + state.msgs.length >= 20 && ( <p className="page"> - <Link to={{ pathname: props.location.pathname, search: nextpage }} rel="prev">Next →</Link> + <Link to={{ pathname: props.location.pathname, search: state.nextpage }} rel="prev">Next →</Link> </p> ) } - </> + </div> + ) : state.error ? <div>error</div> : state.loading ? <div className="msgs"><Spinner /><Spinner /><Spinner /><Spinner /></div> : <div>No more messages</div> ); - return msgs.length > 0 ? ( - <div className="msgs">{nodes}</div> - ) : error ? <div>error</div> : loading ? <div className="msgs"><Spinner /><Spinner /><Spinner /><Spinner /></div> : <div>No more messages</div>; } diff --git a/vnext/src/ui/Header.js b/vnext/src/ui/Header.js index 2d042bfe..a7663dd3 100644 --- a/vnext/src/ui/Header.js +++ b/vnext/src/ui/Header.js @@ -1,73 +1,71 @@ -import React, { useEffect, useCallback, useRef } from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; +import React, { memo } from 'react'; +import { Link, withRouter } from 'react-router-dom'; -const elClassHidden = 'header--hidden'; +import Icon from './Icon'; +import { UserLink } from './UserInfo'; +import SearchBox from './SearchBox'; -const header = document.getElementById('header'); +function Header({ visitor, search, className }) { + return ( + <div id="header" className={className}> + <div id="header_wrapper"> + { + visitor.uid < 0 ? + <> + <div id="logo"><a href="/" /></div> + <nav id="global"> + <a href="/">Loading...</a> + </nav> + </> + : visitor.uid > 0 ? + <UserLink user={visitor} /> + : <div id="logo"> + <Link to="/">Juick</Link> + </div> + } + { + visitor.uid >= 0 && + <> + <div id="search" className="desktop"> + <SearchBox pathname="/discover" onSearch={search} /> + </div> + <nav id="global"> + {visitor.uid > 0 ? + <Link to={{ pathname: '/' }}> + <Icon name="ei-bell" size="s" /><span className="desktop">Discuss</span> + { + visitor.unreadCount && + <span className="badge">{visitor.unreadCount}</span> + } + </Link> + : + <Link to='/?media=1' rel="nofollow"> + <Icon name="ei-camera" size="s" /> + <span className="desktop">Photos</span> + </Link> + } + <Link to={{ pathname: '/discover' }} rel="nofollow"> + <Icon name="ei-search" size="s" /> + <span className="desktop">Discover</span> + </Link> -export default function Header({ children }) { - let dHeight = useRef(0); - let wHeight = useRef(0); - let wScrollCurrent = useRef(0); - let wScrollBefore = useRef(0); - let wScrollDiff = useRef(0); - - /** - * @param {number} delay - * @param {{ (): void; apply?: any; }} fn - */ - let throttle = (delay, fn) => { - var last, deferTimer; - return function () { - var context = this, args = arguments, now = +new Date; - if (last && now < last + delay) { - clearTimeout(deferTimer); - deferTimer = setTimeout( - function () { - last = now; - fn.apply(context, args); - }, - delay); - } else { - last = now; - fn.apply(context, args); - } - }; - }; - let updateHeader = useCallback(() => { - dHeight.current = document.body.offsetHeight; - wHeight.current = window.innerHeight; - wScrollCurrent.current = window.pageYOffset; - wScrollDiff.current = wScrollBefore.current - wScrollCurrent.current; - - if (wScrollCurrent.current <= 0) { - // scrolled to the very top; element sticks to the top - header.classList.remove(elClassHidden); - } else if (wScrollDiff.current > 0 && header.classList.contains(elClassHidden)) { - // scrolled up; element slides in - header.classList.remove(elClassHidden); - } else if (wScrollDiff.current < 0) { - // scrolled down - if (wScrollCurrent.current + wHeight.current >= dHeight.current && header.classList.contains(elClassHidden)) { - // scrolled to the very bottom; element slides in - header.classList.remove(elClassHidden); - } else { - // scrolled down; element slides out - header.classList.add(elClassHidden); - } - } - wScrollBefore.current = wScrollCurrent.current; - }, []); - - useEffect(() => { - window.addEventListener('scroll', () => (!window.requestAnimationFrame) - ? throttle(250, updateHeader) - : window.requestAnimationFrame(updateHeader), false); - }, [updateHeader]); - return ReactDOM.createPortal(children, header); + {visitor.uid > 0 ? + <Link to={{ pathname: '/post' }}> + <Icon name="ei-pencil" size="s" /> + <span className="desktop">Post</span> + </Link> + : + <Link to={{ pathname: '/login', state: { retpath: window.location.pathname } }}> + <Icon name="ei-user" size="s" /> + <span className="desktop">Login</span> + </Link> + } + </nav> + </> + } + </div> + </div> + ); } -Header.propTypes = { - children: PropTypes.node -}; +export default memo(withRouter(Header)); |