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 all 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;
57 changes: 57 additions & 0 deletions src/components/button-votes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

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

import cx from 'classnames';
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;
vertical?: boolean;
}

const VoteButtons = ({
points, onVote, myVote, vertical
}: CommentVotesProps) => (
<div className={cx('flex justify-center items-center', {
'flex-col': !vertical,
'flex-row': vertical
})}
>
<VoteButton label="Upvote arrow" onClick={() => onVote(myVote === 1 ? 0 : 1)}>
<Icon IconType={ArrowUpIcon} size={ICON_SIZE.SMALL} textClassName={`${myVote === 1 ? 'text-blue-600' : 'text-gray-700 dark:text-white'} hover:text-blue-400 dark:hover:text-blue-400`} />
</VoteButton>
<PaleBodyText title="Vote score" className="text-xs">{points}</PaleBodyText>
<VoteButton label="Downvote arrow" onClick={() => onVote(myVote === -1 ? 0 : -1)}>
<Icon IconType={ArrowDownIcon} size={ICON_SIZE.SMALL} textClassName={`${myVote === -1 ? 'text-red-600' : 'text-gray-700 dark:text-white'} dark:hover:text-red-400 hover:text-red-400`} />
</VoteButton>
</div>
);

export default VoteButtons;
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 Icon, { ICON_SIZE } from '../icon';
import LinkButton from '../button-link';
import VoteButtons from '../button-votes';

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">
<VoteButtons points={votes.score} onVote={handleVote} myVote={myVote} vertical />
<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>

);
};
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>
);
};
13 changes: 9 additions & 4 deletions 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,20 +23,23 @@ 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;
textClassName?: string;
isInteractable?: boolean;
}

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