More uniform home page spacing, close mobile menu on click

This commit is contained in:
Max Leiter 2022-12-04 14:26:05 -08:00
parent a84dad1dde
commit 72633c6ad2
18 changed files with 164 additions and 230 deletions

View file

@ -1,5 +1,4 @@
.card {
margin: var(--gap) auto;
padding: var(--gap);
border: 1px solid var(--lighter-gray);
border-radius: var(--radius);

View file

@ -252,7 +252,7 @@ const Post = ({
)
return (
<div style={{ paddingBottom: 200 }}>
<div className={styles.root}>
<Title title={title} onChange={onChangeTitle} />
<Description description={description} onChange={onChangeDescription} />
<FileDropzone setDocs={uploadDocs} />

View file

@ -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) {

View file

@ -23,7 +23,7 @@ type props = {
const Title = ({ onChange, title }: props) => {
return (
<div className={styles.title}>
<h1>Drift</h1>
<h1 style={{ margin: 0, padding: 0 }}>Drift</h1>
<Input
placeholder={placeholder}
value={title}

View file

@ -3,7 +3,7 @@
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: var(--gap);
gap: inherit;
}
@media screen and (max-width: 650px) {

View file

@ -10,8 +10,8 @@ type TitleProps = {
loading?: boolean
displayName?: string
visibility?: string
createdAt?: Date
expiresAt?: Date
createdAt?: string
expiresAt?: string
authorId?: string
}

View file

@ -1,5 +1,4 @@
.card {
margin: var(--gap) auto;
padding: var(--gap);
border: 1px solid var(--light-gray);
border-radius: var(--radius);

View file

@ -20,7 +20,7 @@ type Props = {
preview: string
}
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
const DownloadButtons = ({ rawLink }: { rawLink?: string }) => {
return (
<div className={styles.actionWrapper}>
<ButtonGroup className={styles.actions}>
@ -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 = ({
/>
</Link>
<div className={styles.descriptionContainer}>
<DownloadButton rawLink={rawLink()} />
<DownloadButtons rawLink={`/api/file/raw/${id}`} />
<DocumentTabs
defaultTab={initialTab}
preview={preview}

View file

@ -57,6 +57,7 @@ const getPost = async (id: string) => {
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 ({
/>
<PostTitle
title={post.title}
createdAt={post.createdAt}
createdAt={post.createdAt.toString()}
expiresAt={post.expiresAt?.toString()}
displayName={post.author?.displayName || ""}
visibility={post.visibility}
authorId={post.authorId}

View file

@ -45,7 +45,6 @@
opacity: 1;
}
.selectContent {
width: auto;
height: 18px;
@ -64,6 +63,10 @@
display: none;
}
.contentWrapper {
background: var(--bg);
}
@media only screen and (max-width: 768px) {
.wrapper [data-tab="github"] {
display: none;
@ -71,6 +74,7 @@
.mobile {
margin-top: var(--gap);
margin-bottom: var(--gap);
display: flex;
}
@ -79,6 +83,23 @@
flex-direction: column;
}
.dropdownItem:not(:first-child):not(:last-child) :global(button) {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.dropdownItem:first-child :global(button) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.dropdownItem:last-child :global(button) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.dropdownItem a,
.dropdownItem button {
width: 100%;

View file

@ -22,7 +22,7 @@ import {
} from "react-feather"
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import buttonStyles from "@components/button/button.module.css"
import { useEffect, useMemo, useState } from "react"
import { useMemo } from "react"
type Tab = {
name: string
@ -155,17 +155,13 @@ const Header = () => {
]
}, [isAdmin, resolvedTheme, isSignedIn, setTheme])
// // TODO: this should not be necessary.
// if (!clientHydrated) {
// return (
// <header>
// <div className={styles.tabs}>{getPages(true).map(getButton)}</div>
// </header>
// )
// }
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 (
<header className={clsx(styles.header, {
[styles.loading]: isLoading,
@ -188,6 +184,7 @@ const Header = () => {
<DropdownMenu.Item
key={button?.key}
className={styles.dropdownItem}
onClick={onClick}
>
{button}
</DropdownMenu.Item>

View file

@ -2,15 +2,6 @@ import clsx from "clsx"
import React from "react"
import styles from "./input.module.css"
type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Pick<
T,
Exclude<keyof T, Keys>
> &
{
[K in Keys]-?: Required<Pick<T, K>> &
Partial<Record<Exclude<Keys, K>, undefined>>
}[Keys]
type Props = React.HTMLProps<HTMLInputElement> & {
label?: string
width?: number | string
@ -18,8 +9,30 @@ type Props = React.HTMLProps<HTMLInputElement> & {
labelClassName?: string
}
type InputProps = RequireOnlyOne<Props, "label" | "aria-label">
// 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<Props, "onChange" | "value" | "label" | "aria-label"> &
(
| {
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<HTMLInputElement, InputProps>(
({ label, className, width, height, labelClassName, ...props }, ref) => {

View file

@ -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);

View file

@ -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<HTMLButtonElement>) => {
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<HTMLInputElement>) => {
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}
/>
</div>
{!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>}
{!posts?.length && (
{searching && (
<ul>
<ListItemSkeleton />
<ListItemSkeleton />

View file

@ -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",

View file

@ -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

View file

@ -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");

View file

@ -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")
}