aboutsummaryrefslogtreecommitdiff
path: root/vnext/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'vnext/src/ui')
-rw-r--r--vnext/src/ui/Feeds.js137
-rw-r--r--vnext/src/ui/Header.js134
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&nbsp;</span><b>{tag}</b>
+ <Link to={{ pathname: `/tag/${state.tag}` }}>
+ <span>← All posts with tag&nbsp;</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));