Skip to content

Commit

Permalink
adding comments counter, imporve styles
Browse files Browse the repository at this point in the history
  • Loading branch information
Bardala committed Nov 19, 2023
1 parent 97702ca commit 0219811
Show file tree
Hide file tree
Showing 22 changed files with 206 additions and 117 deletions.
13 changes: 13 additions & 0 deletions backend/src/controllers/blog.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
ERROR,
Like,
LikedUser,
NumOfCommentsReq,
NumOfCommentsRes,
RemoveLikeReq,
RemoveLikeRes,
SpaceBlogsReq,
Expand All @@ -43,6 +45,7 @@ export interface blogController {
unLikeBlog: HandlerWithParams<{ blogId: string }, RemoveLikeReq, RemoveLikeRes>;
getBlogLikes: HandlerWithParams<{ blogId: string }, BlogLikesReq, BlogLikesRes>;
getBlogLikesList: HandlerWithParams<{ blogId: string }, BlogLikesListReq, BlogLikesListRes>;
getNumOfComments: HandlerWithParams<{ blogId: string }, NumOfCommentsReq, NumOfCommentsRes>;
}

export class BlogController implements blogController {
Expand All @@ -51,6 +54,16 @@ export class BlogController implements blogController {
constructor(db: DataStoreDao) {
this.db = db;
}

getNumOfComments: HandlerWithParams<{ blogId: string }, NumOfCommentsReq, NumOfCommentsRes> =
async (req, res) => {
const blogId = req.params.blogId;
if (!blogId) return res.status(400).send({ error: ERROR.PARAMS_MISSING });

const numOfComments = await this.db.getNumOfComments(blogId);
return res.status(200).send({ numOfComments });
};

likeBlog: HandlerWithParams<{ blogId: string }, CreateLikeReq, CreateLikeRes> = async (
req,
res
Expand Down
3 changes: 2 additions & 1 deletion backend/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AllUnReadMsgsReq,
AllUnReadMsgsRes,
DefaultSpaceId,
ERROR,
FollowUserReq,
FollowUserRes,
Expand Down Expand Up @@ -188,7 +189,7 @@ export class UserController implements userController {
};

await this.db.createUser(user);
await this.db.addMember({ spaceId: this.db.defaultSpcId, memberId: user.id, isAdmin: false });
await this.db.addMember({ spaceId: DefaultSpaceId, memberId: user.id, isAdmin: false });

return res.send({ jwt: createToken(user.id), username: user.username, id: user.id });
};
Expand Down
1 change: 1 addition & 0 deletions backend/src/dataStore/dao/Blog.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface BlogDao {
deleteBlog(blogId: string): Promise<void>;

getComments(blogId: string): Promise<CommentWithUser[]>;
getNumOfComments(blogId: string): Promise<number>;
deleteComments(blogId: string): Promise<void>;
blogLikes(blogId: string, userId: string): Promise<{ likes: number; isLiked: boolean }>;
blogLikesList(blogId: string): Promise<LikedUser[]>;
Expand Down
1 change: 0 additions & 1 deletion backend/src/dataStore/dao/Space.dao.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Blog, ChatMessage, LastReadMsg, Space, SpaceMember } from '@nest/shared';

export interface SpaceDao {
defaultSpcId: string;
createSpace(space: Space): Promise<void>;
updateSpace(space: Space): Promise<Space | undefined>;
getSpace(spaceId: string): Promise<Space | undefined>;
Expand Down
54 changes: 29 additions & 25 deletions backend/src/dataStore/sql/SqlDataStore.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,37 @@ import { DataStoreDao } from '..';

export class SqlDataStore implements DataStoreDao {
private pool!: Pool;
public defaultSpcId = '1';
private prodProps: mysql.PoolOptions = {
host: process.env.MY_SQL_DB_HOST,
user: process.env.MY_SQL_DB_USER,
database: process.env.MY_SQL_DB_DATABASE,
password: process.env.MY_SQL_DB_PASSWORD,
socketPath: process.env.MY_SQL_DB_SOCKET_PATH,
};
private devProps: mysql.PoolOptions = {
host: process.env.MY_SQL_DB_HOST,
user: process.env.MY_SQL_DB_USER,
database: process.env.MY_SQL_DB_DATABASE,
password: process.env.MY_SQL_DB_PASSWORD,
};

async runDB() {
const prodProps: mysql.PoolOptions = {
host: process.env.MY_SQL_DB_HOST,
user: process.env.MY_SQL_DB_USER,
database: process.env.MY_SQL_DB_DATABASE,
password: process.env.MY_SQL_DB_PASSWORD,
socketPath: process.env.MY_SQL_DB_SOCKET_PATH,
};

const devProps: mysql.PoolOptions = {
host: process.env.MY_SQL_DB_HOST,
user: process.env.MY_SQL_DB_USER,
database: process.env.MY_SQL_DB_DATABASE,
password: process.env.MY_SQL_DB_PASSWORD,
};

this.pool = mysql
.createPool({
...(process.env.NODE_ENV === 'prod' ? prodProps : devProps),
...(process.env.NODE_ENV === 'prod' ? this.prodProps : this.devProps),
})
.promise();

return this;
}

async getNumOfComments(blogId: string): Promise<number> {
const query = `
SELECT COUNT(*) AS comments FROM comments WHERE blogId=?
`;
const [rows] = await this.pool.query<RowDataPacket[]>(query, blogId);
return rows[0]['comments'] as Promise<number>;
}

async getLastMsgId(spaceId: string): Promise<string> {
const query = `
SELECT id FROM chat WHERE spaceId=? ORDER BY timestamp DESC LIMIT 1
Expand Down Expand Up @@ -411,13 +415,13 @@ export class SqlDataStore implements DataStoreDao {
async getUserCard(userId: string, cardOwnerId: string): Promise<UserCard | undefined> {
const [rows] = await this.pool.query<RowDataPacket[]>(
`
SELECT users.id, users.username, users.email, users.timestamp,
(SELECT COUNT(*) FROM follows WHERE follows.followingId = users.id) AS followersNum,
(SELECT COUNT(*) FROM follows WHERE follows.followerId = users.id) AS followingNum,
(SELECT COUNT(*) FROM follows WHERE follows.followerId = ? AND follows.followingId = users.id) AS isFollowing
FROM users
WHERE users.id = ?
`,
SELECT users.id, users.username, users.email, users.timestamp,
(SELECT COUNT(*) FROM follows WHERE follows.followingId = users.id) AS followersNum,
(SELECT COUNT(*) FROM follows WHERE follows.followerId = users.id) AS followingNum,
(SELECT COUNT(*) FROM follows WHERE follows.followerId = ? AND follows.followingId = users.id) AS isFollowing
FROM users
WHERE users.id = ?
`,
[userId, cardOwnerId]
);

Expand Down
1 change: 1 addition & 0 deletions backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { errorHandler } from './middleware/errorHandler';
app.get(ENDPOINT.GET_BLOG_LIKES_LIST, requireAuth, asyncHandler(blog.getBlogLikesList));
app.post(ENDPOINT.LIKE_BLOG, requireAuth, checkEmptyInput, asyncHandler(blog.likeBlog));
app.delete(ENDPOINT.UNLIKE_BLOG, requireAuth, asyncHandler(blog.unLikeBlog));
app.get(ENDPOINT.NUM_OF_COMMENTS, requireAuth, asyncHandler(blog.getNumOfComments));
// app.get(ENDPOINT.TEST_INFINITE_SCROLL, requireAuth, asyncHandler(blog.testInfiniteScrollBlogs));

// *Comment
Expand Down
45 changes: 22 additions & 23 deletions frontend/src/components/BlogIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { Blog } from '@nest/shared';
import { formatDistanceToNow } from 'date-fns';
import { Blog, DefaultSpaceId } from '@nest/shared';
import { LiaCommentSolid } from 'react-icons/lia';
import { RiGroup2Fill } from 'react-icons/ri';
import { Link } from 'react-router-dom';

import { isArabic } from '../utils/assists';
import { useCommCounts } from '../hooks/useBlog';
import { formatTimeShort, isArabic } from '../utils/assists';
import { LikeBlogButton } from './LikeBlogButton';
import { MyMarkdown } from './MyMarkdown';

export const BlogIcon: React.FC<{ post: Blog }> = ({ post }) => {
const { numOfComments } = useCommCounts(post.id!);

return (
<div className="blog-preview" key={post.id}>
<div className="blog-content">
<Link to={`/b/${post.id}`} className="blog-link">
<div className="blog-header">
<h2>{post.title}</h2>
</div>
</Link>
<div className="blog-header">
<Link to={`/b/${post.id}`} className="blog-link">
<h2 className={isArabic(post.title) ? 'arabic' : ''}>{post.title}</h2>
</Link>
</div>

<div className="blog-meta">
<Link to={`/u/${post.userId}`} className="author-link">
Expand All @@ -23,28 +27,23 @@ export const BlogIcon: React.FC<{ post: Blog }> = ({ post }) => {

<LikeBlogButton post={post} />

{post.spaceId !== '1' && (
<Link to={`/space/${post?.spaceId}`} className="space-link">
Spaced
<span className="comms-count">
{numOfComments.data?.numOfComments} <LiaCommentSolid size={20} />
</span>

{post.spaceId !== DefaultSpaceId && (
<Link to={`/space/${post?.spaceId}`} className="space-link" title="Spaced">
<RiGroup2Fill size={20} />
</Link>
)}

<time className="created-at" dateTime={String(post.timestamp)}>
{formatDistanceToNow(new Date(post.timestamp as number))}
{formatTimeShort(new Date(post.timestamp!))}
</time>
</div>

<div className="blog-excerpt">
<div
style={{
direction: isArabic(post.content) ? 'rtl' : 'ltr',
textAlign: isArabic(post.content) ? 'right' : 'left',
unicodeBidi: isArabic(post.content) ? 'embed' : 'normal',
whiteSpace: 'pre-wrap',
}}
>
<MyMarkdown markdown={post.content} />
</div>
{/* <p className={isArabic(post.content) ? 'arabic' : ''}>{post.content} </p> */}
<MyMarkdown markdown={post.content} />
</div>
</div>
</div>
Expand Down
46 changes: 25 additions & 21 deletions frontend/src/components/MyMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,32 @@ import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vs } from 'react-syntax-highlighter/dist/esm/styles/prism';

import { isArabic } from '../utils/assists';

export const MyMarkdown: FC<{ markdown: string }> = ({ markdown }) => {
return (
<ReactMarkdown
children={markdown}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
{...props}
children={String(children).replace(/\n$/, '')}
style={vs}
language={match[1]}
PreTag="div"
/>
) : (
<code {...props} className={className}>
{children}
</code>
);
},
}}
/>
<div className={isArabic(markdown) ? 'arabic' : ''}>
<ReactMarkdown
children={markdown}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
{...props}
children={String(children).replace(/\n$/, '')}
style={vs}
language={match[1]}
PreTag="div"
/>
) : (
<code {...props} className={className}>
{children}
</code>
);
},
}}
/>
</div>
);
};
2 changes: 1 addition & 1 deletion frontend/src/components/NotificationMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { TbMessageCirclePlus } from 'react-icons/tb';
import { Link } from 'react-router-dom';

Expand Down
37 changes: 23 additions & 14 deletions frontend/src/components/ShortForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { useParams } from 'react-router-dom';

import { useCreateShort } from '../hooks/useBlog';
import { isArabic } from '../utils/assists';
import { MyMarkdown } from './MyMarkdown';

export const ShortForm = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
// const remaining = ShortLength - content.length;
const [preview, setPreview] = useState(false);
const id = useParams().id || DefaultSpaceId;
const { createShortMutation } = useCreateShort(id, title, content);

Expand All @@ -29,6 +30,15 @@ export const ShortForm = () => {
<>
{createShortMutation.isError && <p className="error">{createShortMutation.error.message}</p>}
<form className="create-blog-form" onSubmit={handleSubmit}>
<div className="button-container">
<button type="submit" disabled={createShortMutation.isLoading}>
Create
</button>
<button type="button" onClick={() => setPreview(!preview)}>
Preview
</button>
</div>

<input
placeholder="Title"
type="text"
Expand All @@ -37,20 +47,19 @@ export const ShortForm = () => {
value={title}
onChange={e => setTitle(e.target.value)}
/>
<textarea
placeholder="Content"
name="content"
id="content"
value={content}
onChange={e => setContent(e.target.value)}
// maxLength={ShortLength}
style={{ direction: isArabic(content) ? 'rtl' : 'ltr' }}
/>
{/* <i className="remaining-char">{remaining} remaining characters</i> */}
{preview ? (
<MyMarkdown markdown={content} />
) : (
<textarea
placeholder="Content"
name="content"
id="content"
value={content}
onChange={e => setContent(e.target.value)}
style={{ direction: isArabic(content) ? 'rtl' : 'ltr' }}
/>
)}

<button type="submit" disabled={createShortMutation.isLoading}>
Create
</button>
{createShortMutation.isLoading && <p>Creating...</p>}
{createShortMutation.isSuccess && <p className="success">Created successfully!</p>}
</form>
Expand Down
Loading

0 comments on commit 0219811

Please sign in to comment.