button style improvements, homepage and navbar refactors

This commit is contained in:
Max Leiter 2023-01-28 23:53:45 -08:00
parent a64cc78eed
commit f3d588c0eb
19 changed files with 214 additions and 201 deletions

View file

@ -132,8 +132,9 @@ function Auth({
<Button width={"100%"} type="submit" loading={submitting}> <Button width={"100%"} type="submit" loading={submitting}>
Sign {signText} Sign {signText}
</Button> </Button>
{isGithubEnabled ? <hr style={{ width: "100%" }} /> : null}
{isGithubEnabled ? ( {isGithubEnabled ? (
<>
<hr style={{ width: "100%" }} />
<Button <Button
type="submit" type="submit"
width="100%" width="100%"
@ -151,6 +152,7 @@ function Auth({
> >
Sign {signText.toLowerCase()} with GitHub Sign {signText.toLowerCase()} with GitHub
</Button> </Button>
</>
) : null} ) : null}
</div> </div>
<div className={styles.formContentSpace}> <div className={styles.formContentSpace}>

View file

@ -8,12 +8,12 @@ import { fetchWithUser } from "src/app/lib/fetch-with-user"
type Props = { type Props = {
height?: number | string height?: number | string
fileId?: string fileId?: string
content?: string
title?: string title?: string
children?: string
} }
function MarkdownPreview({ height = 500, fileId, content = "", title }: Props) { function MarkdownPreview({ height = 500, fileId, title, children }: Props) {
const [preview, setPreview] = useState<string>(content) const [preview, setPreview] = useState<string>(children || "")
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true)
useEffect(() => { useEffect(() => {
async function fetchPost() { async function fetchPost() {
@ -24,7 +24,7 @@ function MarkdownPreview({ height = 500, fileId, content = "", title }: Props) {
? undefined ? undefined
: JSON.stringify({ : JSON.stringify({
title: title || "", title: title || "",
content: content content: children
}) })
const resp = await fetchWithUser(path, { const resp = await fetchWithUser(path, {
@ -43,14 +43,14 @@ function MarkdownPreview({ height = 500, fileId, content = "", title }: Props) {
setIsLoading(false) setIsLoading(false)
} }
fetchPost() fetchPost()
}, [content, fileId, title]) }, [children, fileId, title])
return ( return (
<> <>
{isLoading ? ( {isLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
<StaticPreview preview={preview} height={height} /> <StaticPreview height={height}>{preview}</StaticPreview>
)} )}
</> </>
) )
@ -59,16 +59,16 @@ function MarkdownPreview({ height = 500, fileId, content = "", title }: Props) {
export default memo(MarkdownPreview) export default memo(MarkdownPreview)
export function StaticPreview({ export function StaticPreview({
preview, children,
height = 500 height = 500
}: { }: {
preview: string children: string
height: string | number height: string | number
}) { }) {
return ( return (
<article <article
className={styles.markdownPreview} className={styles.markdownPreview}
dangerouslySetInnerHTML={{ __html: preview }} dangerouslySetInnerHTML={{ __html: children }}
style={{ style={{
height height
}} }}

View file

@ -13,8 +13,8 @@ type Props = RadixTabs.TabsProps & {
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
title?: string title?: string
content?: string staticPreview?: string
preview?: string children: string;
} }
export default function DocumentTabs({ export default function DocumentTabs({
@ -23,8 +23,8 @@ export default function DocumentTabs({
handleOnContentChange, handleOnContentChange,
onPaste, onPaste,
title, title,
content, staticPreview: preview,
preview, children,
...props ...props
}: Props) { }: Props) {
const codeEditorRef = useRef<TextareaMarkdownRef>(null) const codeEditorRef = useRef<TextareaMarkdownRef>(null)
@ -72,7 +72,7 @@ export default function DocumentTabs({
onPaste={onPaste ? onPaste : undefined} onPaste={onPaste ? onPaste : undefined}
ref={codeEditorRef} ref={codeEditorRef}
placeholder="" placeholder=""
value={content} value={children}
onChange={handleOnContentChange} onChange={handleOnContentChange}
// TODO: Textarea should grow to fill parent if height == 100% // TODO: Textarea should grow to fill parent if height == 100%
style={{ flex: 1, minHeight: 350 }} style={{ flex: 1, minHeight: 350 }}
@ -83,9 +83,9 @@ export default function DocumentTabs({
</RadixTabs.Content> </RadixTabs.Content>
<RadixTabs.Content value="preview"> <RadixTabs.Content value="preview">
{isEditing ? ( {isEditing ? (
<Preview height={"100%"} title={title} content={content} /> <Preview height={"100%"} title={title}>{children}</Preview>
) : ( ) : (
<StaticPreview height={"100%"} preview={preview || ""} /> <StaticPreview height={"100%"}>{preview}</StaticPreview>
)} )}
</RadixTabs.Content> </RadixTabs.Content>
</RadixTabs.Root> </RadixTabs.Root>

View file

@ -45,7 +45,6 @@ const PostFiles = ({ post: _initialPost }: Props) => {
const isProtected = post?.visibility === "protected" const isProtected = post?.visibility === "protected"
const hasFetched = post?.files !== undefined const hasFetched = post?.files !== undefined
console.log({ isProtected, hasFetched })
if (isProtected && !hasFetched) { if (isProtected && !hasFetched) {
return ( return (
<PasswordModalWrapper <PasswordModalWrapper

View file

@ -112,7 +112,7 @@ const Document = ({ skeleton, ...props }: Props) => {
{/* Not /api/ because of rewrites defined in next.config.mjs */} {/* Not /api/ because of rewrites defined in next.config.mjs */}
<DocumentTabs <DocumentTabs
defaultTab={initialTab} defaultTab={initialTab}
preview={preview} staticPreview={preview}
content={content} content={content}
isEditing={false} isEditing={false}
/> />

View file

@ -3,7 +3,7 @@
cursor: pointer; cursor: pointer;
border-radius: var(--radius); border-radius: var(--radius);
border: 1px solid var(--border); border: 1px solid var(--border);
padding: var(--gap-half) var(--gap); /* padding: var(--gap-half) var(--gap); */
color: var(--darker-gray); color: var(--darker-gray);
} }

View file

@ -31,11 +31,12 @@ const Button = forwardRef<HTMLButtonElement, Props>(
disabled = false, disabled = false,
iconRight, iconRight,
iconLeft, iconLeft,
height, height = 40,
width, width,
padding, padding = 10,
margin, margin,
loading, loading,
style,
...props ...props
}, },
ref ref
@ -49,7 +50,7 @@ const Button = forwardRef<HTMLButtonElement, Props>(
})} })}
disabled={disabled || loading} disabled={disabled || loading}
onClick={onClick} onClick={onClick}
style={{ height, width, margin, padding }} style={{ height, width, margin, padding, ...style }}
{...props} {...props}
> >
{children && iconLeft && ( {children && iconLeft && (

View file

@ -3,8 +3,8 @@
border-radius: var(--radius); border-radius: var(--radius);
color: var(--fg); color: var(--fg);
border: 1px solid var(--light-gray); border: 1px solid var(--light-gray);
width: auto; width: 100%;
height: auto; height: 100%;
} }

View file

@ -38,11 +38,6 @@
.header { .header {
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
opacity: 0;
}
.header:not(.loading) {
opacity: 1;
} }
.selectContent { .selectContent {

View file

@ -9,7 +9,6 @@ import Button from "@components/button"
import clsx from "clsx" import clsx from "clsx"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { import {
GitHub,
Home, Home,
Menu, Menu,
Moon, Moon,
@ -17,13 +16,13 @@ import {
Settings, Settings,
Sun, Sun,
User, User,
UserPlus,
UserX UserX
} from "react-feather" } from "react-feather"
import * as DropdownMenu from "@radix-ui/react-dropdown-menu" import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import buttonStyles from "@components/button/button.module.css" import buttonStyles from "@components/button/button.module.css"
import { useMemo } from "react" import { useMemo } from "react"
import { useSessionSWR } from "@lib/use-session-swr" import { useSessionSWR } from "@lib/use-session-swr"
import Skeleton from "@components/skeleton"
type Tab = { type Tab = {
name: string name: string
@ -34,7 +33,7 @@ type Tab = {
} }
const Header = () => { const Header = () => {
const { isAuthenticated, isAdmin, isLoading, mutate } = useSessionSWR() const { isAdmin, isAuthenticated, isLoading, mutate } = useSessionSWR()
const pathname = usePathname() const pathname = usePathname()
const { setTheme, resolvedTheme } = useTheme() const { setTheme, resolvedTheme } = useTheme()
@ -52,6 +51,7 @@ const Header = () => {
aria-label={tab.name} aria-label={tab.name}
aria-current={isActive ? "page" : undefined} aria-current={isActive ? "page" : undefined}
data-tab={tab.value} data-tab={tab.value}
width="auto"
> >
{tab.name ? tab.name : undefined} {tab.name ? tab.name : undefined}
</Button> </Button>
@ -59,7 +59,7 @@ const Header = () => {
} else if (tab.href) { } else if (tab.href) {
return ( return (
<Link key={tab.value} href={tab.href} data-tab={tab.value}> <Link key={tab.value} href={tab.href} data-tab={tab.value}>
<Button className={activeStyle} iconLeft={tab.icon}> <Button className={activeStyle} iconLeft={tab.icon} width="auto">
{tab.name ? tab.name : undefined} {tab.name ? tab.name : undefined}
</Button> </Button>
</Link> </Link>
@ -69,12 +69,12 @@ const Header = () => {
const pages = useMemo(() => { const pages = useMemo(() => {
const defaultPages: Tab[] = [ const defaultPages: Tab[] = [
{ // {
name: "GitHub", // name: "GitHub",
href: "https://github.com/maxleiter/drift", // href: "https://github.com/maxleiter/drift",
icon: <GitHub />, // icon: <GitHub />,
value: "github" // value: "github"
} // }
] ]
if (isAdmin) { if (isAdmin) {
@ -95,8 +95,37 @@ const Header = () => {
value: "theme" value: "theme"
}) })
if (isAuthenticated) // the is loading case is handled in the JSX
if (!isLoading) {
if (isAuthenticated) {
defaultPages.push({
name: "Sign Out",
icon: <UserX />,
value: "signout",
onClick: () => {
mutate(undefined)
signOut({
callbackUrl: "/"
})
}
})
} else {
defaultPages.push({
name: "Sign in",
icon: <User />,
value: "signin",
href: "/signin"
})
}
}
return [ return [
{
name: "Home",
icon: <Home />,
value: "home",
href: "/home"
},
{ {
name: "New", name: "New",
icon: <PlusCircle />, icon: <PlusCircle />,
@ -115,56 +144,32 @@ const Header = () => {
value: "settings", value: "settings",
href: "/settings" href: "/settings"
}, },
...defaultPages, ...defaultPages
{
name: "Sign Out",
icon: <UserX />,
value: "signout",
onClick: () => {
mutate(undefined)
signOut({
callbackUrl: "/"
})
}
}
] ]
else }, [isAdmin, resolvedTheme, isLoading, setTheme, isAuthenticated, mutate])
return [
{
name: "Home",
icon: <Home />,
value: "home",
href: "/"
},
...defaultPages,
{
name: "Sign in",
icon: <User />,
value: "signin",
href: "/signin"
},
{
name: "Sign up",
icon: <UserPlus />,
value: "signup",
href: "/signup"
}
]
}, [isAdmin, resolvedTheme, isAuthenticated, setTheme, mutate])
const buttons = pages.map(getButton) const buttons = pages.map(getButton)
if (isLoading) {
buttons.push(
<Button iconLeft={<User />} key="loading">
Sign{" "}
<Skeleton
width={20}
height={15}
style={{ display: "inline-block", verticalAlign: "middle" }}
/>
</Button>
)
}
// TODO: this is a hack to close the radix ui menu when a next link is clicked // TODO: this is a hack to close the radix ui menu when a next link is clicked
const onClick = () => { const onClick = () => {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })) document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))
} }
return ( return (
<header <header className={styles.header}>
className={clsx(styles.header, {
[styles.loading]: isLoading
})}
>
<div className={styles.tabs}> <div className={styles.tabs}>
<div className={styles.buttons}>{buttons}</div> <div className={styles.buttons}>{buttons}</div>
</div> </div>

View file

@ -10,6 +10,7 @@ import { ListItemSkeleton } from "./list-item-skeleton"
import Link from "@components/link" import Link from "@components/link"
import debounce from "lodash.debounce" import debounce from "lodash.debounce"
import { fetchWithUser } from "src/app/lib/fetch-with-user" import { fetchWithUser } from "src/app/lib/fetch-with-user"
import { Stack } from "@components/stack"
type Props = { type Props = {
initialPosts: string | PostWithFiles[] initialPosts: string | PostWithFiles[]
@ -63,7 +64,6 @@ const PostList = ({
} }
) )
const json = await res.json() const json = await res.json()
console.log(json)
setPosts(json) setPosts(json)
setSearching(false) setSearching(false)
} }
@ -101,7 +101,7 @@ const PostList = ({
) )
return ( return (
<div className={styles.container}> <Stack className={styles.container} alignItems="center">
{!hideSearch && ( {!hideSearch && (
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<Input <Input
@ -122,7 +122,6 @@ const PostList = ({
</ul> </ul>
)} )}
{!showSkeleton && posts && posts.length > 0 ? ( {!showSkeleton && posts && posts.length > 0 ? (
<div>
<ul> <ul>
{posts.map((post) => { {posts.map((post) => {
return ( return (
@ -136,9 +135,8 @@ const PostList = ({
) )
})} })}
</ul> </ul>
</div>
) : null} ) : null}
</div> </Stack>
) )
} }

View file

@ -4,6 +4,10 @@
margin: 0; margin: 0;
} }
.container > * {
width: 100%;
}
.container ul li { .container ul li {
padding: 0.5rem 0; padding: 0.5rem 0;
} }

View file

@ -3,13 +3,18 @@ import styles from "./skeleton.module.css"
export default function Skeleton({ export default function Skeleton({
width = 100, width = 100,
height = 24, height = 24,
borderRadius = 4 borderRadius = 4,
style
}: { }: {
width?: number | string width?: number | string
height?: number | string height?: number | string
borderRadius?: number | string borderRadius?: number | string
style?: React.CSSProperties
}) { }) {
return ( return (
<div className={styles.skeleton} style={{ width, height, borderRadius }} /> <div
className={styles.skeleton}
style={{ width, height, borderRadius, ...style }}
/>
) )
} }

View file

@ -4,7 +4,8 @@ import Layout from "@components/layout"
import { Toasts } from "@components/toasts" import { Toasts } from "@components/toasts"
import Header from "@components/header" import Header from "@components/header"
import { Inter } from "@next/font/google" import { Inter } from "@next/font/google"
import { PropsWithChildren } from "react" import { PropsWithChildren, Suspense } from "react"
import { Spinner } from "@components/spinner"
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" }) const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
@ -19,7 +20,9 @@ export default async function RootLayout({
<Toasts /> <Toasts />
<Layout> <Layout>
<Providers> <Providers>
<Suspense fallback={<Spinner />}>
<Header /> <Header />
</Suspense>
{children} {children}
</Providers> </Providers>
</Layout> </Layout>

View file

@ -3,6 +3,7 @@ import { getPostsByUser } from "@lib/server/prisma"
import PostList from "@components/post-list" import PostList from "@components/post-list"
import { getCurrentUser } from "@lib/server/session" import { getCurrentUser } from "@lib/server/session"
import { authOptions } from "@lib/server/auth" import { authOptions } from "@lib/server/auth"
import { Suspense } from "react"
export default async function Mine() { export default async function Mine() {
const userId = (await getCurrentUser())?.id const userId = (await getCurrentUser())?.id
@ -15,12 +16,14 @@ export default async function Mine() {
const stringifiedPosts = JSON.stringify(posts) const stringifiedPosts = JSON.stringify(posts)
return ( return (
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
<PostList <PostList
userId={userId} userId={userId}
initialPosts={stringifiedPosts} initialPosts={stringifiedPosts}
isOwner={true} isOwner={true}
hideSearch={false} hideSearch={false}
/> />
</Suspense>
) )
} }

View file

@ -2,17 +2,72 @@ import Image from "next/image"
import Card from "@components/card" import Card from "@components/card"
import { getWelcomeContent } from "src/pages/api/welcome" import { getWelcomeContent } from "src/pages/api/welcome"
import DocumentTabs from "./(posts)/components/tabs" import DocumentTabs from "./(posts)/components/tabs"
import { getAllPosts, Post } from "@lib/server/prisma" import { getAllPosts } from "@lib/server/prisma"
import PostList, { NoPostsFound } from "@components/post-list" import PostList, { NoPostsFound } from "@components/post-list"
import { Suspense } from "react" import { cache, Suspense } from "react"
import ErrorBoundary from "@components/error/fallback"
import { Stack } from "@components/stack"
export async function getWelcomeData() { const getWelcomeData = cache(async () => {
const welcomeContent = await getWelcomeContent() const welcomeContent = await getWelcomeContent()
return welcomeContent return welcomeContent
} })
export default async function Page() { export default async function Page() {
const getPostsPromise = getAllPosts({ const { title } = await getWelcomeData()
return (
<Stack direction="column">
<Stack direction="row" alignItems="center">
<Image
src={"/assets/logo.svg"}
width={48}
height={48}
alt=""
priority
/>
<h1 style={{ marginLeft: "var(--gap)" }}>{title}</h1>
</Stack>
{/* @ts-expect-error because of async RSC */}
<WelcomePost />
<h2>Recent public posts</h2>
<ErrorBoundary>
<Suspense
fallback={
<PostList
skeleton
hideActions
hideSearch
initialPosts={JSON.stringify({})}
/>
}
>
{/* @ts-expect-error because of async RSC */}
<PublicPostList />
</Suspense>
</ErrorBoundary>
</Stack>
)
}
async function WelcomePost() {
const { content, rendered, title } = await getWelcomeData()
return (
<Card>
<DocumentTabs
defaultTab="preview"
isEditing={false}
staticPreview={rendered as string}
title={title}
>
{content}
</DocumentTabs>
</Card>
)
}
async function PublicPostList() {
const posts = await getAllPosts({
select: { select: {
id: true, id: true,
title: true, title: true,
@ -38,55 +93,6 @@ export default async function Page() {
createdAt: "desc" createdAt: "desc"
} }
}) })
const { content, rendered, title } = await getWelcomeData()
return (
<div
style={{ display: "flex", flexDirection: "column", gap: "var(--gap)" }}
>
<div
style={{ display: "flex", flexDirection: "row", alignItems: "center" }}
>
<Image
src={"/assets/logo.svg"}
width={48}
height={48}
alt=""
priority
/>
<h1 style={{ marginLeft: "var(--gap)" }}>{title}</h1>
</div>
<Card>
<DocumentTabs
defaultTab="preview"
isEditing={false}
content={content}
preview={rendered as string}
title={title}
/>
</Card>
<div>
<h2>Recent public posts</h2>
<Suspense
fallback={
<PostList skeleton hideSearch initialPosts={JSON.stringify({})} />
}
>
{/* @ts-expect-error because of async RSC */}
<PublicPostList getPostsPromise={getPostsPromise} />
</Suspense>
</div>
</div>
)
}
async function PublicPostList({
getPostsPromise
}: {
getPostsPromise: Promise<Post[]>
}) {
try {
const posts = await getPostsPromise
if (posts.length === 0) { if (posts.length === 0) {
return <NoPostsFound /> return <NoPostsFound />
@ -95,9 +101,4 @@ async function PublicPostList({
return ( return (
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch /> <PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
) )
} catch (error) {
return <NoPostsFound />
}
} }
export const revalidate = 60

View file

@ -12,7 +12,6 @@ import { User } from "@prisma/client"
function Profile() { function Profile() {
const { session } = useSessionSWR() const { session } = useSessionSWR()
console.log(session)
const { data: userData } = useSWR<User>( const { data: userData } = useSWR<User>(
session?.user?.id ? `/api/user/${session?.user?.id}` : null session?.user?.id ? `/api/user/${session?.user?.id}` : null
) )
@ -104,7 +103,7 @@ function Profile() {
type="email" type="email"
width={"100%"} width={"100%"}
placeholder="my@email.io" placeholder="my@email.io"
value={session?.user.email || undefined} value={session?.user.email || ""}
disabled disabled
aria-label="Email" aria-label="Email"
/> />

View file

@ -49,7 +49,6 @@ const providers = () => {
// @ts-expect-error TODO: fix types // @ts-expect-error TODO: fix types
credentials: credentialsOptions() as unknown, credentials: credentialsOptions() as unknown,
async authorize(credentials) { async authorize(credentials) {
console.log("credentials")
if (!credentials || !credentials.username || !credentials.password) { if (!credentials || !credentials.username || !credentials.password) {
throw new Error("Missing credentials") throw new Error("Missing credentials")
} }

View file

@ -5,7 +5,6 @@ import { withMethods } from "@lib/api-middleware/with-methods"
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => { const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
const { id, download } = req.query const { id, download } = req.query
const file = await prisma.file.findUnique({ const file = await prisma.file.findUnique({
where: { where: {
id: parseQueryParam(id) id: parseQueryParam(id)