From f470636a70943a8ecad8bddc791a1c2dddd28e1e Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Sat, 4 May 2019 21:13:12 +0300 Subject: Components -> UI --- vnext/src/ui/Avatar.css | 27 +++ vnext/src/ui/Avatar.js | 44 ++++ vnext/src/ui/Button.css | 13 + vnext/src/ui/Button.js | 11 + vnext/src/ui/Chat.css | 9 + vnext/src/ui/Chat.js | 85 +++++++ vnext/src/ui/Contact.js | 21 ++ vnext/src/ui/Contacts.js | 49 ++++ vnext/src/ui/Feeds.js | 171 +++++++++++++ vnext/src/ui/Header.js | 69 ++++++ vnext/src/ui/Icon.js | 38 +++ vnext/src/ui/Input.css | 8 + vnext/src/ui/Input.js | 17 ++ vnext/src/ui/LoginButton.js | 90 +++++++ vnext/src/ui/Message.css | 210 ++++++++++++++++ vnext/src/ui/Message.js | 142 +++++++++++ vnext/src/ui/MessageInput.js | 107 ++++++++ vnext/src/ui/Modal.css | 95 ++++++++ vnext/src/ui/Modal.js | 29 +++ vnext/src/ui/NavigationIcon.css | 4 + vnext/src/ui/NavigationIcon.js | 21 ++ vnext/src/ui/PM.js | 51 ++++ vnext/src/ui/Post.js | 38 +++ vnext/src/ui/SearchBox.js | 26 ++ vnext/src/ui/Settings.js | 268 +++++++++++++++++++++ vnext/src/ui/Spinner.js | 42 ++++ vnext/src/ui/Thread.js | 181 ++++++++++++++ vnext/src/ui/Types.js | 15 ++ vnext/src/ui/UploadButton.js | 42 ++++ vnext/src/ui/UserInfo.css | 11 + vnext/src/ui/UserInfo.js | 117 +++++++++ vnext/src/ui/Users.js | 44 ++++ vnext/src/ui/__tests__/Avatar.test.js | 15 ++ vnext/src/ui/__tests__/LoginButton.test.js | 21 ++ vnext/src/ui/__tests__/MessageInput-test.js | 95 ++++++++ vnext/src/ui/__tests__/UserLink.test.js | 20 ++ .../ui/__tests__/__snapshots__/Avatar.test.js.snap | 41 ++++ .../__snapshots__/LoginButton.test.js.snap | 178 ++++++++++++++ .../__tests__/__snapshots__/UserLink.test.js.snap | 33 +++ 39 files changed, 2498 insertions(+) create mode 100644 vnext/src/ui/Avatar.css create mode 100644 vnext/src/ui/Avatar.js create mode 100644 vnext/src/ui/Button.css create mode 100644 vnext/src/ui/Button.js create mode 100644 vnext/src/ui/Chat.css create mode 100644 vnext/src/ui/Chat.js create mode 100644 vnext/src/ui/Contact.js create mode 100644 vnext/src/ui/Contacts.js create mode 100644 vnext/src/ui/Feeds.js create mode 100644 vnext/src/ui/Header.js create mode 100644 vnext/src/ui/Icon.js create mode 100644 vnext/src/ui/Input.css create mode 100644 vnext/src/ui/Input.js create mode 100644 vnext/src/ui/LoginButton.js create mode 100644 vnext/src/ui/Message.css create mode 100644 vnext/src/ui/Message.js create mode 100644 vnext/src/ui/MessageInput.js create mode 100644 vnext/src/ui/Modal.css create mode 100644 vnext/src/ui/Modal.js create mode 100644 vnext/src/ui/NavigationIcon.css create mode 100644 vnext/src/ui/NavigationIcon.js create mode 100644 vnext/src/ui/PM.js create mode 100644 vnext/src/ui/Post.js create mode 100644 vnext/src/ui/SearchBox.js create mode 100644 vnext/src/ui/Settings.js create mode 100644 vnext/src/ui/Spinner.js create mode 100644 vnext/src/ui/Thread.js create mode 100644 vnext/src/ui/Types.js create mode 100644 vnext/src/ui/UploadButton.js create mode 100644 vnext/src/ui/UserInfo.css create mode 100644 vnext/src/ui/UserInfo.js create mode 100644 vnext/src/ui/Users.js create mode 100644 vnext/src/ui/__tests__/Avatar.test.js create mode 100644 vnext/src/ui/__tests__/LoginButton.test.js create mode 100644 vnext/src/ui/__tests__/MessageInput-test.js create mode 100644 vnext/src/ui/__tests__/UserLink.test.js create mode 100644 vnext/src/ui/__tests__/__snapshots__/Avatar.test.js.snap create mode 100644 vnext/src/ui/__tests__/__snapshots__/LoginButton.test.js.snap create mode 100644 vnext/src/ui/__tests__/__snapshots__/UserLink.test.js.snap (limited to 'vnext/src/ui') diff --git a/vnext/src/ui/Avatar.css b/vnext/src/ui/Avatar.css new file mode 100644 index 00000000..7bdb3115 --- /dev/null +++ b/vnext/src/ui/Avatar.css @@ -0,0 +1,27 @@ +.Avatar { + display: flex; + width: 100%; +} +.msg-avatar { + max-height: 48px; + margin-right: 10px; + max-width: 48px; +} +.msg-avatar img { + max-height: 48px; + vertical-align: top; + max-width: 48px; +} + +.info-avatar img { + max-height: 24px; + max-width: 24px; + padding: 6px; + vertical-align: middle; +} + +@media screen and (min-width: 450px) { + .Avatar { + width: 300px; + } +} diff --git a/vnext/src/ui/Avatar.js b/vnext/src/ui/Avatar.js new file mode 100644 index 00000000..dda5449f --- /dev/null +++ b/vnext/src/ui/Avatar.js @@ -0,0 +1,44 @@ +import React, { memo } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; + +import { UserType } from './Types'; + +import Icon from './Icon'; + +import './Avatar.css'; + +function Avatar({ user, style, link, children}) { + return ( +
+
+ { + user.uname ? + + {user.avatar ? + {`${user.uname}`} + : } + + : + } +
+
+ + + {user.uname} + + + {children} +
+
+ ); +} + +export default memo(Avatar); + +Avatar.propTypes = { + user: UserType, + link: PropTypes.string, + style: PropTypes.object, + children: PropTypes.node +}; diff --git a/vnext/src/ui/Button.css b/vnext/src/ui/Button.css new file mode 100644 index 00000000..2acb87be --- /dev/null +++ b/vnext/src/ui/Button.css @@ -0,0 +1,13 @@ +.Button { + background: #fff; + border: 1px solid #eee; + color: #888; + cursor: pointer; + display: inline-block; + margin: 5px; + padding: 4px 10px; +} +.Button:hover { + background: #f8f8f8; + border-bottom: 1px solid #ff339a; +} diff --git a/vnext/src/ui/Button.js b/vnext/src/ui/Button.js new file mode 100644 index 00000000..18cab0a7 --- /dev/null +++ b/vnext/src/ui/Button.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import './Button.css'; + +function Button(props) { + return ( + + + + + + ); +} + +LoginButton.propTypes = { + title: PropTypes.string.isRequired, + onAuth: PropTypes.func.isRequired +}; + +const socialButtonsStyle = { + display: 'flex', + justifyContent: 'space-evenly', + padding: '4px' +}; + +const facebookButtonStyle = { + color: '#fff', + padding: '2px 14px', + background: '#3b5998' +}; + +const vkButtonStyle = { + color: '#fff', + padding: '2px 14px', + background: '#4c75a3' +}; + +export default LoginButton; diff --git a/vnext/src/ui/Message.css b/vnext/src/ui/Message.css new file mode 100644 index 00000000..18d3d0d5 --- /dev/null +++ b/vnext/src/ui/Message.css @@ -0,0 +1,210 @@ +.msg-cont .ir { + padding: 12px; +} +.msg-cont .ir img { + max-width: 100%; + height: auto; +} +.msg-cont > .h, +.msg-cont .msg-header { + padding: 12px; +} +.msg-cont > .l { + border-top: 1px solid #eee; + display: flex; + align-items: center; + justify-content: space-around; + background: #fdfdfe; +} +.msg-cont > .l a { + color: #88958d; + margin-right: 15px; + font-size: small; +} +.msg-tags { + color: #88958d; + margin-top: 12px; + min-height: 1px; +} +.badge, +.msg-tags > a { + color: #88958d; + display: inline-block; + font-size: small; +} +.msgthread { + margin-bottom: 0; +} +.msg-cont { + background: #FFF; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + margin-bottom: 12px; +} +.reply-new .msg-cont { + border-right: 5px solid #0C0; +} +.msg-ts { + font-size: small; + vertical-align: top; +} +.msg-ts, +.msg-ts > a { + color: #88958d; +} +.msg-txt { + margin: 0 0 12px; + padding: 12px; + word-wrap: break-word; + overflow-wrap: break-word; +} +q:before, +q:after { + content: ""; +} +q, +blockquote { + border-left: 3px solid #CCC; + color: #666; + display: block; + margin: 10px 0 10px 10px; + padding-left: 10px; + word-break: break-word; +} +.msg-media { + text-align: center; +} +.msg-links { + color: #88958d; + font-size: small; + margin: 5px 0 0 0; + padding: 12px; +} +.msg-comments { + color: #88958d; + font-size: small; + margin-top: 10px; + overflow: hidden; + text-indent: 10px; +} +.ta-wrapper { + border: 1px solid #DDD; + display: flex; + flex-grow: 1; +} +.msg-comment { + display: flex; + margin-top: 10px; +} +.msg-comment-hidden { + display: none; +} +.msg-comment textarea { + border: 0; + flex-grow: 1; + outline: none !important; + padding: 4px; + resize: vertical; + vertical-align: top; +} +.attach-photo { + cursor: pointer; +} +.attach-photo-active { + color: green; +} +.msg-comment input { + align-self: flex-start; + background: #EEE; + border: 1px solid #CCC; + color: #999; + margin: 0 0 0 6px; + position: sticky; + top: 70px; + vertical-align: top; + width: 50px; +} +.msg-recomms { + color: #88958d; + background: #fdfdfe; + font-size: small; + margin-bottom: 10px; + padding: 6px; + border-bottom: 1px solid #eee; + overflow: hidden; + text-indent: 10px; +} +.msg-summary, +.msg-summary a { + color: #88958d; + font-size: small; + padding: 12px; + text-align: right; +} +#replies .msg-txt, +#private-messages .msg-txt { + margin: 0; +} +.title2 { + background: #fff; + margin: 20px 0; + padding: 10px 20px; +} +.title2-right { + float: right; + line-height: 24px; +} +#content .title2 h2 { + font-size: x-large; + margin: 0; +} + +.embedContainer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + padding: 12px; + margin: 30px -3px 15px -3px; +} +.embedContainer > * { + box-sizing: border-box; + flex-grow: 1; + margin: 3px; + min-width: 49%; +} +.embedContainer > .compact { + flex-grow: 0; +} +.embedContainer .picture img { + display: block; +} +.embedContainer img, +.embedContainer video { + max-width: 100%; + max-height: 80vh; +} +.embedContainer > .audio, +.embedContainer > .youtube { + min-width: 90%; +} +.embedContainer audio { + width: 100%; +} +.embedContainer iframe { + overflow: hidden; + resize: vertical; + display: block; +} +.msg-cont .nsfw .embedContainer img, +.msg-cont .nsfw .embedContainer video, +.msg-cont .nsfw .embedContainer iframe, +.msg-cont .nsfw .ir img { + opacity: 0.1; +} +.msg-cont .nsfw .embedContainer img:hover, +.msg-cont .nsfw .embedContainer video:hover, +.msg-cont .nsfw .embedContainer iframe:hover, +.msg-cont .nsfw .ir img:hover { + opacity: 1; +} diff --git a/vnext/src/ui/Message.js b/vnext/src/ui/Message.js new file mode 100644 index 00000000..eb008bfe --- /dev/null +++ b/vnext/src/ui/Message.js @@ -0,0 +1,142 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import moment from 'moment'; + +import { UserType, MessageType } from './Types'; +import Icon from './Icon'; +import Avatar from './Avatar'; +import { UserLink } from './UserInfo'; + +import { format, embedUrls } from '../utils/embed'; + +import './Message.css'; + +export default function Message({ data, visitor, children, ...rest }) { + const isCode = (data.tags || []).indexOf('code') >= 0; + const likesSummary = data.likes ? `${data.likes}` : 'Recommend'; + const commentsSummary = data.replies ? `${data.replies}` : 'Comment'; + const embedRef = useRef(); + const msgRef = useRef(); + useEffect(() => { + if (msgRef.current) { + embedUrls(msgRef.current.querySelectorAll('a'), embedRef.current); + if (!embedRef.current.hasChildNodes()) { + embedRef.current.style.display = 'none'; + } + } + }, []); + return ( +
+ +
+ +
+ + + +
+
+ +
+ { + data.body && +
+ +
+ } + { + data.photo && +
+ + + +
+ } +
+ + {children} +
+ ); +} + +function MessageContainer({ isCode, data }) { + return isCode ? (
) : (

); +} + +function Tags({ data, user, ...rest }) { + return data.length > 0 && ( +

+ { + data.map(tag => { + return ({tag}); + }).reduce((prev, curr) => [prev, ', ', curr]) + } +
+ ); +} + +const TagsList = React.memo(Tags); + +function Recommends({ forMessage, ...rest }) { + const { likes, recommendations } = forMessage; + return recommendations && recommendations.length > 0 && ( +
{'♡ by '} + { + recommendations.map(it => ( + + )).reduce((prev, curr) => [prev, ', ', curr]) + } + { + likes > recommendations.length && ( and {likes - recommendations.length} others) + } +
+ ) || null; +} + +const Recommendations = React.memo(Recommends); + +Message.propTypes = { + data: MessageType, + visitor: UserType.isRequired, + children: PropTypes.node +}; + +MessageContainer.propTypes = { + isCode: PropTypes.bool.isRequired, + data: PropTypes.object.isRequired +}; + +Tags.propTypes = { + user: UserType.isRequired, + data: PropTypes.array +}; + diff --git a/vnext/src/ui/MessageInput.js b/vnext/src/ui/MessageInput.js new file mode 100644 index 00000000..e4988d59 --- /dev/null +++ b/vnext/src/ui/MessageInput.js @@ -0,0 +1,107 @@ +import React, { useState, useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; + +import { useFormState } from 'react-use-form-state'; + +import { MessageType } from './Types'; + +import Icon from './Icon'; +import Button from './Button'; + +import UploadButton from './UploadButton'; + +export default function MessageInput({ text, data, rows, children, onSend }) { + let textareaRef = useRef(); + let fileinput = useRef(); + + let updateFocus = () => { + const isDesktop = window.matchMedia('(min-width: 62.5rem)'); + if (isDesktop.matches) { + textareaRef.current.focus(); + } + }; + useEffect(() => { + textareaRef.current.value = text || ''; + updateFocus(); + }, [text]); + + let handleCtrlEnter = (event) => { + if (event.ctrlKey && (event.charCode == 10 || event.charCode == 13)) { + onSubmit({}); + } + }; + let textChanged = (event) => { + const el = textareaRef.current; + const offset = el.offsetHeight - el.clientHeight; + const height = el.scrollHeight + offset; + el.style.height = `${height + offset}px`; + }; + const [attach, setAttach] = useState(''); + const [formState, { textarea }] = useFormState(); + let uploadValueChanged = (attach) => { + setAttach(attach); + }; + let onSubmit = (event) => { + if (event.preventDefault) { + event.preventDefault(); + } + const input = fileinput.current; + onSend({ + mid: data.mid, + rid: data.rid || 0, + body: formState.values.body, + attach: attach ? input.files[0] : '', + to: data.to || {} + }); + setAttach(''); + formState.clearField('body'); + textareaRef.current.style.height = ''; + updateFocus(); + }; + return ( +
+
+