Skip to content

Commit ee72de8

Browse files
authored
Merge pull request #9 from aseerkt/feat/followers
Feature: Follow
2 parents ac7550c + 911bbeb commit ee72de8

39 files changed

+1170
-251
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
- `node v^14.5`
2222
- `yarn v^1.22.5` - required for file upload feature to work on backend
2323
- `git` - version control
24-
- PostgreSQL - for storing data
25-
- Redis / MongoDB - for session authentication
24+
- PostgreSQL - Database
25+
- Cloudinary Account
2626

2727
### Installing
2828

@@ -49,19 +49,19 @@
4949

5050
## Roadmap
5151

52-
- [x] JWT authentication
52+
- [x] JWT cookie based authentication
5353
- [x] Upload images to cloudinary
5454
- [x] Add, like or unlike post
5555
- [x] Comment on post
5656
- [x] Edit Profile Photo with Image Crop
5757
- [x] Edit Profile Credentials
5858
- [x] Post pagination
5959
- [x] Edit / Delete post
60+
- [ ] Follow / Unfollow Feature
61+
- [ ] Follow Suggestions
6062
- [ ] Post Feed
61-
- [ ] Smiley support for caption and comments
6263
- [ ] Hashtag support
63-
- [ ] Reduce image quality/size
64-
- [ ] Follow Suggestions
64+
- [ ] Smiley support for caption and comments
6565
- [ ] Mention poeples in caption and comments
6666
- [ ] Notifications
6767

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Profile from './routes/Profile';
1414
import SinglePost from './routes/SinglePost';
1515
import MessageProvider from './context/MessageContext';
1616
import Message from './components-ui/Message';
17+
import Explore from './routes/Explore';
1718

1819
const App: React.FC = () => {
1920
const { loading, error } = useMeQuery({ fetchPolicy: 'network-only' });
@@ -31,6 +32,7 @@ const App: React.FC = () => {
3132
<div className='pb-10'>
3233
<Switch>
3334
<PrivateRoute exact path='/' component={Posts} />
35+
<PrivateRoute exact path='/explore' component={Explore} />
3436
<Route exact path='/login' component={Login} />
3537
<Route exact path='/register' component={Register} />
3638
<PrivateRoute exact path='/p/:postId' component={SinglePost} />

client/src/components-ui/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const Button: React.FC<ButtonProps> = ({
3535
className
3636
)}
3737
>
38-
{isLoading && <ButtonLoader />}
38+
{isLoading && <ButtonLoader color={color} />}
3939
<div>{children}</div>
4040
</button>
4141
);

client/src/components-ui/ButtonLoader.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const ButtonLoader = () => (
1+
export const ButtonLoader: React.FC<{ color?: 'light' | 'dark' }> = ({
2+
color,
3+
}) => (
24
<svg
35
className='w-5 h-5 mr-3 text-white animate-spin'
46
xmlns='http://www.w3.org/2000/svg'
@@ -10,12 +12,12 @@ export const ButtonLoader = () => (
1012
cx='12'
1113
cy='12'
1214
r='10'
13-
stroke='currentColor'
15+
stroke={color === 'dark' ? 'currentColor' : 'gray'}
1416
strokeWidth='4'
1517
></circle>
1618
<path
1719
className='opacity-75'
18-
fill='currentColor'
20+
stroke={color === 'dark' ? 'currentColor' : 'gray'}
1921
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
2022
></path>
2123
</svg>

client/src/components-ui/Spinner.tsx

Lines changed: 141 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,151 @@
1-
import React from 'react';
1+
import cn from 'classnames';
22

33
interface SpinnerProps {
44
size?: 'small' | 'default';
55
}
66

77
const Spinner: React.FC<SpinnerProps> = ({ size = 'default' }) => {
8-
const sizeFactor = size === 'default' ? '100px' : '50px';
8+
const sizeFactor = size === 'default' ? 'w-32 h-32' : 'w-9 h-9';
9+
910
return (
10-
<img
11-
src='/loader.gif'
12-
alt='Loading...'
13-
height={sizeFactor}
14-
width={sizeFactor}
15-
className='m-auto max-h-96'
16-
/>
11+
<svg
12+
aria-label='Loading...'
13+
className={cn('block mx-auto animate-spin ', sizeFactor)}
14+
viewBox='0 0 100 100'
15+
>
16+
<rect
17+
fill='#555'
18+
height='6'
19+
opacity='0'
20+
rx='3'
21+
ry='3'
22+
transform='rotate(-90 50 50)'
23+
width='25'
24+
x='72'
25+
y='47'
26+
></rect>
27+
<rect
28+
fill='#555'
29+
height='6'
30+
opacity='0.08333333333333333'
31+
rx='3'
32+
ry='3'
33+
transform='rotate(-60 50 50)'
34+
width='25'
35+
x='72'
36+
y='47'
37+
></rect>
38+
<rect
39+
fill='#555'
40+
height='6'
41+
opacity='0.16666666666666666'
42+
rx='3'
43+
ry='3'
44+
transform='rotate(-30 50 50)'
45+
width='25'
46+
x='72'
47+
y='47'
48+
></rect>
49+
<rect
50+
fill='#555'
51+
height='6'
52+
opacity='0.25'
53+
rx='3'
54+
ry='3'
55+
transform='rotate(0 50 50)'
56+
width='25'
57+
x='72'
58+
y='47'
59+
></rect>
60+
<rect
61+
fill='#555'
62+
height='6'
63+
opacity='0.3333333333333333'
64+
rx='3'
65+
ry='3'
66+
transform='rotate(30 50 50)'
67+
width='25'
68+
x='72'
69+
y='47'
70+
></rect>
71+
<rect
72+
fill='#555'
73+
height='6'
74+
opacity='0.4166666666666667'
75+
rx='3'
76+
ry='3'
77+
transform='rotate(60 50 50)'
78+
width='25'
79+
x='72'
80+
y='47'
81+
></rect>
82+
<rect
83+
fill='#555'
84+
height='6'
85+
opacity='0.5'
86+
rx='3'
87+
ry='3'
88+
transform='rotate(90 50 50)'
89+
width='25'
90+
x='72'
91+
y='47'
92+
></rect>
93+
<rect
94+
fill='#555'
95+
height='6'
96+
opacity='0.5833333333333334'
97+
rx='3'
98+
ry='3'
99+
transform='rotate(120 50 50)'
100+
width='25'
101+
x='72'
102+
y='47'
103+
></rect>
104+
<rect
105+
fill='#555'
106+
height='6'
107+
opacity='0.6666666666666666'
108+
rx='3'
109+
ry='3'
110+
transform='rotate(150 50 50)'
111+
width='25'
112+
x='72'
113+
y='47'
114+
></rect>
115+
<rect
116+
fill='#555'
117+
height='6'
118+
opacity='0.75'
119+
rx='3'
120+
ry='3'
121+
transform='rotate(180 50 50)'
122+
width='25'
123+
x='72'
124+
y='47'
125+
></rect>
126+
<rect
127+
fill='#555'
128+
height='6'
129+
opacity='0.8333333333333334'
130+
rx='3'
131+
ry='3'
132+
transform='rotate(210 50 50)'
133+
width='25'
134+
x='72'
135+
y='47'
136+
></rect>
137+
<rect
138+
fill='#555'
139+
height='6'
140+
opacity='0.9166666666666666'
141+
rx='3'
142+
ry='3'
143+
transform='rotate(240 50 50)'
144+
width='25'
145+
x='72'
146+
y='47'
147+
></rect>
148+
</svg>
17149
);
18150
};
19151

client/src/components/AddPost.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const AddPost: React.FC<AddPostProps> = ({ className, setIsOpen }) => {
3232
update: (cache, { data }) => {
3333
if (data?.addPost.post) {
3434
cache.evict({ fieldName: 'getPosts' });
35+
cache.evict({ fieldName: 'getExplorePosts' });
3536
}
3637
},
3738
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { FaTimes } from 'react-icons/fa';
2+
import Modal from '../components-ui/Modal';
3+
import Spinner from '../components-ui/Spinner';
4+
import { FollowEnum, useGetFollowsQuery, User } from '../generated/graphql';
5+
import SuggestionItem from './SuggestionItem';
6+
7+
interface FollowModalProps {
8+
isOpen: boolean;
9+
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
10+
username: string;
11+
modalTitle: FollowEnum;
12+
}
13+
14+
const FollowModal: React.FC<FollowModalProps> = ({
15+
isOpen,
16+
setIsOpen,
17+
modalTitle,
18+
username,
19+
}) => {
20+
const { data, loading } = useGetFollowsQuery({
21+
variables: { selector: modalTitle, username },
22+
fetchPolicy: 'network-only',
23+
});
24+
if (loading && isOpen) {
25+
return <Spinner />;
26+
}
27+
28+
return (
29+
<Modal isOpen={isOpen} setIsOpen={setIsOpen}>
30+
<main className='bg-white rounded-md'>
31+
<header className='flex items-center justify-between p-3 border-b-2'>
32+
<div> </div>
33+
<h1 className='font-bold'>{modalTitle}</h1>
34+
<FaTimes
35+
className='ml-auto cursor-pointer'
36+
size='1.2em'
37+
onClick={() => setIsOpen(false)}
38+
/>
39+
</header>
40+
<div className='px-3'>
41+
{data?.getFollows.map((u) => (
42+
<SuggestionItem darkFollowButton s={u as User} />
43+
))}
44+
{!data ||
45+
!data.getFollows ||
46+
(data.getFollows.length === 0 && (
47+
<p className='py-3'>You have no {modalTitle.toLowerCase()}</p>
48+
))}
49+
</div>
50+
</main>
51+
</Modal>
52+
);
53+
};
54+
55+
export default FollowModal;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const FollowModalItem = () => {
2+
return <div></div>;
3+
};
4+
5+
export default FollowModalItem;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from 'react';
2+
import { FollowEnum } from '../generated/graphql';
3+
import FollowModal from './FollowModal';
4+
5+
interface FollowersModalProps {
6+
followersCount: number;
7+
username: string;
8+
}
9+
10+
const FollowersModal: React.FC<FollowersModalProps> = ({
11+
followersCount,
12+
username,
13+
}) => {
14+
const [isOpen, setIsOpen] = useState(false);
15+
16+
return (
17+
<>
18+
<button
19+
onClick={() => {
20+
if (followersCount > 0) setIsOpen(!isOpen);
21+
}}
22+
className='md:flex'
23+
>
24+
<strong className='md:mr-1'>{followersCount}</strong>
25+
<p className='text-gray-600 md:text-black'>followers</p>
26+
</button>
27+
<FollowModal
28+
modalTitle={FollowEnum.Followers}
29+
isOpen={isOpen}
30+
setIsOpen={setIsOpen}
31+
username={username}
32+
/>
33+
</>
34+
);
35+
};
36+
37+
export default FollowersModal;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from 'react';
2+
import { FollowEnum } from '../generated/graphql';
3+
import FollowModal from './FollowModal';
4+
5+
interface FollowingsModalProps {
6+
followingsCount: number;
7+
username: string;
8+
}
9+
10+
const FollowingsModal: React.FC<FollowingsModalProps> = ({
11+
followingsCount,
12+
username,
13+
}) => {
14+
const [isOpen, setIsOpen] = useState(false);
15+
16+
return (
17+
<>
18+
<button
19+
onClick={() => {
20+
if (followingsCount > 0) setIsOpen(!isOpen);
21+
}}
22+
className='md:flex'
23+
>
24+
<strong className='md:mr-1'>{followingsCount}</strong>
25+
<p className='text-gray-600 md:text-black'>following</p>
26+
</button>
27+
<FollowModal
28+
modalTitle={FollowEnum.Followings}
29+
isOpen={isOpen}
30+
setIsOpen={setIsOpen}
31+
username={username}
32+
/>
33+
</>
34+
);
35+
};
36+
37+
export default FollowingsModal;

0 commit comments

Comments
 (0)