Browse Source

Add profile view

develop
Apostolos Fanakis 4 years ago
parent
commit
810143f6d6
  1. 8
      packages/concordia-app/public/locales/en/translation.json
  2. 5
      packages/concordia-app/src/Routes.jsx
  3. 46
      packages/concordia-app/src/components/CustomLoadingTabPane.jsx
  4. 22
      packages/concordia-app/src/constants/ProfileTabs.js
  5. 155
      packages/concordia-app/src/views/Profile/GeneralTab/index.jsx
  6. 6
      packages/concordia-app/src/views/Profile/GeneralTab/styles.css
  7. 111
      packages/concordia-app/src/views/Profile/index.jsx

8
packages/concordia-app/public/locales/en/translation.json

@ -39,5 +39,11 @@
"topic.create.form.subject.field.placeholder": "Subject", "topic.create.form.subject.field.placeholder": "Subject",
"topic.list.row.author.date": "Created by {{author}}, {{timeAgo}}", "topic.list.row.author.date": "Created by {{author}}, {{timeAgo}}",
"topic.list.row.number.of.replies": "{{numberOfReplies}} replies", "topic.list.row.number.of.replies": "{{numberOfReplies}} replies",
"topic.list.row.topic.id": "#{{id}}" "topic.list.row.topic.id": "#{{id}}",
"custom.loading.tab.pane.default.generic.message": "Magic in the background",
"profile.user.has.no.topics.header.message": "{{user}} has created no topics yet",
"profile.user.has.no.posts.header.message": "{{user}} has not posted yet",
"profile.general.tab.title": "General",
"profile.topics.tab.title": "Topics",
"profile.posts.tab.title": "Posts"
} }

5
packages/concordia-app/src/Routes.jsx

@ -44,6 +44,11 @@ const routesConfig = [
path: '/topics/:id(\\bnew\\b|\\d+)', path: '/topics/:id(\\bnew\\b|\\d+)',
component: lazy(() => import('./views/Topic')), component: lazy(() => import('./views/Topic')),
}, },
{
exact: true,
path: ['/users/:id', '/profiles/:id', '/profile'],
component: lazy(() => import('./views/Profile')),
},
{ {
component: () => <Redirect to="/404" />, component: () => <Redirect to="/404" />,
}, },

46
packages/concordia-app/src/components/CustomLoadingTabPane.jsx

@ -0,0 +1,46 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import {
Dimmer, Loader, Placeholder, Segment, Tab,
} from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
const CustomLoadingTabPane = (props) => {
const { loading, loadingMessage, children } = props;
const { t } = useTranslation();
return useMemo(() => {
if (loading) {
return (
<Tab.Pane>
<Dimmer active inverted>
<Loader inverted>
{loadingMessage !== undefined
? loadingMessage
: t('custom.loading.tab.pane.default.generic.message')}
</Loader>
</Dimmer>
<Placeholder fluid>
<Placeholder.Line length="very long" />
<Placeholder.Line length="medium" />
<Placeholder.Line length="long" />
</Placeholder>
</Tab.Pane>
);
}
return (
<Tab.Pane>
{children}
</Tab.Pane>
);
}, [children, loading, loadingMessage, t]);
};
CustomLoadingTabPane.propTypes = {
loading: PropTypes.bool,
loadingMessage: PropTypes.string,
children: PropTypes.element.isRequired,
};
export default CustomLoadingTabPane;

22
packages/concordia-app/src/constants/ProfileTabs.js

@ -0,0 +1,22 @@
export const GENERAL_TAB = {
id: 'general-tab',
intl_display_name_id: 'profile.general.tab.title',
};
export const TOPICS_TAB = {
id: 'topics-tab',
intl_display_name_id: 'profile.topics.tab.title',
};
export const POSTS_TAB = {
id: 'posts-tab',
intl_display_name_id: 'profile.posts.tab.title',
};
const profileTabs = [
GENERAL_TAB,
TOPICS_TAB,
POSTS_TAB,
];
export default profileTabs;

155
packages/concordia-app/src/views/Profile/GeneralTab/index.jsx

@ -0,0 +1,155 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Icon, Image, Placeholder, Table,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { useDispatch, useSelector } from 'react-redux';
import determineKVAddress from '../../../utils/orbitUtils';
import databases, { USER_DATABASE } from '../../../constants/OrbitDatabases';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import { breeze } from '../../../redux/store';
import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../constants/UserDatabaseKeys';
import './styles.css';
const { orbit } = breeze;
const GeneralTab = (props) => {
const {
profileAddress, username, numberOfTopics, numberOfPosts, userRegistrationTimestamp,
} = props;
const [userInfoOrbitAddress, setUserInfoOrbitAddress] = useState(null);
const [userTopicsOrbitAddress, setUserTopicsOrbitAddress] = useState(null);
const [userPostsOrbitAddress, setUserPostsOrbitAddress] = useState(null);
const [profileMeta, setProfileMeta] = useState(null);
const users = useSelector((state) => state.orbitData.users);
const dispatch = useDispatch();
useEffect(() => {
if (profileAddress) {
Promise
.all(databases
.map((database) => determineKVAddress({
orbit,
dbName: database.address,
userAddress: profileAddress,
})))
.then((values) => {
const [userOrbitAddress, topicsOrbitAddress, postsOrbitAddress] = values;
setUserInfoOrbitAddress(userOrbitAddress);
setUserTopicsOrbitAddress(topicsOrbitAddress);
setUserPostsOrbitAddress(postsOrbitAddress);
const userFound = users
.find((user) => user.id === userOrbitAddress);
if (userFound) {
setProfileMeta(userFound);
} else {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: USER_DATABASE,
userAddress: userOrbitAddress,
});
}
}).catch((error) => {
console.error('Error during determination of key-value DB address:', error);
});
}
}, [dispatch, profileAddress, users]);
const authorAvatar = useMemo(() => (profileMeta !== null && profileMeta[USER_PROFILE_PICTURE]
? (
<Image
className="general-tab-profile-picture"
centered
size="tiny"
src={profileMeta[USER_PROFILE_PICTURE]}
/>
)
: (
<Icon
name="user circle"
size="massive"
inverted
color="black"
verticalAlign="middle"
/>
)), [profileMeta]);
return useMemo(() => (
<Table basic="very" singleLine>
<Table.Body>
<Table.Row textAlign="center">
<Table.Cell colSpan="3">{authorAvatar}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Username:</strong></Table.Cell>
<Table.Cell>{username}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Account address:</strong></Table.Cell>
<Table.Cell>{profileAddress}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>UserDB:</strong></Table.Cell>
<Table.Cell>
{userInfoOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>TopicsDB:</strong></Table.Cell>
<Table.Cell>
{userTopicsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>PostsDB:</strong></Table.Cell>
<Table.Cell>
{userPostsOrbitAddress || (<Placeholder><Placeholder.Line /></Placeholder>)}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Number of topics created:</strong></Table.Cell>
<Table.Cell>
{numberOfTopics}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Number of posts:</strong></Table.Cell>
<Table.Cell>
{numberOfPosts}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Number of posts:</strong></Table.Cell>
<Table.Cell>
{profileMeta !== null && profileMeta[USER_LOCATION]
? profileMeta[USER_LOCATION]
: <Placeholder><Placeholder.Line length="medium" /></Placeholder>}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>Member since:</strong></Table.Cell>
<Table.Cell>
{moment(userRegistrationTimestamp * 1000).format('dddd, MMMM Do YYYY, h:mm:ss A')}
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
), [
authorAvatar, numberOfPosts, numberOfTopics, profileAddress, profileMeta, userInfoOrbitAddress,
userPostsOrbitAddress, userRegistrationTimestamp, userTopicsOrbitAddress, username,
]);
};
GeneralTab.propTypes = {
profileAddress: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
numberOfTopics: PropTypes.number.isRequired,
numberOfPosts: PropTypes.number.isRequired,
userRegistrationTimestamp: PropTypes.string.isRequired,
};
export default GeneralTab;

6
packages/concordia-app/src/views/Profile/GeneralTab/styles.css

@ -0,0 +1,6 @@
.general-tab-profile-picture {
max-width: 112px;
max-height: 112px;
margin: 0;
vertical-align: middle;
}

111
packages/concordia-app/src/views/Profile/index.jsx

@ -0,0 +1,111 @@
import React, {
memo, useEffect, useMemo, useState,
} from 'react';
import { Container, Header, Tab } from 'semantic-ui-react';
import { useSelector } from 'react-redux';
import { useHistory, useRouteMatch } from 'react-router';
import { useTranslation } from 'react-i18next';
import { drizzle } from '../../redux/store';
import { FORUM_CONTRACT } from '../../constants/ContractNames';
import CustomLoadingTabPane from '../../components/CustomLoadingTabPane';
import TopicList from '../../components/TopicList';
import PostList from '../../components/PostList';
import GeneralTab from './GeneralTab';
import { GENERAL_TAB, POSTS_TAB, TOPICS_TAB } from '../../constants/ProfileTabs';
const { contracts: { [FORUM_CONTRACT]: { methods: { getUser } } } } = drizzle;
const Profile = () => {
const [userCallHash, setUserCallHash] = useState('');
const getUserResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getUser);
const [profileAddress, setProfileAddress] = useState();
const [username, setUsername] = useState(null);
const [userTopicIds, setUserTopicIds] = useState([]);
const [userPostIds, setUserPostIds] = useState([]);
const [userRegistrationTimestamp, setUserRegistrationTimestamp] = useState(null);
const [loading, setLoading] = useState(true);
const self = useSelector((state) => state.user);
const { t } = useTranslation();
const match = useRouteMatch();
const history = useHistory();
useEffect(() => {
if (history.location.pathname === '/profile') {
if (self.hasSignedUp) {
setProfileAddress(self.address);
} else {
history.push('/');
}
} else {
const { id: userAddress } = match.params;
setProfileAddress(userAddress);
}
}, [history, match.params, self.address, self.hasSignedUp]);
useEffect(() => {
if (profileAddress) {
setUserCallHash(getUser.cacheCall(profileAddress));
}
}, [profileAddress]);
useEffect(() => {
if (getUserResults[userCallHash] !== undefined && getUserResults[userCallHash].value) {
const [lUsername, topicIds, postIds, registrationTimestamp] = getUserResults[userCallHash].value;
setUsername(lUsername);
setUserTopicIds(topicIds.map((userTopicId) => parseInt(userTopicId, 10)));
setUserPostIds(postIds.map((userPostId) => parseInt(userPostId, 10)));
setUserRegistrationTimestamp(registrationTimestamp);
setLoading(false);
}
}, [getUserResults, userCallHash]);
const generalTab = useMemo(() => (loading
? null
: (
<GeneralTab
profileAddress={profileAddress}
username={username}
numberOfTopics={userTopicIds.length}
numberOfPosts={userPostIds.length}
userRegistrationTimestamp={userRegistrationTimestamp}
/>
)), [loading, profileAddress, userPostIds.length, userRegistrationTimestamp, userTopicIds.length, username]);
const topicsTab = useMemo(() => (userTopicIds.length > 0
? (<TopicList topicIds={userTopicIds} />)
: (
<Header textAlign="center" as="h2">
{t('profile.user.has.no.topics.header.message', { user: username })}
</Header>
)
), [t, userTopicIds, username]);
const postsTab = useMemo(() => (userPostIds.length > 0
? (<PostList postIds={userPostIds} />)
: (
<Header textAlign="center" as="h2">
{t('profile.user.has.no.posts.header.message', { user: username })}
</Header>
)), [t, userPostIds, username]);
const panes = useMemo(() => {
const generalTabPane = (<CustomLoadingTabPane loading={loading}>{generalTab}</CustomLoadingTabPane>);
const topicsTabPane = (<CustomLoadingTabPane loading={loading}>{topicsTab}</CustomLoadingTabPane>);
const postsTabPane = (<CustomLoadingTabPane loading={loading}>{postsTab}</CustomLoadingTabPane>);
return ([
{ menuItem: t(GENERAL_TAB.intl_display_name_id), render: () => generalTabPane },
{ menuItem: t(TOPICS_TAB.intl_display_name_id), render: () => topicsTabPane },
{ menuItem: t(POSTS_TAB.intl_display_name_id), render: () => postsTabPane },
]);
}, [generalTab, loading, postsTab, t, topicsTab]);
return useMemo(() => (
<Container id="home-container" textAlign="center">
<Tab panes={panes} />
</Container>
), [panes]);
};
export default memo(Profile);
Loading…
Cancel
Save