- {
-
- }
- placeholderText="Won't expire"
- selected={expiresAt}
- showTimeInput={true}
- // @ts-ignore
- customTimeInput={
}
- timeInputLabel="Time:"
- dateFormat="MM/dd/yyyy h:mm aa"
- className={styles.datePicker}
- clearButtonTitle={"Clear"}
- // TODO: investigate why this causes margin shift if true
- enableTabLoop={false}
- minDate={new Date()}
- />
- }
+
+ }
+ placeholderText="Won't expire"
+ selected={expiresAt}
+ showTimeInput={true}
+ // @ts-ignore
+ customTimeInput={
}
+ timeInputLabel="Time:"
+ dateFormat="MM/dd/yyyy h:mm aa"
+ className={styles.datePicker}
+ clearButtonTitle={"Clear"}
+ // TODO: investigate why this causes margin shift if true
+ enableTabLoop={false}
+ minDate={new Date()}
+ />
onSubmit("unlisted")}>
Create Unlisted
diff --git a/client/app/(posts)/post/[id]/components/post-page/index.tsx b/client/app/(posts)/post/[id]/components/post-page/index.tsx
index 73864158..a8635efd 100644
--- a/client/app/(posts)/post/[id]/components/post-page/index.tsx
+++ b/client/app/(posts)/post/[id]/components/post-page/index.tsx
@@ -19,13 +19,13 @@ import VisibilityControl from "@components/badges/visibility-control"
import { File, PostWithFiles } from "@lib/server/prisma"
type Props = {
- post: PostWithFiles
+ post: string | PostWithFiles
isProtected?: boolean
isAuthor?: boolean
}
const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
- const [post, setPost] = useState(initialPost)
+ const [post, setPost] = useState(typeof initialPost === "string" ? JSON.parse(initialPost) : initialPost)
const [visibility, setVisibility] = useState(post.visibility)
const [isExpired, setIsExpired] = useState(
post.expiresAt ? new Date(post.expiresAt) < new Date() : null
@@ -50,7 +50,7 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
if (post.expiresAt) {
interval = setInterval(() => {
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
- setIsExpired(expirationDate < new Date())
+ if (expirationDate < new Date()) setIsExpired(true)
}, 4000)
}
return () => {
@@ -128,7 +128,7 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
- {post.title}
+ {post.title}
@@ -138,7 +138,7 @@ const PostPage = ({ post: initialPost, isProtected, isAuthor }: Props) => {
{post.description && (
-
{post.description}
+
{post.description}
)}
{/* {post.files.length > 1 &&
} */}
diff --git a/client/app/(posts)/post/[id]/components/post-page/view-document/index.tsx b/client/app/(posts)/post/[id]/components/post-page/view-document/index.tsx
index 8047b1b1..c0584c35 100644
--- a/client/app/(posts)/post/[id]/components/post-page/view-document/index.tsx
+++ b/client/app/(posts)/post/[id]/components/post-page/view-document/index.tsx
@@ -1,8 +1,8 @@
-import { memo, useRef, useState } from "react"
+import { memo, useRef } from "react"
import styles from "./document.module.css"
import Download from "@geist-ui/icons/download"
import ExternalLink from "@geist-ui/icons/externalLink"
-import Skeleton from "react-loading-skeleton"
+import Skeleton from "@components/skeleton"
import Link from "next/link"
import {
@@ -71,15 +71,12 @@ const Document = ({
id
}: Props) => {
const codeEditorRef = useRef
(null)
- const [tab, setTab] = useState(initialTab)
- // const height = editable ? "500px" : '100%'
const height = "100%"
const handleTabChange = (newTab: string) => {
if (newTab === "edit") {
codeEditorRef.current?.focus()
}
- setTab(newTab as "edit" | "preview")
}
const rawLink = () => {
diff --git a/client/app/(posts)/post/[id]/page.tsx b/client/app/(posts)/post/[id]/page.tsx
index 64985690..a80a8a3b 100644
--- a/client/app/(posts)/post/[id]/page.tsx
+++ b/client/app/(posts)/post/[id]/page.tsx
@@ -1,23 +1,25 @@
-import type { GetServerSideProps } from "next"
-
import type { Post } from "@lib/types"
import PostPage from "app/(posts)/post/[id]/components/post-page"
import { notFound } from "next/navigation"
import { getAllPosts, getPostById } from "@lib/server/prisma"
-import { getCurrentUser, getSession } from "@lib/server/session"
+import { getCurrentUser } from "@lib/server/session"
export type PostProps = {
post: Post
isProtected?: boolean
}
-export async function generateStaticParams() {
- const posts = await getAllPosts()
+// export async function generateStaticParams() {
+// const posts = await getAllPosts({
+// where: {
+// visibility: "public"
+// }
+// })
- return posts.map((post) => ({
- id: post.id
- }))
-}
+// return posts.map((post) => ({
+// id: post.id
+// }))
+// }
const getPost = async (id: string) => {
const post = await getPostById(id, true)
@@ -30,7 +32,7 @@ const getPost = async (id: string) => {
const isAuthor = user?.id === post?.authorId
if (post.visibility === "public") {
- return { post, isAuthor, signedIn: Boolean(user) }
+ return { post, isAuthor }
}
// must be authed to see unlisted/private
@@ -49,12 +51,11 @@ const getPost = async (id: string) => {
return {
post,
isProtected: true,
- isAuthor,
- signedIn: Boolean(user)
+ isAuthor
}
}
- return { post, isAuthor, signedIn: Boolean(user) }
+ return { post, isAuthor }
}
const PostView = async ({
@@ -64,8 +65,10 @@ const PostView = async ({
id: string
}
}) => {
- const { post, isProtected, isAuthor, signedIn } = await getPost(params.id)
- return
+ const { post, isProtected, isAuthor } = await getPost(params.id)
+ // TODO: serialize dates in prisma middleware instead of passing as JSON
+ const stringifiedPost = JSON.stringify(post);
+ return
}
// export const getServerSideProps: GetServerSideProps = async ({
diff --git a/client/app/admin/page.tsx b/client/app/admin/page.tsx
index 52a2a66e..23b18d53 100644
--- a/client/app/admin/page.tsx
+++ b/client/app/admin/page.tsx
@@ -1,7 +1,5 @@
-import { isUserAdmin } from "@lib/server/prisma"
import { getCurrentUser } from "@lib/server/session"
import Admin from "./components/admin"
-import { cookies } from "next/headers"
import { notFound } from "next/navigation"
const AdminPage = async () => {
diff --git a/client/app/components/badges/badge.module.css b/client/app/components/badges/badge.module.css
new file mode 100644
index 00000000..78643d8a
--- /dev/null
+++ b/client/app/components/badges/badge.module.css
@@ -0,0 +1,39 @@
+.container {
+ display: inline-block;
+}
+
+.badge {
+ display: inline-block;
+ padding: .25em .5em;
+ border-radius: var(--radius);
+ background-color: var(--light-gray);
+ font-size: .85em;
+}
+
+.badgeText {
+ font-size: var(--font-size-1);
+ font-weight: var(--font-weight-bold);
+}
+
+.primary {
+ background-color: var(--fg);
+ color: var(--bg);
+}
+
+.primary::selection {
+ background-color: var(--bg);
+ color: var(--fg);
+}
+
+.secondary {
+ background-color: var(--light-gray);
+}
+
+.warning {
+ background-color: var(--warning);
+ color: var(--bg);
+}
+
+.error {
+ background-color: red;
+}
diff --git a/client/app/components/badges/badge.tsx b/client/app/components/badges/badge.tsx
new file mode 100644
index 00000000..241de983
--- /dev/null
+++ b/client/app/components/badges/badge.tsx
@@ -0,0 +1,17 @@
+import styles from "./badge.module.css"
+type BadgeProps = {
+ type: "primary" | "secondary" | "error" | "warning"
+ children: React.ReactNode
+}
+
+const Badge = ({ type, children }: BadgeProps) => {
+ return (
+
+ )
+}
+
+export default Badge
diff --git a/client/app/components/badges/created-ago-badge/index.tsx b/client/app/components/badges/created-ago-badge/index.tsx
index 5ecb8fc3..799eaca8 100644
--- a/client/app/components/badges/created-ago-badge/index.tsx
+++ b/client/app/components/badges/created-ago-badge/index.tsx
@@ -1,7 +1,7 @@
import Tooltip from "@components/tooltip"
-import { Badge } from "@geist-ui/core/dist"
import { timeAgo } from "@lib/time-ago"
import { useMemo, useState, useEffect } from "react"
+import Badge from "../badge"
const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
@@ -19,7 +19,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
{" "}
- <>Created {time}>
+ <>{time}>
)
diff --git a/client/app/components/badges/expiration-badge/index.tsx b/client/app/components/badges/expiration-badge/index.tsx
index a4deb6a7..ef42f185 100644
--- a/client/app/components/badges/expiration-badge/index.tsx
+++ b/client/app/components/badges/expiration-badge/index.tsx
@@ -1,7 +1,7 @@
import Tooltip from "@components/tooltip"
-import { Badge } from "@geist-ui/core/dist"
import { timeUntil } from "@lib/time-ago"
import { useEffect, useMemo, useState } from "react"
+import Badge from "../badge"
const ExpirationBadge = ({
postExpirationDate
@@ -36,7 +36,7 @@ const ExpirationBadge = ({
}, [expirationDate])
const isExpired = useMemo(() => {
- return timeUntilString && timeUntilString === "in 0 seconds"
+ return timeUntilString === "in 0 seconds"
}, [timeUntilString])
// useEffect(() => {
diff --git a/client/app/components/badges/visibility-badge/index.tsx b/client/app/components/badges/visibility-badge/index.tsx
index d90cc3f8..04e81f3f 100644
--- a/client/app/components/badges/visibility-badge/index.tsx
+++ b/client/app/components/badges/visibility-badge/index.tsx
@@ -1,5 +1,5 @@
-import { Badge } from "@geist-ui/core/dist"
import type { PostVisibility } from "@lib/types"
+import Badge from "../badge"
type CastPostVisibility = PostVisibility | string
@@ -8,18 +8,7 @@ type Props = {
}
const VisibilityBadge = ({ visibility }: Props) => {
- const getBadgeType = () => {
- switch (visibility) {
- case "public":
- return "success"
- case "private":
- return "warning"
- case "unlisted":
- return "default"
- }
- }
-
- return {visibility}
+ return {visibility}
}
export default VisibilityBadge
diff --git a/client/app/components/page-seo/index.tsx b/client/app/components/page-seo/index.tsx
index 9dec0100..ae550560 100644
--- a/client/app/components/page-seo/index.tsx
+++ b/client/app/components/page-seo/index.tsx
@@ -1,3 +1,4 @@
+import config from "@lib/config"
import React from "react"
type PageSeoProps = {
@@ -14,11 +15,74 @@ const PageSeo = ({
}: PageSeoProps) => {
return (
<>
- Drift - {title}
+ Drift{title ? ` - ${title}` : ""}
+
{!isPrivate && }
{isPrivate && }
+
+ {/* TODO: verify the correct meta tags */}
+
+
+
>
)
}
export default PageSeo
+
+const ThemeAndIcons = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+)
+
+const URLs = () => (
+ <>
+
+
+ {/* TODO: OG image */}
+
+
+
+
+
+ >
+)
diff --git a/client/app/components/post-list/index.tsx b/client/app/components/post-list/index.tsx
index 6fa0c2ad..aa2e0096 100644
--- a/client/app/components/post-list/index.tsx
+++ b/client/app/components/post-list/index.tsx
@@ -8,19 +8,25 @@ import ListItem from "./list-item"
import { ChangeEvent, useCallback, useEffect, useState } from "react"
import useDebounce from "@lib/hooks/use-debounce"
import Link from "@components/link"
-import { TOKEN_COOKIE_NAME } from "@lib/constants"
import type { PostWithFiles } from "@lib/server/prisma"
-import DriftTooltip from "@components/tooltip"
-import { Search } from "@geist-ui/icons"
type Props = {
- initialPosts: PostWithFiles[]
+ initialPosts: string | PostWithFiles[]
morePosts: boolean
+ userId?: string
}
-const PostList = ({ morePosts, initialPosts }: Props) => {
+const PostList = ({
+ morePosts,
+ initialPosts: initialPostsMaybeJSON,
+ userId
+}: Props) => {
+ const initialPosts =
+ typeof initialPostsMaybeJSON === "string"
+ ? JSON.parse(initialPostsMaybeJSON)
+ : initialPostsMaybeJSON
const [search, setSearchValue] = useState("")
- const [posts, setPosts] = useState(initialPosts)
+ const [posts, setPosts] = useState(initialPosts)
const [searching, setSearching] = useState(false)
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
@@ -51,54 +57,39 @@ const PostList = ({ morePosts, initialPosts }: Props) => {
// update posts on search
useEffect(() => {
if (debouncedSearchValue) {
- // fetch results from /server-api/posts/search
- const fetchResults = async () => {
- setSearching(true)
- //encode search
+ setSearching(true)
+ async function fetchPosts() {
const res = await fetch(
- `/server-api/posts/search?q=${encodeURIComponent(
+ `/api/post/search?q=${encodeURIComponent(
debouncedSearchValue
- )}`,
+ )}&userId=${userId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json"
- // "tok": process.env.SECRET_KEY || ''
}
}
)
- const data = await res.json()
- setPosts(data)
+ const json = await res.json()
+ setPosts(json.posts)
setSearching(false)
}
- fetchResults()
+ fetchPosts()
} else {
setPosts(initialPosts)
}
- }, [initialPosts, debouncedSearchValue])
+ // TODO: fix cyclical dependency issue
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [debouncedSearchValue, userId])
const handleSearchChange = (e: ChangeEvent) => {
setSearchValue(e.target.value)
}
- // const debouncedSearchHandler = useMemo(
- // () => debounce(handleSearchChange, 300),
- // []
- // )
-
- // useEffect(() => {
- // return () => {
- // debouncedSearchHandler.cancel()
- // }
- // }, [debouncedSearchHandler])
-
const deletePost = useCallback(
(postId: string) => async () => {
- const res = await fetch(`/server-api/posts/${postId}`, {
+ const res = await fetch(`/api/post/${postId}`, {
method: "DELETE",
- headers: {
- "Content-Type": "application/json"
- }
})
if (!res.ok) {
@@ -116,14 +107,13 @@ const PostList = ({ morePosts, initialPosts }: Props) => {
{!posts && Failed to load.}
- {!posts.length && searching && (
+ {!posts?.length && searching && (
-
diff --git a/client/app/components/post-list/list-item-skeleton.tsx b/client/app/components/post-list/list-item-skeleton.tsx
index 71bd1a18..bd50f781 100644
--- a/client/app/components/post-list/list-item-skeleton.tsx
+++ b/client/app/components/post-list/list-item-skeleton.tsx
@@ -1,4 +1,4 @@
-import Skeleton from "react-loading-skeleton"
+import Skeleton from "@components/skeleton"
import { Card, Divider, Grid, Spacer } from "@geist-ui/core/dist"
const ListItemSkeleton = () => (
diff --git a/client/app/components/post-list/list-item.module.css b/client/app/components/post-list/list-item.module.css
index f3577667..5a934da0 100644
--- a/client/app/components/post-list/list-item.module.css
+++ b/client/app/components/post-list/list-item.module.css
@@ -17,6 +17,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ margin: var(--gap-quarter) 0;
}
@media screen and (max-width: 700px) {
diff --git a/client/app/components/post-list/list-item.tsx b/client/app/components/post-list/list-item.tsx
index 307b95a1..d74c8a28 100644
--- a/client/app/components/post-list/list-item.tsx
+++ b/client/app/components/post-list/list-item.tsx
@@ -1,11 +1,5 @@
import VisibilityBadge from "../badges/visibility-badge"
-import {
- Text,
- Card,
- Divider,
- Badge,
- Button
-} from "@geist-ui/core/dist"
+import { Text, Card, Divider, Button } from "@geist-ui/core/dist"
import FadeIn from "@components/fade-in"
import Trash from "@geist-ui/icons/trash"
import ExpirationBadge from "@components/badges/expiration-badge"
@@ -19,6 +13,7 @@ import type { PostWithFiles } from "@lib/server/prisma"
import type { PostVisibility } from "@lib/types"
import type { File } from "@lib/server/prisma"
import Tooltip from "@components/tooltip"
+import Badge from "@components/badges/badge"
// TODO: isOwner should default to false so this can be used generically
const ListItem = ({
@@ -45,14 +40,16 @@ const ListItem = ({
-
-
-
- {post.title}
-
+
+
+
+ {post.title}
+
+
{isOwner && (
{post.parentId && (
@@ -72,22 +69,20 @@ const ListItem = ({
)}
-
+
{post.description && (
-
- {post.description}
-
+ {post.description}
)}
-
{post.files?.length === 1
? "1 file"
: `${post.files?.length || 0} files`}
+
diff --git a/client/app/components/skeleton/index.tsx b/client/app/components/skeleton/index.tsx
new file mode 100644
index 00000000..62506b37
--- /dev/null
+++ b/client/app/components/skeleton/index.tsx
@@ -0,0 +1,11 @@
+import styles from "./skeleton.module.css"
+
+export default function Skeleton({
+ width = 100,
+ height = 24,
+}: {
+ width?: number | string
+ height?: number | string
+}) {
+ return
+}
diff --git a/client/app/components/skeleton/skeleton.module.css b/client/app/components/skeleton/skeleton.module.css
new file mode 100644
index 00000000..d86eb857
--- /dev/null
+++ b/client/app/components/skeleton/skeleton.module.css
@@ -0,0 +1,4 @@
+.skeleton {
+ background-color: var(--lighter-gray);
+ border-radius: var(--radius);
+}
diff --git a/client/app/layout.tsx b/client/app/layout.tsx
index 522e0390..47dbba6e 100644
--- a/client/app/layout.tsx
+++ b/client/app/layout.tsx
@@ -9,50 +9,18 @@ interface RootLayoutProps {
}
export default async function RootLayout({ children }: RootLayoutProps) {
+ // TODO: this opts out of SSG
const cookiesList = cookies();
const hasNextAuth = cookiesList.get("next-auth.session-token") !== undefined;
return (
-
-
-
-
-
-
-
-
-
-
-
- Drift
+
{children}
diff --git a/client/app/mine/page.tsx b/client/app/mine/page.tsx
index 50926248..27ef448c 100644
--- a/client/app/mine/page.tsx
+++ b/client/app/mine/page.tsx
@@ -14,5 +14,6 @@ export default async function Mine() {
const posts = await getPostsByUser(userId, true)
const hasMore = false
- return
+ const stringifiedPosts = JSON.stringify(posts)
+ return
}
diff --git a/client/app/root-layout-wrapper.tsx b/client/app/root-layout-wrapper.tsx
index 8a755b0c..11e3acc9 100644
--- a/client/app/root-layout-wrapper.tsx
+++ b/client/app/root-layout-wrapper.tsx
@@ -3,7 +3,6 @@
import Header from "@components/header"
import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist"
import { ThemeProvider } from "next-themes"
-import { SkeletonTheme } from "react-loading-skeleton"
import * as RadixTooltip from "@radix-ui/react-tooltip"
export function LayoutWrapper({
@@ -56,24 +55,19 @@ export function LayoutWrapper({
return (
-
-
-
-
-
-
-
- {children}
-
-
-
+
+
+
+
+
+ {children}
+
+
)
diff --git a/client/app/settings/components/sections/profile.tsx b/client/app/settings/components/sections/profile.tsx
index 46ad3bc1..7dea3528 100644
--- a/client/app/settings/components/sections/profile.tsx
+++ b/client/app/settings/components/sections/profile.tsx
@@ -7,7 +7,6 @@ import { useState } from "react"
const Profile = ({ user }: { user: User }) => {
const [name, setName] = useState(user.name || "")
- const [email, setEmail] = useState(user.email || "")
const [bio, setBio] = useState()
const { setToast } = useToasts()
@@ -16,17 +15,13 @@ const Profile = ({ user }: { user: User }) => {
setName(e.target.value)
}
- const handleEmailChange = (e: React.ChangeEvent) => {
- setEmail(e.target.value)
- }
-
const handleBioChange = (e: React.ChangeEvent) => {
setBio(e.target.value)
}
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- if (!name && !email && !bio) {
+ if (!name && !bio) {
setToast({
text: "Please fill out at least one field",
type: "error"
@@ -36,7 +31,6 @@ const Profile = ({ user }: { user: User }) => {
const data = {
displayName: name,
- email,
bio
}
@@ -92,8 +86,8 @@ const Profile = ({ user }: { user: User }) => {
htmlType="email"
width={"100%"}
placeholder="my@email.io"
- value={email || ""}
- onChange={handleEmailChange}
+ value={user.email || undefined}
+ disabled
/>
diff --git a/client/app/styles/globals.css b/client/app/styles/globals.css
index 920afdb1..72ab8241 100644
--- a/client/app/styles/globals.css
+++ b/client/app/styles/globals.css
@@ -111,7 +111,9 @@ input,
button,
textarea,
select {
+ border: var(--border);
font-size: 1rem;
+ background: var(--bg);
}
blockquote {
diff --git a/client/app/styles/syntax.css b/client/app/styles/syntax.css
index 04a2e0da..77fbcf56 100644
--- a/client/app/styles/syntax.css
+++ b/client/app/styles/syntax.css
@@ -1,8 +1,3 @@
-.keyword {
- font-weight: bold;
- color: var(--darker-gray);
-}
-
.token.operator,
.token.punctuation,
.token.builtin,
@@ -10,6 +5,7 @@
color: var(--token);
}
+.token.keyword,
.token.string,
.token.number,
.token.boolean {
diff --git a/client/lib/config.ts b/client/lib/config.ts
index 766e1228..52d24bd7 100644
--- a/client/lib/config.ts
+++ b/client/lib/config.ts
@@ -30,21 +30,25 @@ export const config = (env: Environment): Config => {
}
}
- const throwIfUndefined = (str: EnvironmentValue, name: string): string => {
- if (str === undefined) {
- throw new Error(`Missing environment variable: ${name}`)
+ // TODO: improve `key` type
+ const throwIfUndefined = (key: keyof Environment): string => {
+ const value = env[key]
+ if (value === undefined) {
+ throw new Error(`Missing environment variable: ${key}`)
}
- return str
+
+ return value
}
const defaultIfUndefined = (
- str: EnvironmentValue,
+ str: string,
defaultValue: string
): string => {
- if (str === undefined) {
+ const value = env[str]
+ if (value === undefined) {
return defaultValue
}
- return str
+ return value
}
const validNodeEnvs = (str: EnvironmentValue) => {
@@ -61,12 +65,11 @@ export const config = (env: Environment): Config => {
const is_production = env.NODE_ENV === "production"
const developmentDefault = (
- str: EnvironmentValue,
name: string,
defaultValue: string
): string => {
- if (is_production) return throwIfUndefined(str, name)
- return defaultIfUndefined(str, defaultValue)
+ if (is_production) return throwIfUndefined(name)
+ return defaultIfUndefined(name, defaultValue)
}
validNodeEnvs(env.NODE_ENV)
@@ -78,11 +81,11 @@ export const config = (env: Environment): Config => {
is_production,
memory_db: stringToBoolean(env.MEMORY_DB),
enable_admin: stringToBoolean(env.ENABLE_ADMIN),
- secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"),
+ secret_key: developmentDefault("SECRET_KEY", "secret"),
registration_password: env.REGISTRATION_PASSWORD ?? "",
welcome_content: env.WELCOME_CONTENT ?? "",
welcome_title: env.WELCOME_TITLE ?? "",
- url: "http://localhost:3000",
+ url: throwIfUndefined("DRIFT_URL"),
GITHUB_CLIENT_ID: env.GITHUB_CLIENT_ID ?? "",
GITHUB_CLIENT_SECRET: env.GITHUB_CLIENT_SECRET ?? "",
}
diff --git a/client/lib/server/prisma.ts b/client/lib/server/prisma.ts
index 288c2408..19048dd8 100644
--- a/client/lib/server/prisma.ts
+++ b/client/lib/server/prisma.ts
@@ -3,7 +3,7 @@ declare global {
}
import config from "@lib/config"
-import { Post, PrismaClient, File, User } from "@prisma/client"
+import { Post, PrismaClient, File, User, Prisma } from "@prisma/client"
// we want to update iff they exist the createdAt/updated/expired/deleted items
// the input could be an array, in which case we'd check each item in the array
@@ -12,16 +12,16 @@ import { Post, PrismaClient, File, User } from "@prisma/client"
const updateDateForItem = (item: any) => {
if (item.createdAt) {
- item.createdAt = item.createdAt.toISOString()
+ item.createdAt = item.createdAt.toString()
}
if (item.updatedAt) {
- item.updatedAt = item.updatedAt.toISOString()
+ item.updatedAt = item.updatedAt.toString()
}
if (item.expiresAt) {
- item.expiresAt = item.expiresAt.toISOString()
+ item.expiresAt = item.expiresAt.toString()
}
if (item.deletedAt) {
- item.deletedAt = item.deletedAt.toISOString()
+ item.deletedAt = item.deletedAt.toString()
}
return item
}
@@ -40,6 +40,11 @@ export const prisma =
log: ["query"]
})
+// prisma.$use(async (params, next) => {
+// const result = await next(params)
+// return updateDates(result)
+// })
+
if (process.env.NODE_ENV !== "production") global.prisma = prisma
export type { User, File, Post } from "@prisma/client"
@@ -159,13 +164,59 @@ export const getPostById = async (postId: Post["id"], withFiles = false) => {
return post as PostWithFiles
}
-export const getAllPosts = async (withFiles = false) => {
+export const getAllPosts = async ({
+ withFiles = false,
+ take = 100,
+ ...rest
+}: {
+ withFiles?: boolean
+} & Prisma.PostFindManyArgs = {}) => {
const posts = await prisma.post.findMany({
include: {
files: withFiles
},
// TODO: optimize which to grab
- take: 100
+ take,
+ ...rest
+ })
+
+ return posts as PostWithFiles[]
+}
+
+export const searchPosts = async (
+ query: string,
+ {
+ withFiles = false,
+ userId,
+ }: {
+ withFiles?: boolean
+ userId?: User["id"]
+ } = {}
+): Promise
=> {
+ const posts = await prisma.post.findMany({
+ where: {
+ OR: [
+ {
+ title: {
+ search: query
+ },
+ authorId: userId
+ },
+ {
+ files: {
+ some: {
+ content: {
+ search: query
+ },
+ userId: userId
+ }
+ }
+ }
+ ]
+ },
+ include: {
+ files: withFiles
+ }
})
return posts as PostWithFiles[]
diff --git a/client/middleware.ts b/client/middleware.ts
index cfa6daa7..86a2b20c 100644
--- a/client/middleware.ts
+++ b/client/middleware.ts
@@ -4,8 +4,8 @@ import { NextResponse } from "next/server"
export default withAuth(
async function middleware(req) {
- console.log("middleware")
const token = await getToken({ req })
+
const isAuth = !!token
const isAuthPage =
req.nextUrl.pathname.startsWith("/signup") ||
diff --git a/client/package.json b/client/package.json
index 34d34ba6..ef240879 100644
--- a/client/package.json
+++ b/client/package.json
@@ -22,6 +22,7 @@
"bcrypt": "^5.1.0",
"client-zip": "2.2.1",
"clsx": "^1.2.1",
+ "cookies-next": "^2.1.1",
"next": "13.0.3-canary.4",
"next-auth": "^4.16.4",
"next-themes": "npm:@wits/next-themes@0.2.7",
@@ -31,7 +32,6 @@
"react-dom": "18.2.0",
"react-dropzone": "14.2.3",
"react-hot-toast": "^2.4.0",
- "react-loading-skeleton": "3.1.0",
"server-only": "^0.0.1",
"swr": "1.3.0",
"textarea-markdown-editor": "0.1.13"
diff --git a/client/pages/api/post/[id].ts b/client/pages/api/post/[id].ts
index db440a4d..9126d7a8 100644
--- a/client/pages/api/post/[id].ts
+++ b/client/pages/api/post/[id].ts
@@ -9,9 +9,10 @@ import * as crypto from "crypto"
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") return handleGet(req, res)
else if (req.method === "PUT") return handlePut(req, res)
+ else if (req.method === "DELETE") return handleDelete(req, res)
}
-export default withMethods(["GET", "PUT"], handler)
+export default withMethods(["GET", "PUT", "DELETE"], handler)
async function handleGet(req: NextApiRequest, res: NextApiResponse) {
const id = parseQueryParam(req.query.id)
@@ -114,3 +115,34 @@ async function handlePut(req: NextApiRequest, res: NextApiResponse) {
visibility: updatedPost.visibility
})
}
+
+async function handleDelete(req: NextApiRequest, res: NextApiResponse) {
+ const id = parseQueryParam(req.query.id)
+
+ if (!id) {
+ return res.status(400).json({ error: "Missing id" })
+ }
+
+ const post = await getPostById(id, false)
+
+ if (!post) {
+ return res.status(404).json({ message: "Post not found" })
+ }
+
+ const session = await getSession({ req })
+
+ const isAuthor = session?.user.id === post.authorId
+ const isAdmin = session?.user.role === "admin"
+
+ if (!isAuthor && !isAdmin) {
+ return res.status(403).json({ message: "Unauthorized" })
+ }
+
+ await prisma.post.delete({
+ where: {
+ id
+ }
+ })
+
+ res.json({ message: "Post deleted" })
+}
diff --git a/client/pages/api/post/search.ts b/client/pages/api/post/search.ts
new file mode 100644
index 00000000..12d43386
--- /dev/null
+++ b/client/pages/api/post/search.ts
@@ -0,0 +1,27 @@
+import { withMethods } from "@lib/api-middleware/with-methods"
+import { parseQueryParam } from "@lib/server/parse-query-param"
+import { searchPosts } from "@lib/server/prisma"
+import { NextApiRequest, NextApiResponse } from "next"
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ const { q, userId } = req.query
+
+ const query = parseQueryParam(q)
+ if (!query) {
+ res.status(400).json({ error: "Invalid query" })
+ return
+ }
+
+ try {
+ const posts = await searchPosts(query, {
+ userId: parseQueryParam(userId),
+ })
+
+ res.status(200).json(posts)
+ } catch (err) {
+ console.error(err)
+ res.status(500).json({ error: "Internal server error" })
+ }
+}
+
+export default withMethods(["GET"], handler)
diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml
index 85e9d982..81a80418 100644
--- a/client/pnpm-lock.yaml
+++ b/client/pnpm-lock.yaml
@@ -17,6 +17,7 @@ specifiers:
bcrypt: ^5.1.0
client-zip: 2.2.1
clsx: ^1.2.1
+ cookies-next: ^2.1.1
cross-env: 7.0.3
eslint: 8.27.0
eslint-config-next: 13.0.3-canary.4
@@ -33,7 +34,6 @@ specifiers:
react-dom: 18.2.0
react-dropzone: 14.2.3
react-hot-toast: ^2.4.0
- react-loading-skeleton: 3.1.0
server-only: ^0.0.1
sharp: ^0.31.2
swr: 1.3.0
@@ -52,6 +52,7 @@ dependencies:
bcrypt: 5.1.0
client-zip: 2.2.1
clsx: 1.2.1
+ cookies-next: 2.1.1
next: 13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y
next-auth: 4.16.4_hsmqkug4agizydugca45idewda
next-themes: /@wits/next-themes/0.2.7_hsmqkug4agizydugca45idewda
@@ -61,7 +62,6 @@ dependencies:
react-dom: 18.2.0_react@18.2.0
react-dropzone: 14.2.3_react@18.2.0
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
- react-loading-skeleton: 3.1.0_react@18.2.0
server-only: 0.0.1
swr: 1.3.0_react@18.2.0
textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y
@@ -728,6 +728,10 @@ packages:
'@types/node': 17.0.23
dev: true
+ /@types/cookie/0.4.1:
+ resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==}
+ dev: false
+
/@types/debug/4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies:
@@ -758,6 +762,10 @@ packages:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: false
+ /@types/node/16.18.3:
+ resolution: {integrity: sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==}
+ dev: false
+
/@types/node/17.0.23:
resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==}
dev: true
@@ -1364,11 +1372,24 @@ packages:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
dev: false
+ /cookie/0.4.2:
+ resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
+ engines: {node: '>= 0.6'}
+ dev: false
+
/cookie/0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
+ /cookies-next/2.1.1:
+ resolution: {integrity: sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==}
+ dependencies:
+ '@types/cookie': 0.4.1
+ '@types/node': 16.18.3
+ cookie: 0.4.2
+ dev: false
+
/copy-anything/2.0.6:
resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
dependencies:
@@ -4327,14 +4348,6 @@ packages:
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- /react-loading-skeleton/3.1.0_react@18.2.0:
- resolution: {integrity: sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw==}
- peerDependencies:
- react: '>=16.8.0'
- dependencies:
- react: 18.2.0
- dev: false
-
/react-onclickoutside/6.12.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==}
peerDependencies:
diff --git a/client/prisma/schema.prisma b/client/prisma/schema.prisma
index 07dc4007..61dae83f 100644
--- a/client/prisma/schema.prisma
+++ b/client/prisma/schema.prisma
@@ -1,6 +1,6 @@
generator client {
provider = "prisma-client-js"
- previewFeatures = ["referentialIntegrity"]
+ previewFeatures = ["referentialIntegrity", "fullTextSearch"]
}
datasource db {