Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Comments part 1 #34

Merged
merged 13 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/app/user/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ModeratesList, ModeratesProps } from '@/components/moderates-list';
import { PersonDetailSelection } from '@/components/person-comments-posts';
import sublinksClient from '@/utils/client';

import { CommentView, PostView } from 'sublinks-js-client';
import * as testData from '../../../../test-person-data.json';

interface UserViewProps {
Expand All @@ -27,8 +28,7 @@ const User = async ({ params: { username } }: UserViewProps) => {
avatar, banner, bio, display_name: displayName, name
}, is_admin: isAdmin
} = userData.person_view;
const { posts, moderates } = userData;

const { posts, comments, moderates } = userData;
return (
<div>
<div className="mb-12">
Expand All @@ -52,7 +52,10 @@ const User = async ({ params: { username } }: UserViewProps) => {
</MainCard>
</div>
<MainCard>
<PersonDetailSelection postViews={posts} />
<PersonDetailSelection
postViews={posts as PostView[]}
commentViews={comments as CommentView[]}
/>
</MainCard>
</div>
);
Expand Down
21 changes: 21 additions & 0 deletions src/components/button-link/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import cx from 'classnames';

interface LinkButtonProps {
children: React.ReactNode;
type: 'button' | 'submit' | 'reset';
id?: string;
ariaLabel?: string;
className?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

const LinkButton = ({
ariaLabel, children, className, id, type, onClick
}: LinkButtonProps) => (
// Rule doesn't like type being a variable even though types force it to be a valid option
// eslint-disable-next-line react/button-has-type
<button type={type} aria-label={ariaLabel} id={id} onClick={onClick} className={cx('text-black dark:text-white hover:text-gray-400 dark:hover:text-gray-400', className)}>{children}</button>
);

export default LinkButton;
7 changes: 4 additions & 3 deletions src/components/button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import cx from 'classnames';
interface ButtonProps {
children: React.ReactNode;
type: 'button' | 'submit' | 'reset';
id: string;
id?: string;
ariaLabel?: string;
className?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

const Button = ({
ariaLabel, children, className, id, type
ariaLabel, children, className, id, type, onClick
}: ButtonProps) => (
// Rule doesn't like type being a variable even though types force it to be a valid option
// eslint-disable-next-line react/button-has-type
<button type={type} aria-label={ariaLabel} id={id} className={cx('bg-brand dark:bg-brand-dark hover:bg-opacity-90 rounded-md px-23 py-12', className)}>{children}</button>
<button type={type} aria-label={ariaLabel} id={id} onClick={onClick} className={cx('bg-brand dark:bg-brand-dark hover:bg-opacity-90 rounded-md px-23 py-12', className)}>{children}</button>
);

export default Button;
55 changes: 55 additions & 0 deletions src/components/comment-actions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import React from 'react';

import { CommentAggregates } from 'sublinks-js-client';
import { EllipsisVerticalIcon } from '@heroicons/react/24/outline';
import sublinksClient from '@/utils/client';
import CommentVotes from '../comment-votes';
import Icon, { ICON_SIZE } from '../icon';
import LinkButton from '../button-link';

interface CommentActionProps {
votes: CommentAggregates;
myVote?: number;
}

export const CommentAction = ({
votes,
myVote
}: CommentActionProps) => {
const handleVote = async (vote: number) => {
if (!process.env.SUBLINKS_API_BASE_URL) return;

await sublinksClient().likeComment({
comment_id: votes.comment_id,
score: vote
});
};

return (
<div className="flex relative">
<CommentVotes points={votes.score} onVote={handleVote} myVote={myVote} />
<LinkButton
className="py-0 px-2 text-xs"
ariaLabel="Reply To Comment Button"
type="button"
onClick={e => {
e.preventDefault();
}}
>
Reply
</LinkButton>
<LinkButton
className="py-0 px-2 ml-4 text-xs"
ariaLabel="More Comment Actions Button"
type="button"
onClick={e => {
e.preventDefault();
}}
>
<Icon className="text-black dark:text-white hover:text-gray-400 dark:hover:text-gray-400" IconType={EllipsisVerticalIcon} size={ICON_SIZE.VERYSMALL} isInteractable />
</LinkButton>
</div>
);
};
25 changes: 25 additions & 0 deletions src/components/comment-feed/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

import { CommentView } from 'sublinks-js-client';
import { H1 } from '../text';
import { CommentCard } from '../comment';

interface CommentFeedProps {
data: CommentView[];
}

const CommentFeed = ({ data: comments }: CommentFeedProps) => (
<div className="bg-primary dark:bg-primary-dark flex flex-col gap-8">
{comments && comments.length > 0 ? comments.map(commentData => (
<div key={commentData.comment.ap_id} className="mb-8">
<CommentCard
comment={commentData.comment}
creator={commentData.creator}
counts={commentData.counts}
/>
</div>
)) : (<H1 className="text-center">No comments available!</H1>)}
</div>
);

export default CommentFeed;
57 changes: 57 additions & 0 deletions src/components/comment-header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';

import {
Person
} from 'sublinks-js-client';
import Image from 'next/image';
import Link from 'next/link';
import { HomeIcon, LinkIcon } from '@heroicons/react/24/outline';
import {
BodyText,
H2
} from '../text';
import Icon, { ICON_SIZE } from '../icon';

interface CommentHeaderProps {
creator: Person;
href: string;
apId: string;
createdAt: string;
updatedAt: string | undefined;
}

export const CommentHeader = ({
creator,
href,
apId,
createdAt,
updatedAt
}: CommentHeaderProps) => {
const { display_name: authorDisplayName, name: authorName } = creator;

const showName = authorDisplayName || authorName;

const updated = updatedAt !== undefined;

return (
<div className="relative flex items-center">
<Link href={href} className="flex items-center">
<Image
alt={`Avatar from: ${showName}`}
src={creator.avatar || '/logo.png'}
width={20}
height={20}
className="rounded-md"
/>
<H2 className="text-left h-full ml-4">{showName}</H2>
<Icon IconType={LinkIcon} size={ICON_SIZE.VERYSMALL} className="ml-4 pl-4 h-full" />
</Link>
<Link href={apId}>
<Icon IconType={HomeIcon} size={ICON_SIZE.VERYSMALL} className="ml-4p pl-4 h-full" />
</Link>
<BodyText className="text-right h-full ml-4 text-sm">{new Date(updated ? updatedAt : createdAt).toLocaleString()}</BodyText>
{updated && <BodyText className="text-left h-full ml-4 text-xs -translate-y-4">Edited</BodyText>}
</div>

);
};
49 changes: 49 additions & 0 deletions src/components/comment-votes/index.tsx
Pdzly marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import React from 'react';
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/20/solid';

import Icon, { ICON_SIZE } from '../icon';
import { PaleBodyText } from '../text';

interface VoteButtonProps {
children: React.ReactNode,
label: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}

const VoteButton = ({ children, label, onClick }: VoteButtonProps) => (
<button
type="button"
onClick={e => {
e.preventDefault();
if (onClick) {
onClick(e);
}
}}
aria-label={label}
className="hover:bg-secondary dark:hover:bg-secondary-dark rounded-full p-2"
>
{children}
</button>
);

interface CommentVotesProps {
points: number;
onVote: (score: number) => void;
myVote?: number;
}

const CommentVotes = ({ points, onVote, myVote }: CommentVotesProps) => (
<div className="flex flex-row justify-center items-center">
<VoteButton label="Upvote arrow" onClick={() => onVote(myVote === 1 ? 0 : 1)}>
<Icon IconType={ArrowUpIcon} size={ICON_SIZE.SMALL} SvgClassName={`hover:fill-blue-400 ${myVote === 1 && 'fill-blue-600'}`} />
</VoteButton>
<PaleBodyText title="Post score" className="text-xs">{points}</PaleBodyText>
<VoteButton label="Downvote arrow" onClick={() => onVote(myVote === -1 ? 0 : -1)}>
<Icon IconType={ArrowDownIcon} size={ICON_SIZE.SMALL} SvgClassName={`hover:fill-red-400 ${myVote === -1 && 'fill-red-600'}`} />
</VoteButton>
</div>
);

export default CommentVotes;
56 changes: 56 additions & 0 deletions src/components/comment/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';

import {
Person, Comment, CommentAggregates
} from 'sublinks-js-client';
import Markdown from 'react-markdown';
import { CommentHeader } from '../comment-header';
import { CommentAction } from '../comment-actions';

interface CommentCardProps {
comment: Comment;
creator: Person;
counts: CommentAggregates;
myVote?: number;
}

export const CommentCard = ({
comment,
creator,
counts,
myVote
}: CommentCardProps) => {
const {
id, content, ap_id: apId, published, updated
} = comment;

// @todo: Make our own URLs until Sublinks API connects URLs to all entities
const commentHref = `/comment/${id}`;

return (
<div key={id}>
<div className="mb-4 relative">
<div className="w-full mb-4">
<div className="mb-4">
<CommentHeader
href={commentHref}
apId={apId}
createdAt={published}
updatedAt={updated}
creator={creator}
/>
</div>
<div className="pl-8">
<Markdown className="text-gray-600 dark:text-gray-200 text-sm line-clamp-2">
{content}
Pdzly marked this conversation as resolved.
Show resolved Hide resolved
</Markdown>
</div>
</div>
<div className="items-center relative flex">
<CommentAction votes={counts} myVote={myVote} />
</div>
</div>
<div className="border-b-2 border-secondary dark:border-secondary-dark" />
</div>
);
};
7 changes: 6 additions & 1 deletion src/components/icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import React from 'react';
import cx from 'classnames';

export const ICON_SIZE = {
VERYSMALL: 0,
SMALL: 1,
MEDIUM: 2
};

const wrapperSizeClassMap = {
[ICON_SIZE.VERYSMALL]: 'h-20 w-20',
[ICON_SIZE.SMALL]: 'h-24 w-24',
[ICON_SIZE.MEDIUM]: 'h-32 w-32'
};
Expand All @@ -21,22 +23,25 @@ interface IconProps {
titleId?: string;
height?: string | number;
width?: string | number;
className?: string;
}>;
size: typeof ICON_SIZE[keyof typeof ICON_SIZE];
title?: string;
titleId?: string;
className?: string;
SvgClassName?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SvgClassName isn't needed. We can use className instead to change the styling of the SVG icon. By using text- classes rather than fill-. Like we're doing in PostVotes.

Copy link
Member Author

@Pdzly Pdzly Dec 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you notice that it doesnt work? Those are svg icons not font icons!

text- is good for font icons in our case its svg.

And i would keep it as so, when we want to change the div in the background.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works just fine for me.

<Icon IconType={ArrowUpIcon} size={ICON_SIZE.SMALL} className={`hover:text-blue-400 ${myVote === 1 && 'text-blue-600'}`} />

And i would keep it as so, when we want to change the div in the background.

If we want to change the background color we'll just use a bg- class. It won't interfere with the SVG.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm then on firefox this doesnt work. ( or just on my side )

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep it does NOT work on firefox and its probably just a sideeffect of chromes svg implementation.

Because of this i would propose to stay what i done for firefox compatibility

isInteractable?: boolean;
}

const Icon = ({
IconType, size, title, titleId, className, isInteractable
IconType, size, title, titleId, className, SvgClassName, isInteractable
}: IconProps) => (
<div className={cx(wrapperSizeClassMap[size], 'text-gray-700 dark:text-white', {
'hover:text-brand dark:hover:text-brand-dark': isInteractable
}, className)}
>
<IconType
className={SvgClassName}
title={title}
titleId={titleId}
height={iconSizeClassMap[size]}
Expand Down
Loading
Loading