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,25 +132,27 @@ function Auth({
<Button width={"100%"} type="submit" loading={submitting}>
Sign {signText}
</Button>
{isGithubEnabled ? <hr style={{ width: "100%" }} /> : null}
{isGithubEnabled ? (
<Button
type="submit"
width="100%"
style={{
color: "var(--fg)"
}}
iconLeft={<GitHub />}
onClick={(e) => {
e.preventDefault()
signIn("github", {
callbackUrl: "/",
registration_password: serverPassword
})
}}
>
Sign {signText.toLowerCase()} with GitHub
</Button>
<>
<hr style={{ width: "100%" }} />
<Button
type="submit"
width="100%"
style={{
color: "var(--fg)"
}}
iconLeft={<GitHub />}
onClick={(e) => {
e.preventDefault()
signIn("github", {
callbackUrl: "/",
registration_password: serverPassword
})
}}
>
Sign {signText.toLowerCase()} with GitHub
</Button>
</>
) : null}
</div>
<div className={styles.formContentSpace}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,6 @@ import Button from "@components/button"
import clsx from "clsx"
import { useTheme } from "next-themes"
import {
GitHub,
Home,
Menu,
Moon,
@ -17,13 +16,13 @@ import {
Settings,
Sun,
User,
UserPlus,
UserX
} from "react-feather"
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import buttonStyles from "@components/button/button.module.css"
import { useMemo } from "react"
import { useSessionSWR } from "@lib/use-session-swr"
import Skeleton from "@components/skeleton"
type Tab = {
name: string
@ -34,7 +33,7 @@ type Tab = {
}
const Header = () => {
const { isAuthenticated, isAdmin, isLoading, mutate } = useSessionSWR()
const { isAdmin, isAuthenticated, isLoading, mutate } = useSessionSWR()
const pathname = usePathname()
const { setTheme, resolvedTheme } = useTheme()
@ -52,6 +51,7 @@ const Header = () => {
aria-label={tab.name}
aria-current={isActive ? "page" : undefined}
data-tab={tab.value}
width="auto"
>
{tab.name ? tab.name : undefined}
</Button>
@ -59,7 +59,7 @@ const Header = () => {
} else if (tab.href) {
return (
<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}
</Button>
</Link>
@ -69,12 +69,12 @@ const Header = () => {
const pages = useMemo(() => {
const defaultPages: Tab[] = [
{
name: "GitHub",
href: "https://github.com/maxleiter/drift",
icon: <GitHub />,
value: "github"
}
// {
// name: "GitHub",
// href: "https://github.com/maxleiter/drift",
// icon: <GitHub />,
// value: "github"
// }
]
if (isAdmin) {
@ -95,28 +95,10 @@ const Header = () => {
value: "theme"
})
if (isAuthenticated)
return [
{
name: "New",
icon: <PlusCircle />,
value: "new",
href: "/new"
},
{
name: "Yours",
icon: <User />,
value: "yours",
href: "/mine"
},
{
name: "Settings",
icon: <Settings />,
value: "settings",
href: "/settings"
},
...defaultPages,
{
// the is loading case is handled in the JSX
if (!isLoading) {
if (isAuthenticated) {
defaultPages.push({
name: "Sign Out",
icon: <UserX />,
value: "signout",
@ -126,45 +108,68 @@ const Header = () => {
callbackUrl: "/"
})
}
}
]
else
return [
{
name: "Home",
icon: <Home />,
value: "home",
href: "/"
},
...defaultPages,
{
})
} else {
defaultPages.push({
name: "Sign in",
icon: <User />,
value: "signin",
href: "/signin"
},
{
name: "Sign up",
icon: <UserPlus />,
value: "signup",
href: "/signup"
}
]
}, [isAdmin, resolvedTheme, isAuthenticated, setTheme, mutate])
})
}
}
return [
{
name: "Home",
icon: <Home />,
value: "home",
href: "/home"
},
{
name: "New",
icon: <PlusCircle />,
value: "new",
href: "/new"
},
{
name: "Yours",
icon: <User />,
value: "yours",
href: "/mine"
},
{
name: "Settings",
icon: <Settings />,
value: "settings",
href: "/settings"
},
...defaultPages
]
}, [isAdmin, resolvedTheme, isLoading, setTheme, isAuthenticated, mutate])
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
const onClick = () => {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))
}
return (
<header
className={clsx(styles.header, {
[styles.loading]: isLoading
})}
>
<header className={styles.header}>
<div className={styles.tabs}>
<div className={styles.buttons}>{buttons}</div>
</div>

View file

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

View file

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

View file

@ -3,13 +3,18 @@ import styles from "./skeleton.module.css"
export default function Skeleton({
width = 100,
height = 24,
borderRadius = 4
borderRadius = 4,
style
}: {
width?: number | string
height?: number | string
borderRadius?: number | string
style?: React.CSSProperties
}) {
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 Header from "@components/header"
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" })
@ -19,7 +20,9 @@ export default async function RootLayout({
<Toasts />
<Layout>
<Providers>
<Header />
<Suspense fallback={<Spinner />}>
<Header />
</Suspense>
{children}
</Providers>
</Layout>

View file

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

View file

@ -2,17 +2,72 @@ import Image from "next/image"
import Card from "@components/card"
import { getWelcomeContent } from "src/pages/api/welcome"
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 { 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()
return welcomeContent
}
})
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: {
id: true,
title: true,
@ -38,66 +93,12 @@ export default async function Page() {
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) {
return <NoPostsFound />
}
return (
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
)
} catch (error) {
if (posts.length === 0) {
return <NoPostsFound />
}
}
export const revalidate = 60
return (
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
)
}

View file

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

View file

@ -49,7 +49,6 @@ const providers = () => {
// @ts-expect-error TODO: fix types
credentials: credentialsOptions() as unknown,
async authorize(credentials) {
console.log("credentials")
if (!credentials || !credentials.username || !credentials.password) {
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 { id, download } = req.query
const file = await prisma.file.findUnique({
where: {
id: parseQueryParam(id)