Skip to content

Commit 4c3b8bc

Browse files
authored
Merge branch 'main' into storybook
2 parents 31a7033 + 6d0120c commit 4c3b8bc

30 files changed

+2776
-1164
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,19 @@
2424
"@editorjs/table": "^2.0.2",
2525
"@firstcontributions/editorjs-codeflask": "https://github.com/firstcontributions/editorjs-codeflask",
2626
"@react-icons/all-files": "^4.1.0",
27+
"dayjs": "^1.11.5",
2728
"form-data": "^4.0.0",
2829
"formidable": "^2.0.1",
2930
"install": "^0.13.0",
3031
"md5": "^2.3.0",
3132
"multiparty": "^4.2.3",
3233
"next": "^12.1.6",
33-
"node-fetch": "^3.2.0",
34+
"node-fetch": "^3.2.10",
3435
"react": "^18.2.0",
3536
"react-dom": "^18.2.0",
3637
"react-error-boundary": "^3.1.4",
3738
"react-relay": "^13.0.2",
39+
"react-virtuoso": "^2.18.0",
3840
"relay-nextjs": "^0.6.0",
3941
"relay-runtime": "^13.0.2",
4042
"simple-oauth2": "^4.3.0",
@@ -80,6 +82,7 @@
8082
"relay-config": "^12.0.1",
8183
"relay-test-utils": "^14.0.0",
8284
"storybook-css-modules-preset": "^1.1.1",
85+
"tailwind-scrollbar": "^2.0.1",
8386
"tailwindcss": "^3.0.23",
8487
"typescript": "4.5.5"
8588
},

schema/schema.graphql

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ schema {
66
scalar Time
77

88
enum SortOrder {
9-
asc,
109
desc,
10+
asc,
1111
}
1212

1313
type Query {
@@ -18,7 +18,7 @@ type Query {
1818
# The ID of an object
1919
id: ID!
2020
): Node
21-
feeds(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): StoriesConnection!
21+
feeds(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: StorySortBy): StoriesConnection!
2222
user(handle: String): User!
2323
}
2424

@@ -55,6 +55,7 @@ type BadgeEdge {
5555
}
5656

5757
enum BadgeSortBy {
58+
points,
5859
time_created,
5960
}
6061

@@ -170,11 +171,11 @@ input ReputationInput {
170171

171172
type Story implements Node {
172173
abstractContent: String!
173-
comments(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): CommentsConnection!
174+
comments(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: CommentSortBy): CommentsConnection!
174175
contentJson: String!
175176
createdBy: User!
176177
id: ID!
177-
reactions(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): ReactionsConnection!
178+
reactions(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: ReactionSortBy): ReactionsConnection!
178179
thumbnail: String!
179180
timeCreated: Time!
180181
timeUpdated: Time!
@@ -191,6 +192,7 @@ input StoryInput {
191192
}
192193
input UpdateStoryInput {
193194
id: ID!
195+
title: String
194196
}
195197

196198
type StoriesConnection {
@@ -211,17 +213,17 @@ enum StorySortBy {
211213

212214
type User implements Node {
213215
avatar: String!
214-
badges(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): BadgesConnection!
216+
badges(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: BadgeSortBy): BadgesConnection!
215217
bio: String!
216218
gitContributionStats: GitContributionStats!
217219
handle: String!
218220
id: ID!
219-
issuesFromLastRepo(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): IssuesConnection!
220-
issuesFromOtherRecentRepos(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): IssuesConnection!
221+
issuesFromLastRepo(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: IssueSortBy): IssuesConnection!
222+
issuesFromOtherRecentRepos(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: IssueSortBy): IssuesConnection!
221223
name: String!
222-
relevantIssues(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): IssuesConnection!
224+
relevantIssues(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: IssueSortBy): IssuesConnection!
223225
reputation: Reputation!
224-
stories(first: Int, last: Int, after: String, before: String, sortBy: String, sortOrder: SortOrder): StoriesConnection!
226+
stories(first: Int, last: Int, after: String, before: String, sortOrder: SortOrder, sortBy: StorySortBy): StoriesConnection!
225227
timeCreated: Time!
226228
timeUpdated: Time!
227229
}

src/components/Container.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ReactNode } from "react";
1+
import React, { ReactNode } from 'react'
22

3-
export default function Container ({children}: any) {
4-
return (
5-
<div className="p-16">
6-
{children}
7-
</div>
8-
)
9-
}
3+
type ContainerProps = {
4+
children: ReactNode
5+
}
6+
7+
export default function Container({ children }: ContainerProps) {
8+
return <div className="p-16">{children}</div>
9+
}

src/components/ExpandingTextarea.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react'
2+
3+
const MIN_TEXTAREA_HEIGHT = 32
4+
5+
type ExpandingTextareaProps = {
6+
value: string
7+
setValue: (value: string) => void
8+
className?: string
9+
placeholder?: string
10+
}
11+
12+
export default function ExpandingTextarea({
13+
value,
14+
setValue,
15+
className,
16+
placeholder,
17+
}: ExpandingTextareaProps) {
18+
const textareaRef = React.useRef<null | HTMLTextAreaElement>(null)
19+
const onChange = (event: React.ChangeEvent<HTMLTextAreaElement>) =>
20+
setValue(event.target.value)
21+
22+
React.useLayoutEffect(() => {
23+
if (textareaRef.current) {
24+
textareaRef.current.style.height = 'inherit'
25+
// Set height
26+
textareaRef.current.style.height = `${Math.max(
27+
textareaRef.current.scrollHeight,
28+
MIN_TEXTAREA_HEIGHT
29+
)}px`
30+
}
31+
// Reset height - important to shrink on delete
32+
}, [value])
33+
34+
return (
35+
<textarea
36+
className={className}
37+
onChange={onChange}
38+
placeholder={placeholder}
39+
ref={textareaRef}
40+
style={{
41+
minHeight: MIN_TEXTAREA_HEIGHT,
42+
resize: 'none',
43+
}}
44+
value={value}
45+
/>
46+
)
47+
}

src/components/comment/Comment.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import dayjs from 'dayjs'
2+
import relativeTime from 'dayjs/plugin/relativeTime'
3+
import { graphql, useFragment } from 'react-relay'
4+
5+
type CommentProps = {
6+
comment: any
7+
}
8+
9+
const Comment = ({ comment }: CommentProps) => {
10+
const data = useFragment(
11+
graphql`
12+
fragment Comment_node on Comment {
13+
abstractContent
14+
contentJson
15+
createdBy {
16+
avatar
17+
name
18+
handle
19+
}
20+
id
21+
timeCreated
22+
}
23+
`,
24+
comment
25+
)
26+
27+
dayjs.extend(relativeTime)
28+
29+
return (
30+
<div className="flex flex-col p-j ">
31+
<div className="flex flex-row">
32+
<img
33+
className="w-8 h-8 rounded-full"
34+
src={data.createdBy.avatar}
35+
alt={data.createdBy.name}
36+
/>
37+
<div className="ml-2 flex flex-col">
38+
<div className="flex flex-row text-sm">
39+
<span className="mr-4">{data.createdBy.handle}</span>
40+
<span>{`${dayjs(data.timeCreated).toNow(true)} ago`}</span>
41+
</div>
42+
<span className="text-l dark:text-gray-300">{data.contentJson}</span>
43+
</div>
44+
</div>
45+
</div>
46+
)
47+
}
48+
49+
export default Comment

src/components/comment/Comments.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { graphql, usePaginationFragment } from 'react-relay'
2+
import { Comments_story$key } from '../../queries/__generated__/Comments_story.graphql'
3+
import NewComment from './NewComment'
4+
import Comment from './Comment'
5+
6+
type CommentsProps = {
7+
story: Comments_story$key
8+
}
9+
10+
const Comments = ({ story }: CommentsProps) => {
11+
const { data, loadNext, hasNext, refetch } = usePaginationFragment(
12+
graphql`
13+
fragment Comments_story on Story
14+
@refetchable(queryName: "Comments_storyQuery")
15+
@argumentDefinitions(
16+
count: { type: "Int", defaultValue: 20 }
17+
cursor: { type: "String" }
18+
) {
19+
comments(first: $count, after: $cursor)
20+
@connection(key: "Comments_story__comments") {
21+
edges {
22+
node {
23+
id
24+
...Comment_node
25+
}
26+
}
27+
}
28+
}
29+
`,
30+
story
31+
)
32+
33+
if (!data || !data.comments) {
34+
return <></>
35+
}
36+
37+
return (
38+
<div className="space-y-4 mt-4">
39+
<NewComment storyId={story.id} refetch={refetch} />
40+
{data.comments.edges.map(
41+
(comment: any) =>
42+
comment && <Comment comment={comment.node} key={comment.node.id} />
43+
)}
44+
{hasNext ? (
45+
<button
46+
className="text-gray-600 dark:text-gray-300"
47+
onClick={() => {
48+
loadNext(2)
49+
}}
50+
>
51+
Load more
52+
</button>
53+
) : null}
54+
</div>
55+
)
56+
}
57+
58+
export default Comments

src/components/comment/NewComment.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useState } from 'react'
2+
import { graphql, RefetchFnDynamic, useMutation } from 'react-relay'
3+
import { Options } from 'react-relay/relay-hooks/useRefetchableFragmentNode'
4+
import { OperationType } from 'relay-runtime'
5+
import Button from '../Button'
6+
import ExpandingTextarea from '../ExpandingTextarea'
7+
8+
type NewCommentProps = {
9+
storyId: string
10+
refetch: RefetchFnDynamic<OperationType, any, Options>
11+
}
12+
13+
export default function NewComment({ storyId, refetch }: NewCommentProps) {
14+
const [comment, setComment] = useState('')
15+
16+
const [commitMutation, isMutationInFlight] = useMutation(
17+
graphql`
18+
mutation NewCommentMutation($input: CommentInput!) {
19+
createComment(comment: $input) {
20+
id
21+
}
22+
}
23+
`
24+
)
25+
26+
const handleCommentPost = () => {
27+
commitMutation({
28+
variables: {
29+
input: { storyID: storyId, contentJson: comment, abstractContent: '' },
30+
},
31+
onCompleted: () => {
32+
refetch({}, { fetchPolicy: 'network-only' })
33+
},
34+
})
35+
}
36+
37+
return (
38+
<div className="w-full dark:text-gray-300 dark:bg-dark-600 flex flex-col items-end">
39+
<div className="textarea-container w-full">
40+
<ExpandingTextarea
41+
value={comment}
42+
setValue={setComment}
43+
placeholder="Write a comment..."
44+
className="w-full dark:text-gray-300 dark:bg-dark-600 focus-visible:outline-none"
45+
/>
46+
</div>
47+
<Button onClick={() => handleCommentPost()}>Post</Button>
48+
</div>
49+
)
50+
}

src/components/feed/Feed.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { graphql, usePaginationFragment } from 'react-relay'
2+
import { Virtuoso } from 'react-virtuoso'
23
import { FeedsQuery$key } from '../../queries/__generated__/FeedsQuery.graphql'
34
import StoryPreview from './StoryPreview'
45

@@ -7,12 +8,12 @@ type FeedProps = {
78
}
89

910
const Feed = ({ root }: FeedProps) => {
10-
const { data } = usePaginationFragment(
11+
const { data, loadNext } = usePaginationFragment(
1112
graphql`
1213
fragment FeedsQuery on Query
1314
@refetchable(queryName: "FeedsRoot_Query")
1415
@argumentDefinitions(
15-
count: { type: "Int", defaultValue: 10 }
16+
count: { type: "Int", defaultValue: 4 }
1617
cursor: { type: "String" }
1718
) {
1819
feeds(first: $count, after: $cursor)
@@ -30,11 +31,19 @@ const Feed = ({ root }: FeedProps) => {
3031
)
3132

3233
return (
33-
<>
34-
{data.feeds.edges.map(
35-
(edge) => edge && <StoryPreview story={edge?.node} key={edge.node.id} />
36-
)}
37-
</>
34+
<div>
35+
<Virtuoso
36+
className="min-h-screen scrollbar-thin scrollbar-thumb-gray-400 dark:scrollbar-thumb-gray-700 scrollbar-track-gray-300 dark:scrollbar-track-gray-500"
37+
data={data.feeds.edges}
38+
endReached={loadNext}
39+
overscan={200}
40+
itemContent={(index, edge) => {
41+
return (
42+
edge?.node && <StoryPreview story={edge?.node} key={edge.node.id} />
43+
)
44+
}}
45+
/>
46+
</div>
3847
)
3948
}
4049

src/components/feed/StoryPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const StoryPreview = ({ story }: StoryPreviewProps) => {
3232

3333
return (
3434
<ErrorBoundary FallbackComponent={ErrorFallback}>
35-
<div className="my-8 flex flex-col bg-white dark:bg-dark-700 rounded-lg">
35+
<div className="my-8 mr-2 flex flex-col bg-white dark:bg-dark-700 rounded-lg">
3636
<div>
3737
{data.thumbnail ? (
3838
<img className="cover-image" src={data.thumbnail} alt="" />

0 commit comments

Comments
 (0)