Progress
This commit is contained in:
@@ -6,6 +6,7 @@ import LinkList from './LinkList';
|
|||||||
import CreateLink from './CreateLink'
|
import CreateLink from './CreateLink'
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Login from './Login'
|
import Login from './Login'
|
||||||
|
import Search from './Search';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@@ -21,6 +22,7 @@ const App = () => {
|
|||||||
component={CreateLink}
|
component={CreateLink}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/login" component={Login} />
|
<Route exact path="/login" component={Login} />
|
||||||
|
<Route exact path="/search" component={Search} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useMutation, gql } from '@apollo/client';
|
import { useMutation, gql } from '@apollo/client';
|
||||||
|
import { FEED_QUERY } from './LinkList';
|
||||||
|
|
||||||
const CREATE_LINK_MUTATION = gql`
|
const CREATE_LINK_MUTATION = gql`
|
||||||
mutation PostMutation(
|
mutation PostMutation(
|
||||||
@@ -28,9 +29,38 @@ const CreateLink = () => {
|
|||||||
variables: {
|
variables: {
|
||||||
description: formState.description,
|
description: formState.description,
|
||||||
url: formState.url
|
url: formState.url
|
||||||
|
},
|
||||||
|
update: (cache, { data: { post } }) => {
|
||||||
|
const take = LINKS_PER_PAGE;
|
||||||
|
const skip = 0;
|
||||||
|
const orderBy = { createdAt: 'desc' };
|
||||||
|
|
||||||
|
const data = cache.readQuery({
|
||||||
|
query: FEED_QUERY,
|
||||||
|
variables: {
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
orderBy
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cache.writeQuery({
|
||||||
|
query: FEED_QUERY,
|
||||||
|
data: {
|
||||||
|
feed: {
|
||||||
|
links: [post, ...data.feed.links]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
orderBy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCompleted: () => history.push('/new/1')
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ const Header = () => {
|
|||||||
top
|
top
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ml1">|</div>
|
<div className="ml1">|</div>
|
||||||
<Link
|
<Link to="/search" className="ml1 no-underline black">
|
||||||
to="/search"
|
|
||||||
className="ml1 no-underline black"
|
|
||||||
>
|
|
||||||
search
|
search
|
||||||
</Link>
|
</Link>
|
||||||
{authToken && (
|
{authToken && (
|
||||||
|
|||||||
@@ -1,14 +1,104 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useMutation, gql } from '@apollo/client';
|
||||||
|
import { AUTH_TOKEN, LINKS_PER_PAGE } from '../constants';
|
||||||
|
import { timeDifferenceForDate } from '../utils'
|
||||||
|
|
||||||
|
const VOTE_MUTATION = gql`
|
||||||
|
mutation VoteMutation($linkId: ID!) {
|
||||||
|
vote(linkId: $linkId) {
|
||||||
|
id
|
||||||
|
link {
|
||||||
|
id
|
||||||
|
votes {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const Link = (props) => {
|
const Link = (props) => {
|
||||||
const { link } = props;
|
const { link } = props;
|
||||||
|
const authToken = localStorage.getItem(AUTH_TOKEN);
|
||||||
|
const take = LINKS_PER_PAGE;
|
||||||
|
const skip = 0;
|
||||||
|
const orderBy = { createdAt: 'desc' };
|
||||||
|
|
||||||
|
const [vote] = useMutation(VOTE_MUTATION, {
|
||||||
|
variables: {
|
||||||
|
linkId: link.id
|
||||||
|
},
|
||||||
|
update(cache, { data: { vote } }) {
|
||||||
|
const { feed } = cache.readQuery({
|
||||||
|
query: FEED_QUERY
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedLinks = feed.links.map((feedLink) => {
|
||||||
|
if (feedLink.id === link.id) {
|
||||||
|
return {
|
||||||
|
...feedLink,
|
||||||
|
votes: [...feedLink.votes, vote]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return feedLink;
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.writeQuery({
|
||||||
|
query: FEED_QUERY,
|
||||||
|
data: {
|
||||||
|
feed: {
|
||||||
|
links: updatedLinks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex mt2 items-start">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="gray">{props.index + 1}.</span>
|
||||||
|
{authToken && (
|
||||||
|
<div
|
||||||
|
className="ml1 gray f11"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={vote}
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ml1">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
{link.description} ({link.url})
|
||||||
{link.id} - {link.description} ({link.url})
|
</div>
|
||||||
|
{authToken && (
|
||||||
|
<div className="f6 lh-copy gray">
|
||||||
|
{link.votes.length} votes | by{' '}
|
||||||
|
{link.postedBy ? link.postedBy.name : 'Unknown'}{' '}
|
||||||
|
{timeDifferenceForDate(link.createdAt)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const Link = (props) => {
|
||||||
|
// const { link } = props;
|
||||||
|
// return (
|
||||||
|
// <div>
|
||||||
|
// <div>
|
||||||
|
// {link.id} - {link.description} ({link.url})
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
export default Link;
|
export default Link;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from './Link';
|
import Link from './Link';
|
||||||
|
import { LINKS_PER_PAGE } from '../constants';
|
||||||
import { useQuery, gql } from '@apollo/client';
|
import { useQuery, gql } from '@apollo/client';
|
||||||
|
|
||||||
const FEED_QUERY = gql`
|
export const FEED_QUERY = gql`
|
||||||
{
|
{
|
||||||
feed {
|
feed {
|
||||||
id
|
id
|
||||||
@@ -12,6 +12,16 @@ const FEED_QUERY = gql`
|
|||||||
createdAt
|
createdAt
|
||||||
url
|
url
|
||||||
description
|
description
|
||||||
|
postedBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
votes {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,8 +34,8 @@ const LinkList = () => {
|
|||||||
<div>
|
<div>
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
{data.feed.links.map((link) => (
|
{data.feed.links.map((link, index) => (
|
||||||
<Link key={link.id} link={link} />
|
<Link key={link.id} link={link} index={index} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
62
client/src/components/Search.js
Normal file
62
client/src/components/Search.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
import Link from './Link';
|
||||||
|
|
||||||
|
const FEED_SEARCH_QUERY = gql`
|
||||||
|
query FeedSearchQuery($filter: String!) {
|
||||||
|
feed(filter: $filter) {
|
||||||
|
id
|
||||||
|
links {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
description
|
||||||
|
createdAt
|
||||||
|
postedBy {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
votes {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
const [executeSearch, { data }] = useLazyQuery(
|
||||||
|
FEED_SEARCH_QUERY
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
Search
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
executeSearch({
|
||||||
|
variables: { filter: searchFilter }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{data &&
|
||||||
|
data.feed.links.map((link, index) => (
|
||||||
|
<Link key={link.id} link={link} index={index} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
||||||
@@ -1 +1,2 @@
|
|||||||
export const AUTH_TOKEN = 'auth-token';
|
export const AUTH_TOKEN = 'auth-token';
|
||||||
|
export const LINKS_PER_PAGE = 3;
|
||||||
45
client/src/utils.js
Normal file
45
client/src/utils.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
function timeDifference(current, previous) {
|
||||||
|
const milliSecondsPerMinute = 60 * 1000;
|
||||||
|
const milliSecondsPerHour = milliSecondsPerMinute * 60;
|
||||||
|
const milliSecondsPerDay = milliSecondsPerHour * 24;
|
||||||
|
const milliSecondsPerMonth = milliSecondsPerDay * 30;
|
||||||
|
const milliSecondsPerYear = milliSecondsPerDay * 365;
|
||||||
|
|
||||||
|
const elapsed = current - previous;
|
||||||
|
|
||||||
|
if (elapsed < milliSecondsPerMinute / 3) {
|
||||||
|
return 'just now';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed < milliSecondsPerMinute) {
|
||||||
|
return 'less than 1 min ago';
|
||||||
|
} else if (elapsed < milliSecondsPerHour) {
|
||||||
|
return (
|
||||||
|
Math.round(elapsed / milliSecondsPerMinute) +
|
||||||
|
' min ago'
|
||||||
|
);
|
||||||
|
} else if (elapsed < milliSecondsPerDay) {
|
||||||
|
return (
|
||||||
|
Math.round(elapsed / milliSecondsPerHour) + ' h ago'
|
||||||
|
);
|
||||||
|
} else if (elapsed < milliSecondsPerMonth) {
|
||||||
|
return (
|
||||||
|
Math.round(elapsed / milliSecondsPerDay) + ' days ago'
|
||||||
|
);
|
||||||
|
} else if (elapsed < milliSecondsPerYear) {
|
||||||
|
return (
|
||||||
|
Math.round(elapsed / milliSecondsPerMonth) + ' mo ago'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
Math.round(elapsed / milliSecondsPerYear) +
|
||||||
|
' years ago'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeDifferenceForDate(date) {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const updated = new Date(date).getTime();
|
||||||
|
return timeDifference(now, updated);
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user