Style improvements, re-enable themes, bump next

This commit is contained in:
Max Leiter 2022-12-25 20:00:26 -08:00
parent e41dc292b8
commit b848aa9e40
36 changed files with 385 additions and 425 deletions

View file

@ -55,7 +55,6 @@ const Auth = ({
message: res.error message: res.error
}) })
} else { } else {
console.log("res", res)
startTransition(() => { startTransition(() => {
router.push("/new") router.push("/new")
router.refresh() router.refresh()

View file

@ -2,7 +2,6 @@ import { memo, useEffect, useState } from "react"
import styles from "./preview.module.css" import styles from "./preview.module.css"
import "@styles/markdown.css" import "@styles/markdown.css"
import "@styles/syntax.css" import "@styles/syntax.css"
import Skeleton from "@components/skeleton"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
type Props = { type Props = {

View file

@ -311,7 +311,7 @@ const Post = ({
enableTabLoop={false} enableTabLoop={false}
minDate={new Date()} minDate={new Date()}
/> />
<ButtonDropdown iconHeight={40}> <ButtonDropdown>
<Button <Button
height={40} height={40}
width={251} width={251}

View file

@ -13,19 +13,22 @@ export const PostButtons = ({
files, files,
loading, loading,
postId, postId,
parentId parentId,
}: { }: {
title: string title: string
files?: Pick<PostWithFiles, "files">["files"] files?: Pick<PostWithFiles, "files">["files"]
loading?: boolean loading?: boolean
postId?: string postId?: string
parentId?: string parentId?: string
visibility?: string
authorId?: string
}) => { }) => {
const router = useRouter() const router = useRouter()
const downloadClick = async () => { const downloadClick = async () => {
if (!files?.length) return if (!files?.length) return
const downloadZip = (await import("client-zip")).downloadZip const downloadZip = (await import("client-zip")).downloadZip
const blob = await downloadZip( const blob = await downloadZip(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
files.map((file: any) => { files.map((file: any) => {
return { return {
name: file.title, name: file.title,
@ -52,11 +55,7 @@ export const PostButtons = ({
return ( return (
<span className={styles.buttons}> <span className={styles.buttons}>
<ButtonGroup verticalIfMobile> <ButtonGroup verticalIfMobile>
<Button <Button iconLeft={<Edit />} onClick={editACopy}>
iconLeft={<Edit />}
onClick={editACopy}
style={{ textTransform: "none" }}
>
Edit a Copy Edit a Copy
</Button> </Button>
{parentId && ( {parentId && (
@ -64,11 +63,7 @@ export const PostButtons = ({
View Parent View Parent
</Button> </Button>
)} )}
<Button <Button onClick={downloadClick} iconLeft={<Archive />}>
onClick={downloadClick}
iconLeft={<Archive />}
style={{ textTransform: "none" }}
>
Download as ZIP Archive Download as ZIP Archive
</Button> </Button>
<FileDropdown loading={loading} files={files || []} /> <FileDropdown loading={loading} files={files || []} />

View file

@ -34,7 +34,9 @@ export const PostTitle = ({
{title}{" "} {title}{" "}
<span style={{ color: "var(--gray)" }}> <span style={{ color: "var(--gray)" }}>
by{" "} by{" "}
<Link href={`/author/${authorId}`}>{displayName || "anonymous"}</Link> <Link colored href={`/author/${authorId}`}>
{displayName || "anonymous"}
</Link>
</span> </span>
</h1> </h1>
{!loading && ( {!loading && (

View file

@ -17,6 +17,13 @@
display: inline-block; display: inline-block;
} }
.titleWithDropdown {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.title { .title {
flex-direction: column; flex-direction: column;

View file

@ -5,8 +5,7 @@ import DocumentComponent from "./view-document"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import PasswordModalPage from "./password-modal-wrapper" import PasswordModalPage from "./password-modal-wrapper"
import { File, PostWithFilesAndAuthor } from "@lib/server/prisma" import { PostWithFilesAndAuthor } from "@lib/server/prisma"
import { useSession } from "next-auth/react"
type Props = { type Props = {
post: string | PostWithFilesAndAuthor post: string | PostWithFilesAndAuthor
@ -14,66 +13,46 @@ type Props = {
isAuthor?: boolean isAuthor?: boolean
} }
const PostFiles = ({ const PostFiles = ({ post: _initialPost }: Props) => {
post: _initialPost,
isAuthor: isAuthorFromServer
}: Props) => {
const { data: session, status } = useSession()
const isLoading = status === "loading"
const initialPost = const initialPost =
typeof _initialPost === "string" typeof _initialPost === "string"
? (JSON.parse(_initialPost) as PostWithFilesAndAuthor) ? (JSON.parse(_initialPost) as PostWithFilesAndAuthor)
: _initialPost : _initialPost
const [post, setPost] = useState<PostWithFilesAndAuthor>(initialPost) const [post, setPost] = useState<PostWithFilesAndAuthor>(initialPost)
const isProtected = initialPost.visibility === "protected"
// We generate public and unlisted posts at build time, so we can't use
// the session to determine if the user is the author on the server. We need to check
// the post's authorId against the session's user id.
const isAuthor = isAuthorFromServer
? true
: session?.user?.id === post?.authorId
const router = useRouter() const router = useRouter()
useEffect(() => { if (post.expiresAt) {
if (post.expiresAt) { if (new Date(post.expiresAt) < new Date()) {
if (new Date(post.expiresAt) < new Date()) { router.push("/expired")
if (!isAuthor) {
router.push("/expired")
}
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
if (!isAuthor && expirationDate < new Date()) {
router.push("/expired")
}
let interval: NodeJS.Timer | null = null
if (post.expiresAt) {
interval = setInterval(() => {
const expirationDate = new Date(
post.expiresAt ? post.expiresAt : ""
)
if (expirationDate < new Date()) {
if (!isAuthor) {
router.push("/expired")
}
clearInterval(interval!)
}
}, 4000)
}
return () => {
if (interval) clearInterval(interval)
}
}
} }
}, [isAuthor, post.expiresAt, router])
if (isLoading) {
return <DocumentComponent skeleton={true} initialTab={"preview"} />
} }
if (isProtected) { useEffect(() => {
return <PasswordModalPage setPost={setPost} postId={post.id} /> let interval: NodeJS.Timer | null = null
if (post.expiresAt) {
interval = setInterval(() => {
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
if (expirationDate < new Date()) {
router.push("/expired")
if (interval) clearInterval(interval)
}
}, 4000)
}
return () => {
if (interval) clearInterval(interval)
}
}, [post.expiresAt, router])
const isProtected = post.visibility === "protected"
const hasFetched = post.files !== undefined
if (isProtected && !hasFetched) {
return (
<PasswordModalPage
authorId={post.authorId}
setPost={setPost}
postId={post.id}
/>
)
} }
return ( return (

View file

@ -3,20 +3,27 @@
import { Post, PostWithFilesAndAuthor } from "@lib/server/prisma" import { Post, PostWithFilesAndAuthor } from "@lib/server/prisma"
import PasswordModal from "@components/password-modal" import PasswordModal from "@components/password-modal"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useState } from "react" import { useCallback, useEffect, useState } from "react"
import { useToasts } from "@components/toasts" import { useToasts } from "@components/toasts"
import { useSession } from "next-auth/react"
type Props = { type Props = {
setPost: (post: PostWithFilesAndAuthor) => void setPost: (post: PostWithFilesAndAuthor) => void
postId: Post["id"] postId: Post["id"]
authorId: Post["authorId"]
} }
const PasswordModalPage = ({ setPost, postId }: Props) => { const PasswordModalPage = ({ setPost, postId, authorId }: Props) => {
const router = useRouter() const router = useRouter()
const { setToast } = useToasts() const { setToast } = useToasts()
const { data: session, status } = useSession()
const isAuthor =
status === "loading"
? undefined
: session?.user && session?.user?.id === authorId
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true)
const onSubmit = async (password: string) => { const onSubmit = useCallback(async (password: string) => {
const res = await fetch(`/api/post/${postId}?password=${password}`, { const res = await fetch(`/api/post/${postId}?password=${password}`, {
method: "GET", method: "GET",
headers: { headers: {
@ -44,13 +51,24 @@ const PasswordModalPage = ({ setPost, postId }: Props) => {
setPost(data) setPost(data)
} }
} }
} }, [postId, setPost, setToast])
const onClose = () => { const onClose = () => {
setIsPasswordModalOpen(false) setIsPasswordModalOpen(false)
router.push("/") router.push("/")
} }
useEffect(() => {
if (isAuthor) {
onSubmit("author")
setToast({
message:
"You're the author of this post, so you automatically have access to it.",
type: "default"
})
}
}, [isAuthor, onSubmit, setToast])
return ( return (
<PasswordModal <PasswordModal
creating={false} creating={false}

View file

@ -68,7 +68,7 @@ const Document = ({ skeleton, ...props }: Props) => {
<Skeleton width={"100%"} height={36} /> <Skeleton width={"100%"} height={36} />
</div> </div>
<div className={styles.documentContainer}> <div className={styles.documentContainer}>
<Skeleton width={145} height={36} borderRadius={"4px 4px 0 0"} /> <Skeleton width={175} height={36} borderRadius={"4px 4px 0 0"} />
<Skeleton <Skeleton
width={"100%"} width={"100%"}
height={350} height={350}

View file

@ -11,6 +11,7 @@
} }
.controls { .controls {
height: 40px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }

View file

@ -1,111 +1,10 @@
import { notFound, redirect } from "next/navigation" import { PostWithFilesAndAuthor } from "@lib/server/prisma"
import { getPostById, Post, PostWithFilesAndAuthor } from "@lib/server/prisma"
import { getCurrentUser } from "@lib/server/session"
import ScrollToTop from "@components/scroll-to-top" import ScrollToTop from "@components/scroll-to-top"
import { title } from "process" import { title } from "process"
import { PostButtons } from "./components/header/post-buttons" import { PostButtons } from "./components/header/post-buttons"
import styles from "./layout.module.css" import styles from "./layout.module.css"
import { PostTitle } from "./components/header/title" import { PostTitle } from "./components/header/title"
import VisibilityControl from "@components/badges/visibility-control" import { getPost } from "./page"
export type PostProps = {
post: Post
isProtected?: boolean
}
// export async function generateStaticParams() {
// const posts = await getAllPosts({
// where: {
// visibility: {
// equals: "public"
// }
// }
// })
// return posts.map((post) => ({
// id: post.id
// }))
// }
// export const dynamic = 'error';
const getPost = async (id: string) => {
const [post, user] = await Promise.all([
getPostById(id, {
select: {
visibility: true,
authorId: true,
title: true,
description: true,
id: true,
createdAt: true,
expiresAt: true,
parentId: true,
author: {
select: {
displayName: true,
image: true
}
},
files: {
select: {
id: true,
content: true,
updatedAt: true,
title: true
}
}
}
}).then((post) => {
if (!post) {
return notFound()
}
return post as PostWithFilesAndAuthor
}),
getCurrentUser()
])
const isAuthorOrAdmin = user?.id === post?.authorId || user?.role === "admin"
if (post.visibility === "public" || post.visibility === "unlisted") {
return { post, isAuthorOrAdmin }
}
if (post.visibility === "private" && !isAuthorOrAdmin) {
return redirect("/signin")
}
if (post.visibility === "protected" && !isAuthorOrAdmin) {
return {
// TODO: remove this. It's temporary to appease typescript
post: {
visibility: "protected",
id: post.id,
files: [],
parentId: "",
title: "",
createdAt: "",
expiresAt: "",
author: {
displayName: ""
},
description: "",
authorId: ""
},
isAuthor: isAuthorOrAdmin
}
}
// if expired
if (post.expiresAt && !isAuthorOrAdmin) {
const expirationDate = new Date(post.expiresAt)
if (expirationDate < new Date()) {
return redirect("/expired")
}
}
return { post, isAuthor: isAuthorOrAdmin }
}
export default async function PostLayout({ export default async function PostLayout({
children, children,
@ -116,38 +15,41 @@ export default async function PostLayout({
} }
children: React.ReactNode children: React.ReactNode
}) { }) {
const { post, isAuthor } = await getPost(params.id) const { post } = (await getPost(params.id)) as {
post: PostWithFilesAndAuthor
}
return ( return (
<div className={styles.root}> <div className={styles.root}>
<div className={styles.header}> <div className={styles.header}>
<PostButtons {/* post.title is not set when the post is protected */}
parentId={post.parentId || undefined} {post.title && (
postId={post.id} <PostButtons
files={post.files} parentId={post.parentId || undefined}
title={title} postId={post.id}
/> files={post.files}
<PostTitle title={title}
title={post.title} authorId={post.authorId}
createdAt={post.createdAt.toString()} visibility={post.visibility}
expiresAt={post.expiresAt?.toString()} />
// displayName is an optional param )}
displayName={post.author?.displayName || undefined} {post.title && (
visibility={post.visibility} <PostTitle
authorId={post.authorId} title={post.title}
/> createdAt={post.createdAt.toString()}
expiresAt={post.expiresAt?.toString()}
// displayName is an optional param
displayName={post.author?.displayName || undefined}
visibility={post.visibility}
authorId={post.authorId}
/>
)}
</div> </div>
{post.description && ( {post.description && (
<div> <div>
<p>{post.description}</p> <p>{post.description}</p>
</div> </div>
)} )}
{isAuthor && (
<span className={styles.controls}>
<VisibilityControl postId={post.id} visibility={post.visibility} />
</span>
)}
<ScrollToTop /> <ScrollToTop />
{children} {children}
</div> </div>

View file

@ -1,5 +1,3 @@
import { PostButtons } from "./components/header/post-buttons"
import { PostTitle } from "./components/header/title"
import DocumentComponent from "./components/post-files/view-document" import DocumentComponent from "./components/post-files/view-document"
import styles from "./layout.module.css" import styles from "./layout.module.css"
@ -7,8 +5,6 @@ export default function PostLoading() {
return ( return (
<> <>
<div className={styles.header}> <div className={styles.header}>
<PostButtons loading title="" />
<PostTitle title="" loading />
<DocumentComponent skeleton initialTab="preview" /> <DocumentComponent skeleton initialTab="preview" />
</div> </div>
</> </>

View file

@ -1,86 +1,71 @@
import VisibilityControl from "@components/badges/visibility-control"
import { getPostById } from "@lib/server/prisma" import { getPostById } from "@lib/server/prisma"
import { getCurrentUser } from "@lib/server/session" import { getCurrentUser } from "@lib/server/session"
import { notFound, redirect } from "next/navigation" import { notFound, redirect } from "next/navigation"
import { cache } from "react"
import PostFiles from "./components/post-files" import PostFiles from "./components/post-files"
const getPost = async (id: string) => { export const getPost = cache(async (id: string) => {
const [post, user] = await Promise.all([ const post = await getPostById(id, {
getPostById(id, { select: {
select: { visibility: true,
visibility: true, authorId: true,
authorId: true, title: true,
title: true, description: true,
description: true, id: true,
id: true, createdAt: true,
createdAt: true, expiresAt: true,
expiresAt: true, parentId: true,
parentId: true, author: {
author: { select: {
select: { displayName: true,
displayName: true, image: true
image: true }
} },
}, files: {
files: { select: {
select: { id: true,
id: true, content: true,
content: true, updatedAt: true,
updatedAt: true, title: true,
title: true, html: true
html: true
}
} }
} }
}).then((post) => {
if (!post) {
return notFound()
}
return post
}),
getCurrentUser()
])
const isAuthorOrAdmin = user?.id === post?.authorId || user?.role === "admin"
// if expired
if (post.expiresAt && !isAuthorOrAdmin) {
const expirationDate = new Date(post.expiresAt)
if (expirationDate < new Date()) {
return redirect("/expired")
} }
})
if (!post) {
return notFound()
}
if (post.expiresAt && new Date(post.expiresAt) < new Date()) {
return redirect("/expired")
} }
if (post.visibility === "public" || post.visibility === "unlisted") { if (post.visibility === "public" || post.visibility === "unlisted") {
return { post, isAuthorOrAdmin } return { post }
} }
if (post.visibility === "private" && !isAuthorOrAdmin) { if (post.visibility === "private") {
return redirect("/signin") const user = await getCurrentUser()
if (user?.id === post.authorId || user.role === "admin") {
return { post }
}
return redirect("/new")
} }
if (post.visibility === "protected" && !isAuthorOrAdmin) { if (post.visibility === "protected") {
return { return {
// TODO: remove this. It's temporary to appease typescript
post: { post: {
visibility: "protected", visibility: "protected",
id: post.id, authorId: post.authorId,
files: [], id: post.id
parentId: "", }
title: "",
createdAt: "",
expiresAt: "",
author: {
displayName: ""
},
description: "",
authorId: ""
},
isAuthor: isAuthorOrAdmin
} }
} }
return { post, isAuthor: isAuthorOrAdmin } return { post }
} })
export default async function PostPage({ export default async function PostPage({
params params
@ -89,7 +74,16 @@ export default async function PostPage({
id: string id: string
} }
}) { }) {
const { post, isAuthor } = await getPost(params.id) const { post } = await getPost(params.id)
const stringifiedPost = JSON.stringify(post) const stringifiedPost = JSON.stringify(post)
return <PostFiles post={stringifiedPost} isAuthor={isAuthor} /> return (
<>
<PostFiles post={stringifiedPost} />
<VisibilityControl
authorId={post.authorId}
postId={post.id}
visibility={post.visibility}
/>
</>
)
} }

View file

@ -43,10 +43,10 @@ export default async function UserPage({
return ( return (
<Image <Image
src={user.image} src={user.image}
alt="User avatar" alt=""
className="w-12 h-12 rounded-full"
width={48} width={48}
height={48} height={48}
style={{ borderRadius: "50%", height: 32, width: 32 }}
/> />
) )
} }

View file

@ -37,8 +37,11 @@ const ExpirationBadge = ({
}, [expirationDate]) }, [expirationDate])
const isExpired = useMemo(() => { const isExpired = useMemo(() => {
return timeUntilString === "in 0 seconds" if (!expirationDate) {
}, [timeUntilString]) return false
}
return expirationDate < new Date()
}, [expirationDate])
if (!expirationDate) { if (!expirationDate) {
return null return null
@ -49,7 +52,9 @@ const ExpirationBadge = ({
<Tooltip <Tooltip
content={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`} content={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}
> >
<>{isExpired ? "Expired" : `Expires ${timeUntilString}`}</> <span suppressHydrationWarning>
{isExpired ? "Expired" : `Expires ${timeUntilString}`}
</span>
</Tooltip> </Tooltip>
</Badge> </Badge>
) )

View file

@ -6,13 +6,17 @@ import ButtonGroup from "@components/button-group"
import Button from "@components/button" import Button from "@components/button"
import { useToasts } from "@components/toasts" import { useToasts } from "@components/toasts"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
import { useSession } from "next-auth/react"
type Props = { type Props = {
authorId: string
postId: string postId: string
visibility: string visibility: string
} }
const VisibilityControl = ({ postId, visibility: postVisibility }: Props) => { const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Props) => {
const { data: session } = useSession()
const isAuthor = session?.user && session?.user?.id === authorId
const [visibility, setVisibility] = useState<string>(postVisibility) const [visibility, setVisibility] = useState<string>(postVisibility)
const [isSubmitting, setSubmitting] = useState<string | null>() const [isSubmitting, setSubmitting] = useState<string | null>()
@ -66,9 +70,16 @@ const VisibilityControl = ({ postId, visibility: postVisibility }: Props) => {
const submitPassword = (password: string) => onSubmit("protected", password) const submitPassword = (password: string) => onSubmit("protected", password)
if (!isAuthor) {
return null
}
return ( return (
<> <>
<ButtonGroup verticalIfMobile> <ButtonGroup style={{
maxWidth: 600,
margin: "var(--gap) auto",
}}>
<Button <Button
disabled={visibility === "private"} disabled={visibility === "private"}
onClick={() => onSubmit("private")} onClick={() => onSubmit("private")}

View file

@ -5,10 +5,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import { ArrowDown } from "react-feather" import { ArrowDown } from "react-feather"
type Props = { type Props = {
type?: "primary" | "secondary" type?: "primary" | "secondary"
loading?: boolean height?: number | string
disabled?: boolean
className?: string
iconHeight?: number
} }
type Attrs = Omit<React.HTMLAttributes<any>, keyof Props> type Attrs = Omit<React.HTMLAttributes<any>, keyof Props>
@ -16,14 +13,14 @@ type ButtonDropdownProps = Props & Attrs
const ButtonDropdown: React.FC< const ButtonDropdown: React.FC<
React.PropsWithChildren<ButtonDropdownProps> React.PropsWithChildren<ButtonDropdownProps>
> = ({ type, className, disabled, loading, iconHeight = 24, ...props }) => { > = ({ type, ...props }) => {
if (!Array.isArray(props.children)) { if (!Array.isArray(props.children)) {
return null return null
} }
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<div className={styles.dropdown}> <div className={styles.dropdown} style={{ height: props.height }}>
{props.children[0]} {props.children[0]}
<DropdownMenu.Trigger <DropdownMenu.Trigger
style={{ style={{

View file

@ -9,7 +9,7 @@
.card .content { .card .content {
padding: var(--gap-half); padding: var(--gap);
width: 100%; width: 100%;
height: auto; height: auto;
} }

View file

@ -7,7 +7,7 @@ import { usePathname } from "next/navigation"
import { signOut, useSession } from "next-auth/react" import { signOut, useSession } from "next-auth/react"
import Button from "@components/button" import Button from "@components/button"
import clsx from "clsx" import clsx from "clsx"
import { useTheme } from "@wits/next-themes" import { useTheme } from "next-themes"
import { import {
GitHub, GitHub,
Home, Home,

View file

@ -18,8 +18,8 @@ const PasswordModal = ({
onSubmit: onSubmitAfterVerify, onSubmit: onSubmitAfterVerify,
creating creating
}: Props) => { }: Props) => {
const [password, setPassword] = useState<string>() const [password, setPassword] = useState<string>("")
const [confirmPassword, setConfirmPassword] = useState<string>() const [confirmPassword, setConfirmPassword] = useState<string>("")
const [error, setError] = useState<string>() const [error, setError] = useState<string>()
const onSubmit = () => { const onSubmit = () => {
@ -60,6 +60,12 @@ const PasswordModal = ({
: "Enter the password to access the post"} : "Enter the password to access the post"}
</Dialog.Description> </Dialog.Description>
<fieldset className={styles.fieldset}> <fieldset className={styles.fieldset}>
{!error && creating && (
<Note type="warning">
This doesn&apos;t protect your post from the server
administrator.
</Note>
)}
{error && <Note type="error">{error}</Note>} {error && <Note type="error">{error}</Note>}
<Input <Input
width={"100%"} width={"100%"}
@ -79,15 +85,11 @@ const PasswordModal = ({
onChange={(e) => setConfirmPassword(e.currentTarget.value)} onChange={(e) => setConfirmPassword(e.currentTarget.value)}
/> />
)} )}
{!error && creating && (
<Note type="warning">
This doesn&apos;t protect your post from the server
administrator.
</Note>
)}
</fieldset> </fieldset>
<Button onClick={onClose}>Cancel</Button> <footer className={styles.footer}>
<Button onClick={onSubmit}>Submit</Button> <Button onClick={onClose}>Cancel</Button>
<Button onClick={onSubmit}>Submit</Button>
</footer>
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>

View file

@ -34,7 +34,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--gap); gap: var(--gap);
margin-bottom: var(--gap-half); margin-bottom: var(--gap);
} }
.content:focus { .content:focus {
@ -48,6 +48,12 @@
gap: var(--gap-half); gap: var(--gap-half);
} }
.footer {
display: flex;
justify-content: flex-end;
gap: var(--gap-half);
}
@keyframes overlayShow { @keyframes overlayShow {
from { from {
opacity: 0; opacity: 0;

View file

@ -16,13 +16,17 @@ type Props = {
morePosts?: boolean morePosts?: boolean
userId?: string userId?: string
hideSearch?: boolean hideSearch?: boolean
hideActions?: boolean
isOwner?: boolean
} }
const PostList = ({ const PostList = ({
morePosts, morePosts,
initialPosts: initialPostsMaybeJSON, initialPosts: initialPostsMaybeJSON,
userId, userId,
hideSearch hideSearch,
hideActions,
isOwner
}: Props) => { }: Props) => {
const initialPosts = const initialPosts =
typeof initialPostsMaybeJSON === "string" typeof initialPostsMaybeJSON === "string"
@ -156,6 +160,8 @@ const PostList = ({
deletePost={deletePost(post.id)} deletePost={deletePost(post.id)}
post={post} post={post}
key={post.id} key={post.id}
hideActions={hideActions}
isOwner={isOwner}
/> />
) )
})} })}

View file

@ -3,6 +3,12 @@
justify-content: space-between; justify-content: space-between;
} }
.titleText {
display: flex;
gap: var(--gap-half);
align-items: center;
}
.badges { .badges {
display: flex; display: flex;
gap: var(--gap-half); gap: var(--gap-half);
@ -33,6 +39,11 @@
.title h3 { .title h3 {
margin: 0; margin: 0;
} }
.titleText {
flex-direction: column;
align-items: flex-start;
}
} }
.files { .files {

View file

@ -24,12 +24,14 @@ import { codeFileExtensions } from "@lib/constants"
// TODO: isOwner should default to false so this can be used generically // TODO: isOwner should default to false so this can be used generically
const ListItem = ({ const ListItem = ({
post, post,
isOwner = true, isOwner,
deletePost deletePost,
hideActions
}: { }: {
post: PostWithFiles post: PostWithFiles
isOwner?: boolean isOwner?: boolean
deletePost: () => void deletePost: () => void
hideActions?: boolean
}) => { }) => {
const router = useRouter() const router = useRouter()
@ -69,33 +71,45 @@ const ListItem = ({
<Card style={{ overflowY: "scroll" }}> <Card style={{ overflowY: "scroll" }}>
<> <>
<div className={styles.title}> <div className={styles.title}>
<h3 style={{ display: "inline-block", margin: 0 }}> <span className={styles.titleText}>
<Link <h3 style={{ display: "inline-block", margin: 0 }}>
colored <Link
style={{ marginRight: "var(--gap)" }} colored
href={`/post/${post.id}`} style={{ marginRight: "var(--gap)" }}
> href={`/post/${post.id}`}
{post.title} >
</Link> {post.title}
</h3> </Link>
{isOwner && ( </h3>
<span className={styles.buttons}> <div className={styles.badges}>
{post.parentId && ( <VisibilityBadge visibility={post.visibility} />
<Tooltip content={"View parent"}> <Badge type="secondary">
<Button {post.files?.length === 1
iconRight={<ArrowUpCircle />} ? "1 file"
onClick={viewParentClick} : `${post.files?.length || 0} files`}
height={38} </Badge>
/> <CreatedAgoBadge createdAt={post.createdAt} />
</Tooltip> <ExpirationBadge postExpirationDate={post.expiresAt} />
)} </div>
<Tooltip content={"Make a copy"}> </span>
{!hideActions ? <span className={styles.buttons}>
{post.parentId && (
<Tooltip content={"View parent"}>
<Button <Button
iconRight={<Edit />} iconRight={<ArrowUpCircle />}
onClick={editACopy} onClick={viewParentClick}
height={38} height={38}
/> />
</Tooltip> </Tooltip>
)}
<Tooltip content={"Make a copy"}>
<Button
iconRight={<Edit />}
onClick={editACopy}
height={38}
/>
</Tooltip>
{isOwner && (
<Tooltip content={"Delete"}> <Tooltip content={"Delete"}>
<Button <Button
iconRight={<Trash />} iconRight={<Trash />}
@ -103,35 +117,28 @@ const ListItem = ({
height={38} height={38}
/> />
</Tooltip> </Tooltip>
</span> )}
)} </span> : null}
</div> </div>
{post.description && ( {post.description && (
<p className={styles.oneline}>{post.description}</p> <p className={styles.oneline}>{post.description}</p>
)} )}
<div className={styles.badges}>
<VisibilityBadge visibility={post.visibility} />
<Badge type="secondary">
{post.files?.length === 1
? "1 file"
: `${post.files?.length || 0} files`}
</Badge>
<CreatedAgoBadge createdAt={post.createdAt} />
<ExpirationBadge postExpirationDate={post.expiresAt} />
</div>
</> </>
<ul className={styles.files}> <ul className={styles.files}>
{post?.files?.map( {post?.files?.map(
(file: Pick<PostWithFiles, "files">["files"][0]) => { (file: Pick<PostWithFiles, "files">["files"][0]) => {
return ( return (
<li key={file.id}> <li key={file.id}>
<Link colored href={`/post/${post.id}#${file.title}`} style={{ <Link
display: "flex", colored
alignItems: "center" href={`/post/${post.id}#${file.title}`}
}}> style={{
`` {getIconFromFilename(file.title)} display: "flex",
alignItems: "center"
}}
>
{getIconFromFilename(file.title)}
{file.title || "Untitled file"} {file.title || "Untitled file"}
</Link> </Link>
</li> </li>

View file

@ -8,12 +8,6 @@
padding: 0.5rem 0; padding: 0.5rem 0;
} }
.container ul li::before {
content: "";
padding: 0;
margin: 0;
}
.postHeader { .postHeader {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -25,6 +25,7 @@ export default async function Mine() {
userId={userId} userId={userId}
morePosts={hasMore} morePosts={hasMore}
initialPosts={stringifiedPosts} initialPosts={stringifiedPosts}
isOwner={true}
/> />
) )
} }

View file

@ -11,7 +11,6 @@ const getWelcomeData = async () => {
} }
export default async function Page() { export default async function Page() {
const { content, rendered, title } = await getWelcomeData()
const getPostsPromise = getAllPosts({ const getPostsPromise = getAllPosts({
select: { select: {
id: true, id: true,
@ -21,18 +20,24 @@ export default async function Page() {
select: { select: {
name: true name: true
} }
} },
visibility: true,
files: {
select: {
id: true,
title: true
}
},
authorId: true
}, },
where: { where: {
deletedAt: null, visibility: "public"
expiresAt: {
gt: new Date()
}
}, },
orderBy: { orderBy: {
createdAt: "desc" createdAt: "desc"
} }
}) })
const { content, rendered, title } = await getWelcomeData()
return ( return (
<div <div
@ -79,9 +84,10 @@ async function PublicPostList({
userId={undefined} userId={undefined}
morePosts={false} morePosts={false}
initialPosts={JSON.stringify(posts)} initialPosts={JSON.stringify(posts)}
hideActions
hideSearch hideSearch
/> />
) )
} }
export const revalidate = 30 export const revalidate = 60

View file

@ -2,12 +2,15 @@
import * as RadixTooltip from "@radix-ui/react-tooltip" import * as RadixTooltip from "@radix-ui/react-tooltip"
import { SessionProvider } from "next-auth/react" import { SessionProvider } from "next-auth/react"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return ( return (
<SessionProvider> <SessionProvider>
<RadixTooltip.Provider delayDuration={200}> <RadixTooltip.Provider delayDuration={200}>
{children} <ThemeProvider enableSystem defaultTheme="dark">
{children}
</ThemeProvider>
</RadixTooltip.Provider> </RadixTooltip.Provider>
</SessionProvider> </SessionProvider>
) )

View file

@ -10,9 +10,8 @@ import styles from "./profile.module.css"
const Profile = () => { const Profile = () => {
const { data: session } = useSession() const { data: session } = useSession()
const [name, setName] = useState<string>(session?.user.displayName || "") const [name, setName] = useState<string>(session?.user.name || "")
const [submitting, setSubmitting] = useState<boolean>(false) const [submitting, setSubmitting] = useState<boolean>(false)
const { setToast } = useToasts() const { setToast } = useToasts()
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {

View file

@ -42,7 +42,7 @@
--darkest-gray: #efefef; --darkest-gray: #efefef;
--article-color: #eaeaea; --article-color: #eaeaea;
--header-bg: rgba(19, 20, 21, 0.45); --header-bg: rgba(19, 20, 21, 0.45);
--gray-alpha: rgba(255, 255, 255, 0.5); --gray-alpha: rgba(19, 20, 21, 0.6);
--selection: rgba(255, 255, 255, 0.99); --selection: rgba(255, 255, 255, 0.99);
--border: var(--light-gray); --border: var(--light-gray);
--warning: #ff6700; --warning: #ff6700;

View file

@ -6,7 +6,7 @@ const nextConfig = {
experimental: { experimental: {
// esmExternals: true, // esmExternals: true,
appDir: true, appDir: true,
serverComponentsExternalPackages: ["prisma", "@prisma/client"], serverComponentsExternalPackages: ["prisma", "@prisma/client"]
}, },
output: "standalone", output: "standalone",
rewrites() { rewrites() {
@ -20,6 +20,9 @@ const nextConfig = {
destination: "/" destination: "/"
} }
] ]
},
images: {
domains: ["avatars.githubusercontent.com"]
} }
} }

View file

@ -14,8 +14,8 @@
}, },
"dependencies": { "dependencies": {
"@next-auth/prisma-adapter": "^1.0.5", "@next-auth/prisma-adapter": "^1.0.5",
"@next/eslint-plugin-next": "13.0.7-canary.4", "@next/eslint-plugin-next": "13.1.1-canary.1",
"@next/font": "13.0.8-canary.0", "@next/font": "13.1.1-canary.1",
"@prisma/client": "^4.7.1", "@prisma/client": "^4.7.1",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.1", "@radix-ui/react-dropdown-menu": "^2.0.1",
@ -28,8 +28,9 @@
"client-zip": "2.2.1", "client-zip": "2.2.1",
"jest": "^29.3.1", "jest": "^29.3.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "13.0.8-canary.0", "next": "13.1.1-canary.1",
"next-auth": "^4.18.6", "next-auth": "^4.18.6",
"next-themes": "^0.2.1",
"prisma": "^4.7.1", "prisma": "^4.7.1",
"react": "18.2.0", "react": "18.2.0",
"react-datepicker": "4.8.0", "react-datepicker": "4.8.0",

View file

@ -61,14 +61,14 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse<unknown>) {
...post ...post
}) })
} else { } else {
return { return res.json({
isProtected: true, isProtected: true,
post: { post: {
id: post.id, id: post.id,
visibility: post.visibility, visibility: post.visibility,
title: post.title authorId: post.authorId,
} }
} })
} }
} }

View file

@ -42,7 +42,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}) })
} }
case "GET": case "GET":
return res.json(currUser) return res.json({
...currUser,
displayName: user.displayName
})
case "DELETE": case "DELETE":
if (currUser?.role !== "admin" && currUser?.id !== id) { if (currUser?.role !== "admin" && currUser?.id !== id) {
return res.status(403).json({ message: "Unauthorized" }) return res.status(403).json({ message: "Unauthorized" })

View file

@ -3,8 +3,8 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@next-auth/prisma-adapter': ^1.0.5 '@next-auth/prisma-adapter': ^1.0.5
'@next/bundle-analyzer': 13.0.7-canary.4 '@next/bundle-analyzer': 13.0.7-canary.4
'@next/eslint-plugin-next': 13.0.7-canary.4 '@next/eslint-plugin-next': 13.1.1-canary.1
'@next/font': 13.0.8-canary.0 '@next/font': 13.1.1-canary.1
'@prisma/client': ^4.7.1 '@prisma/client': ^4.7.1
'@radix-ui/react-dialog': ^1.0.2 '@radix-ui/react-dialog': ^1.0.2
'@radix-ui/react-dropdown-menu': ^2.0.1 '@radix-ui/react-dropdown-menu': ^2.0.1
@ -31,8 +31,9 @@ specifiers:
eslint-config-next: 13.0.3 eslint-config-next: 13.0.3
jest: ^29.3.1 jest: ^29.3.1
lodash.debounce: ^4.0.8 lodash.debounce: ^4.0.8
next: 13.0.8-canary.0 next: 13.1.1-canary.1
next-auth: ^4.18.6 next-auth: ^4.18.6
next-themes: ^0.2.1
next-unused: 0.0.6 next-unused: 0.0.6
prettier: 2.6.2 prettier: 2.6.2
prisma: ^4.7.1 prisma: ^4.7.1
@ -52,8 +53,8 @@ specifiers:
dependencies: dependencies:
'@next-auth/prisma-adapter': 1.0.5_64qbzg5ec56bux2misz3l4u6g4 '@next-auth/prisma-adapter': 1.0.5_64qbzg5ec56bux2misz3l4u6g4
'@next/eslint-plugin-next': 13.0.7-canary.4 '@next/eslint-plugin-next': 13.1.1-canary.1
'@next/font': 13.0.8-canary.0 '@next/font': 13.1.1-canary.1
'@prisma/client': 4.7.1_prisma@4.7.1 '@prisma/client': 4.7.1_prisma@4.7.1
'@radix-ui/react-dialog': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u '@radix-ui/react-dialog': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
'@radix-ui/react-dropdown-menu': 2.0.1_jbvntnid6ohjelon6ccj5dhg2u '@radix-ui/react-dropdown-menu': 2.0.1_jbvntnid6ohjelon6ccj5dhg2u
@ -61,13 +62,14 @@ dependencies:
'@radix-ui/react-tabs': 1.0.1_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-tabs': 1.0.1_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-tooltip': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u '@radix-ui/react-tooltip': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
'@wcj/markdown-to-html': 2.1.2 '@wcj/markdown-to-html': 2.1.2
'@wits/next-themes': 0.2.14_rhfownvlqkszea7w3lnpwl7bzy '@wits/next-themes': 0.2.14_jcrpix7mbfpfu5movksylxa5c4
client-only: 0.0.1 client-only: 0.0.1
client-zip: 2.2.1 client-zip: 2.2.1
jest: 29.3.1_@types+node@17.0.23 jest: 29.3.1_@types+node@17.0.23
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
next: 13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y next: 13.1.1-canary.1_biqbaboplfbrettd7655fr4n2y
next-auth: 4.18.6_rhfownvlqkszea7w3lnpwl7bzy next-auth: 4.18.6_jcrpix7mbfpfu5movksylxa5c4
next-themes: 0.2.1_jcrpix7mbfpfu5movksylxa5c4
prisma: 4.7.1 prisma: 4.7.1
react: 18.2.0 react: 18.2.0
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
@ -790,7 +792,7 @@ packages:
next-auth: ^4 next-auth: ^4
dependencies: dependencies:
'@prisma/client': 4.7.1_prisma@4.7.1 '@prisma/client': 4.7.1_prisma@4.7.1
next-auth: 4.18.6_rhfownvlqkszea7w3lnpwl7bzy next-auth: 4.18.6_jcrpix7mbfpfu5movksylxa5c4
dev: false dev: false
/@next/bundle-analyzer/13.0.7-canary.4: /@next/bundle-analyzer/13.0.7-canary.4:
@ -802,8 +804,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@next/env/13.0.8-canary.0: /@next/env/13.1.1-canary.1:
resolution: {integrity: sha512-IiZM9mAUE9F3p9q/ydZBGlvmleOaMO6fBDBJzvQa4t3Ezg5e3NfGlTO01MTWvKPEKYPeAwFp+tcVh9ivA28+Dw==} resolution: {integrity: sha512-h8DEj69dLJpFUuXOVPQeJ4/X1LbW5mtZSsaS5Xr/pt2VbrRN50eAV/6rMY+l6U6p/4AX1/F5aK4UBzLQJbwFzw==}
dev: false dev: false
/@next/eslint-plugin-next/13.0.3: /@next/eslint-plugin-next/13.0.3:
@ -812,18 +814,18 @@ packages:
glob: 7.1.7 glob: 7.1.7
dev: true dev: true
/@next/eslint-plugin-next/13.0.7-canary.4: /@next/eslint-plugin-next/13.1.1-canary.1:
resolution: {integrity: sha512-jNgarJTQSia+yTcQr6dF9wZhCfCFIXbc0WzSIAfyx4Z8FZjfmTmeVDGL1JHSYlcSmTnJ6o6Z36MsFh/mreuE4g==} resolution: {integrity: sha512-gKabWQJ+Aps/u/lzVC3FoGwbG+r1t3cwoUXPbcpC3igrpBSbkhEoK9u3MYeMlkAUywJ7IvsENWVYqjzRuaso4Q==}
dependencies: dependencies:
glob: 7.1.7 glob: 7.1.7
dev: false dev: false
/@next/font/13.0.8-canary.0: /@next/font/13.1.1-canary.1:
resolution: {integrity: sha512-sdi4WoYMbJdiDev9UICg9dMhhA7I2xaJT07e8DZoSDWWTZaKk5zKNKuQFQoP47461R1WZAIQwIYTq+pVxQ87Gw==} resolution: {integrity: sha512-cygeAS0h5OuWaorcQ6Ry8c/E0fwZEGQfZy7kUjXHVn6DP4sekB2RgR9aNWL3cUoJ35RwjwtsR7K6FufUyzfGag==}
dev: false dev: false
/@next/swc-android-arm-eabi/13.0.8-canary.0: /@next/swc-android-arm-eabi/13.1.1-canary.1:
resolution: {integrity: sha512-U6nayRvWuASLLBwqG4nN9540ako+JEBblN8479BpGvW1F2FyQPUx/zq+WO0b47KPyJI2XNPBIenHGvtSY7yN/Q==} resolution: {integrity: sha512-0McGEjTnNXdBTlghWxkuM07qpKMr44afLeGFpS/zwIlDV7lNOXFzCpyHdJoJsFL4kBJgfbyCi8aamnhwqlwZxA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
@ -831,8 +833,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-android-arm64/13.0.8-canary.0: /@next/swc-android-arm64/13.1.1-canary.1:
resolution: {integrity: sha512-GtUW5CCIfN1FUln+pRm0rAWe8k957rcKhYDPGBrfr+jaKvUgjI4NgMcXRJ0R83j+vcM4+DIhIkIO+OYQ1vU4RA==} resolution: {integrity: sha512-XCmPqmhtsc52lv0Qs/maThRrQpHMRK1AqFhgFXfFG9wclbFBtQIUapD/qD7nOlXbch+7RDONbABPf/8pE2T0cQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
@ -840,8 +842,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-arm64/13.0.8-canary.0: /@next/swc-darwin-arm64/13.1.1-canary.1:
resolution: {integrity: sha512-dqUn4ERXHT+g/L+paIi+IhNP3P7HiF95ZBIjQvn++n0IhdT8rRfaQK3ubps/NopL14jHA33J7HnK73vgUBIvwg==} resolution: {integrity: sha512-qz+et20cTetOppH6stlDW171tTo1vG4eHGmXY1Zwa3D/sZPk5IRsqsmpdzxuBsVxdk5x7zaliYZowOlQM2awnw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@ -849,8 +851,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-x64/13.0.8-canary.0: /@next/swc-darwin-x64/13.1.1-canary.1:
resolution: {integrity: sha512-jGaI2idOd2Z0Dvlnz0WYHC+hbqQPIhaso/upJQebknWeu1VsSrwH5oDbCgMBaXLkHO7rMOITWC5FjxdXjSGK6g==} resolution: {integrity: sha512-rPGOUsxturFtSkqtbSis1kBuu0mNzCPibWEMihsM32EzdXeDXJMgl5EP3+RiwGfrawON5lcTEz0r52Zll+0kmw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@ -858,8 +860,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-freebsd-x64/13.0.8-canary.0: /@next/swc-freebsd-x64/13.1.1-canary.1:
resolution: {integrity: sha512-ieM8XwqX9m/frFGpSwrXubzZYPT+ZzOEJsDgCNo3CD0DGHu8hZz1XLRym0Nl2mZAnBlxgENN+RlGwutWKBQMHg==} resolution: {integrity: sha512-tEnpdXSEzltEEbeh32w4eQN1znR35xjX0pMC7leud8XhJvppWwdEqfdOp3OuviPmb8p6LzFqYyknNe710cFy+Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
@ -867,8 +869,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm-gnueabihf/13.0.8-canary.0: /@next/swc-linux-arm-gnueabihf/13.1.1-canary.1:
resolution: {integrity: sha512-/9CnPhcqu/kudpk07zCkApFRUwF0wbwdFm5CqtguZ6yubqhoAV1Wjmrs1gnt+MUBHsVnKRdoGkz6KupQEZqz7g==} resolution: {integrity: sha512-EJdCFjRHVoyDC8Q0N8ULCJ7+5hl4NhaELlu+04cCcgQ3qFZkFZIfTLrXnCT1aa2Y8yyR5FvyBeHgvusL5abqpQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
@ -876,8 +878,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-gnu/13.0.8-canary.0: /@next/swc-linux-arm64-gnu/13.1.1-canary.1:
resolution: {integrity: sha512-KUQs6KdX3lMxJu4ym/jNzotQvbkpXD/ne8KgjUuzTdgw3LYSfEMsTzORj71IR48H5yMDSLGPvCJA+B8FuVzS8Q==} resolution: {integrity: sha512-BRN7Beg1OASa2F7FGYAdYL3O+bA2wFX6ow9QnHD312+JHCf/IKun3FSxSXBaSnc8ZJCnexmSWIz+hewKN1jGQQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -885,8 +887,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-musl/13.0.8-canary.0: /@next/swc-linux-arm64-musl/13.1.1-canary.1:
resolution: {integrity: sha512-bisV2RUrsQMJodK2xGszfqK9G/BuDlqVLeDZVrOENWaZnOVDtrP+WlqrN0vS1r8xn/OepJWKkMnibO4aLCruvw==} resolution: {integrity: sha512-WE1muJmocpSHUHBH02iMOy9RR4Hz7XFM6tjAevY40svXNmGNszhYzsm0MQ+/VnlqP9f9l1/dEiPN6tSbMAlH9A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -894,8 +896,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-gnu/13.0.8-canary.0: /@next/swc-linux-x64-gnu/13.1.1-canary.1:
resolution: {integrity: sha512-X8pcTN7nPZ7gDXb04oGWOS/qPvPaPK5x753AmevQgVa7FwqXQ6IkJeD3sr8ergmu6Fcfr6c4LcnBEQzpIxOxYA==} resolution: {integrity: sha512-aeBiutM8gFndpUkDA6t8DKzD9TcYg48+b7QxuL2XyRJo+47muhNbXaB6y/MwarxwjnsAry0hMs/ycP3lOL7vnw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -903,8 +905,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-musl/13.0.8-canary.0: /@next/swc-linux-x64-musl/13.1.1-canary.1:
resolution: {integrity: sha512-Kg+tsnDmQ21qYfLik3YH+ZOYMmoNyhYqLMZE6qSASA5uN448J1cJUHIdpJxUpidZHtWBV+kTVR2Hw7+We+BiKQ==} resolution: {integrity: sha512-JyJzejDuu68bZj1jrdbgJEIyj0xQy8N0R363T6Rx5/F5Htk2vVzXaP+MkANcWuZjvmH/BHjQc515liiTwQ328Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -912,8 +914,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-arm64-msvc/13.0.8-canary.0: /@next/swc-win32-arm64-msvc/13.1.1-canary.1:
resolution: {integrity: sha512-tde5+ZQFT0+Pr/BKINQ32+8C/AEaZLzU69AvpD7dvbUEJ5fReIiSBPIL1ov3pZYR+EPwl7wFPoj7NLxTU1E8WA==} resolution: {integrity: sha512-y/VxMhjXrTt4fGzrJwdfa6MM2ZauZ0dX20aRGDX/6VeaxO5toBsmXF7cwoDC97C65l93FY/X9vyc75WSLrXFrA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@ -921,8 +923,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-ia32-msvc/13.0.8-canary.0: /@next/swc-win32-ia32-msvc/13.1.1-canary.1:
resolution: {integrity: sha512-CKs0Os7cDKa9GZANf4HbOgkQodjQ2GtJZBBwdZ7OaFMWmWet/0JCkakaF/+EUl0vx0QP83qpIK8LHEkYXxJItg==} resolution: {integrity: sha512-Nk1DdvC+Ocdqnj4Ra+qWJK/PQ68hrWmSg3FXL4I3pooX2IZcUSF8nPFNS0r8V47inTAXbwatcFEKSBRjFBS2ww==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
@ -930,8 +932,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-x64-msvc/13.0.8-canary.0: /@next/swc-win32-x64-msvc/13.1.1-canary.1:
resolution: {integrity: sha512-DTICRWenuqExpO3WmFzkhvYwKgLuPweb3eWiYeybSwHB6ji/cC5ZQjh3AvGbff548Ye8Z1bf4SUAIjdcg0Y/fA==} resolution: {integrity: sha512-/7q6tjUebSaUYTGZRpp4qAmrcL6+tiKfHN5YgW6zpX5MWLEk1DkdnuBjO/jSvCJd0510byBkN6drlzmfTMjzzg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -1802,14 +1804,14 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@wits/next-themes/0.2.14_rhfownvlqkszea7w3lnpwl7bzy: /@wits/next-themes/0.2.14_jcrpix7mbfpfu5movksylxa5c4:
resolution: {integrity: sha512-fHKb/tRcWbYNblGHZtfvAQztDhzUB9d7ZkYOny0BisSPh6EABcsqxKB48ABUQztcmKywlp2zEMkLcSRj/PQBSw==} resolution: {integrity: sha512-fHKb/tRcWbYNblGHZtfvAQztDhzUB9d7ZkYOny0BisSPh6EABcsqxKB48ABUQztcmKywlp2zEMkLcSRj/PQBSw==}
peerDependencies: peerDependencies:
next: '*' next: '*'
react: '*' react: '*'
react-dom: '*' react-dom: '*'
dependencies: dependencies:
next: 13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y next: 13.1.1-canary.1_biqbaboplfbrettd7655fr4n2y
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
dev: false dev: false
@ -5215,7 +5217,7 @@ packages:
dev: true dev: true
optional: true optional: true
/next-auth/4.18.6_rhfownvlqkszea7w3lnpwl7bzy: /next-auth/4.18.6_jcrpix7mbfpfu5movksylxa5c4:
resolution: {integrity: sha512-0TQwbq5X9Jyd1wUVYUoyvHJh4JWXeW9UOcMEl245Er/Y5vsSbyGJHt8M7xjRMzk9mORVMYehoMdERgyiq/jCgA==} resolution: {integrity: sha512-0TQwbq5X9Jyd1wUVYUoyvHJh4JWXeW9UOcMEl245Er/Y5vsSbyGJHt8M7xjRMzk9mORVMYehoMdERgyiq/jCgA==}
engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0} engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0}
peerDependencies: peerDependencies:
@ -5231,7 +5233,7 @@ packages:
'@panva/hkdf': 1.0.2 '@panva/hkdf': 1.0.2
cookie: 0.5.0 cookie: 0.5.0
jose: 4.11.0 jose: 4.11.0
next: 13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y next: 13.1.1-canary.1_biqbaboplfbrettd7655fr4n2y
oauth: 0.9.15 oauth: 0.9.15
openid-client: 5.3.0 openid-client: 5.3.0
preact: 10.11.2 preact: 10.11.2
@ -5241,6 +5243,18 @@ packages:
uuid: 8.3.2 uuid: 8.3.2
dev: false dev: false
/next-themes/0.2.1_jcrpix7mbfpfu5movksylxa5c4:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies:
next: '*'
react: '*'
react-dom: '*'
dependencies:
next: 13.1.1-canary.1_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/next-unused/0.0.6: /next-unused/0.0.6:
resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==} resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==}
hasBin: true hasBin: true
@ -5252,8 +5266,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/next/13.0.8-canary.0_biqbaboplfbrettd7655fr4n2y: /next/13.1.1-canary.1_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-+LP4KZGBp+97TRgYExChOvoONZY1qfJmtB6IjG2HXDshgYpQmsAPEMy9r0rWbvhOveChCJ6sv+yEFAOCNc4yKQ==} resolution: {integrity: sha512-20EeQyfGs9dGUAPrXAod5jay1plcM0itItL/7z9BMczYM55/it8TxS1OPTmseyM9Y8uuybTRoCHeKh6TCI09tg==}
engines: {node: '>=14.6.0'} engines: {node: '>=14.6.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -5270,27 +5284,27 @@ packages:
sass: sass:
optional: true optional: true
dependencies: dependencies:
'@next/env': 13.0.8-canary.0 '@next/env': 13.1.1-canary.1
'@swc/helpers': 0.4.14 '@swc/helpers': 0.4.14
caniuse-lite: 1.0.30001431 caniuse-lite: 1.0.30001431
postcss: 8.4.14 postcss: 8.4.14
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
styled-jsx: 5.1.0_react@18.2.0 styled-jsx: 5.1.1_react@18.2.0
optionalDependencies: optionalDependencies:
'@next/swc-android-arm-eabi': 13.0.8-canary.0 '@next/swc-android-arm-eabi': 13.1.1-canary.1
'@next/swc-android-arm64': 13.0.8-canary.0 '@next/swc-android-arm64': 13.1.1-canary.1
'@next/swc-darwin-arm64': 13.0.8-canary.0 '@next/swc-darwin-arm64': 13.1.1-canary.1
'@next/swc-darwin-x64': 13.0.8-canary.0 '@next/swc-darwin-x64': 13.1.1-canary.1
'@next/swc-freebsd-x64': 13.0.8-canary.0 '@next/swc-freebsd-x64': 13.1.1-canary.1
'@next/swc-linux-arm-gnueabihf': 13.0.8-canary.0 '@next/swc-linux-arm-gnueabihf': 13.1.1-canary.1
'@next/swc-linux-arm64-gnu': 13.0.8-canary.0 '@next/swc-linux-arm64-gnu': 13.1.1-canary.1
'@next/swc-linux-arm64-musl': 13.0.8-canary.0 '@next/swc-linux-arm64-musl': 13.1.1-canary.1
'@next/swc-linux-x64-gnu': 13.0.8-canary.0 '@next/swc-linux-x64-gnu': 13.1.1-canary.1
'@next/swc-linux-x64-musl': 13.0.8-canary.0 '@next/swc-linux-x64-musl': 13.1.1-canary.1
'@next/swc-win32-arm64-msvc': 13.0.8-canary.0 '@next/swc-win32-arm64-msvc': 13.1.1-canary.1
'@next/swc-win32-ia32-msvc': 13.0.8-canary.0 '@next/swc-win32-ia32-msvc': 13.1.1-canary.1
'@next/swc-win32-x64-msvc': 13.0.8-canary.0 '@next/swc-win32-x64-msvc': 13.1.1-canary.1
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
@ -6589,8 +6603,8 @@ packages:
inline-style-parser: 0.1.1 inline-style-parser: 0.1.1
dev: false dev: false
/styled-jsx/5.1.0_react@18.2.0: /styled-jsx/5.1.1_react@18.2.0:
resolution: {integrity: sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==} resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
peerDependencies: peerDependencies:
'@babel/core': '*' '@babel/core': '*'

View file

@ -24,6 +24,5 @@ declare module "next-auth" {
email?: string | null email?: string | null
role?: string | null role?: string | null
id: UserId id: UserId
displayName?: string | null
} }
} }