diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..d967b76 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,37 @@ +/** @type {import("eslint").Linter.Config} */ +const config = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + plugins: ['@typescript-eslint'], + extends: [ + 'plugin:@next/next/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + ], + rules: { + // These opinionated rules are enabled in stylistic-type-checked above. + // Feel free to reconfigure them to your own preference. + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/consistent-type-definitions': 'off', + + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { + prefer: 'type-imports', + fixStyle: 'inline-type-imports', + }, + ], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: { attributes: false }, + }, + ], + }, +} + +module.exports = config diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 35d6918..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx pretty-quick --staged diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 82dc6e3..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 PlanetScale - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index a64047a..841daf8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Beam is a simple tool that allows members to write posts to share across your or ### Install dependencies ```bash -npm install +pnpm i ``` ### Create a database @@ -58,7 +58,7 @@ If you'd like to have new Beam posts published to a Slack channel, follow [these ## Running the app locally ```bash -npm run dev +pnpm dev ``` Open [http://localhost:3000](http://localhost:3000) in your browser. diff --git a/components/author-with-date.tsx b/components/author-with-date.tsx deleted file mode 100644 index 8091fcf..0000000 --- a/components/author-with-date.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Avatar } from '@/components/avatar' -import type { Author } from '@/lib/types' -import formatDistanceToNow from 'date-fns/formatDistanceToNow' -import Link from 'next/link' - -type AuthorWithDateProps = { - author: Author - date: Date -} - -export function AuthorWithDate({ author, date }: AuthorWithDateProps) { - return ( -
- - - - - - - - - - -
-
- - - {author.name} - - -
- -

- {' '} - ago -

-
-
- ) -} diff --git a/components/button-link.tsx b/components/button-link.tsx deleted file mode 100644 index 4611ecb..0000000 --- a/components/button-link.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { buttonClasses, ButtonVariant } from '@/components/button' -import Link, { LinkProps } from 'next/link' -import * as React from 'react' - -type ButtonLinkProps = { - variant?: ButtonVariant - responsive?: boolean -} & Omit, 'href'> & - LinkProps - -export const ButtonLink = React.forwardRef( - ( - { - href, - as, - replace, - scroll, - shallow, - passHref, - prefetch, - locale, - className, - variant = 'primary', - responsive, - ...rest - }, - forwardedRef - ) => { - return ( - - - - ) - } -) - -ButtonLink.displayName = 'ButtonLink' diff --git a/components/button.tsx b/components/button.tsx deleted file mode 100644 index 9d86ece..0000000 --- a/components/button.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { SpinnerIcon } from '@/components/icons' -import { classNames } from '@/lib/classnames' -import * as React from 'react' - -export type ButtonVariant = 'primary' | 'secondary' - -type ButtonProps = { - variant?: ButtonVariant - responsive?: boolean - isLoading?: boolean - loadingChildren?: React.ReactNode -} & React.ComponentPropsWithoutRef<'button'> - -export function buttonClasses({ - className, - variant = 'primary', - responsive, - isLoading, - disabled, -}: ButtonProps) { - return classNames( - 'inline-flex items-center justify-center font-semibold transition-colors rounded-full focus-ring', - responsive - ? 'px-3 h-8 text-xs sm:px-4 sm:text-sm sm:h-button' - : 'px-4 text-sm h-button', - variant === 'primary' && - 'text-secondary-inverse bg-secondary-inverse hover:text-primary-inverse hover:bg-primary-inverse', - variant === 'secondary' && - 'border text-primary border-secondary bg-primary hover:bg-secondary', - (disabled || isLoading) && 'opacity-50 cursor-default', - className - ) -} - -export const Button = React.forwardRef( - ( - { - className, - variant = 'primary', - responsive, - type = 'button', - isLoading = false, - loadingChildren, - disabled, - children, - ...rest - }, - forwardedRef - ) => { - return ( - - ) - } -) - -Button.displayName = 'Button' diff --git a/components/dialog.tsx b/components/dialog.tsx deleted file mode 100644 index 1fa9337..0000000 --- a/components/dialog.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { XIcon } from '@/components/icons' -import { Dialog as HeadlessDialog, Transition } from '@headlessui/react' -import * as React from 'react' - -type DialogProps = { - isOpen: boolean - onClose: () => void - children: React.ReactNode - initialFocus?: React.MutableRefObject -} - -export function Dialog({ - isOpen, - onClose, - children, - initialFocus, -}: DialogProps) { - return ( - - -
- - - - - -
- {children} -
-
-
-
-
- ) -} - -export function DialogContent({ children }: { children: React.ReactNode }) { - return
{children}
-} - -export function DialogActions({ children }: { children: React.ReactNode }) { - return
{children}
-} - -export function DialogTitle({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -export function DialogDescription({ - children, - className, -}: { - children: React.ReactNode - className?: string -}) { - return ( - - {children} - - ) -} - -export function DialogCloseButton({ onClick }: { onClick: () => void }) { - return ( -
- -
- ) -} diff --git a/components/icon-button.tsx b/components/icon-button.tsx deleted file mode 100644 index c1f6cc2..0000000 --- a/components/icon-button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ButtonVariant } from '@/components/button' -import { classNames } from '@/lib/classnames' -import * as React from 'react' - -export type IconButtonOwnProps = { - variant?: ButtonVariant -} - -type IconButtonProps = IconButtonOwnProps & - React.ComponentPropsWithoutRef<'button'> - -export const IconButton = React.forwardRef( - ( - { className, variant = 'primary', type = 'button', ...rest }, - forwardedRef - ) => { - return ( -
- - -

- {likedBy - .slice(0, MAX_LIKED_BY_SHOWN) - .map((item) => - item.user.id === session!.user.id ? 'You' : item.user.name - ) - .join(', ')} - {likeCount > MAX_LIKED_BY_SHOWN && - ` and ${likeCount - MAX_LIKED_BY_SHOWN} more`} -

- -
- - ) -} diff --git a/components/menu.tsx b/components/menu.tsx deleted file mode 100644 index 954e1f7..0000000 --- a/components/menu.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { classNames } from '@/lib/classnames' -import { Menu as HeadlessMenu, Transition } from '@headlessui/react' -import Link, { LinkProps } from 'next/link' -import * as React from 'react' - -export function Menu({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} - -export const MenuButton = HeadlessMenu.Button - -export function MenuItems({ - children, - className, -}: { - children: React.ReactNode - className?: string -}) { - return ( - - - {children} - - - ) -} - -export function MenuItemsContent({ children }: { children: React.ReactNode }) { - return
{children}
-} - -function NextLink({ - href, - children, - ...rest -}: Omit, 'href'> & LinkProps) { - return ( - - {children} - - ) -} - -function menuItemClasses({ - active, - className, -}: { - active: boolean - className?: string -}) { - return classNames( - active && 'bg-secondary', - 'block w-full text-left px-4 py-2 text-sm text-primary transition-colors', - className - ) -} - -export function MenuItemLink({ - className, - href, - children, -}: { - className?: string - href: string - children: React.ReactNode -}) { - return ( - - {({ active }) => ( - - {children} - - )} - - ) -} - -export function MenuItemButton({ - className, - children, - onClick, -}: { - className?: string - children: React.ReactNode - onClick: () => void -}) { - return ( - - {({ active }) => ( - - )} - - ) -} diff --git a/components/pagination.tsx b/components/pagination.tsx deleted file mode 100644 index 98174fe..0000000 --- a/components/pagination.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { ButtonLink } from '@/components/button-link' -import { ChevronLeftIcon, ChevronRightIcon } from '@/components/icons' -import { useRouter } from 'next/router' - -type PaginationProps = { - itemCount: number - itemsPerPage: number - currentPageNumber: number -} - -export function getQueryPaginationInput( - itemsPerPage: number, - currentPageNumber: number -) { - return { - take: itemsPerPage, - skip: - currentPageNumber === 1 - ? undefined - : itemsPerPage * (currentPageNumber - 1), - } -} - -export function Pagination({ - itemCount, - itemsPerPage, - currentPageNumber, -}: PaginationProps) { - const router = useRouter() - - const totalPages = Math.ceil(itemCount / itemsPerPage) - - if (totalPages <= 1) { - return null - } - - return ( -
- - - - - Newer posts - - - Older posts{' '} - - - - -
- ) -} diff --git a/components/post-summary-skeleton.tsx b/components/post-summary-skeleton.tsx deleted file mode 100644 index 820a700..0000000 --- a/components/post-summary-skeleton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { classNames } from '@/lib/classnames' - -type PostSummarySkeletonProps = { - hideAuthor?: boolean -} - -export function PostSummarySkeleton({ hideAuthor }: PostSummarySkeletonProps) { - return ( -
-
-
-
- {!hideAuthor && ( -
- )} -
-
- {!hideAuthor && ( -
- )} -
-
-
-
-
-
-
-
-
-
-
- ) -} diff --git a/components/post-summary.tsx b/components/post-summary.tsx deleted file mode 100644 index 81950c0..0000000 --- a/components/post-summary.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { AuthorWithDate } from '@/components/author-with-date' -import { Banner } from '@/components/banner' -import { HtmlView } from '@/components/html-view' -import { - ChevronRightIcon, - HeartFilledIcon, - HeartIcon, - MessageIcon, -} from '@/components/icons' -import { MAX_LIKED_BY_SHOWN } from '@/components/like-button' -import { classNames } from '@/lib/classnames' -import { InferQueryOutput } from '@/lib/trpc' -import * as Tooltip from '@radix-ui/react-tooltip' -import formatDistanceToNow from 'date-fns/formatDistanceToNow' -import { useSession } from 'next-auth/react' -import { summarize } from '@/lib/text' -import Link from 'next/link' -import * as React from 'react' - -export type PostSummaryProps = { - post: InferQueryOutput<'post.feed'>['posts'][number] - hideAuthor?: boolean - onLike: () => void - onUnlike: () => void -} - -export function PostSummary({ - post, - hideAuthor = false, - onLike, - onUnlike, -}: PostSummaryProps) { - const { summary, hasMore } = React.useMemo( - () => summarize(post.contentHtml), - [post.contentHtml] - ) - - const { data: session } = useSession() - - const isLikedByCurrentUser = Boolean( - post.likedBy.find((item) => item.user.id === session!.user.id) - ) - const likeCount = post.likedBy.length - - return ( -
- {post.hidden && ( - - This post has been hidden and is only visible to administrators. - - )} -
- - -

- {post.title} -

-
- - -
- {hideAuthor ? ( -

- {' '} - ago -

- ) : ( - - )} -
- - - -
- {hasMore && ( - - - Continue reading - - - )} -
- - { - event.preventDefault() - }} - onMouseDown={(event) => { - event.preventDefault() - }} - > -
- {isLikedByCurrentUser ? ( - - ) : ( - - )} - - {likeCount} - -
-
- -

- {post.likedBy - .slice(0, MAX_LIKED_BY_SHOWN) - .map((item) => - item.user.id === session!.user.id ? 'You' : item.user.name - ) - .join(', ')} - {likeCount > MAX_LIKED_BY_SHOWN && - ` and ${likeCount - MAX_LIKED_BY_SHOWN} more`} -

- -
-
- -
- - - {post._count.comments} - -
-
-
-
-
- ) -} diff --git a/components/search-dialog.tsx b/components/search-dialog.tsx deleted file mode 100644 index 5b6638e..0000000 --- a/components/search-dialog.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { SearchIcon, SpinnerIcon } from '@/components/icons' -import { classNames } from '@/lib/classnames' -import { InferQueryOutput, trpc } from '@/lib/trpc' -import { Dialog, Transition } from '@headlessui/react' -import Link from 'next/link' -import { useRouter } from 'next/router' -import * as React from 'react' -import { useDebounce } from 'use-debounce' -import { ItemOptions, useItemList } from 'use-item-list' - -type SearchDialogProps = { - isOpen: boolean - onClose: () => void -} - -function SearchResult({ - useItem, - result, -}: { - useItem: ({ ref, text, value, disabled }: ItemOptions) => { - id: string - index: number - highlight: () => void - select: () => void - selected: any - useHighlighted: () => Boolean - } - result: InferQueryOutput<'post.search'>[number] -}) { - const ref = React.useRef(null) - const { id, index, highlight, select, useHighlighted } = useItem({ - ref, - value: result, - }) - const highlighted = useHighlighted() - - return ( -
  • - - - {result.title} - - -
  • - ) -} - -function SearchField({ onSelect }: { onSelect: () => void }) { - const [value, setValue] = React.useState('') - const [debouncedValue] = useDebounce(value, 1000) - const router = useRouter() - - const feedQuery = trpc.useQuery( - [ - 'post.search', - { - query: debouncedValue, - }, - ], - { - enabled: debouncedValue.trim().length > 0, - } - ) - - const { moveHighlightedItem, selectHighlightedItem, useItem } = useItemList({ - onSelect: (item) => { - router.push(`/post/${item.value.id}`) - onSelect() - }, - }) - - React.useEffect(() => { - function handleKeydownEvent(event: KeyboardEvent) { - const { code } = event - - if (code === 'ArrowUp' || code === 'ArrowDown' || code === 'Enter') { - event.preventDefault() - } - - if (code === 'ArrowUp') { - moveHighlightedItem(-1) - } - - if (code === 'ArrowDown') { - moveHighlightedItem(1) - } - - if (code === 'Enter') { - selectHighlightedItem() - } - } - - document.addEventListener('keydown', handleKeydownEvent) - return () => { - document.removeEventListener('keydown', handleKeydownEvent) - } - }, [moveHighlightedItem, selectHighlightedItem, router]) - - return ( -
    -
    -
    - -
    -
    -
    - { - setValue(event.target.value) - }} - /> -
    - {feedQuery.data && - (feedQuery.data.length > 0 ? ( -
      - {feedQuery.data.map((result) => ( - - ))} -
    - ) : ( -
    - No results. Try something else -
    - ))} - {feedQuery.isError && ( -
    - Error: {feedQuery.error.message} -
    - )} -
    - ) -} - -export function SearchDialog({ isOpen, onClose }: SearchDialogProps) { - return ( - - -
    - - - - - -
    - {isOpen ? ( - - ) : ( -
    - )} -
    - -
    -
    -
    - ) -} diff --git a/components/textarea.tsx b/components/textarea.tsx deleted file mode 100644 index bdf3086..0000000 --- a/components/textarea.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { classNames } from '@/lib/classnames' -import * as React from 'react' - -export type TextareaOwnProps = { - label?: string -} - -type TextareaProps = TextareaOwnProps & - React.ComponentPropsWithoutRef<'textarea'> - -export const Textarea = React.forwardRef( - ({ label, id, name, className, ...rest }, forwardedRef) => { - return ( -
    - {label && ( - - )} -