diff --git a/src/components/header.tsx b/src/components/header.tsx
new file mode 100644
index 0000000..4fb7535
--- /dev/null
+++ b/src/components/header.tsx
@@ -0,0 +1,15 @@
+import Link from 'next/link'
+import Logo from '~/components/svg/logo'
+
+import { Actions } from '~/components/actions'
+
+export const Header = async () => {
+ return (
+
+ )
+}
diff --git a/components/html-view.tsx b/src/components/html-view.tsx
similarity index 85%
rename from components/html-view.tsx
rename to src/components/html-view.tsx
index a033763..8adb818 100644
--- a/components/html-view.tsx
+++ b/src/components/html-view.tsx
@@ -1,4 +1,4 @@
-import { classNames } from '@/lib/classnames'
+import { classNames } from '~/utils/core'
type HtmlViewProps = {
html: string
diff --git a/src/components/liked-by.tsx b/src/components/liked-by.tsx
new file mode 100644
index 0000000..e32ec29
--- /dev/null
+++ b/src/components/liked-by.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import { classNames } from '~/utils/core'
+import * as Tooltip from '@radix-ui/react-tooltip'
+import { type RouterOutputs } from '~/trpc/shared'
+import { MAX_LIKED_BY_SHOWN } from './reaction-button'
+import { type ReactNode } from 'react'
+import { useSession } from 'next-auth/react'
+
+type LikedByProps = {
+ trigger: ReactNode
+ likedBy: RouterOutputs['post']['detail']['likedBy']
+}
+
+export const LikedBy = ({ trigger, likedBy }: LikedByProps) => {
+ const likeCount = likedBy.length
+ const { data: session } = useSession()
+
+ return (
+
+
+ {
+ event.preventDefault()
+ }}
+ onMouseDown={(event) => {
+ event.preventDefault()
+ }}
+ asChild
+ >
+ {trigger}
+
+
+
+ {likedBy
+ .slice(0, MAX_LIKED_BY_SHOWN)
+ .map(({ user }) =>
+ session?.user.name === user.name ? 'You' : user.name,
+ )
+ .join(', ')}
+ {likeCount > MAX_LIKED_BY_SHOWN &&
+ ` and ${likeCount - MAX_LIKED_BY_SHOWN} more`}
+
+
+
+
+
+ )
+}
diff --git a/components/markdown-editor.tsx b/src/components/markdown-editor.tsx
similarity index 76%
rename from components/markdown-editor.tsx
rename to src/components/markdown-editor.tsx
index 65b8c62..11d9aa4 100644
--- a/components/markdown-editor.tsx
+++ b/src/components/markdown-editor.tsx
@@ -1,34 +1,31 @@
-import { HtmlView } from '@/components/html-view'
-import { BoldIcon, ItalicIcon, LinkIcon, ListIcon } from '@/components/icons'
-import { browserEnv } from '@/env/browser'
-import { classNames } from '@/lib/classnames'
-import {
- getSuggestionData,
- uploadImageCommandHandler,
- markdownToHtml,
-} from '@/lib/editor'
-import { trpc } from '@/lib/trpc'
-import { Switch } from '@headlessui/react'
+'use client'
+
+import * as Switch from '@radix-ui/react-switch'
import { matchSorter } from 'match-sorter'
import * as React from 'react'
import { useDetectClickOutside } from 'react-detect-click-outside'
-import { useQuery } from 'react-query'
+import { type ItemOptions, useItemList } from 'use-item-list'
+
import TextareaAutosize, {
- TextareaAutosizeProps,
+ type TextareaAutosizeProps,
} from 'react-textarea-autosize'
import getCaretCoordinates from 'textarea-caret'
-import TextareaMarkdown, { TextareaMarkdownRef } from 'textarea-markdown-editor'
-import { ItemOptions, useItemList } from 'use-item-list'
-
-type MarkdownEditorProps = {
- label?: string
- value: string
- onChange: (value: string) => void
- onTriggerSubmit?: () => void
-} & Omit<
- TextareaAutosizeProps,
- 'value' | 'onChange' | 'onKeyDown' | 'onInput' | 'onPaste' | 'onDrop'
->
+import TextareaMarkdown, {
+ type TextareaMarkdownRef,
+} from 'textarea-markdown-editor'
+
+import BoldIcon from '~/components/svg/bold-icon'
+import { classNames } from '~/utils/core'
+import ItalicIcon from '~/components/svg/italic-icon'
+import LinkIcon from '~/components/svg/link-icon'
+import ListIcon from '~/components/svg/list-icon'
+import { HtmlView } from './html-view'
+import { type SuggestionType, getSuggestionData } from '~/utils/suggestion'
+import { env } from '~/env'
+import { api } from '~/trpc/react'
+import { uploadImageCommandHandler } from '~/server/cloudinary'
+import { markdownToHtml } from '~/utils/text'
+import { useEffect, useRef } from 'react'
type SuggestionResult = {
label: string
@@ -40,8 +37,6 @@ type SuggestionPosition = {
left: number
}
-type SuggestionType = 'mention' | 'emoji'
-
type SuggestionState = {
isOpen: boolean
type: SuggestionType | null
@@ -63,10 +58,10 @@ type SuggestionActionType =
| { type: 'close' }
| { type: 'updateQuery'; payload: string }
-function suggestionReducer(
+const suggestionReducer = (
state: SuggestionState,
- action: SuggestionActionType
-) {
+ action: SuggestionActionType,
+) => {
switch (action.type) {
case 'open':
return {
@@ -114,7 +109,7 @@ const TOOLBAR_ITEMS = [
},
]
-function MarkdownPreview({ markdown }: { markdown: string }) {
+const MarkdownPreview = ({ markdown }: { markdown: string }) => {
return (
{markdown ? (
@@ -126,14 +121,24 @@ function MarkdownPreview({ markdown }: { markdown: string }) {
)
}
-export function MarkdownEditor({
+type MarkdownEditorProps = {
+ label?: string
+ value: string
+ onChange: (value: string) => void
+ onTriggerSubmit?: () => void
+} & Omit<
+ TextareaAutosizeProps,
+ 'value' | 'onChange' | 'onKeyDown' | 'onInput' | 'onPaste' | 'onDrop'
+>
+
+export const MarkdownEditor = ({
label,
value,
minRows = 15,
onChange,
onTriggerSubmit,
...rest
-}: MarkdownEditorProps) {
+}: MarkdownEditorProps) => {
const textareaMarkdownRef = React.useRef
(null)
const [showPreview, setShowPreview] = React.useState(false)
const [suggestionState, suggestionDispatch] = React.useReducer(
@@ -144,15 +149,15 @@ export function MarkdownEditor({
position: null,
triggerIdx: null,
query: '',
- }
+ },
)
- function closeSuggestion() {
+ const closeSuggestion = () => {
suggestionDispatch({ type: 'close' })
}
return (
-
+ <>
{label &&
{label} }
@@ -163,12 +168,12 @@ export function MarkdownEditor({
type="button"
onClick={() => {
textareaMarkdownRef.current?.trigger(
- toolbarItem.commandTrigger
+ toolbarItem.commandTrigger,
)
}}
className={classNames(
'rounded inline-flex items-center justify-center h-8 w-8 disabled:opacity-50 disabled:cursor-default focus:border focus-ring',
- !showPreview && 'transition-colors hover:text-blue'
+ !showPreview && 'transition-colors hover:text-blue',
)}
disabled={showPreview}
title={toolbarItem.name}
@@ -178,34 +183,33 @@ export function MarkdownEditor({
))}
-
- {
- if (value === false) {
- textareaMarkdownRef.current?.focus()
- }
- setShowPreview(value)
- }}
+ {
+ if (value === false) {
+ textareaMarkdownRef.current?.focus()
+ }
+ setShowPreview(value)
+ }}
+ >
+
-
-
+
+
Preview
-
-
+
+
@@ -231,7 +235,7 @@ export function MarkdownEditor({
} else {
const coords = getCaretCoordinates(
event.currentTarget,
- triggerIdx + 1
+ triggerIdx + 1,
)
suggestionDispatch({
type: 'open',
@@ -253,8 +257,8 @@ export function MarkdownEditor({
onTriggerSubmit?.()
}
}}
- onPaste={(event) => {
- if (browserEnv.NEXT_PUBLIC_ENABLE_IMAGE_UPLOAD) {
+ onPaste={async (event) => {
+ if (env.NEXT_PUBLIC_ENABLE_IMAGE_UPLOAD) {
const filesArray = Array.from(event.clipboardData.files)
if (filesArray.length === 0) {
@@ -262,7 +266,7 @@ export function MarkdownEditor({
}
const imageFiles = filesArray.filter((file) =>
- /image/i.test(file.type)
+ /image/i.test(file.type),
)
if (imageFiles.length === 0) {
@@ -271,11 +275,14 @@ export function MarkdownEditor({
event.preventDefault()
- uploadImageCommandHandler(event.currentTarget, imageFiles)
+ await uploadImageCommandHandler(
+ event.currentTarget,
+ imageFiles,
+ )
}
}}
- onDrop={(event) => {
- if (browserEnv.NEXT_PUBLIC_ENABLE_IMAGE_UPLOAD) {
+ onDrop={async (event) => {
+ if (env.NEXT_PUBLIC_ENABLE_IMAGE_UPLOAD) {
const filesArray = Array.from(event.dataTransfer.files)
if (filesArray.length === 0) {
@@ -283,7 +290,7 @@ export function MarkdownEditor({
}
const imageFiles = filesArray.filter((file) =>
- /image/i.test(file.type)
+ /image/i.test(file.type),
)
if (imageFiles.length === 0) {
@@ -292,7 +299,10 @@ export function MarkdownEditor({
event.preventDefault()
- uploadImageCommandHandler(event.currentTarget, imageFiles)
+ await uploadImageCommandHandler(
+ event.currentTarget,
+ imageFiles,
+ )
}
}}
className="block w-full rounded shadow-sm bg-secondary border-secondary focus-ring"
@@ -300,12 +310,12 @@ export function MarkdownEditor({
/>
- {
const preSuggestion = value.slice(0, suggestionState.triggerIdx!)
const postSuggestion = value.slice(
- textareaMarkdownRef.current?.selectionStart
+ textareaMarkdownRef.current?.selectionStart,
)
let suggestionInsertion = ''
@@ -330,7 +340,7 @@ export function MarkdownEditor({
textareaMarkdownRef.current?.focus()
textareaMarkdownRef.current?.setSelectionRange(
caretPosition,
- caretPosition
+ caretPosition,
)
}, 0)
}}
@@ -340,11 +350,11 @@ export function MarkdownEditor({
{showPreview && }
-
+ >
)
}
-function Suggestion({
+const Suggestions = ({
state,
onSelect,
onClose,
@@ -352,23 +362,16 @@ function Suggestion({
state: SuggestionState
onSelect: (suggestionResult: SuggestionResult) => void
onClose: () => void
-}) {
+}) => {
const isMentionType = state.type === 'mention'
const isEmojiType = state.type === 'emoji'
- const emojiListQuery = useQuery(
- 'emojiList',
- async () => {
- const gemoji = (await import('gemoji')).gemoji
- return gemoji
- },
- {
- enabled: state.isOpen && isEmojiType,
- staleTime: Infinity,
- }
- )
+ const emojiListQuery = api.post.emojiList.useQuery(undefined, {
+ enabled: isEmojiType,
+ staleTime: Infinity,
+ })
- const mentionListQuery = trpc.useQuery(['user.mentionList'], {
+ const mentionListQuery = api.user.mentionList.useQuery(undefined, {
enabled: state.isOpen && isMentionType,
staleTime: 5 * 60 * 1000,
})
@@ -395,7 +398,7 @@ function Suggestion({
}))
}
- if (!state.isOpen || !state.position || suggestionList.length === 0) {
+ if (!state.position) {
return null
}
@@ -403,32 +406,32 @@ function Suggestion({
)
}
-function SuggestionList({
+const SuggestionList = ({
suggestionList,
position,
- onSelect,
+ onSuggestionSelect,
onClose,
}: {
suggestionList: SuggestionResult[]
position: SuggestionPosition
- onSelect: (suggestionResult: SuggestionResult) => void
+ onSuggestionSelect: (suggestionResult: SuggestionResult) => void
onClose: () => void
-}) {
+}) => {
const ref = useDetectClickOutside({ onTriggered: onClose })
const { moveHighlightedItem, selectHighlightedItem, useItem } = useItemList({
- onSelect: (item) => {
- onSelect(item.value)
+ onSelect: (item: { value: SuggestionResult }) => {
+ onSuggestionSelect(item.value)
},
})
- React.useEffect(() => {
+ useEffect(() => {
function handleKeydownEvent(event: KeyboardEvent) {
const { code } = event
@@ -470,7 +473,16 @@ function SuggestionList({
{suggestionList.map((suggestionResult) => (
{
+ id: string
+ index: number
+ highlight: () => void
+ select: () => void
+ selected: boolean
+ useHighlighted: () => boolean
+ }
+ }
suggestionResult={suggestionResult}
/>
))}
@@ -478,7 +490,8 @@ function SuggestionList({
)
}
-function SuggestionResult({
+
+const SuggestionResult = ({
useItem,
suggestionResult,
}: {
@@ -487,13 +500,13 @@ function SuggestionResult({
index: number
highlight: () => void
select: () => void
- selected: any
- useHighlighted: () => Boolean
+ selected: boolean
+ useHighlighted: () => boolean
}
suggestionResult: SuggestionResult
-}) {
- const ref = React.useRef
(null)
- const { id, index, highlight, select, useHighlighted } = useItem({
+}) => {
+ const ref = useRef(null)
+ const { id, highlight, select, useHighlighted } = useItem({
ref,
value: suggestionResult,
})
@@ -509,7 +522,7 @@ function SuggestionResult({
aria-selected={highlighted ? 'true' : 'false'}
className={classNames(
'px-4 py-2 text-sm text-left transition-colors cursor-pointer ',
- highlighted ? 'bg-blue-600 text-white' : 'text-primary'
+ highlighted ? 'bg-blue-600 text-white' : 'text-primary',
)}
>
{suggestionResult.label}
diff --git a/src/components/menu.tsx b/src/components/menu.tsx
new file mode 100644
index 0000000..a7ef185
--- /dev/null
+++ b/src/components/menu.tsx
@@ -0,0 +1,90 @@
+'use client'
+
+import * as DropdownPrimitive from '@radix-ui/react-dropdown-menu'
+import Link, { type LinkProps } from 'next/link'
+import * as React from 'react'
+import { forwardRef, type ComponentPropsWithRef, type Ref } from 'react'
+
+import { classNames } from '~/utils/core'
+
+export const Menu = ({ children }: { children: React.ReactNode }) => {
+ return {children}
+}
+
+export const MenuButton = DropdownPrimitive.Trigger
+export const MenuItem = ({
+ asChild = true,
+ ...props
+}: DropdownPrimitive.DropdownMenuItemProps) => {
+ return
+}
+
+type MenuItemButtonProps = React.ComponentPropsWithRef<'button'>
+
+export const MenuItemButton = forwardRef(
+ (
+ { className, ...props }: MenuItemButtonProps,
+ ref: Ref,
+ ) => {
+ return (
+
+ )
+ },
+)
+
+MenuItemButton.displayName = 'MenuItemButton'
+
+type MenuItemLink = Omit, 'href'> & LinkProps
+
+export const MenuItemLink = forwardRef(
+ ({ className, ...props }: MenuItemLink, ref: Ref) => {
+ return (
+
+ )
+ },
+)
+
+MenuItemLink.displayName = 'MenuItemLink'
+
+export const MenuItems = ({
+ children,
+ className,
+}: {
+ children: React.ReactNode
+ className?: string
+}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export const MenuItemsContent = ({
+ children,
+}: {
+ children: React.ReactNode
+}) => {
+ return {children}
+}
diff --git a/src/components/pagination.tsx b/src/components/pagination.tsx
new file mode 100644
index 0000000..8f77c9b
--- /dev/null
+++ b/src/components/pagination.tsx
@@ -0,0 +1,64 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import { Button } from './button'
+import ChevronLeftIcon from '~/components/svg/chevron-left-icon'
+import ChevronRightIcon from '~/components/svg/chevron-right-icon'
+import { POSTS_PER_PAGE } from '~/utils/core'
+
+type PaginationProps = {
+ itemCount: number
+ currentPageNumber: number
+}
+
+export const Pagination = ({
+ itemCount,
+ currentPageNumber,
+}: PaginationProps) => {
+ const pathname = usePathname()
+
+ const totalPages = Math.ceil(itemCount / POSTS_PER_PAGE)
+
+ if (totalPages <= 1) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+ Newer posts
+
+
+ Older posts{' '}
+
+
+
+
+
+ )
+}
diff --git a/src/components/post-action.tsx b/src/components/post-action.tsx
new file mode 100644
index 0000000..eee2753
--- /dev/null
+++ b/src/components/post-action.tsx
@@ -0,0 +1,302 @@
+'use client'
+
+import { useRouter } from 'next/navigation'
+import { toast } from 'react-hot-toast'
+import { Button } from '~/components/button'
+import {
+ AlertDialogAction,
+ AlertDialogActions,
+ AlertDialogCloseButton,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogTitle,
+ AlertDialogCancel,
+} from '~/components/alert-dialog'
+import {
+ Menu,
+ MenuButton,
+ MenuItemButton,
+ MenuItems,
+ MenuItem,
+ MenuItemsContent,
+ MenuItemLink,
+} from '~/components/menu'
+import { useDialogStore } from '~/hooks/use-dialog-store'
+import DotsIcon from '~/components/svg/dots-icon'
+import EditIcon from '~/components/svg/edit-icon'
+import EyeIcon from '~/components/svg/eye-icon'
+import TrashIcon from '~/components/svg/trash-icon'
+import { api } from '~/trpc/react'
+import { getFeedPagination } from './post-summary'
+
+type PostActionProps = {
+ isUserAdmin: boolean
+ isHidden: boolean
+ postBelongsToUser: boolean
+ postId: number
+}
+
+export const PostAction = ({
+ isUserAdmin,
+ isHidden,
+ postBelongsToUser,
+ postId,
+}: PostActionProps) => {
+ const { handleDialog } = useDialogStore()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {isUserAdmin &&
+ (isHidden ? (
+
+
+ handleDialog({
+ component: (
+
+ ),
+ })
+ }
+ >
+ Unhide
+
+
+ ) : (
+
+
+ handleDialog({
+ component: ,
+ })
+ }
+ >
+ Hide
+
+
+ ))}
+ {postBelongsToUser && (
+ <>
+
+
+ Edit
+
+
+
+
+ handleDialog({
+ component: ,
+ })
+ }
+ className="text-red"
+ >
+ Delete
+
+
+ >
+ )}
+
+
+
+
+
+ {isUserAdmin &&
+ (isHidden ? (
+
+ handleDialog({
+ component: ,
+ })
+ }
+ >
+
+
+ ) : (
+
+ handleDialog({
+ component: ,
+ })
+ }
+ >
+
+
+ ))}
+ {postBelongsToUser && (
+ <>
+
+
+
+
+ handleDialog({
+ component: ,
+ })
+ }
+ >
+
+
+ >
+ )}
+
+ >
+ )
+}
+
+const ConfirmDeleteDialog = ({ postId }: { postId: number }) => {
+ const { handleDialogClose } = useDialogStore()
+ const router = useRouter()
+ const utils = api.useUtils()
+ const deletePostMutation = api.post.delete.useMutation({
+ onMutate: () => {
+ const previousQuery = utils.post.feed.getData(
+ getFeedPagination({ currentPageNumber: 1 }),
+ )
+
+ if (previousQuery) {
+ utils.post.feed.setData(getFeedPagination({ currentPageNumber: 1 }), {
+ ...previousQuery,
+ posts: previousQuery.posts.filter((post) => post.id !== postId),
+ })
+ }
+ },
+ onSuccess: () => {
+ router.push('/')
+ },
+ onError: (error) => {
+ toast.error(`Something went wrong: ${error.message}`)
+ },
+ })
+
+ return (
+ <>
+
+ Delete post
+
+ Are you sure you want to delete this post?
+
+
+
+
+
+ {
+ deletePostMutation.mutate(postId, {
+ onSuccess: () => router.push('/'),
+ })
+ }}
+ >
+ Delete post
+
+
+
+
+ Cancel
+
+
+
+ >
+ )
+}
+
+const ConfirmHidePostDialog = ({ postId }: { postId: number }) => {
+ const { handleDialogClose } = useDialogStore()
+
+ const hidePostMutation = api.post.hide.useMutation({
+ onError: (error) => {
+ toast.error(`Something went wrong: ${error.message}`)
+ },
+ })
+
+ return (
+ <>
+
+ Hide post
+
+ Are you sure you want to hide this post?
+
+
+
+
+
+ hidePostMutation.mutate(postId, {
+ onSuccess: () => {
+ toast.success('Post hidden')
+ handleDialogClose()
+ },
+ })
+ }
+ >
+ Hide post
+
+
+ Cancel
+
+
+ >
+ )
+}
+
+const ConfirmUnhidePostDialog = ({ postId }: { postId: number }) => {
+ const { handleDialogClose } = useDialogStore()
+
+ const unhidePostMutation = api.post.unhide.useMutation({
+ onError: (error) => {
+ toast.error(`Something went wrong: ${error.message}`)
+ },
+ })
+
+ return (
+ <>
+
+ Hide post
+
+ Are you sure you want to unhide this post?
+
+
+
+
+
+ unhidePostMutation.mutate(postId, {
+ onSuccess: () => {
+ toast.success('Post unhidden')
+ handleDialogClose()
+ },
+ })
+ }
+ >
+ Hide post
+
+
+ Cancel
+
+
+ >
+ )
+}
diff --git a/src/components/post-feed.tsx b/src/components/post-feed.tsx
new file mode 100644
index 0000000..272a8dd
--- /dev/null
+++ b/src/components/post-feed.tsx
@@ -0,0 +1,129 @@
+'use client'
+
+import { api } from '~/trpc/react'
+import { PostSummary, getFeedPagination } from './post-summary'
+import { Pagination } from './pagination'
+import { PostSkeleton } from './post-skeleton'
+import { useSession } from 'next-auth/react'
+
+type PostFeedProps = {
+ fallbackMessage: string
+ currentPageNumber?: number
+ authorId?: string
+}
+
+export const PostFeed = ({
+ fallbackMessage,
+ currentPageNumber,
+ authorId,
+}: PostFeedProps) => {
+ const { data, isLoading } = api.post.feed.useQuery(
+ getFeedPagination({ authorId, currentPageNumber }),
+ )
+
+ const { like, unlike } = useReaction({ currentPageNumber, authorId })
+
+ if (isLoading) return
+
+ return (
+ <>
+ {data?.postCount === 0 ? (
+
+ {fallbackMessage}
+
+ ) : (
+ <>
+
+ {data?.posts.map((post) => {
+ return (
+
+ like.mutate({ id: post.id })}
+ onUnlike={() => unlike.mutate({ id: post.id })}
+ post={post}
+ />
+
+ )
+ })}
+
+
+ >
+ )}
+ >
+ )
+}
+
+const useReaction = ({
+ authorId,
+ currentPageNumber,
+}: {
+ authorId?: string
+ currentPageNumber?: number
+}) => {
+ const { data: session } = useSession()
+
+ const utils = api.useUtils()
+ const previousQuery = utils.post.feed.getData(
+ getFeedPagination({ authorId, currentPageNumber }),
+ )
+
+ const like = api.post.like.useMutation({
+ onMutate: async ({ id }) => {
+ if (previousQuery) {
+ utils.post.feed.setData(
+ getFeedPagination({ authorId, currentPageNumber }),
+ {
+ ...previousQuery,
+ posts: previousQuery.posts.map((post) =>
+ post.id === id
+ ? {
+ ...post,
+ likedBy: [
+ ...post.likedBy,
+ {
+ user: {
+ id: session!.user.id,
+ name: session!.user.name,
+ },
+ },
+ ],
+ }
+ : post,
+ ),
+ },
+ )
+ }
+ },
+ })
+
+ const unlike = api.post.unlike.useMutation({
+ onMutate: async ({ id }) => {
+ if (previousQuery) {
+ utils.post.feed.setData(
+ getFeedPagination({ authorId, currentPageNumber }),
+ {
+ ...previousQuery,
+ posts: previousQuery.posts.map((post) =>
+ post.id === id
+ ? {
+ ...post,
+ likedBy: post.likedBy.filter(
+ (item) => item.user.id !== session!.user.id,
+ ),
+ }
+ : post,
+ ),
+ },
+ )
+ }
+ },
+ })
+
+ return {
+ like,
+ unlike,
+ }
+}
diff --git a/components/post-form.tsx b/src/components/post-form.tsx
similarity index 53%
rename from components/post-form.tsx
rename to src/components/post-form.tsx
index 53ef38e..ba0fa35 100644
--- a/components/post-form.tsx
+++ b/src/components/post-form.tsx
@@ -1,11 +1,14 @@
-import { Button } from '@/components/button'
-import { ButtonLink } from '@/components/button-link'
-import { MarkdownIcon } from '@/components/icons'
-import { MarkdownEditor } from '@/components/markdown-editor'
-import { TextField } from '@/components/text-field'
-import { useLeaveConfirm } from '@/lib/form'
+'use client'
+
import * as React from 'react'
-import { Controller, SubmitHandler, useForm } from 'react-hook-form'
+import { Controller, useForm } from 'react-hook-form'
+import { TextField } from '~/components/text-field'
+import { Button } from '~/components/button'
+import MarkdownIcon from '~/components/svg/markdown-icon'
+import { MarkdownEditor } from '~/components/markdown-editor'
+import { markdownToHtml } from '~/utils/text'
+import { useRouter } from 'next/navigation'
+import { api } from '~/trpc/react'
type FormData = {
title: string
@@ -13,32 +16,37 @@ type FormData = {
}
type PostFormProps = {
+ postId?: number
defaultValues?: FormData
isSubmitting?: boolean
backTo: string
- onSubmit: SubmitHandler
}
export function PostForm({
+ postId,
defaultValues,
isSubmitting,
backTo,
- onSubmit,
}: PostFormProps) {
- const { control, register, formState, getValues, reset, handleSubmit } =
+ const { control, register, handleSubmit, reset, getValues } =
useForm({
defaultValues,
})
- useLeaveConfirm({ formState })
-
- const { isSubmitSuccessful } = formState
+ const { newPost, editPost } = usePostMutations(postId, () =>
+ reset(getValues()),
+ )
- React.useEffect(() => {
- if (isSubmitSuccessful) {
- reset(getValues())
+ const onSubmit = (values: FormData) => {
+ if (postId) {
+ editPost.mutate({
+ id: postId,
+ data: values,
+ })
+ } else {
+ newPost.mutate(values)
}
- }, [isSubmitSuccessful, reset, getValues])
+ }
return (
{!isSubmitting && (
- {postQuery.data.comments.map((comment) => ( --
-
-
- ))}
-
- )} -