Bug fixes, code cleanup, made root dir /

This commit is contained in:
Max Leiter 2023-01-07 13:02:52 -08:00
parent c21ca52a59
commit d9e7aa5ecf
78 changed files with 394 additions and 352 deletions

View file

@ -1,11 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true,
"ignorePatterns": ["node_modules/", "__tests__/"]
}

35
src/.gitignore vendored
View file

@ -1,35 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# production env
.env
# vercel
.vercel
# typescript
*.tsbuildinfo

View file

@ -1,7 +0,0 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": false,
"printWidth": 80,
"useTabs": true
}

View file

@ -1,4 +0,0 @@
{
"typescript.tsdk": "node_modules/.pnpm/typescript@4.6.4/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

View file

@ -1,34 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View file

@ -1,5 +1,5 @@
import Auth from "../components"
import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
import config from "@lib/config"
const getPasscode = async () => {

View file

@ -1,7 +1,7 @@
import { Popover } from "@components/popover"
import { codeFileExtensions } from "@lib/constants"
import clsx from "clsx"
import type { PostWithFiles } from "lib/server/prisma"
import type { PostWithFiles } from "src/lib/server/prisma"
import styles from "./dropdown.module.css"
import buttonStyles from "@components/button/button.module.css"
import { ChevronDown, Code, File as FileIcon } from "react-feather"

View file

@ -1,7 +1,7 @@
"use client"
import * as RadixTabs from "@radix-ui/react-tabs"
import FormattingIcons from "app/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
import FormattingIcons from "src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
import { ChangeEvent, useRef } from "react"
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
import Preview, { StaticPreview } from "../preview"

View file

@ -11,7 +11,6 @@ import styles from "./formatting-icons.module.css"
import { TextareaMarkdownRef } from "textarea-markdown-editor"
import Tooltip from "@components/tooltip"
import Button from "@components/button"
import ButtonGroup from "@components/button-group"
import clsx from "clsx"
// TODO: clean up

View file

@ -2,7 +2,7 @@ import { ChangeEvent, useCallback } from "react"
import styles from "./document.module.css"
import Button from "@components/button"
import Input from "@components/input"
import DocumentTabs from "app/(posts)/components/tabs"
import DocumentTabs from "src/app/(posts)/components/tabs"
import { Trash } from "react-feather"
type Props = {

View file

@ -14,5 +14,5 @@
}
.labelAndInput {
font-size: 1.2rem;
font-size: 1.2rem;
}

View file

@ -1,4 +1,4 @@
import NewPost from "app/(posts)/new/components/new"
import NewPost from "src/app/(posts)/new/components/new"
import "./react-datepicker.css"
const New = () => <NewPost />

View file

@ -2,7 +2,7 @@
import Button from "@components/button"
import ButtonGroup from "@components/button-group"
import FileDropdown from "app/(posts)/components/file-dropdown"
import FileDropdown from "src/app/(posts)/components/file-dropdown"
import { Edit, ArrowUpCircle, Archive } from "react-feather"
import styles from "./post-buttons.module.css"
import { useRouter } from "next/navigation"

View file

@ -22,7 +22,7 @@ export const PostTitle = ({
createdAt,
expiresAt,
loading,
authorId
// authorId
}: TitleProps) => {
return (
<span className={styles.title}>
@ -33,10 +33,9 @@ export const PostTitle = ({
>
{title}{" "}
<span style={{ color: "var(--gray)" }}>
by{" "}
<Link colored href={`/author/${authorId}`}>
{displayName || "anonymous"}
</Link>
by {/* <Link colored href={`/author/${authorId}`}> */}
{displayName || "anonymous"}
{/* </Link> */}
</span>
</h1>
{!loading && (

View file

@ -3,6 +3,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: var(--gap);
}
.title .badges {
@ -11,12 +12,6 @@
flex-wrap: wrap;
}
.title h3 {
margin: 0;
padding: 0;
display: inline-block;
}
.titleWithDropdown {
display: flex;
flex-direction: row;

View file

@ -4,7 +4,7 @@ import Button from "@components/button"
import ButtonGroup from "@components/button-group"
import Skeleton from "@components/skeleton"
import Tooltip from "@components/tooltip"
import DocumentTabs from "app/(posts)/components/tabs"
import DocumentTabs from "src/app/(posts)/components/tabs"
import Link from "next/link"
import { memo } from "react"
import { Download, ExternalLink } from "react-feather"

View file

@ -14,9 +14,10 @@ async function PostListWrapper({
const data = (await posts).filter((post) => post.visibility === "public")
return (
<PostList
morePosts={false}
userId={userId}
initialPosts={JSON.stringify(data)}
hideSearch
hideActions
/>
)
}
@ -62,9 +63,9 @@ export default async function UserPage({
<h1>Public posts by {user?.displayName || "Anonymous"}</h1>
<Avatar />
</div>
<Suspense fallback={<PostList initialPosts={JSON.stringify({})} />}>
<Suspense fallback={<PostList hideSearch skeleton initialPosts={[]} />}>
{/* @ts-expect-error because TS async JSX support is iffy */}
<PostListWrapper posts={posts} userId={id} />
<PostListWrapper hideSearch posts={posts} userId={id} />
</Suspense>
</>
)

View file

@ -3,12 +3,12 @@ import styles from "./badge.module.css"
type BadgeProps = {
type: "primary" | "secondary" | "error" | "warning"
children: React.ReactNode
}
} & React.HTMLAttributes<HTMLDivElement>
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
({ type, children }: BadgeProps, ref) => {
({ type, children, ...rest}: BadgeProps, ref) => {
return (
<div className={styles.container}>
<div className={styles.container} {...rest}>
<div className={`${styles.badge} ${styles[type]}`} ref={ref}>
{children}
</div>

View file

@ -1,6 +1,8 @@
"use client"
import { useToasts } from "@components/toasts"
import Tooltip from "@components/tooltip"
import { timeAgo } from "@lib/time-ago"
import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
import { timeAgo } from "src/app/lib/time-ago"
import { useMemo, useState, useEffect } from "react"
import Badge from "../badge"
@ -8,6 +10,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
const [time, setTimeAgo] = useState(timeAgo(createdDate))
const { setToast } = useToasts()
useEffect(() => {
const interval = setInterval(() => {
setTimeAgo(timeAgo(createdDate))
@ -15,11 +18,19 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
return () => clearInterval(interval)
}, [createdDate])
function onClick() {
copyToClipboard(createdDate.toISOString())
setToast({
message: "Copied to clipboard",
type: "success"
})
}
const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}`
return (
// TODO: investigate tooltip not showing
<Tooltip content={formattedTime}>
<Badge type="secondary">
<Badge type="secondary" onClick={onClick}>
{" "}
<>{time}</>
</Badge>

View file

@ -1,7 +1,7 @@
"use client"
import Tooltip from "@components/tooltip"
import { timeUntil } from "@lib/time-ago"
import { timeUntil } from "src/app/lib/time-ago"
import { useEffect, useMemo, useState } from "react"
import Badge from "../badge"

View file

@ -39,6 +39,11 @@ const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Pro
const json = await res.json()
setVisibility(json.visibility)
router.refresh()
setToast({
message: "Visibility updated",
type: "success"
})
} else {
setToast({
message: "An error occurred",
@ -47,7 +52,7 @@ const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Pro
setPasswordModalVisible(false)
}
},
[postId, setToast, setVisibility]
[postId, router, setToast]
)
const onSubmit = useCallback(

View file

@ -38,7 +38,7 @@
.header {
transition: opacity 0.2s ease-in-out;
opacity: .5;
opacity: 0;
}
.header:not(.loading) {

View file

@ -3,66 +3,42 @@
import styles from "./post-list.module.css"
import ListItem from "./list-item"
import { ChangeEvent, useCallback, useState } from "react"
import Link from "@components/link"
import type { PostWithFiles } from "@lib/server/prisma"
import Input from "@components/input"
import Button from "@components/button"
import { useToasts } from "@components/toasts"
import { ListItemSkeleton } from "./list-item-skeleton"
import Link from "@components/link"
import debounce from "lodash.debounce"
type Props = {
initialPosts: string | PostWithFiles[]
morePosts?: boolean
userId?: string
hideSearch?: boolean
hideActions?: boolean
isOwner?: boolean
skeleton?: boolean
searchValue?: string
userId?: string
}
const PostList = ({
morePosts,
initialPosts: initialPostsMaybeJSON,
userId,
hideSearch,
hideActions,
isOwner
isOwner,
skeleton,
userId
}: Props) => {
const initialPosts =
typeof initialPostsMaybeJSON === "string"
? JSON.parse(initialPostsMaybeJSON)
: initialPostsMaybeJSON
const [search, setSearchValue] = useState("")
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
const [searchValue, setSearchValue] = useState("")
const [searching, setSearching] = useState(false)
const [hasMorePosts] = useState(morePosts)
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
const { setToast } = useToasts()
const loadMoreClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
if (hasMorePosts) {
// eslint-disable-next-line no-inner-declarations
async function fetchPosts() {
// const res = await fetch(`/api/posts/mine`, {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// "x-page": `${posts.length / 10 + 1}`
// },
// next: {
// revalidate: 10
// }
// })
// const json = await res.json()
// setPosts([...posts, ...json.posts])
// setHasMorePosts(json.morePosts)
}
fetchPosts()
}
},
[hasMorePosts]
)
const showSkeleton = skeleton || searching
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: address this
const onSearch = useCallback(
@ -131,27 +107,18 @@ const PostList = ({
disabled={!posts}
style={{ maxWidth: 300 }}
aria-label="Search"
value={search}
value={searchValue}
/>
</div>
)}
{!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>}
{searching && (
{showSkeleton && (
<ul>
<ListItemSkeleton />
<ListItemSkeleton />
</ul>
)}
{!searching && posts?.length === 0 && posts && (
<p>
No posts found. Create one{" "}
<Link colored href="/new">
here
</Link>
.
</p>
)}
{!searching && posts?.length > 0 && (
{!showSkeleton && posts?.length > 0 && (
<div>
<ul>
{posts.map((post) => {
@ -168,15 +135,20 @@ const PostList = ({
</ul>
</div>
)}
{!searching && hasMorePosts && !setSearchValue && (
<div>
<Button width={"100%"} onClick={loadMoreClick}>
Load more
</Button>
</div>
)}
</div>
)
}
export default PostList
export function NoPostsFound() {
return (
<p>
No posts found. Create one{" "}
<Link colored href="/new">
here
</Link>
.
</p>
)
}

View file

@ -72,7 +72,7 @@ const ListItem = ({
<>
<div className={styles.title}>
<span className={styles.titleText}>
<h3 style={{ display: "inline-block", margin: 0 }}>
<h4 style={{ display: "inline-block", margin: 0 }}>
<Link
colored
style={{ marginRight: "var(--gap)" }}
@ -80,7 +80,7 @@ const ListItem = ({
>
{post.title}
</Link>
</h3>
</h4>
<div className={styles.badges}>
<VisibilityBadge visibility={post.visibility} />
<Badge type="secondary">

View file

@ -0,0 +1,73 @@
import { ApiToken } from "@prisma/client"
import useSWR from "swr"
type ConvertDateToString<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P]
}
export type SerializedApiToken = ConvertDateToString<ApiToken>
type UseApiTokens = {
userId?: string
initialTokens?: SerializedApiToken[]
}
const TOKENS_ENDPOINT = "/api/user/tokens"
export function useApiTokens({ userId, initialTokens }: UseApiTokens) {
const { data, mutate, error, isLoading } = useSWR<SerializedApiToken[]>(
"/api/user/tokens?userId=" + userId,
async (url: string) => {
return fetch(url).then(async (res) => {
const data = await res.json()
if (data.error) {
throw new Error(data.error)
}
return data
})
},
{
refreshInterval: 10000,
fallbackData: initialTokens
}
)
async function createToken(newToken: string) {
if (!newToken) {
throw new Error("Token name is required")
}
const res = await fetch(
`${TOKENS_ENDPOINT}?userId=${userId}&name=${newToken}`,
{
method: "POST"
}
)
const response = await res.json()
if (response.error) {
throw new Error(response.error)
return
}
mutate([...(data || []), response])
return response as SerializedApiToken
}
const expireToken = async (id: string) => {
await fetch(`${TOKENS_ENDPOINT}?userId=${userId}&tokenId=${id}`, {
method: "DELETE"
})
mutate(data?.filter((token) => token.id !== id))
}
return {
data,
isLoading,
error,
createToken,
expireToken
}
}

View file

@ -1,6 +1,5 @@
import "@styles/globals.css"
import { Providers } from "./providers"
// import { ServerThemeProvider } from "@wits/next-themes"
import Page from "@components/page"
import { Toasts } from "@components/toasts"
import Header from "@components/header"
@ -14,13 +13,6 @@ interface RootLayoutProps {
export default async function RootLayout({ children }: RootLayoutProps) {
return (
// <ServerThemeProvider
// enableSystem={true}
// disableTransitionOnChange
// cookieName={"drift-theme"}
// attribute="data-theme"
// enableColorScheme={true}
// >
<html lang="en" className={inter.variable}>
<head />
<body>

View file

@ -0,0 +1,3 @@
export async function copyToClipboard(text: string ) {
await navigator.clipboard.writeText(text)
}

View file

@ -0,0 +1,17 @@
// A wrapper around next-auth/use-session that refreshes the page if the session changes
import { useEffect } from "react"
import { useSession as useSessionOriginal } from "next-auth/react"
import { useRouter } from "next/navigation"
export function useSession() {
const { data: session, status } = useSessionOriginal()
const router = useRouter();
useEffect(() => {
router.refresh();
}, [router, status])
return { session, status }
}

View file

@ -1,13 +1,8 @@
import { redirect } from "next/navigation"
import { getPostsByUser, User } from "@lib/server/prisma"
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 { cache } from "react"
const cachedGetPostsByUser = cache(
async (userId: User["id"]) => await getPostsByUser(userId, true)
)
export default async function Mine() {
const userId = (await getCurrentUser())?.id
@ -16,16 +11,17 @@ export default async function Mine() {
return redirect(authOptions.pages?.signIn || "/new")
}
const posts = await cachedGetPostsByUser(userId)
const posts = await getPostsByUser(userId, true)
const hasMore = false
const stringifiedPosts = JSON.stringify(posts)
return (
<PostList
userId={userId}
morePosts={hasMore}
initialPosts={stringifiedPosts}
isOwner={true}
hideSearch={false}
/>
)
}
export const revalidate = 0;

View file

@ -1,9 +1,10 @@
import Image from "next/image"
import Card from "@components/card"
import { getWelcomeContent } from "pages/api/welcome"
import { getWelcomeContent } from "src/pages/api/welcome"
import DocumentTabs from "./(posts)/components/tabs"
import { getAllPosts, Post } from "@lib/server/prisma"
import PostList from "@components/post-list"
import PostList, { NoPostsFound } from "@components/post-list"
import { Suspense } from "react"
const getWelcomeData = async () => {
const welcomeContent = await getWelcomeContent()
@ -66,8 +67,12 @@ export default async function Page() {
</Card>
<div>
<h2>Recent public posts</h2>
{/* @ts-expect-error because of async RSC */}
<PublicPostList getPostsPromise={getPostsPromise} />
<Suspense
fallback={<PostList skeleton hideSearch initialPosts={JSON.stringify({})} />}
>
{/* @ts-expect-error because of async RSC */}
<PublicPostList getPostsPromise={getPostsPromise} />
</Suspense>
</div>
</div>
)
@ -80,25 +85,16 @@ async function PublicPostList({
}) {
try {
const posts = await getPostsPromise
if (posts.length === 0) {
return <NoPostsFound />
}
return (
<PostList
userId={undefined}
morePosts={false}
initialPosts={JSON.stringify(posts)}
hideActions
hideSearch
/>
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
)
} catch (error) {
return (
<PostList
userId={undefined}
morePosts={false}
initialPosts={[]}
hideActions
hideSearch
/>
)
return <NoPostsFound />
}
}

View file

@ -2,36 +2,18 @@
display: flex;
flex-direction: column;
gap: var(--gap);
max-width: 300px;
max-width: 350px;
margin-top: var(--gap);
}
.upload {
position: relative;
/* fieldset contains an input and button. I want button to be small next to input */
.form .fieldset {
display: flex;
flex-direction: column;
flex-direction: row;
gap: var(--gap);
max-width: 300px;
margin-top: var(--gap);
cursor: pointer;
}
.uploadInput {
position: absolute;
opacity: 0;
cursor: pointer;
width: 300px;
height: 37px;
cursor: pointer;
}
.uploadButton {
width: 100%;
}
/* hover should affect button */
.uploadInput:hover + button {
border: 1px solid var(--fg);
border: none;
padding: 0;
margin: 0;
}
.tokens {

View file

@ -5,97 +5,56 @@ import Input from "@components/input"
import Note from "@components/note"
import { Spinner } from "@components/spinner"
import { useToasts } from "@components/toasts"
import { ApiToken } from "@prisma/client"
import { SerializedApiToken, useApiTokens } from "src/app/hooks/swr/use-api-tokens"
import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
import { useSession } from "next-auth/react"
import { useState } from "react"
import useSWR from "swr"
import styles from "./api-keys.module.css"
type ConvertDateToString<T> = {
[P in keyof T]: T[P] extends Date ? string : T[P]
}
type SerializedApiToken = ConvertDateToString<ApiToken>
// need to pass in the accessToken
const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) => {
const APIKeys = ({
tokens: initialTokens
}: {
tokens?: SerializedApiToken[]
}) => {
const session = useSession()
const { setToast } = useToasts()
const { data, error, mutate } = useSWR<SerializedApiToken[]>(
"/api/user/tokens?userId=" + session?.data?.user?.id,
{
fetcher: async (url: string) => {
if (session.status === "loading") return initialTokens
return fetch(url).then(async (res) => {
const data = await res.json()
if (data.error) {
setError(data.error)
return
} else {
setError(undefined)
}
return data
})
},
fallbackData: initialTokens
}
)
const { data, error, createToken, expireToken } = useApiTokens({
userId: session.data?.user.id,
initialTokens
})
const [submitting, setSubmitting] = useState<boolean>(false)
const [newToken, setNewToken] = useState<string>("")
const [errorText, setError] = useState<string>()
const createToken = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
if (!newToken) {
return
}
setSubmitting(true)
const res = await fetch(
`/api/user/tokens?userId=${session.data?.user.id}&name=${newToken}`,
{
method: "POST",
}
)
const response = await res.json()
if (response.error) {
setError(response.error)
return
} else {
setError(undefined)
}
setSubmitting(false)
navigator.clipboard.writeText(response.token)
mutate([...(data || []), response])
setNewToken("")
setToast({
message: "Copied to clipboard!",
type: "success"
})
}
const expireToken = async (id: string) => {
setSubmitting(true)
await fetch(`/api/user/tokens?userId=${session.data?.user.id}&tokenId=${id}`, {
method: "DELETE",
headers: {
Authorization: "Bearer " + session?.data?.user.sessionToken
}
})
setSubmitting(false)
mutate(data?.filter((token) => token.id !== id))
}
const onChangeNewToken = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewToken(e.target.value)
}
const hasError = Boolean(error || errorText)
const onCreateTokenClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
setSubmitting(true)
try {
const createdToken = await createToken(newToken)
setNewToken("")
await copyToClipboard(createdToken?.token || "")
setToast({
message: "Your new API key has been copied to your clipboard.",
type: "success"
})
setSubmitting(false)
} catch (e) {
if (e instanceof Error) {
setToast({
message: e.message,
type: "error"
})
}
setSubmitting(false)
}
}
const hasError = Boolean(error)
return (
<>
{!hasError && (
@ -103,31 +62,32 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
API keys allow you to access the API from 3rd party tools.
</Note>
)}
{hasError && <Note type="error">{error?.message || errorText}</Note>}
{hasError && <Note type="error">{error?.message}</Note>}
<form className={styles.form}>
<h3>Create new</h3>
<Input
type="text"
value={newToken}
onChange={onChangeNewToken}
aria-label="API Key name"
placeholder="Name"
/>
<Button
type="button"
onClick={createToken}
loading={submitting}
disabled={!newToken}
>
Submit
</Button>
<h5>Create new</h5>
<fieldset className={styles.fieldset}>
<Input
type="text"
value={newToken}
onChange={onChangeNewToken}
aria-label="API Key name"
placeholder="Name"
/>
<Button
type="button"
onClick={onCreateTokenClick}
loading={submitting}
disabled={!newToken}
>
Submit
</Button>
</fieldset>
</form>
<div className={styles.tokens}>
{data ? (
data?.length ? (
<table width={'100%'}>
<table width={"100%"}>
<thead>
<tr>
<th>Name</th>
@ -144,7 +104,6 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
<Button
type="button"
onClick={() => expireToken(token.id)}
loading={submitting}
>
Revoke
</Button>

View file

@ -5,7 +5,7 @@ import Input from "@components/input"
import Note from "@components/note"
import { useToasts } from "@components/toasts"
import { useSession } from "next-auth/react"
import { useState } from "react"
import { useEffect, useState } from "react"
import styles from "./profile.module.css"
const Profile = () => {
@ -14,6 +14,12 @@ const Profile = () => {
const [submitting, setSubmitting] = useState<boolean>(false)
const { setToast } = useToasts()
useEffect(() => {
if (!name) {
setName(session?.user.name || "")
}
}, [name, session])
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}
@ -71,7 +77,7 @@ const Profile = () => {
return (
<>
<Note type="warning">
This information will be publicly available on your profile.
Your display name is publicly available on your profile.
</Note>
<form onSubmit={onSubmit} className={styles.form}>
<div>

View file

@ -11,7 +11,8 @@ export default function SettingsLayout({
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
marginBottom: "var(--gap)"
marginBottom: "var(--gap)",
marginTop: "var(--gap)"
}}
>
{children}

View file

@ -1,5 +1,5 @@
import SettingsGroup from "../components/settings-group"
import Profile from "app/settings/components/sections/profile"
import Profile from "src/app/settings/components/sections/profile"
import APIKeys from "./components/sections/api-keys"
export default async function SettingsPage() {

View file

@ -175,4 +175,43 @@ textarea {
textarea:focus {
border-color: var(--light-gray);
outline: none;
}
}
h1 {
font-size: 2.5rem;
margin: 0;
}
h2 {
font-size: 2rem;
margin: 0;
}
h3 {
font-size: 1.5rem;
margin: 0;
}
h4 {
font-size: 1.25rem;
margin: 0;
}
h5 {
font-size: 1rem;
margin: 0;
}
h6 {
font-size: 0.875rem;
margin: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700;
}

View file

@ -1,11 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
setupFiles: ["<rootDir>/test/setup-tests.ts"],
moduleNameMapper: {
"@lib/(.*)": "<rootDir>/src/lib/$1",
"@routes/(.*)": "<rootDir>/src/routes/$1"
},
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/dist/"]
}

View file

@ -0,0 +1,9 @@
import config from "./config"
export const revalidatePage = async (path: string) => {
const res = await fetch(
`${process.env.DRIFT_URL}/api/revalidate?secret=${config.nextauth_secret}&path=${path}`
)
const json = await res.json()
return json
}

View file

@ -119,6 +119,7 @@ const providers = () => {
}
})
console.log("New user created")
return newUser
}
}

View file

@ -117,6 +117,7 @@ export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
})
}
})
console.log(posts)
return posts
}
@ -272,6 +273,7 @@ export const searchPosts = async (
}
}
},
authorId: userId,
visibility: publicOnly ? "public" : undefined
}
]

View file

@ -46,9 +46,18 @@ const parseAndCheckAuthToken = async (req: NextApiRequest) => {
token
},
select: {
userId: true
userId: true,
expiresAt: true
}
})
if (!user) {
return null
}
if (user.expiresAt < new Date()) {
return null
}
return user?.userId
}

5
src/next-env.d.ts vendored
View file

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -1,31 +0,0 @@
import bundleAnalyzer from "@next/bundle-analyzer"
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
// esmExternals: true,
appDir: true,
serverComponentsExternalPackages: ["prisma", "@prisma/client"]
},
output: "standalone",
rewrites() {
return [
{
source: "/file/raw/:id",
destination: `/api/raw/:id`
},
{
source: "/home",
destination: "/"
}
]
},
images: {
domains: ["avatars.githubusercontent.com"]
}
}
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
nextConfig
)

View file

@ -1,86 +0,0 @@
{
"name": "drift",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start --port 3000",
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,app,pages}/**/*.{ts,tsx}' --write",
"analyze": "cross-env ANALYZE=true next build",
"find:unused": "next-unused",
"prisma": "prisma",
"jest": "jest"
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.5",
"@next/eslint-plugin-next": "13.1.2-canary.0",
"@next/font": "13.1.2-canary.0",
"@prisma/client": "^4.8.0",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.1",
"@radix-ui/react-popover": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-tooltip": "^1.0.2",
"@wcj/markdown-to-html": "^2.1.2",
"@wits/next-themes": "0.2.14",
"client-only": "^0.0.1",
"client-zip": "2.2.1",
"jest": "^29.3.1",
"lodash.debounce": "^4.0.8",
"next": "13.1.2-canary.0",
"next-auth": "^4.18.6",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-datepicker": "4.8.0",
"react-dom": "18.2.0",
"react-dropzone": "14.2.3",
"react-feather": "^2.0.10",
"react-hot-toast": "2.4.0-beta.0",
"server-only": "^0.0.1",
"swr": "^2.0.0",
"textarea-markdown-editor": "1.0.4",
"ts-jest": "^29.0.3",
"uuid": "^9.0.0"
},
"devDependencies": {
"@next/bundle-analyzer": "13.0.7-canary.4",
"@types/bcrypt": "^5.0.0",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "17.0.23",
"@types/react": "18.0.9",
"@types/react-datepicker": "4.4.1",
"@types/react-dom": "18.0.3",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"clsx": "^1.2.1",
"cross-env": "7.0.3",
"csstype": "^3.1.1",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"next-unused": "0.0.6",
"prettier": "2.6.2",
"prisma": "^4.8.0",
"typescript": "4.6.4",
"typescript-plugin-css-modules": "3.4.0"
},
"optionalDependencies": {
"sharp": "^0.31.2"
},
"next-unused": {
"alias": {
"@components": "components/",
"@lib": "lib/",
"@styles": "styles/"
},
"include": [
"components",
"lib"
]
},
"overrides": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

View file

@ -1,7 +1,7 @@
import { withMethods } from "@lib/api-middleware/with-methods"
import { parseQueryParam } from "@lib/server/parse-query-param"
import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "lib/server/prisma"
import { prisma } from "src/lib/server/prisma"
import { getSession } from "next-auth/react"
import { deleteUser } from "../user/[id]"

View file

@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "lib/server/prisma"
import { prisma } from "src/lib/server/prisma"
import { parseQueryParam } from "@lib/server/parse-query-param"
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {

View file

@ -3,7 +3,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
import { getPostById } from "@lib/server/prisma"
import type { NextApiRequest, NextApiResponse } from "next"
import { getSession } from "next-auth/react"
import { prisma } from "lib/server/prisma"
import { prisma } from "src/lib/server/prisma"
import * as crypto from "crypto"
const handler = async (req: NextApiRequest, res: NextApiResponse) => {

View file

@ -95,7 +95,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
.catch((error) => {
return res.status(500).json(error)
})
return res.json(post)
} catch (error) {
return res.status(500).json(error)

View file

@ -2,12 +2,12 @@ import { withMethods } from "@lib/api-middleware/with-methods"
import { parseQueryParam } from "@lib/server/parse-query-param"
import { searchPosts, ServerPostWithFiles } from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next"
import { getSession } from "next-auth/react"
import { unstable_getServerSession } from "next-auth"
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { q, userId } = req.query
const session = await getSession()
const session = await unstable_getServerSession()
const query = parseQueryParam(q)
const user = parseQueryParam(userId)

View file

@ -0,0 +1,28 @@
// https://beta.nextjs.org/docs/data-fetching/revalidating#on-demand-revalidation
import config from "@lib/config"
import { parseQueryParam } from "@lib/server/parse-query-param"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
// TODO: create a new secret?
if (req.query.secret !== config.nextauth_secret) {
return res.status(401).json({ message: "Invalid token" })
}
const path = parseQueryParam(req.query.path)
try {
if (path) {
await res.revalidate(path)
return res.json({ revalidated: true })
}
} catch (err) {
// If there was an error, Next.js will continue
// to show the last successfully generated page
return res.status(500).send("Error revalidating")
}
}

View file

@ -1,7 +1,7 @@
import { parseQueryParam } from "@lib/server/parse-query-param"
import { getUserById } from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "lib/server/prisma"
import { prisma } from "src/lib/server/prisma"
import { withMethods } from "@lib/api-middleware/with-methods"
import { getSession } from "next-auth/react"

7280
src/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

View file

@ -1,19 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="72" height="72"><svg width="72" height="72" version="1.1" viewBox="0 0 19.05 19.05" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="SvgjsClipPath1001">
<circle cx="115.27" cy="135.33" r="9.1406"></circle>
</clipPath>
<clipPath id="SvgjsClipPath1000">
<circle cx="115.27" cy="135.33" r="9.1406"></circle>
</clipPath>
</defs>
<g transform="translate(-106.13 -126.19)">
<rect transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" x="106.13" y="126.19" width="18.281" height="18.281" clip-path="url(#clipPath7864)" fill="#1b1b1b"></rect>
<g transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" clip-path="url(#clipPath7860)" stroke-width=".95964">
<path d="m132.15 142c-10.707-9.0354-17.374-8.908-17.374-8.908s2.3881 3.4829 0.94799 7.4545c-1.4401 3.9716-7.6664 7.7467-7.6664 7.7467z" fill="#fff"></path>
<path d="m108.89 148.35s6.2294-3.8135 7.6695-7.7851c1.4401-3.9716-1.7851-7.4745-1.7851-7.4745s1.2204 3.091-1.0184 6.7752c-2.239 3.6841-8.9226 4.9787-8.9226 4.9787z" fill="#e7e7e7"></path>
<path d="m105.93 146.03s6.6058-2.1876 8.8448-5.8717c2.2387-3.6841-5e-3 -7.0674-5e-3 -7.0674s-1.3628 3.2487-4.9368 5.2561-10.125 2.2728-10.125 2.2728z" fill="#c6c6c6"></path>
</g>
</g>
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
</style></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72" height="72" version="1.1" viewBox="0 0 19.05 19.05" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="clipPath7860">
<circle cx="115.27" cy="135.33" r="9.1406" />
</clipPath>
<clipPath id="clipPath7864">
<circle cx="115.27" cy="135.33" r="9.1406" />
</clipPath>
</defs>
<g transform="translate(-106.13 -126.19)">
<rect transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" x="106.13" y="126.19" width="18.281" height="18.281" clip-path="url(#clipPath7864)" fill="#1b1b1b" />
<g transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" clip-path="url(#clipPath7860)" stroke-width=".95964">
<path d="m132.15 142c-10.707-9.0354-17.374-8.908-17.374-8.908s2.3881 3.4829 0.94799 7.4545c-1.4401 3.9716-7.6664 7.7467-7.6664 7.7467z" fill="#fff" />
<path d="m108.89 148.35s6.2294-3.8135 7.6695-7.7851c1.4401-3.9716-1.7851-7.4745-1.7851-7.4745s1.2204 3.091-1.0184 6.7752c-2.239 3.6841-8.9226 4.9787-8.9226 4.9787z" fill="#e7e7e7" />
<path d="m105.93 146.03s6.6058-2.1876 8.8448-5.8717c2.2387-3.6841-5e-3 -7.0674-5e-3 -7.0674s-1.3628 3.2487-4.9368 5.2561-10.125 2.2728-10.125 2.2728z" fill="#c6c6c6" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View file

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="72.000008"
height="72"
viewBox="0 0 19.05 19.05"
version="1.1"
id="svg5"
inkscape:export-filename="/home/reese/git/github.com/maxleiter/drift/logo.png"
inkscape:export-xdpi="682.66669"
inkscape:export-ydpi="682.66669"
inkscape:version="1.1.2 (1:1.1+202202050950+0a00cf5339)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:document-units="px"
showgrid="false"
showguides="false"
inkscape:zoom="13.877295"
inkscape:cx="10.448722"
inkscape:cy="34.444753"
inkscape:current-layer="g3632"
units="px"
viewbox-width="19.05" />
<defs
id="defs2">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath7860">
<circle
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.93688;stroke-linecap:round"
id="circle7862"
cx="115.27311"
cy="135.3275"
r="9.1405506" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath7864">
<circle
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.93688;stroke-linecap:round"
id="circle7866"
cx="115.27311"
cy="135.3275"
r="9.1405506" />
</clipPath>
</defs>
<g
inkscape:label="source strokes"
inkscape:groupmode="layer"
id="layer1"
style="display:none"
transform="translate(-106.13256,-126.18696)">
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 114.7741,133.0871 c 0,0 2.24373,3.38322 0.005,7.06735 -2.23896,3.68413 -8.84476,5.87171 -8.84476,5.87171"
id="path1824"
sodipodi:nodetypes="csc" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 99.71221,140.61603 c 0,0 6.55112,-0.26544 10.1251,-2.27285 3.57398,-2.00741 4.93679,-5.25608 4.93679,-5.25608"
id="path857"
sodipodi:nodetypes="czc" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 114.7741,133.0871 c 0,0 3.22515,3.50294 1.78507,7.47454 -1.44009,3.97159 -7.66948,7.78507 -7.66948,7.78507"
id="path949"
sodipodi:nodetypes="czc" />
<path
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 114.7741,133.0871 c 0,0 6.66681,-0.12736 17.37373,8.90799"
id="path1345"
sodipodi:nodetypes="cc" />
</g>
<g
inkscape:label="Layer 1 copy"
inkscape:groupmode="layer"
id="g3632"
transform="translate(-106.13256,-126.18696)">
<rect
style="display:inline;fill:#1b1b1b;fill-opacity:1;stroke:none;stroke-width:2.81834;stroke-linecap:round"
id="rect6284"
width="18.28112"
height="18.28112"
x="106.13255"
y="126.18695"
clip-path="url(#clipPath7864)"
transform="matrix(1.0420598,0,0,1.0420598,-4.4639102,-5.3073932)" />
<g
id="g937"
inkscape:label="drift"
clip-path="url(#clipPath7860)"
mask="none"
style="display:inline;stroke-width:0.959638"
transform="matrix(1.0420598,0,0,1.0420598,-4.4639102,-5.3073932)">
<path
id="path935"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.253904px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 132.14783,141.99509 c -10.70692,-9.03535 -17.37373,-8.90799 -17.37373,-8.90799 0,0 2.38807,3.48286 0.94799,7.45446 -1.44009,3.97159 -7.66636,7.74668 -7.66636,7.74668 z"
sodipodi:nodetypes="csccc" />
<path
id="path931"
style="fill:#e7e7e7;fill-opacity:1;stroke:none;stroke-width:0.253904px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 108.88969,148.34671 c 0,0 6.22939,-3.81348 7.66948,-7.78507 1.44008,-3.9716 -1.78507,-7.47454 -1.78507,-7.47454 0,0 1.22037,3.09102 -1.01836,6.77515 -2.23896,3.68413 -8.92258,4.9787 -8.92258,4.9787 z"
sodipodi:nodetypes="cscccc" />
<path
id="path933"
style="fill:#c6c6c6;fill-opacity:1;stroke:none;stroke-width:0.253904px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 105.93434,146.02616 c 0,0 6.6058,-2.18758 8.84476,-5.87171 2.23873,-3.68413 -0.005,-7.06735 -0.005,-7.06735 0,0 -1.36281,3.24867 -4.93679,5.25608 -3.57398,2.00741 -10.1251,2.27285 -10.1251,2.27285 z"
sodipodi:nodetypes="csccc" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,26 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3327 6994 c-1 -1 -47 -4 -103 -8 -55 -4 -110 -8 -122 -11 -11 -2
-45 -6 -74 -9 -29 -4 -65 -8 -80 -11 -16 -3 -39 -7 -53 -9 -337 -59 -654 -165
-970 -326 -365 -185 -635 -383 -918 -670 -436 -443 -735 -969 -893 -1570 -14
-52 -27 -106 -30 -120 -2 -14 -7 -36 -10 -50 -10 -41 -12 -53 -29 -166 -24
-164 -24 -162 -32 -264 -9 -119 -9 -456 0 -574 7 -80 20 -189 32 -257 3 -14 7
-41 9 -60 3 -19 14 -77 26 -129 63 -285 145 -523 272 -790 269 -564 718 -1071
1248 -1412 411 -264 845 -433 1323 -513 200 -33 355 -45 584 -45 177 0 267 6
453 26 93 10 308 53 450 90 728 191 1398 640 1872 1254 78 101 216 311 282
430 182 325 342 781 391 1112 21 148 23 167 32 263 10 115 10 547 0 645 -39
380 -126 724 -270 1065 -45 107 -180 361 -253 476 -196 310 -443 594 -709 814
-33 28 -62 53 -65 56 -12 14 -202 150 -280 201 -248 161 -564 313 -805 388
-22 7 -51 17 -64 22 -36 13 -320 85 -366 92 -36 6 -70 12 -129 22 -44 8 -176
24 -251 30 -71 6 -462 13 -468 8z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,19 +0,0 @@
{
"name": "Drift",
"short_name": "Drift",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View file

@ -1,61 +0,0 @@
{
"compilerOptions": {
"plugins": [
{
"name": "typescript-plugin-css-modules"
},
{
"name": "next"
}
],
"target": "es2020",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@components/*": [
"app/components/*"
],
"@lib/*": [
"lib/*"
],
"@styles/*": [
"app/styles/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View file

@ -1,33 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { User } from "next-auth"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { JWT } from "next-auth/jwt"
type UserId = string
declare module "next-auth/jwt" {
interface JWT {
id: UserId
role: string
sessionToken: string
}
}
declare module "next-auth" {
interface Session {
user: User & {
id: UserId
role: string
sessionToken: string
}
}
// override user
interface User {
username?: string | null
email?: string | null
role?: string | null
id: UserId
token?: string
}
}