diff --git a/client/app/(posts)/new/components/post.module.css b/client/app/(posts)/new/components/post.module.css
index 22c7f6d0..7d423d34 100644
--- a/client/app/(posts)/new/components/post.module.css
+++ b/client/app/(posts)/new/components/post.module.css
@@ -1,3 +1,10 @@
+.root {
+ padding-bottom: 200px;
+ display: flex;
+ flex-direction: column;
+ gap: var(--gap);
+}
+
.buttons {
position: relative;
display: flex;
@@ -19,7 +26,6 @@
.description {
width: 100%;
- margin-bottom: var(--gap);
}
@media screen and (max-width: 650px) {
diff --git a/client/app/(posts)/new/components/title/index.tsx b/client/app/(posts)/new/components/title/index.tsx
index 3e32b8d7..fbc3714f 100644
--- a/client/app/(posts)/new/components/title/index.tsx
+++ b/client/app/(posts)/new/components/title/index.tsx
@@ -23,7 +23,7 @@ type props = {
const Title = ({ onChange, title }: props) => {
return (
-
Drift
+
Drift
{
+const DownloadButtons = ({ rawLink }: { rawLink?: string }) => {
return (
@@ -59,12 +59,6 @@ const Document = ({
skeleton,
id
}: Props) => {
- const rawLink = () => {
- if (id) {
- return `/file/raw/${id}`
- }
- }
-
if (skeleton) {
return (
<>
@@ -98,7 +92,7 @@ const Document = ({
/>
-
+
{
if (post.visibility === "protected" && !isAuthorOrAdmin) {
return {
+ // TODO: remove this. It's temporary to appease typescript
post: {
visibility: "protected",
id: post.id,
@@ -64,6 +65,7 @@ const getPost = async (id: string) => {
parentId: "",
title: "",
createdAt: new Date("1970-01-01"),
+ expiresAt: new Date("1970-01-01"),
author: {
displayName: "",
},
@@ -108,7 +110,8 @@ const PostView = async ({
/>
{
]
}, [isAdmin, resolvedTheme, isSignedIn, setTheme])
- // // TODO: this should not be necessary.
- // if (!clientHydrated) {
- // return (
- //
- // )
- // }
-
const buttons = pages.map(getButton)
+ // 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 (
{
{button}
diff --git a/client/app/components/input/index.tsx b/client/app/components/input/index.tsx
index 7f7185d4..f5ad75b1 100644
--- a/client/app/components/input/index.tsx
+++ b/client/app/components/input/index.tsx
@@ -2,15 +2,6 @@ import clsx from "clsx"
import React from "react"
import styles from "./input.module.css"
-type RequireOnlyOne = Pick<
- T,
- Exclude
-> &
- {
- [K in Keys]-?: Required> &
- Partial, undefined>>
- }[Keys]
-
type Props = React.HTMLProps & {
label?: string
width?: number | string
@@ -18,8 +9,30 @@ type Props = React.HTMLProps & {
labelClassName?: string
}
-type InputProps = RequireOnlyOne
-
+// we have two special rules on top of the props:
+// if onChange or value is passed, we require both
+// if label is passed, we forbid aria-label and vice versa
+type InputProps = Omit &
+ (
+ | {
+ onChange: Props["onChange"]
+ value: Props["value"]
+ } // if onChange or value is passed, we require both
+ | {
+ onChange?: never
+ value?: never
+ }
+ ) &
+ (
+ | {
+ label: Props["label"]
+ "aria-label"?: never
+ } // if label is passed, we forbid aria-label and vice versa
+ | {
+ label?: never
+ "aria-label": Props["aria-label"]
+ }
+ )
// eslint-disable-next-line react/display-name
const Input = React.forwardRef(
({ label, className, width, height, labelClassName, ...props }, ref) => {
diff --git a/client/app/components/input/input.module.css b/client/app/components/input/input.module.css
index bc97a4b4..eb147988 100644
--- a/client/app/components/input/input.module.css
+++ b/client/app/components/input/input.module.css
@@ -51,12 +51,6 @@
white-space: nowrap;
}
-@media screen and (max-width: 768px) {
- .wrapper {
- margin-bottom: var(--gap);
- }
-}
-
.input:disabled {
background-color: var(--lighter-gray);
color: var(--fg);
diff --git a/client/app/components/post-list/index.tsx b/client/app/components/post-list/index.tsx
index 60e6b1f8..e8838c0d 100644
--- a/client/app/components/post-list/index.tsx
+++ b/client/app/components/post-list/index.tsx
@@ -2,14 +2,20 @@
import styles from "./post-list.module.css"
import ListItem from "./list-item"
-import { ChangeEvent, useCallback, useEffect, useState } from "react"
-import useDebounce from "@lib/hooks/use-debounce"
+import {
+ ChangeEvent,
+ useCallback,
+ useDeferredValue,
+ useEffect,
+ 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 debounce from "lodash.debounce"
type Props = {
initialPosts: string | PostWithFiles[]
@@ -32,8 +38,6 @@ const PostList = ({
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
const { setToast } = useToasts()
- const debouncedSearchValue = useDebounce(search, 200)
-
const loadMoreClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
@@ -56,36 +60,30 @@ const PostList = ({
[posts, hasMorePosts]
)
- // update posts on search
- useEffect(() => {
- if (debouncedSearchValue) {
- setSearching(true)
- async function fetchPosts() {
- const res = await fetch(
- `/api/post/search?q=${encodeURIComponent(
- debouncedSearchValue
- )}&userId=${userId}`,
- {
- method: "GET",
- headers: {
- "Content-Type": "application/json"
- }
+ const onSearch = (query: string) => {
+ setSearching(true)
+ async function fetchPosts() {
+ const res = await fetch(
+ `/api/post/search?q=${encodeURIComponent(query)}&userId=${userId}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json"
}
- )
- const json = await res.json()
- setPosts(json)
- setSearching(false)
- }
- fetchPosts()
- } else {
- setPosts(initialPosts)
+ }
+ )
+ const json = await res.json()
+ setPosts(json)
+ setSearching(false)
}
- // TODO: fix cyclical dependency issue
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [debouncedSearchValue, userId])
+ fetchPosts()
+ }
+
+ const debouncedSearch = debounce(onSearch, 500)
const handleSearchChange = (e: ChangeEvent) => {
setSearchValue(e.target.value)
+ debouncedSearch(e.target.value)
}
const deletePost = useCallback(
@@ -117,10 +115,11 @@ const PostList = ({
disabled={!posts}
style={{ maxWidth: 300 }}
aria-label="Search"
+ value={search}
/>
{!posts && Failed to load.
}
- {!posts?.length && (
+ {searching && (
diff --git a/client/package.json b/client/package.json
index e2ff4132..7c0a70d6 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,7 +15,7 @@
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.5",
"@next/eslint-plugin-next": "13.0.5-canary.3",
- "@prisma/client": "^4.6.1",
+ "@prisma/client": "^4.7.1",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.1",
"@radix-ui/react-popover": "^1.0.2",
@@ -26,6 +26,7 @@
"bcrypt": "^5.1.0",
"client-zip": "2.2.1",
"jest": "^29.3.1",
+ "lodash.debounce": "^4.0.8",
"next": "13.0.7-canary.1",
"next-auth": "^4.18.0",
"prisma": "^4.7.1",
@@ -41,6 +42,7 @@
"devDependencies": {
"@next/bundle-analyzer": "12.1.6",
"@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",
diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml
index c0ce9c79..99934ec6 100644
--- a/client/pnpm-lock.yaml
+++ b/client/pnpm-lock.yaml
@@ -4,13 +4,14 @@ specifiers:
'@next-auth/prisma-adapter': ^1.0.5
'@next/bundle-analyzer': 12.1.6
'@next/eslint-plugin-next': 13.0.5-canary.3
- '@prisma/client': ^4.6.1
+ '@prisma/client': ^4.7.1
'@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
'@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
@@ -25,6 +26,7 @@ specifiers:
eslint: 8.27.0
eslint-config-next: 13.0.3
jest: ^29.3.1
+ lodash.debounce: ^4.0.8
next: 13.0.7-canary.1
next-auth: ^4.18.0
next-unused: 0.0.6
@@ -43,9 +45,9 @@ specifiers:
typescript-plugin-css-modules: 3.4.0
dependencies:
- '@next-auth/prisma-adapter': 1.0.5_qwexivae5olc6wqfcmxswm7qjy
+ '@next-auth/prisma-adapter': 1.0.5_hpttyne5hky44pj2anoxcmv4zm
'@next/eslint-plugin-next': 13.0.5-canary.3
- '@prisma/client': 4.6.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-dropdown-menu': 2.0.1_jbvntnid6ohjelon6ccj5dhg2u
'@radix-ui/react-popover': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
@@ -56,6 +58,7 @@ dependencies:
bcrypt: 5.1.0
client-zip: 2.2.1
jest: 29.3.1_@types+node@17.0.23
+ lodash.debounce: 4.0.8
next: 13.0.7-canary.1_biqbaboplfbrettd7655fr4n2y
next-auth: 4.18.0_ihvxcpofhpc4k2aqfys2drrlkq
prisma: 4.7.1
@@ -74,6 +77,7 @@ optionalDependencies:
devDependencies:
'@next/bundle-analyzer': 12.1.6
'@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_biqbaboplfbrettd7655fr4n2y
@@ -785,13 +789,13 @@ packages:
- supports-color
dev: false
- /@next-auth/prisma-adapter/1.0.5_qwexivae5olc6wqfcmxswm7qjy:
+ /@next-auth/prisma-adapter/1.0.5_hpttyne5hky44pj2anoxcmv4zm:
resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==}
peerDependencies:
'@prisma/client': '>=2.26.0 || >=3'
next-auth: ^4
dependencies:
- '@prisma/client': 4.6.1_prisma@4.7.1
+ '@prisma/client': 4.7.1_prisma@4.7.1
next-auth: 4.18.0_ihvxcpofhpc4k2aqfys2drrlkq
dev: false
@@ -969,8 +973,8 @@ packages:
/@popperjs/core/2.11.6:
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
- /@prisma/client/4.6.1_prisma@4.7.1:
- resolution: {integrity: sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==}
+ /@prisma/client/4.7.1_prisma@4.7.1:
+ resolution: {integrity: sha512-/GbnOwIPtjiveZNUzGXOdp7RxTEkHL4DZP3vBaFNadfr6Sf0RshU5EULFzVaSi9i9PIK9PYd+1Rn7z2B2npb9w==}
engines: {node: '>=14.17'}
requiresBuild: true
peerDependencies:
@@ -979,12 +983,12 @@ packages:
prisma:
optional: true
dependencies:
- '@prisma/engines-version': 4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32
+ '@prisma/engines-version': 4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c
prisma: 4.7.1
dev: false
- /@prisma/engines-version/4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32:
- resolution: {integrity: sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==}
+ /@prisma/engines-version/4.7.1-1.272861e07ab64f234d3ffc4094e32bd61775599c:
+ resolution: {integrity: sha512-Bd4LZ+WAnUHOq31e9X/ihi5zPlr4SzTRwUZZYxvWOxlerIZ7HJlVa9zXpuKTKLpI9O1l8Ec4OYCKsivWCs5a3Q==}
dev: false
/@prisma/engines/4.7.1:
@@ -1519,6 +1523,16 @@ packages:
resolution: {integrity: sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==}
dev: false
+ /@types/lodash.debounce/4.0.7:
+ resolution: {integrity: sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==}
+ dependencies:
+ '@types/lodash': 4.14.191
+ dev: true
+
+ /@types/lodash/4.14.191:
+ resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
+ dev: true
+
/@types/mdast/3.0.10:
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
dependencies:
@@ -4584,6 +4598,10 @@ packages:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
dev: true
+ /lodash.debounce/4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+ dev: false
+
/lodash.memoize/4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
dev: false
diff --git a/client/prisma/migrations/20221112095706_init/migration.sql b/client/prisma/migrations/20221112095706_init/migration.sql
deleted file mode 100644
index 9172c0b7..00000000
--- a/client/prisma/migrations/20221112095706_init/migration.sql
+++ /dev/null
@@ -1,109 +0,0 @@
--- CreateTable
-CREATE TABLE "SequelizeMeta" (
- "name" TEXT NOT NULL,
-
- CONSTRAINT "SequelizeMeta_pkey" PRIMARY KEY ("name")
-);
-
--- CreateTable
-CREATE TABLE "files" (
- "id" TEXT NOT NULL,
- "title" TEXT NOT NULL,
- "content" TEXT NOT NULL,
- "sha" TEXT NOT NULL,
- "html" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "deletedAt" TIMESTAMP(3),
- "userId" TEXT NOT NULL,
- "postId" TEXT NOT NULL,
-
- CONSTRAINT "files_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "posts" (
- "id" TEXT NOT NULL,
- "title" TEXT NOT NULL,
- "visibility" TEXT NOT NULL,
- "password" TEXT,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "deletedAt" TIMESTAMP(3),
- "expiresAt" TIMESTAMP(3),
- "parentId" TEXT,
- "description" TEXT,
- "authorId" TEXT NOT NULL,
-
- CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "accounts" (
- "id" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "type" TEXT NOT NULL,
- "provider" TEXT NOT NULL,
- "providerAccountId" TEXT NOT NULL,
- "refresh_token" TEXT,
- "access_token" TEXT,
- "expires_at" INTEGER,
- "token_type" TEXT,
- "scope" TEXT,
- "id_token" TEXT,
- "session_state" TEXT,
- "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "refresh_token_expires_in" INTEGER,
-
- CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Session" (
- "id" TEXT NOT NULL,
- "sessionToken" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "expires" TIMESTAMP(3) NOT NULL,
-
- CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "users" (
- "id" TEXT NOT NULL,
- "name" TEXT,
- "email" TEXT,
- "emailVerified" TIMESTAMP(3),
- "image" TEXT,
- "username" TEXT,
- "role" TEXT DEFAULT 'user',
- "password" TEXT,
-
- CONSTRAINT "users_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "verification_tokens" (
- "identifier" TEXT NOT NULL,
- "token" TEXT NOT NULL,
- "expires" TIMESTAMP(3) NOT NULL
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "accounts_provider_providerAccountId_key" ON "accounts"("provider", "providerAccountId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-
--- CreateIndex
-CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-
--- CreateIndex
-CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-
--- CreateIndex
-CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token");
-
--- CreateIndex
-CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token");
diff --git a/client/prisma/schema.prisma b/client/prisma/schema.prisma
index e0132e7f..16a684bd 100644
--- a/client/prisma/schema.prisma
+++ b/client/prisma/schema.prisma
@@ -1,15 +1,14 @@
generator client {
provider = "prisma-client-js"
- previewFeatures = ["referentialIntegrity", "fullTextSearch"]
+ previewFeatures = ["fullTextSearch"]
}
datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
- referentialIntegrity = "prisma"
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+ relationMode = "prisma"
}
-
model SequelizeMeta {
name String @id
}
@@ -18,7 +17,7 @@ model File {
id String @id @default(cuid())
title String
content String
- sha String
+ sha String
html String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -27,6 +26,7 @@ model File {
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
+ @@index([postId, userId, id])
@@map("files")
}
@@ -41,37 +41,35 @@ model Post {
expiresAt DateTime?
parentId String?
description String?
- author User? @relation(fields: [authorId], references: [id])
authorId String
files File[]
+ author User? @relation(fields: [authorId], references: [id])
+ @@index([authorId, id])
@@map("posts")
}
-// Next auth stuff, from https://next-auth.js.org/adapters/prisma
-
model Account {
- id String @id @default(cuid())
- userId String
- type String
- provider String
- providerAccountId String
- refresh_token String? @db.Text
- access_token String? @db.Text
- expires_at Int?
- token_type String?
- scope String?
- id_token String? @db.Text
- session_state String?
- createdAt DateTime @default(now()) @map(name: "created_at")
- updatedAt DateTime @default(now()) @map(name: "updated_at")
- // https://next-auth.js.org/providers/github
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String?
+ access_token String?
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String?
+ session_state String?
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @default(now()) @map("updated_at")
refresh_token_expires_in Int?
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
- @@map(name: "accounts")
+ @@index([userId, providerAccountId], map: "accounts_provider_account_id")
+ @@map("accounts")
}
model Session {
@@ -80,6 +78,9 @@ model Session {
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@index([userId, expires], map: "sessions_user_id_expires")
+ @@map("sessions")
}
model User {
@@ -88,16 +89,13 @@ model User {
email String? @unique
emailVerified DateTime?
image String?
+ role String? @default("user")
createdAt DateTime @default(now())
+ displayName String?
updatedAt DateTime @updatedAt
-
- accounts Account[]
- sessions Session[]
-
- // custom fields
- posts Post[]
- role String? @default("user")
- displayName String?
+ posts Post[]
+ accounts Account[]
+ sessions Session[]
@@map("users")
}