+
{!isDragActive && (
-
- Drag some files here, or to select
- files
+
+ Drag and drop files here, or click to select
)}
{isDragActive &&
Release to drop the files here
}
diff --git a/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/document.module.css b/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/document.module.css
index 7ade9f44..389e3cce 100644
--- a/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/document.module.css
+++ b/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/document.module.css
@@ -1,6 +1,5 @@
.card {
padding: var(--gap);
- border: 1px solid var(--lighter-gray);
border-radius: var(--radius);
}
diff --git a/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx b/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx
index 225bdcc9..f46476a8 100644
--- a/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx
+++ b/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx
@@ -9,9 +9,10 @@ import {
import { RefObject, useMemo } from "react"
import styles from "./formatting-icons.module.css"
import { TextareaMarkdownRef } from "textarea-markdown-editor"
-import Tooltip from "@components/tooltip"
-import Button from "@components/button"
+import { Tooltip } from "@components/tooltip"
+import { Button } from "@components/button"
import clsx from "clsx"
+import React from "react"
// TODO: clean up
function FormattingIcons({
@@ -69,22 +70,18 @@ function FormattingIcons({
e.preventDefault()}
onClick={action}
- buttonType="secondary"
- />
+ variant="ghost"
+ >
+ {React.cloneElement(icon, {
+ className: "h-4 w-4"
+ })}
+
))}
diff --git a/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/index.tsx b/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/index.tsx
index 44b13542..d091f47d 100644
--- a/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/index.tsx
+++ b/src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/index.tsx
@@ -1,9 +1,10 @@
import { ChangeEvent, ClipboardEvent, useCallback } from "react"
import styles from "./document.module.css"
-import Button from "@components/button"
-import Input from "@components/input"
-import DocumentTabs from "src/app/(drift)/(posts)/components/tabs"
+import { Button } from "@components/button"
+import { Input } from "@components/input"
+import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
import { Trash } from "react-feather"
+import { Card, CardContent, CardHeader } from "@components/card"
type Props = {
title?: string
@@ -49,8 +50,8 @@ function Document({
)
return (
- <>
-
+
+
{remove && (
+ // no left border
}
- height={"39px"}
- width={"48px"}
- padding={0}
- margin={0}
onClick={() => removeFile(remove)}
- style={{
- borderTopLeftRadius: 0,
- borderBottomLeftRadius: 0
- }}
- />
+ variant="outline"
+ className="border-color-[var(--border)] rounded-l-none border-l-0"
+ >
+
+
)}
-
-
- {content}
-
-
-
- >
+
+
+
+ {content}
+
+
+
)
}
diff --git a/src/app/(drift)/(posts)/new/components/new.tsx b/src/app/(drift)/(posts)/new/components/new.tsx
index f7196f30..4a1b5bfd 100644
--- a/src/app/(drift)/(posts)/new/components/new.tsx
+++ b/src/app/(drift)/(posts)/new/components/new.tsx
@@ -3,26 +3,42 @@
import { useRouter } from "next/navigation"
import { useCallback, useState, ClipboardEvent } from "react"
import generateUUID from "@lib/generate-uuid"
-import styles from "./post.module.css"
import EditDocumentList from "./edit-document-list"
import { ChangeEvent } from "react"
import getTitleForPostCopy from "src/app/lib/get-title-for-post-copy"
-import Description from "./description"
+// import Description from "./description"
import { PostWithFiles } from "@lib/server/prisma"
import PasswordModal from "../../../../components/password-modal"
import Title from "./title"
import FileDropzone from "./drag-and-drop"
-import Button from "@components/button"
-import Input from "@components/input"
-import ButtonDropdown from "@components/button-dropdown"
+import { Button, buttonVariants } from "@components/button"
import { useToasts } from "@components/toasts"
import { fetchWithUser } from "src/app/lib/fetch-with-user"
import dynamic from "next/dynamic"
+import ButtonDropdown from "@components/button-dropdown"
+import clsx from "clsx"
+import { Spinner } from "@components/spinner"
+import { cn } from "@lib/cn"
+import { Calendar as CalendarIcon } from "react-feather"
-const DatePicker = dynamic(() => import("react-datepicker"), {
- ssr: false,
- loading: () =>
-})
+const DatePicker = dynamic(
+ () => import("@components/date-picker").then((m) => m.DatePicker),
+ {
+ ssr: false,
+ loading: () => (
+
+
+ Won't expire
+
+ )
+ }
+)
const emptyDoc = {
title: "",
@@ -48,7 +64,9 @@ function Post({
const [title, setTitle] = useState(
getTitleForPostCopy(initialPost?.title) || ""
)
- const [description, setDescription] = useState(initialPost?.description || "")
+ const [description /*, setDescription */] = useState(
+ initialPost?.description || ""
+ )
const [expiresAt, setExpiresAt] = useState
()
const defaultDocs: Document[] = initialPost
@@ -131,7 +149,7 @@ function Post({
if (!docs.length) {
setToast({
- message: "Please add at least one document",
+ message: "Please add at least one file",
type: "error"
})
hasErrored = true
@@ -170,13 +188,13 @@ function Post({
setTitle(e.target.value)
}, [])
- const onChangeDescription = useCallback(
- (e: ChangeEvent) => {
- e.preventDefault()
- setDescription(e.target.value)
- },
- []
- )
+ // const onChangeDescription = useCallback(
+ // (e: ChangeEvent) => {
+ // e.preventDefault()
+ // setDescription(e.target.value)
+ // },
+ // []
+ // )
function onClosePasswordModal() {
setPasswordModalVisible(false)
@@ -187,10 +205,6 @@ function Post({
return onSubmit("protected", password)
}
- function onChangeExpiration(date: Date) {
- return setExpiresAt(date)
- }
-
function updateDocTitle(i: number) {
return (title: string) => {
setDocs((docs) =>
@@ -241,10 +255,9 @@ function Post({
}
return (
-
-
-
-
+
+
+ {/*
*/}
-
-
{
- setDocs([
- ...docs,
- {
- title: "",
- content: "",
- id: generateUUID()
- }
- ])
- }}
- style={{
- flex: 1,
- minWidth: 120
- }}
- >
- Add a File
-
-
-
- }
- placeholderText="Won't expire"
- selected={expiresAt}
- showTimeInput={true}
- // @ts-expect-error fix time input type
- 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")}
- loading={isSubmitting}
- >
- Create Unlisted
-
- onSubmit("private")}>
- Create Private
-
- onSubmit("public")}>
- Create Public
-
- onSubmit("protected")}
- >
- Create with Password
-
-
-
+
+
+
+
+ {
+ setDocs([
+ ...docs,
+ {
+ title: "",
+ content: "",
+ id: generateUUID()
+ }
+ ])
+ }}
+ className="min-w-[120px] max-w-[200px] flex-1"
+ variant={"secondary"}
+ >
+ Add a File
+
+
+
+
+ onSubmit("unlisted")}
+ >
+ {isSubmitting ? : null}
+ Create Unlisted
+
+ onSubmit("private")}
+ >
+ Create Private
+
+ onSubmit("public")}
+ >
+ Create Public
+
+ onSubmit("protected")}
+ >
+ Create with Password
+
+
void
-}) {
- return (
- {
- if (!isNaN(date.getTime())) {
- onChange(e.target.value || date.toISOString().slice(11, 16))
- }
- }}
- style={{
- backgroundColor: "var(--bg)",
- border: "1px solid var(--light-gray)",
- borderRadius: "var(--radius)"
- }}
- required
- />
- )
-}
+// function CustomTimeInput({
+// date,
+// value,
+// onChange
+// }: {
+// date: Date
+// value: string
+// onChange: (date: string) => void
+// }) {
+// return (
+// {
+// if (!isNaN(date.getTime())) {
+// onChange(e.target.value || date.toISOString().slice(11, 16))
+// }
+// }}
+// style={{
+// backgroundColor: "var(--bg)",
+// border: "1px solid var(--light-gray)",
+// borderRadius: "var(--radius)"
+// }}
+// required
+// />
+// )
+// }
diff --git a/src/app/(drift)/(posts)/new/components/post.module.css b/src/app/(drift)/(posts)/new/components/post.module.css
index 4d7758c0..4568e5ce 100644
--- a/src/app/(drift)/(posts)/new/components/post.module.css
+++ b/src/app/(drift)/(posts)/new/components/post.module.css
@@ -2,7 +2,7 @@
padding-bottom: 200px;
display: flex;
flex-direction: column;
- gap: var(--gap);
+ gap: var(--gap-half);
}
.buttons {
@@ -19,9 +19,6 @@
align-items: center;
}
-.datePicker {
- flex: 1;
-}
.description {
width: 100%;
diff --git a/src/app/(drift)/(posts)/new/components/react-datepicker.css b/src/app/(drift)/(posts)/new/components/react-datepicker.css
deleted file mode 100644
index 97949a9c..00000000
--- a/src/app/(drift)/(posts)/new/components/react-datepicker.css
+++ /dev/null
@@ -1,372 +0,0 @@
-.react-datepicker__year-read-view--down-arrow,
-.react-datepicker__month-read-view--down-arrow,
-.react-datepicker__month-year-read-view--down-arrow,
-.react-datepicker__navigation-icon::before {
- border-color: var(--light-gray);
- border-style: solid;
- border-width: 3px 3px 0 0;
- content: "";
- display: block;
- height: 9px;
- position: absolute;
- top: 6px;
- width: 9px;
-}
-.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
-.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle {
- margin-left: -4px;
- position: absolute;
- width: 0;
-}
-
-.react-datepicker-wrapper {
- display: inline-block;
- padding: 0;
- border: 0;
-}
-
-.react-datepicker {
- font-family: var(--font-sans);
- font-size: 0.8rem;
- background-color: var(--bg);
- color: var(--fg);
- border: 1px solid var(--gray);
- border-radius: var(--radius);
- display: inline-block;
- position: relative;
-}
-
-.react-datepicker--time-only .react-datepicker__triangle {
- left: 35px;
-}
-.react-datepicker--time-only .react-datepicker__time-container {
- border-left: 0;
-}
-.react-datepicker--time-only .react-datepicker__time,
-.react-datepicker--time-only .react-datepicker__time-box {
- border-radius: var(--radius);
- border-radius: var(--radius);
-}
-
-.react-datepicker__triangle {
- position: absolute;
- left: 50px;
-}
-
-.react-datepicker-popper {
- z-index: 1;
-}
-.react-datepicker-popper[data-placement^="bottom"] {
- padding-top: 10px;
-}
-.react-datepicker-popper[data-placement="bottom-end"]
- .react-datepicker__triangle,
-.react-datepicker-popper[data-placement="top-end"] .react-datepicker__triangle {
- left: auto;
- right: 50px;
-}
-.react-datepicker-popper[data-placement^="top"] {
- padding-bottom: 10px;
-}
-.react-datepicker-popper[data-placement^="right"] {
- padding-left: 8px;
-}
-.react-datepicker-popper[data-placement^="right"] .react-datepicker__triangle {
- left: auto;
- right: 42px;
-}
-.react-datepicker-popper[data-placement^="left"] {
- padding-right: 8px;
-}
-.react-datepicker-popper[data-placement^="left"] .react-datepicker__triangle {
- left: 42px;
- right: auto;
-}
-
-.react-datepicker__header {
- text-align: center;
- background-color: var(--bg);
- border-bottom: 1px solid var(--gray);
- border-top-left-radius: var(--radius);
- border-top-right-radius: var(--radius);
- padding: 8px 0;
- position: relative;
-}
-
-.react-datepicker__header--time {
- padding-bottom: 8px;
- padding-left: 5px;
- padding-right: 5px;
-}
-
-.react-datepicker__year-dropdown-container--select,
-.react-datepicker__month-dropdown-container--select,
-.react-datepicker__month-year-dropdown-container--select,
-.react-datepicker__year-dropdown-container--scroll,
-.react-datepicker__month-dropdown-container--scroll,
-.react-datepicker__month-year-dropdown-container--scroll {
- display: inline-block;
- margin: 0 2px;
-}
-
-.react-datepicker__current-month,
-.react-datepicker-time__header,
-.react-datepicker-year-header {
- margin-top: 0;
- font-weight: bold;
- font-size: 0.944rem;
-}
-
-.react-datepicker-time__header {
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
-}
-
-.react-datepicker__navigation {
- align-items: center;
- background: none;
- display: flex;
- justify-content: center;
- text-align: center;
- cursor: pointer;
- position: absolute;
- top: 2px;
- padding: 0;
- border: none;
- z-index: 1;
- height: 32px;
- width: 32px;
- text-indent: -999em;
- overflow: hidden;
-}
-.react-datepicker__navigation--previous {
- left: 2px;
-}
-.react-datepicker__navigation--next {
- right: 2px;
-}
-.react-datepicker__navigation--next--with-time:not(.react-datepicker__navigation--next--with-today-button) {
- right: 85px;
-}
-.react-datepicker__navigation--years {
- position: relative;
- top: 0;
- display: block;
- margin-left: auto;
- margin-right: auto;
-}
-.react-datepicker__navigation--years-previous {
- top: 4px;
-}
-.react-datepicker__navigation--years-upcoming {
- top: -4px;
-}
-.react-datepicker__navigation:hover *::before {
- border-color: var(--lighter-gray);
-}
-
-.react-datepicker__navigation-icon {
- position: relative;
- top: -1px;
- font-size: 20px;
- width: 0;
-}
-.react-datepicker__navigation-icon--next {
- left: -2px;
-}
-.react-datepicker__navigation-icon--next::before {
- transform: rotate(45deg);
- left: -7px;
-}
-.react-datepicker__navigation-icon--previous {
- right: -2px;
-}
-.react-datepicker__navigation-icon--previous::before {
- transform: rotate(225deg);
- right: -7px;
-}
-
-.react-datepicker__month-container {
- float: left;
-}
-
-.react-datepicker__year {
- margin: 0.4rem;
- text-align: center;
-}
-.react-datepicker__year-wrapper {
- display: flex;
- flex-wrap: wrap;
- max-width: 180px;
-}
-.react-datepicker__year .react-datepicker__year-text {
- display: inline-block;
- width: 4rem;
- margin: 2px;
-}
-
-.react-datepicker__month {
- margin: 0.4rem;
- text-align: center;
-}
-.react-datepicker__month .react-datepicker__month-text,
-.react-datepicker__month .react-datepicker__quarter-text {
- display: inline-block;
- width: 4rem;
- margin: 2px;
-}
-
-.react-datepicker__input-time-container {
- clear: both;
- width: 100%;
- float: left;
- margin: 5px 0 10px 15px;
- text-align: left;
-}
-.react-datepicker__input-time-container .react-datepicker-time__caption {
- display: inline-block;
-}
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container {
- display: inline-block;
-}
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container
- .react-datepicker-time__input {
- display: inline-block;
- margin-left: 10px;
-}
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container
- .react-datepicker-time__input
- input {
- width: auto;
-}
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container
- .react-datepicker-time__input
- input[type="time"]::-webkit-inner-spin-button,
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container
- .react-datepicker-time__input
- input[type="time"]::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
-}
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container
- .react-datepicker-time__input
- input[type="time"] {
- -moz-appearance: textfield;
-}
-.react-datepicker__input-time-container
- .react-datepicker-time__input-container
- .react-datepicker-time__delimiter {
- margin-left: 5px;
- display: inline-block;
-}
-
-.react-datepicker__day-names,
-.react-datepicker__week {
- white-space: nowrap;
-}
-
-.react-datepicker__day-names {
- margin-bottom: -8px;
-}
-
-.react-datepicker__day-name,
-.react-datepicker__day,
-.react-datepicker__time-name {
- color: var(--fg);
- display: inline-block;
- width: 1.7rem;
- line-height: 1.7rem;
- text-align: center;
- margin: 0.166rem;
-}
-.react-datepicker__day,
-.react-datepicker__month-text,
-.react-datepicker__quarter-text,
-.react-datepicker__year-text {
- cursor: pointer;
-}
-.react-datepicker__day:hover,
-.react-datepicker__month-text:hover,
-.react-datepicker__quarter-text:hover,
-.react-datepicker__year-text:hover {
- border-radius: 0.3rem;
- background-color: var(--light-gray);
-}
-.react-datepicker__day--today,
-.react-datepicker__month-text--today,
-.react-datepicker__quarter-text--today,
-.react-datepicker__year-text--today {
- font-weight: bold;
-}
-.react-datepicker__day--highlighted,
-.react-datepicker__month-text--highlighted,
-.react-datepicker__quarter-text--highlighted,
-.react-datepicker__year-text--highlighted {
- border-radius: 0.3rem;
- background-color: #3dcc4a;
- color: var(--fg);
-}
-.react-datepicker__day--highlighted:hover,
-.react-datepicker__month-text--highlighted:hover,
-.react-datepicker__quarter-text--highlighted:hover,
-.react-datepicker__year-text--highlighted:hover {
- background-color: #32be3f;
-}
-
-.react-datepicker__day--selected,
-.react-datepicker__day--in-selecting-range,
-.react-datepicker__day--in-range,
-.react-datepicker__month-text--selected,
-.react-datepicker__month-text--in-selecting-range,
-.react-datepicker__month-text--in-range,
-.react-datepicker__quarter-text--selected,
-.react-datepicker__quarter-text--in-selecting-range,
-.react-datepicker__quarter-text--in-range,
-.react-datepicker__year-text--selected,
-.react-datepicker__year-text--in-selecting-range,
-.react-datepicker__year-text--in-range {
- border-radius: 0.3rem;
- background-color: var(--light-gray);
- color: var(--fg);
-}
-.react-datepicker__day--selected:hover {
- background-color: var(--gray);
-}
-
-.react-datepicker__day--keyboard-selected,
-.react-datepicker__month-text--keyboard-selected,
-.react-datepicker__quarter-text--keyboard-selected,
-.react-datepicker__year-text--keyboard-selected {
- border-radius: 0.3rem;
- background-color: var(--light-gray);
- color: var(--fg);
-}
-.react-datepicker__day--keyboard-selected:hover {
- background-color: var(--gray);
-}
-
-.react-datepicker__month--selecting-range
- .react-datepicker__day--in-range:not(.react-datepicker__day--in-selecting-range, .react-datepicker__month-text--in-selecting-range, .react-datepicker__quarter-text--in-selecting-range, .react-datepicker__year-text--in-selecting-range) {
- background-color: var(--bg);
- color: var(--fg);
-}
-
-.react-datepicker {
- transform: scale(1.15) translateY(-12px);
-}
-
-.react-datepicker__day--disabled {
- color: var(--darker-gray);
-}
-
-.react-datepicker__day--disabled:hover {
- background-color: transparent;
- cursor: not-allowed;
-}
diff --git a/src/app/(drift)/(posts)/new/components/title/index.tsx b/src/app/(drift)/(posts)/new/components/title/index.tsx
index 13fa0a51..c1d33562 100644
--- a/src/app/(drift)/(posts)/new/components/title/index.tsx
+++ b/src/app/(drift)/(posts)/new/components/title/index.tsx
@@ -1,7 +1,6 @@
import { ChangeEvent, memo } from "react"
-import Input from "@components/input"
-import styles from "./title.module.css"
+import { Input } from "@components/input"
const titlePlaceholders = [
"How to...",
@@ -18,20 +17,17 @@ const placeholder = titlePlaceholders[3]
type props = {
onChange: (e: ChangeEvent) => void
title?: string
+ className?: string
}
-function Title({ onChange, title }: props) {
+function Title({ onChange, title, className }: props) {
return (
-
-
Drift
+
)
diff --git a/src/app/(drift)/(posts)/new/page.tsx b/src/app/(drift)/(posts)/new/page.tsx
index 9d6beb96..74661e7a 100644
--- a/src/app/(drift)/(posts)/new/page.tsx
+++ b/src/app/(drift)/(posts)/new/page.tsx
@@ -1,9 +1,17 @@
import { getMetadata } from "src/app/lib/metadata"
import NewPost from "src/app/(drift)/(posts)/new/components/new"
-import "./components/react-datepicker.css"
+import { PageTitle } from "@components/page-title"
+import { PageWrapper } from "@components/page-wrapper"
export default function New() {
- return
+ return (
+ <>
+
New Post
+
+
+
+ >
+ )
}
export const metadata = getMetadata({
diff --git a/src/app/(drift)/(posts)/post/[id]/components/header/post-buttons/index.tsx b/src/app/(drift)/(posts)/post/[id]/components/header/post-buttons/index.tsx
index 9d43d702..efce08c9 100644
--- a/src/app/(drift)/(posts)/post/[id]/components/header/post-buttons/index.tsx
+++ b/src/app/(drift)/(posts)/post/[id]/components/header/post-buttons/index.tsx
@@ -1,9 +1,8 @@
"use client"
-import Button from "@components/button"
+import { Button } from "@components/button"
import ButtonGroup from "@components/button-group"
import FileDropdown from "src/app/(drift)/(posts)/components/file-dropdown"
-import { Edit, ArrowUpCircle, Archive } from "react-feather"
import styles from "./post-buttons.module.css"
import { useRouter } from "next/navigation"
import { PostWithFiles } from "@lib/server/prisma"
@@ -48,15 +47,19 @@ export const PostButtons = ({
return (
- } onClick={editACopy}>
+
Edit a Copy
{parentId && (
- } onClick={viewParentClick}>
+
View Parent
)}
- }>
+
Download as ZIP Archive
diff --git a/src/app/(drift)/(posts)/post/[id]/components/header/title/index.tsx b/src/app/(drift)/(posts)/post/[id]/components/header/title/index.tsx
index f10e29e0..dcf490db 100644
--- a/src/app/(drift)/(posts)/post/[id]/components/header/title/index.tsx
+++ b/src/app/(drift)/(posts)/post/[id]/components/header/title/index.tsx
@@ -17,13 +17,9 @@ export const PostTitle = ({ post, loading }: TitleProps) => {
const displayName = author?.displayName
return (
-
+
{title}{" "}
-
+
by {/* */}
{displayName || "anonymous"}
{/* */}
diff --git a/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/document.module.css b/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/document.module.css
index f2f82db1..f2c3515e 100644
--- a/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/document.module.css
+++ b/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/document.module.css
@@ -1,4 +1,4 @@
-.card header {
+/* .card header {
display: flex;
align-items: center;
flex-direction: row;
@@ -18,7 +18,7 @@
border: 1px solid var(--lighter-gray);
border-top: none;
border-radius: 0px 0px 8px 8px;
-}
+} */
.textarea {
height: 100%;
diff --git a/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/index.tsx b/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/index.tsx
index 844ca1ee..2cd0df95 100644
--- a/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/index.tsx
+++ b/src/app/(drift)/(posts)/post/[id]/components/post-files/view-document/index.tsx
@@ -1,8 +1,8 @@
-import Button from "@components/button"
+import { Button } from "@components/button"
import ButtonGroup from "@components/button-group"
import Skeleton from "@components/skeleton"
-import Tooltip from "@components/tooltip"
-import DocumentTabs from "src/app/(drift)/(posts)/components/tabs"
+import { Tooltip } from "@components/tooltip"
+import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
import Link from "next/link"
import { memo } from "react"
import { Download, ExternalLink, Globe } from "react-feather"
@@ -10,7 +10,7 @@ import styles from "./document.module.css"
import { getURLFriendlyTitle } from "src/app/lib/get-url-friendly-title"
import { PostWithFiles, ServerPost } from "@lib/server/prisma"
import { isAllowedVisibilityForWebpage } from "@lib/constants"
-
+import { Card, CardContent, CardHeader } from "@components/card"
type SharedProps = {
initialTab: "edit" | "preview"
file?: PostWithFiles["files"][0]
@@ -36,38 +36,50 @@ const DownloadButtons = ({
}) => {
return (
-
+
}
aria-label="Download"
- style={{ border: "none", background: "transparent" }}
- />
+ size="sm"
+ className="bg-transparent border-none"
+ variant={"ghost"}
+ >
+
+ Download
+
{rawLink ? (
-
+
}
aria-label="Open raw file in new tab"
- style={{ border: "none", background: "transparent" }}
- />
+ className="bg-transparent border-none"
+ size="sm"
+ variant={"ghost"}
+ >
+
+ Open raw file in new tab
+
) : null}
{siteLink ? (
-
+
}
aria-label="Open as webpage"
- style={{ border: "none", background: "transparent" }}
- />
+ className="bg-transparent border-none"
+ size="sm"
+ variant={"ghost"}
+ >
+
+ Open as webpage
+
) : null}
@@ -109,18 +121,39 @@ const Document = ({ skeleton, ...props }: Props) => {
}
}
}
+ /* .card header {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 40px;
+ line-height: 40px;
+ padding: 0 16px;
+ background: var(--lighter-gray);
+ border-radius: 8px 8px 0px 0px;
+}
+.documentContainer {
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+ padding: var(--gap);
+ border: 1px solid var(--lighter-gray);
+ border-top: none;
+ border-radius: 0px 0px 8px 8px;
+} */
return (
<>
-
-
+
+
{file?.title}
@@ -134,8 +167,8 @@ const Document = ({ skeleton, ...props }: Props) => {
: undefined
}
/>
-
-
+
+
{
>
{file?.content || ""}
-
-
+
+
>
)
}
diff --git a/src/app/(drift)/(posts)/post/[id]/layout.tsx b/src/app/(drift)/(posts)/post/[id]/layout.tsx
index b3e10427..821c82da 100644
--- a/src/app/(drift)/(posts)/post/[id]/layout.tsx
+++ b/src/app/(drift)/(posts)/post/[id]/layout.tsx
@@ -28,11 +28,7 @@ export default async function PostLayout({
{post.visibility !== "protected" && }
{post.visibility !== "protected" && }
- {post.description && (
-
- )}
+ {/* {post.description && {post.description}
} */}
{children}
diff --git a/src/app/(drift)/(posts)/post/[id]/page.tsx b/src/app/(drift)/(posts)/post/[id]/page.tsx
index 17d7e667..873afcf3 100644
--- a/src/app/(drift)/(posts)/post/[id]/page.tsx
+++ b/src/app/(drift)/(posts)/post/[id]/page.tsx
@@ -21,11 +21,13 @@ export default async function PostPage({
return (
<>
-
+
+
+
>
)
}
diff --git a/src/app/(drift)/admin/components/table.module.css b/src/app/(drift)/admin/components/table.module.css
index 2bdc29de..16c8d4f9 100644
--- a/src/app/(drift)/admin/components/table.module.css
+++ b/src/app/(drift)/admin/components/table.module.css
@@ -1,13 +1,3 @@
-.table {
- width: 100%;
- display: block;
- white-space: nowrap;
-
- thead th {
- font-weight: bold;
- }
-}
-
.id {
width: 130px;
white-space: nowrap;
diff --git a/src/app/(drift)/admin/components/tables.tsx b/src/app/(drift)/admin/components/tables.tsx
index 9c922763..4d2d3996 100644
--- a/src/app/(drift)/admin/components/tables.tsx
+++ b/src/app/(drift)/admin/components/tables.tsx
@@ -1,6 +1,6 @@
"use client"
-import Button from "@components/button"
+import { Button } from "@components/button"
import { Spinner } from "@components/spinner"
import { useToasts } from "@components/toasts"
import { ServerPostWithFilesAndAuthor, UserWithPosts } from "@lib/server/prisma"
@@ -18,7 +18,7 @@ export function UserTable({
id: string
email: string | null
role: string | null
- displayName: string | null
+ username: string | null
}[]
}) {
const { setToast } = useToasts()
@@ -26,6 +26,14 @@ export function UserTable({
const deleteUser = async (id: string) => {
try {
+ const confirmed = confirm("Are you sure you want to delete this user?")
+ if (!confirmed) {
+ setToast({
+ message: "User not deleted",
+ type: "default"
+ })
+ return
+ }
const res = await fetchWithUser("/api/admin?action=delete-user", {
method: "DELETE",
headers: {
@@ -53,8 +61,8 @@ export function UserTable({
}
return (
-
-
+
+
Name
Email
@@ -73,14 +81,20 @@ export function UserTable({
) : null}
{users?.map((user) => (
- {user.displayName ? user.displayName : "no name"}
+ {user.username ? user.username : "no name"}
{user.email}
{user.role}
{user.id}
- deleteUser(user.id)}>Delete
+ deleteUser(user.id)}
+ size={"sm"}
+ >
+ Delete
+
))}
@@ -90,7 +104,7 @@ export function UserTable({
}
export function PostTable({
- posts
+ posts: initialPosts
}: {
posts?: {
createdAt: string
@@ -100,15 +114,54 @@ export function PostTable({
visibility: string
}[]
}) {
+ const [posts, setPosts] = useState(initialPosts)
+ const { setToast } = useToasts()
+ const deletePost = async (id: string) => {
+ try {
+ const confirmed = confirm("Are you sure you want to delete this post?")
+ if (!confirmed) {
+ setToast({
+ message: "Post not deleted",
+ type: "default"
+ })
+ return
+ }
+ const res = await fetchWithUser("/api/admin?action=delete-post", {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ postId: id
+ })
+ })
+
+ if (res.status === 200) {
+ setToast({
+ message: "Post deleted",
+ type: "success"
+ })
+ setPosts(posts?.filter((post) => post.id !== id))
+ }
+ } catch (err) {
+ console.error(err)
+ setToast({
+ message: "Error deleting user",
+ type: "error"
+ })
+ }
+ }
+
return (
-
-
+
+
Title
Author
Created
Visibility
Post ID
+ Actions
@@ -130,6 +183,15 @@ export function PostTable({
{new Date(post.createdAt).toLocaleDateString()}
{post.visibility}
{post.id}
+
+ deletePost(post.id)}
+ >
+ Delete
+
+
))}
diff --git a/src/app/(drift)/admin/loading.tsx b/src/app/(drift)/admin/loading.tsx
index 98042fcf..8125aef9 100644
--- a/src/app/(drift)/admin/loading.tsx
+++ b/src/app/(drift)/admin/loading.tsx
@@ -1,12 +1,13 @@
+import { TypographyH1, TypographyH2 } from "@components/typography"
import { PostTable, UserTable } from "./components/tables"
export default function AdminLoading() {
return (
-
Admin
-
Users
+
Admin
+
Users
-
Posts
+
Posts
)
diff --git a/src/app/(drift)/admin/page.tsx b/src/app/(drift)/admin/page.tsx
index a246a1ca..9a2ae16d 100644
--- a/src/app/(drift)/admin/page.tsx
+++ b/src/app/(drift)/admin/page.tsx
@@ -6,15 +6,20 @@ import {
ServerPostWithFiles
} from "@lib/server/prisma"
import { PostTable, UserTable } from "./components/tables"
+import { PageWrapper } from "@components/page-wrapper"
+import { PageTitle } from "@components/page-title"
export default async function AdminPage() {
const usersPromise = getAllUsers({
select: {
id: true,
- name: true,
- createdAt: true
+ createdAt: true,
+ email: true,
+ role: true,
+ username: true
}
})
+
const postsPromise = getAllPosts({
select: {
id: true,
@@ -43,13 +48,15 @@ export default async function AdminPage() {
})
return (
-
-
Admin
-
Users
- {/* @ts-expect-error Type 'unknown' is not assignable to type */}
-
-
Posts
-
-
+ <>
+ Admin
+
+ Users
+ {/* @ts-expect-error Type 'unknown' is not assignable to type */}
+
+ Posts
+
+
+ >
)
}
diff --git a/src/app/(drift)/author/[username]/page.tsx b/src/app/(drift)/author/[username]/page.tsx
index 7ea192db..b286ee34 100644
--- a/src/app/(drift)/author/[username]/page.tsx
+++ b/src/app/(drift)/author/[username]/page.tsx
@@ -1,4 +1,5 @@
import PostList from "@components/post-list"
+import { TypographyH1 } from "@components/typography"
import {
getPostsByUser,
getUserById,
@@ -59,7 +60,9 @@ export default async function UserPage({
justifyContent: "space-between"
}}
>
- Public posts by {user?.displayName || "Anonymous"}
+
+ Public posts by {user?.displayName || "Anonymous"}
+
}>
diff --git a/src/app/(drift)/layout.tsx b/src/app/(drift)/layout.tsx
index ae1269fd..58e40a89 100644
--- a/src/app/(drift)/layout.tsx
+++ b/src/app/(drift)/layout.tsx
@@ -6,6 +6,8 @@ import Header from "@components/header"
import { Inter } from "next/font/google"
import { getMetadata } from "src/app/lib/metadata"
import dynamic from "next/dynamic"
+import clsx from "clsx"
+import Link from "@components/link"
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false })
@@ -17,14 +19,18 @@ export default async function RootLayout({
}) {
return (
// suppressHydrationWarning is required because of next-themes
-
+
- {children}
+ {children}
diff --git a/src/app/(drift)/mine/loading.tsx b/src/app/(drift)/mine/loading.tsx
index d9c9b9e1..a9a09200 100644
--- a/src/app/(drift)/mine/loading.tsx
+++ b/src/app/(drift)/mine/loading.tsx
@@ -1,7 +1,15 @@
"use client"
+import { PageTitle } from "@components/page-title"
+import { PageWrapper } from "@components/page-wrapper"
import PostList from "@components/post-list"
export default function Loading() {
- return
+ return (
+ <>
+ Your Posts
+
+
+ >
+ )
}
diff --git a/src/app/(drift)/mine/page.tsx b/src/app/(drift)/mine/page.tsx
index 23bdad70..0699300c 100644
--- a/src/app/(drift)/mine/page.tsx
+++ b/src/app/(drift)/mine/page.tsx
@@ -5,6 +5,8 @@ import { Suspense } from "react"
import ErrorBoundary from "@components/error/fallback"
import { getMetadata } from "src/app/lib/metadata"
import { redirect } from "next/navigation"
+import { PageTitle } from "@components/page-title"
+import { PageWrapper } from "@components/page-wrapper"
export default async function Mine() {
const userId = (await getCurrentUser())?.id
@@ -16,16 +18,21 @@ export default async function Mine() {
const posts = (await getPostsByUser(userId, true)).map(serverPostToClientPost)
return (
-
- }>
-
-
-
+ <>
+ Your Posts
+
+
+ }>
+
+
+
+
+ >
)
}
diff --git a/src/app/(drift)/page.tsx b/src/app/(drift)/page.tsx
index af4e9580..9260f457 100644
--- a/src/app/(drift)/page.tsx
+++ b/src/app/(drift)/page.tsx
@@ -1,7 +1,5 @@
-import Image from "next/image"
-import Card from "@components/card"
+import { Card, CardContent } from "@components/card"
import { getWelcomeContent } from "src/pages/api/welcome"
-import DocumentTabs from "./(posts)/components/tabs"
import {
getAllPosts,
serverPostToClientPost,
@@ -10,8 +8,8 @@ import {
import PostList, { NoPostsFound } from "@components/post-list"
import { cache, Suspense } from "react"
import ErrorBoundary from "@components/error/fallback"
-import { Stack } from "@components/stack"
-
+import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
+import { PageWrapper } from "@components/page-wrapper"
export const revalidate = 300
const getWelcomeData = cache(async () => {
@@ -20,23 +18,11 @@ const getWelcomeData = cache(async () => {
})
export default async function Page() {
- const { title } = await getWelcomeData()
-
return (
-
-
-
- {title}
-
+
{/* @ts-expect-error because of async RSC */}
- Recent public posts
+ Recent Public Posts
-
+
)
}
async function WelcomePost() {
const { content, rendered, title } = await getWelcomeData()
return (
-
-
- {content}
-
+
+
+
+ {content}
+
+
)
}
@@ -79,6 +67,7 @@ async function PublicPostList() {
}
},
visibility: true,
+ expiresAt: true,
files: {
select: {
id: true,
diff --git a/src/app/(drift)/providers.tsx b/src/app/(drift)/providers.tsx
index bb2e73f2..33f82a12 100644
--- a/src/app/(drift)/providers.tsx
+++ b/src/app/(drift)/providers.tsx
@@ -10,7 +10,12 @@ export function Providers({ children }: PropsWithChildren) {
return (
-
+
{children}
diff --git a/src/app/(drift)/settings/components/sections/api-keys.tsx b/src/app/(drift)/settings/components/sections/api-keys.tsx
index d49da72e..a7795dff 100644
--- a/src/app/(drift)/settings/components/sections/api-keys.tsx
+++ b/src/app/(drift)/settings/components/sections/api-keys.tsx
@@ -1,7 +1,7 @@
"use client"
-import Button from "@components/button"
-import Input from "@components/input"
+import { Button } from "@components/button"
+import { Input } from "@components/input"
import Note from "@components/note"
import { Spinner } from "@components/spinner"
import { useToasts } from "@components/toasts"
@@ -13,6 +13,7 @@ import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
import { useState } from "react"
import styles from "./api-keys.module.css"
import { useSessionSWR } from "@lib/use-session-swr"
+import { TypographyH4 } from "@components/typography"
// need to pass in the accessToken
const APIKeys = ({
@@ -75,7 +76,7 @@ const APIKeys = ({
)}
{hasError && {error?.message} }
) : (
- You have no API keys.
+
+ No API keys found.
+
)
) : (
diff --git a/src/app/(drift)/settings/components/sections/profile.tsx b/src/app/(drift)/settings/components/sections/profile.tsx
index 55ffb8ac..3757b10e 100644
--- a/src/app/(drift)/settings/components/sections/profile.tsx
+++ b/src/app/(drift)/settings/components/sections/profile.tsx
@@ -1,7 +1,7 @@
"use client"
-import Button from "@components/button"
-import Input from "@components/input"
+import { Button } from "@components/button"
+import { Input } from "@components/input"
import Note from "@components/note"
import { useToasts } from "@components/toasts"
import { useSessionSWR } from "@lib/use-session-swr"
@@ -142,8 +142,7 @@ function Profile() {
*/}
-
-
+
Submit
diff --git a/src/app/(drift)/settings/layout.tsx b/src/app/(drift)/settings/layout.tsx
deleted file mode 100644
index 0d66c911..00000000
--- a/src/app/(drift)/settings/layout.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-export default function SettingsLayout({
- children
-}: {
- children: React.ReactNode
-}) {
- return (
- <>
- Settings
-
- {children}
-
- >
- )
-}
diff --git a/src/app/(drift)/settings/loading.tsx b/src/app/(drift)/settings/loading.tsx
index 147ccdb8..8f4eecf2 100644
--- a/src/app/(drift)/settings/loading.tsx
+++ b/src/app/(drift)/settings/loading.tsx
@@ -1,5 +1,15 @@
+import { PageTitle } from "@components/page-title"
+import { PageWrapper } from "@components/page-wrapper"
import SettingsGroup from "@components/settings-group"
export default function SettingsLoading() {
- return
+ return (
+ <>
+ Settings
+
+
+
+
+ >
+ )
}
diff --git a/src/app/(drift)/settings/page.tsx b/src/app/(drift)/settings/page.tsx
index f549fa9c..86a31bf2 100644
--- a/src/app/(drift)/settings/page.tsx
+++ b/src/app/(drift)/settings/page.tsx
@@ -2,16 +2,21 @@ import { getMetadata } from "src/app/lib/metadata"
import SettingsGroup from "../../components/settings-group"
import APIKeys from "./components/sections/api-keys"
import Profile from "./components/sections/profile"
+import { PageTitle } from "@components/page-title"
+import { PageWrapper } from "@components/page-wrapper"
export default async function SettingsPage() {
return (
<>
-
-
-
-
-
-
+ Settings
+
+
+
+
+
+
+
+
>
)
}
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 00000000..8c324910
--- /dev/null
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,6 @@
+import { authOptions } from "@lib/server/auth"
+import NextAuth from "next-auth"
+
+const handler = NextAuth(authOptions)
+
+export { handler as GET, handler as POST }
diff --git a/src/app/api/auth/requires-passcode/route.ts b/src/app/api/auth/requires-passcode/route.ts
new file mode 100644
index 00000000..e533e08d
--- /dev/null
+++ b/src/app/api/auth/requires-passcode/route.ts
@@ -0,0 +1,41 @@
+import config from "@lib/config"
+import { NextRequest } from "next/server"
+
+export const getRequiresPasscode = async () => {
+ const requiresPasscode = Boolean(config.registration_password)
+ return requiresPasscode
+}
+
+export default async function GET(req: NextRequest) {
+ const searchParams = new URL(req.nextUrl).searchParams
+ const slug = searchParams.get("slug")
+
+ if (!slug || Array.isArray(slug)) {
+ return new Response(null, {
+ status: 400,
+ statusText: "Bad request"
+ })
+ }
+
+ if (slug === "requires-passcode") {
+ // return res.json({ requiresPasscode: await getRequiresPasscode() })
+ return new Response(
+ JSON.stringify({ requiresPasscode: await getRequiresPasscode() }),
+ {
+ status: 200,
+ statusText: "OK",
+ headers: {
+ "Content-Type": "application/json"
+ }
+ }
+ )
+ }
+
+ return new Response(JSON.stringify({ error: "Not found" }), {
+ status: 404,
+ statusText: "Not found",
+ headers: {
+ "Content-Type": "application/json"
+ }
+ })
+}
diff --git a/src/app/components/alert-dialog.tsx b/src/app/components/alert-dialog.tsx
new file mode 100644
index 00000000..58f5ae4e
--- /dev/null
+++ b/src/app/components/alert-dialog.tsx
@@ -0,0 +1,150 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@lib/cn"
+import { buttonVariants } from "@components/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = ({
+ className,
+ children,
+ ...props
+}: AlertDialogPrimitive.AlertDialogPortalProps) => (
+
+
+ {children}
+
+
+)
+AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel
+}
diff --git a/src/app/components/badges/badge.tsx b/src/app/components/badges/badge.tsx
index 2e2668a2..87934e06 100644
--- a/src/app/components/badges/badge.tsx
+++ b/src/app/components/badges/badge.tsx
@@ -1,21 +1,35 @@
-import React from "react"
-import styles from "./badge.module.css"
-type BadgeProps = {
- type: "primary" | "secondary" | "error" | "warning"
-} & React.HTMLAttributes
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@lib/cn"
-const Badge = React.forwardRef(
- ({ type, children, ...rest }: BadgeProps, ref) => {
- return (
-
- )
+const badgeVariants = cva(
+ "inline-flex items-center border rounded-full font-medium px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
+ secondary:
+ "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
+ destructive:
+ "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
+ outline: "text-foreground"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
}
)
-Badge.displayName = "Badge"
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
-export default Badge
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/app/components/badges/created-ago-badge/index.tsx b/src/app/components/badges/created-ago-badge/index.tsx
index 2237eed6..b18b5fd0 100644
--- a/src/app/components/badges/created-ago-badge/index.tsx
+++ b/src/app/components/badges/created-ago-badge/index.tsx
@@ -1,10 +1,10 @@
"use client"
import { useToasts } from "@components/toasts"
-import Tooltip from "@components/tooltip"
+import { Tooltip } from "@components/tooltip"
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"
+import { Badge } from "../badge"
const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
@@ -30,7 +30,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
return (
// TODO: investigate tooltip not showing
-
+
{" "}
<>{time}>
diff --git a/src/app/components/badges/expiration-badge/index.tsx b/src/app/components/badges/expiration-badge/index.tsx
index 636ba994..4a409282 100644
--- a/src/app/components/badges/expiration-badge/index.tsx
+++ b/src/app/components/badges/expiration-badge/index.tsx
@@ -1,9 +1,9 @@
"use client"
-import Tooltip from "@components/tooltip"
+import { Tooltip } from "@components/tooltip"
import { timeUntil } from "src/app/lib/time-ago"
import { useEffect, useMemo, useState } from "react"
-import Badge from "../badge"
+import { Badge } from "../badge"
const ExpirationBadge = ({
postExpirationDate
@@ -43,7 +43,7 @@ const ExpirationBadge = ({
const isExpired = expirationDate < new Date()
return (
-
+
diff --git a/src/app/components/badges/visibility-badge/index.tsx b/src/app/components/badges/visibility-badge/index.tsx
index 60cfc8db..f7813e5b 100644
--- a/src/app/components/badges/visibility-badge/index.tsx
+++ b/src/app/components/badges/visibility-badge/index.tsx
@@ -1,11 +1,11 @@
-import Badge from "../badge"
+import { Badge } from "../badge"
type Props = {
visibility: string
}
const VisibilityBadge = ({ visibility }: Props) => {
- return {visibility}
+ return {visibility}
}
export default VisibilityBadge
diff --git a/src/app/components/badges/visibility-control/index.tsx b/src/app/components/badges/visibility-control/index.tsx
index ff868136..f78822d8 100644
--- a/src/app/components/badges/visibility-control/index.tsx
+++ b/src/app/components/badges/visibility-control/index.tsx
@@ -3,9 +3,8 @@
import PasswordModal from "@components/password-modal"
import { useCallback, useState } from "react"
import ButtonGroup from "@components/button-group"
-import Button from "@components/button"
+import { Button } from "@components/button"
import { useToasts } from "@components/toasts"
-import { Spinner } from "@components/spinner"
import { useRouter } from "next/navigation"
import { useSessionSWR } from "@lib/use-session-swr"
import { fetchWithUser } from "src/app/lib/fetch-with-user"
@@ -89,39 +88,40 @@ function VisibilityControl({
}
return (
-
-
+
+
onSubmit("private")}
+ loading={isSubmitting === "private"}
>
- {isSubmitting === "private" ? : "Make Private"}
+ Make Private
onSubmit("public")}
+ loading={isSubmitting === "public"}
>
- {isSubmitting === "public" ? : "Make Public"}
+ Make Public
onSubmit("unlisted")}
+ loading={isSubmitting === "unlisted"}
>
- {isSubmitting === "unlisted" ? : "Make Unlisted"}
+ Make Unlisted
- onSubmit("protected")}>
- {isSubmitting === "protected" ? (
-
- ) : visibility === "protected" ? (
- "Change Password"
- ) : (
- "Protect with Password"
- )}
+ onSubmit("protected")}
+ variant={"outline"}
+ loading={isSubmitting === "protected"}
+ >
+ {visibility === "protected"
+ ? "Change Password"
+ : "Protect with Password"}
, keyof Props>
-type ButtonDropdownProps = Props & Attrs
+type ButtonDropdownProps = ComponentProps
-const ButtonDropdown: React.FC<
- React.PropsWithChildren
-> = ({ type, ...props }) => {
+const ButtonDropdown: React.FC> = (
+ props
+) => {
return (
-
-
+
+
<>
{Array.isArray(props.children) ? props.children[0] : props.children}
-
- }
- buttonType={type}
- className={styles.icon}
- />
-
+
+
+
+
+
{Array.isArray(props.children) ? (
-
-
+
+
{(props.children as ReactNode[])
?.slice(1)
.map((child, index) => (
- {child}
+ {child}
))}
-
-
+
+
) : null}
>
-
+
)
}
diff --git a/src/app/components/button-group/button-group.module.css b/src/app/components/button-group/button-group.module.css
index a6352a78..8ea67b8c 100644
--- a/src/app/components/button-group/button-group.module.css
+++ b/src/app/components/button-group/button-group.module.css
@@ -3,6 +3,7 @@
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
+ margin-top: 0 !important;
}
.button-group > * {
diff --git a/src/app/components/button/button.module.css b/src/app/components/button/button.module.css
deleted file mode 100644
index 4422cff3..00000000
--- a/src/app/components/button/button.module.css
+++ /dev/null
@@ -1,56 +0,0 @@
-.button {
- user-select: none;
- cursor: pointer;
- border-radius: var(--radius);
- border: 1px solid var(--border);
-}
-
-.button:hover,
-.button:focus {
- color: var(--fg);
- background: var(--bg);
- border: 1px solid var(--darker-gray);
-}
-
-.button[disabled] {
- cursor: not-allowed;
- background: var(--lighter-gray);
- color: var(--gray);
-}
-
-.button[disabled]:hover,
-.button[disabled]:focus {
- border: 1px solid currentColor;
-}
-
-.secondary {
- background: var(--bg);
- color: var(--fg);
-}
-
-.primary {
- background: var(--fg);
- color: var(--bg);
-}
-
-.icon {
- display: inline-block;
- width: 1em;
- height: 1em;
- vertical-align: middle;
-}
-
-.iconRight {
- margin-left: var(--gap-half);
-}
-
-.iconLeft {
- margin-right: var(--gap-half);
-}
-
-.icon svg {
- display: block;
- width: 100%;
- height: 100%;
- transform: scale(1.2) translateY(-0.05em);
-}
diff --git a/src/app/components/button/index.tsx b/src/app/components/button/index.tsx
index 60c10728..7fd34161 100644
--- a/src/app/components/button/index.tsx
+++ b/src/app/components/button/index.tsx
@@ -1,86 +1,65 @@
-import styles from "./button.module.css"
-import { forwardRef } from "react"
-import clsx from "clsx"
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@lib/cn"
import { Spinner } from "@components/spinner"
-type Props = React.DetailedHTMLProps<
- React.ButtonHTMLAttributes
,
- HTMLButtonElement
-> & {
- children?: React.ReactNode
- buttonType?: "primary" | "secondary"
- className?: string
- onClick?: (e: React.MouseEvent) => void
- iconRight?: React.ReactNode
- iconLeft?: React.ReactNode
- height?: string | number
- width?: string | number
- padding?: string | number
- margin?: string | number
+const buttonVariants = cva(
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-75 disabled:pointer-events-none ring-offset-background",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary/80 text-primary-foreground/80 hover:bg-primary/70",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "underline-offset-4 hover:underline text-primary"
+ },
+ size: {
+ default: "h-10 py-2 px-4",
+ sm: "h-9 px-3 rounded-md",
+ lg: "h-11 px-8 rounded-md"
+ }
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default"
+ }
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
loading?: boolean
}
-// eslint-disable-next-line react/display-name
-const Button = forwardRef(
+const Button = React.forwardRef(
(
- {
- children,
- onClick,
- className,
- buttonType = "secondary",
- disabled = false,
- iconRight,
- iconLeft,
- height = 40,
- width,
- padding = 10,
- margin,
- loading,
- style,
- ...props
- },
+ { className, variant, size, loading, children, asChild = false, ...props },
ref
) => {
+ const Comp = asChild ? Slot : "button"
+
return (
-
- {children && iconLeft && (
- {iconLeft}
- )}
- {!loading &&
- (children ? (
- children
- ) : (
- {iconLeft || iconRight}
- ))}
- {loading && (
-
-
-
- )}
- {children && iconRight && (
-
- {iconRight}
-
- )}
-
+ {loading ? : null}
+ {children}
+
)
}
)
-export default Button
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/src/app/components/calendar.tsx b/src/app/components/calendar.tsx
new file mode 100644
index 00000000..122f0aab
--- /dev/null
+++ b/src/app/components/calendar.tsx
@@ -0,0 +1,64 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "react-feather"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@lib/cn"
+import { buttonVariants } from "@components/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({}) =>
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/src/app/components/card/index.tsx b/src/app/components/card/index.tsx
index 219775bf..25ef3c5b 100644
--- a/src/app/components/card/index.tsx
+++ b/src/app/components/card/index.tsx
@@ -1,16 +1,78 @@
-import styles from "./card.module.css"
+import * as React from "react"
+import { cn } from "@lib/cn"
-export default function Card({
- children,
- className,
- ...props
-}: {
- children?: React.ReactNode
- className?: string
-} & React.ComponentProps<"div">) {
- return (
-
- )
-}
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/app/components/cmdk/cmdk.module.css b/src/app/components/cmdk/cmdk.module.css
deleted file mode 100644
index 4436b9e6..00000000
--- a/src/app/components/cmdk/cmdk.module.css
+++ /dev/null
@@ -1,165 +0,0 @@
-/** Based on https://github.com/pacocoursey/cmdk **/
-.cmdk[cmdk-root] {
- overflow: hidden;
- font-family: var(--font-sans);
- box-shadow: 0 0 0 1px var(--lighter-gray), 0 4px 16px rgba(0, 0, 0, 0.2);
- transition: transform 100ms ease;
- border-radius: var(--radius);
-
- .dark & {
- background: rgba(22, 22, 22, 0.7);
- }
-}
-
-.cmdk {
- /* centered */
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- z-index: 999999;
- /* size */
- max-width: 640px;
- width: 100%;
-
- [cmdk-list] {
- background: var(--bg);
- height: 500px;
- overflow: auto;
- overscroll-behavior: contain;
- }
-
- [cmdk-input] {
- font-family: var(--font-sans);
- width: 100%;
- font-size: 17px;
- padding: 8px 8px 16px 8px;
- outline: none;
- /* background: var(--lightest-gray); */
- color: var(--fg);
-
- &::placeholder {
- color: var(--gray);
- }
- }
-
- [cmdk-badge] {
- height: 20px;
- background: var(--grayA3);
- display: inline-flex;
- align-items: center;
- padding: 0 8px;
- font-size: 12px;
- color: var(--grayA11);
- border-radius: 4px;
- margin: 4px 0 4px 4px;
- user-select: none;
- text-transform: capitalize;
- font-weight: 500;
- }
-
- [cmdk-item] {
- content-visibility: auto;
-
- cursor: pointer;
- height: 48px;
- border-radius: 8px;
- font-size: 14px;
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 0 16px;
- color: var(--darker-gray);
- user-select: none;
- will-change: background, color;
- transition: all 150ms ease;
- transition-property: none;
- background: var(--bg);
-
- &[aria-selected="true"] {
- background: var(--lightest-gray);
- color: var(--fg);
- }
-
- &[aria-disabled="true"] {
- /* TODO: improve this */
- color: var(--bg);
- cursor: not-allowed;
- }
-
- &:active {
- transition-property: background;
- background: var(--bg);
- }
-
- & + [cmdk-item] {
- margin-top: 4px;
- }
-
- svg {
- width: 18px;
- height: 18px;
- }
- }
-
- [cmdk-list] {
- max-height: 400px;
- overflow: auto;
- overscroll-behavior: contain;
- transition: 100ms ease;
- transition-property: height;
- }
-
- [cmdk-shortcuts] {
- display: flex;
- margin-left: auto;
- gap: 8px;
-
- kbd {
- font-family: var(--font-sans);
- font-size: 12px;
- min-width: 20px;
- padding: var(--gap-half);
- height: 20px;
- border-radius: 4px;
- color: var(--fg);
- background: var(--light-gray);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- text-transform: uppercase;
- }
- }
-
- [cmdk-separator] {
- height: 1px;
- width: 100%;
- background: var(--light-gray);
- margin: 4px 0;
- }
-
- *:not([hidden]) + [cmdk-group] {
- margin-top: var(--gap);
- }
-
- [cmdk-group-heading] {
- user-select: none;
- font-size: 12px;
- color: var(--gray);
- padding: 0 var(--gap);
- display: flex;
- align-items: center;
- margin-bottom: var(--gap);
- margin-top: var(--gap);
- }
-
- [cmdk-empty] {
- font-size: 14px;
- display: flex;
- align-items: center;
- justify-content: center;
- height: 48px;
- white-space: pre-wrap;
- color: var(--gray);
- }
-}
diff --git a/src/app/components/cmdk/cmdk.tsx b/src/app/components/cmdk/cmdk.tsx
new file mode 100644
index 00000000..88b46242
--- /dev/null
+++ b/src/app/components/cmdk/cmdk.tsx
@@ -0,0 +1,163 @@
+"use client"
+
+import * as React from "react"
+import { DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "react-feather"
+
+import { cn } from "@lib/cn"
+import { Dialog, DialogContent } from "@components/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+type CommandDialogProps = DialogProps
+
+const CommandDialog = React.forwardRef<
+ React.ElementRef,
+ CommandDialogProps
+>(({ children, ...props }, ref) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+})
+
+CommandDialog.displayName = Dialog.displayName
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator
+}
diff --git a/src/app/components/cmdk/dialog.css b/src/app/components/cmdk/dialog.css
index 5e75a6dc..d52dfa17 100644
--- a/src/app/components/cmdk/dialog.css
+++ b/src/app/components/cmdk/dialog.css
@@ -1,16 +1,3 @@
body [cmdk-dialog] {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 999999;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(0, 0, 0, 0.5);
- /* backdrop-filter: blur(4px); */
- transition: opacity 100ms ease;
- pointer-events: none;
- will-change: opacity;
+
}
diff --git a/src/app/components/cmdk/index.tsx b/src/app/components/cmdk/index.tsx
index d3aa1d22..728a6613 100644
--- a/src/app/components/cmdk/index.tsx
+++ b/src/app/components/cmdk/index.tsx
@@ -1,8 +1,7 @@
"use client"
-import { Command } from "cmdk"
+import { CommandDialog, CommandList, CommandInput, CommandEmpty } from "./cmdk"
import { useEffect, useRef, useState } from "react"
-import styles from "./cmdk.module.css"
import "./dialog.css"
import HomePage from "./pages/home"
import PostsPage from "./pages/posts"
@@ -10,7 +9,7 @@ import PostsPage from "./pages/posts"
export type CmdKPage = "home" | "posts"
export default function CmdK() {
const [open, setOpen] = useState(false)
- const ref = useRef(null)
+ const ref = useRef()
const [page, setPage] = useState("home")
// Toggle the menu when ⌘K is pressed
@@ -53,21 +52,15 @@ export default function CmdK() {
}, [page])
return (
-
-
- No results found.
+
+
+ No results found.
{page === "home" ? (
) : null}
{page === "posts" ? : null}
-
-
-
+
+
+
)
}
diff --git a/src/app/components/cmdk/item.tsx b/src/app/components/cmdk/item.tsx
index ca337a88..569b3dcb 100644
--- a/src/app/components/cmdk/item.tsx
+++ b/src/app/components/cmdk/item.tsx
@@ -1,4 +1,4 @@
-import { Command } from "cmdk"
+import { CommandItem } from "@components/cmdk/cmdk"
export default function Item({
children,
@@ -12,7 +12,7 @@ export default function Item({
icon: React.ReactNode
}): JSX.Element {
return (
-
+
{icon}
{children}
{shortcut ? (
@@ -22,6 +22,6 @@ export default function Item({
})}
) : null}
-
+
)
}
diff --git a/src/app/components/cmdk/pages/home.tsx b/src/app/components/cmdk/pages/home.tsx
index d124c34b..94c4fe19 100644
--- a/src/app/components/cmdk/pages/home.tsx
+++ b/src/app/components/cmdk/pages/home.tsx
@@ -1,9 +1,9 @@
-import { Command } from "cmdk"
import { useTheme } from "next-themes"
import { useRouter } from "next/navigation"
import { FilePlus, Moon, Search, Settings, Sun } from "react-feather"
import { CmdKPage } from ".."
import Item from "../item"
+import { CommandGroup } from "@components/cmdk/cmdk"
export default function HomePage({
setOpen,
@@ -16,7 +16,7 @@ export default function HomePage({
const { setTheme, resolvedTheme } = useTheme()
return (
<>
-
+
- {
@@ -36,8 +36,8 @@ export default function HomePage({
>
New Post
-
-
+
+
- {
@@ -57,7 +57,7 @@ export default function HomePage({
>
Go to Settings
-
+
>
)
}
diff --git a/src/app/components/date-picker.tsx b/src/app/components/date-picker.tsx
new file mode 100644
index 00000000..5b10a27a
--- /dev/null
+++ b/src/app/components/date-picker.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import { format } from "date-fns"
+import { Calendar as CalendarIcon } from "react-feather"
+
+import { cn } from "@lib/cn"
+import { Button } from "@components/button"
+import { Calendar } from "@components/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@components/popover"
+
+export function DatePicker({
+ expiresAt,
+ setExpiresAt
+}: {
+ expiresAt?: Date
+ setExpiresAt: React.Dispatch>
+}) {
+ return (
+
+
+
+
+ {expiresAt ? (
+ format(expiresAt, "PPP")
+ ) : (
+ Won't expire
+ )}
+
+
+
+ {
+ setExpiresAt(date)
+ }}
+ initialFocus
+ fromDate={new Date()}
+ />
+
+
+ )
+}
diff --git a/src/app/components/dialog.tsx b/src/app/components/dialog.tsx
new file mode 100644
index 00000000..f1e760be
--- /dev/null
+++ b/src/app/components/dialog.tsx
@@ -0,0 +1,128 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "react-feather"
+
+import { cn } from "@lib/cn"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = ({
+ className,
+ children,
+ ...props
+}: DialogPrimitive.DialogPortalProps) => (
+
+
+ {children}
+
+
+)
+DialogPortal.displayName = DialogPrimitive.Portal.displayName
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription
+}
diff --git a/src/app/components/dropdown-menu/index.tsx b/src/app/components/dropdown-menu/index.tsx
new file mode 100644
index 00000000..c9c816d1
--- /dev/null
+++ b/src/app/components/dropdown-menu/index.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "react-feather"
+
+import { cn } from "@lib/cn"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup
+}
diff --git a/src/app/components/error/fallback.tsx b/src/app/components/error/fallback.tsx
index 8e0cf9f9..b44951a4 100644
--- a/src/app/components/error/fallback.tsx
+++ b/src/app/components/error/fallback.tsx
@@ -1,8 +1,9 @@
"use client"
-import Button from "@components/button"
+import { Button } from "@components/button"
import Link from "@components/link"
import Note from "@components/note"
+import { TypographyH3 } from "@components/typography"
import { useRouter } from "next/navigation"
// an error fallback for react-error-boundary
@@ -15,9 +16,12 @@ import {
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
- Something went wrong:
+ Something went wrong:
{error.message}
-
+
Report an issue
Try again
diff --git a/src/app/components/fade-in/index.tsx b/src/app/components/fade-in/index.tsx
index 355e83fe..eee230a5 100644
--- a/src/app/components/fade-in/index.tsx
+++ b/src/app/components/fade-in/index.tsx
@@ -1,4 +1,5 @@
// https://www.joshwcomeau.com/snippets/react-components/fade-in/
+import React from "react"
import styles from "./fade.module.css"
function FadeIn({
@@ -11,8 +12,18 @@ function FadeIn({
duration?: number
delay?: number
children: React.ReactNode
- as?: React.ElementType
+ as?: React.ElementType | JSX.Element
} & React.HTMLAttributes) {
+ if (as !== null && typeof as === "object") {
+ return React.cloneElement(as, {
+ className: styles.fadeIn,
+ style: {
+ ...(as.props.style || {}),
+ animationDuration: duration + "ms",
+ animationDelay: delay + "ms"
+ }
+ })
+ }
const Element = as || "div"
return (
{tab.name ? tab.name : undefined}
)
} else {
return (
-
-
+
+
{tab.name ? tab.name : undefined}
@@ -146,15 +155,14 @@ export function HeaderButtons(): JSX.Element {
/>
{isAdmin && (
-
- }
- value="admin"
- href="/admin"
- />
-
+ }
+ value="admin"
+ href="/admin"
+ className="transition-opacity duration-500"
+ />
)}
{isAuthenticated === true && (
{
+ setTheme(resolvedTheme === "dark" ? "light" : "dark")
+ }
+
+ useEffect(() => {
+ setIsMounted(true)
+ }, [])
+
return (
-
-
-
+
+
+
+
+ Drift
+
+
+
+ Home
+
+ New
+
+
+ Yours
+
+
+ Settings
+
+ {isAdmin && Admin }
+ {isAuthenticated !== undefined && (
+ <>
+ {isAuthenticated === true && (
+ Sign Out
+ )}
+ {isAuthenticated === false && (
+ Sign In
+ )}
+ >
+ )}
+
+
+
+
+
+
+ {isMounted && (
+
+
+ {resolvedTheme === "dark" ? (
+
+ ) : (
+
+ )}
+
+
+ )}
)
}
+
+type NavLinkProps = PropsWithChildren<{
+ href: string
+ disabled?: boolean
+ active?: boolean
+}>
+
+function NavLink({ href, disabled, children }: NavLinkProps) {
+ const baseClasses =
+ "text-sm text-muted-foreground font-medium transition-colors hover:text-primary"
+ const activeClasses = "text-primary border-primary"
+ const disabledClasses = "text-gray-600 hover:text-gray-400 cursor-not-allowed"
+
+ const segments = useSelectedLayoutSegments()
+ const activeSegment = segments[segments.length - 1]
+ const isActive =
+ activeSegment === href.slice(1) ||
+ // special case / because it's an alias of /home/page.tsx
+ (!activeSegment && href === "/home")
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/app/components/header/mobile.module.css b/src/app/components/header/mobile.module.css
deleted file mode 100644
index 39dc7f57..00000000
--- a/src/app/components/header/mobile.module.css
+++ /dev/null
@@ -1,53 +0,0 @@
-.mobileTrigger {
- display: none;
-}
-
-@media only screen and (max-width: 768px) {
- .header {
- opacity: 1;
- }
-
- .wrapper [data-tab="github"] {
- display: none;
- }
-
- .mobileTrigger {
- margin-top: var(--gap);
- margin-bottom: var(--gap);
- display: flex;
- align-items: center;
- }
-
- .mobileTrigger button {
- display: none;
- }
-
- .dropdownItem a,
- .dropdownItem button {
- width: 100%;
- text-align: left;
- white-space: nowrap;
- display: block;
- }
-
- .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;
- }
-
- .tabs {
- display: none;
- }
-}
diff --git a/src/app/components/header/mobile.tsx b/src/app/components/header/mobile.tsx
index 4f20b3f1..5bd982a4 100644
--- a/src/app/components/header/mobile.tsx
+++ b/src/app/components/header/mobile.tsx
@@ -1,12 +1,10 @@
"use client"
-import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
-import buttonStyles from "@components/button/button.module.css"
-import Button from "@components/button"
+import { Button, buttonVariants } from "@components/button"
import { Menu } from "react-feather"
-import clsx from "clsx"
-import styles from "./mobile.module.css"
import { HeaderButtons } from "./buttons"
+import * as DropdownMenu from "@components/dropdown-menu"
+import React from "react"
export default function MobileHeader() {
// TODO: this is a hack to close the radix ui menu when a next link is clicked
@@ -15,28 +13,27 @@ export default function MobileHeader() {
}
return (
-
-
+
-
+
-
-
-
+
+
+
{HeaderButtons().props.children.map((button: JSX.Element) => (
-
{button}
-
+
))}
-
-
-
+
+
+
)
}
diff --git a/src/app/components/input/index.tsx b/src/app/components/input/index.tsx
index bf3c3a48..71b7ce32 100644
--- a/src/app/components/input/index.tsx
+++ b/src/app/components/input/index.tsx
@@ -1,82 +1,51 @@
-import clsx from "clsx"
-import React from "react"
-import styles from "./input.module.css"
+import * as React from "react"
-type Props = React.HTMLProps & {
+import { cn } from "@lib/cn"
+
+export type InputProps = React.InputHTMLAttributes & {
label?: string
- width?: number | string
- height?: number | string
- labelClassName?: string
+ hideLabel?: boolean
}
-// we have two special rules on top of the props:
-// if onChange or value is passed, we require both, unless `disabled`
-// if label is passed, we forbid aria-label and vice versa
-type InputProps = Omit &
- (
- | {
- onChange: Props["onChange"]
- value: Props["value"]
- }
- | {
- onChange?: never
- value?: never
- }
- | {
- value: Props["value"]
- disabled: true
- onChange?: 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"]
- }
- )
const Input = React.forwardRef(
- (
- { label, className, required, width, height, labelClassName, ...props },
- ref
- ) => {
- const labelId = label?.replace(/\s/g, "-").toLowerCase()
+ ({ className, type, label, hideLabel, ...props }, ref) => {
+ const id = React.useId()
return (
-
- {label && (
+
+ {label && !hideLabel ? (
{label}
- )}
+ ) : null}
+ {label && hideLabel ? (
+
+ {label}
+
+ ) : null}
-
+
)
}
)
-
Input.displayName = "Input"
-export default Input
+export { Input }
diff --git a/src/app/components/layout/index.tsx b/src/app/components/layout/index.tsx
index 8ee6baea..bbec11e3 100644
--- a/src/app/components/layout/index.tsx
+++ b/src/app/components/layout/index.tsx
@@ -2,6 +2,7 @@
import clsx from "clsx"
import styles from "./page.module.css"
+import Link from "@components/link"
export default function Layout({
children,
@@ -12,7 +13,22 @@ export default function Layout({
}) {
return (
)
}
diff --git a/src/app/components/layout/page.module.css b/src/app/components/layout/page.module.css
index 894e203b..c0a62a35 100644
--- a/src/app/components/layout/page.module.css
+++ b/src/app/components/layout/page.module.css
@@ -1,5 +1,4 @@
.page {
- max-width: var(--main-content);
min-height: 100vh;
box-sizing: border-box;
position: relative;
diff --git a/src/app/components/link/index.tsx b/src/app/components/link/index.tsx
index 40a66d41..fff706dd 100644
--- a/src/app/components/link/index.tsx
+++ b/src/app/components/link/index.tsx
@@ -1,15 +1,15 @@
import NextLink from "next/link"
-import styles from "./link.module.css"
+import { cn } from "@lib/cn"
type LinkProps = {
colored?: boolean
children: React.ReactNode
} & React.ComponentProps
-const Link = ({ colored, children, ...props }: LinkProps) => {
- const className = colored ? `${styles.link} ${styles.color}` : styles.link
+const Link = ({ colored, className, children, ...props }: LinkProps) => {
+ const classes = colored ? "text-blue-500 dark:text-blue-400 hover:underline" : "hover:underline"
return (
-
+
{children}
)
diff --git a/src/app/components/navigation-menu.tsx b/src/app/components/navigation-menu.tsx
new file mode 100644
index 00000000..326417e4
--- /dev/null
+++ b/src/app/components/navigation-menu.tsx
@@ -0,0 +1,128 @@
+import * as React from "react"
+import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
+import { cva } from "class-variance-authority"
+import { ChevronDown } from "react-feather"
+
+import { cn } from "@lib/cn"
+
+const NavigationMenu = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
+
+const NavigationMenuList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
+
+const NavigationMenuItem = NavigationMenuPrimitive.Item
+
+const navigationMenuTriggerStyle = cva(
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-accent focus:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none bg-background hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent/50 data-[active]:bg-accent/50 h-10 py-2 px-4 group w-max"
+)
+
+const NavigationMenuTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}{" "}
+
+
+))
+NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
+
+const NavigationMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
+
+const NavigationMenuLink = NavigationMenuPrimitive.Link
+
+const NavigationMenuViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+NavigationMenuViewport.displayName =
+ NavigationMenuPrimitive.Viewport.displayName
+
+const NavigationMenuIndicator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+NavigationMenuIndicator.displayName =
+ NavigationMenuPrimitive.Indicator.displayName
+
+export {
+ navigationMenuTriggerStyle,
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+ NavigationMenuIndicator,
+ NavigationMenuViewport
+}
diff --git a/src/app/components/note/index.tsx b/src/app/components/note/index.tsx
index dca12b0e..4948da52 100644
--- a/src/app/components/note/index.tsx
+++ b/src/app/components/note/index.tsx
@@ -10,7 +10,10 @@ const Note = ({
type: "info" | "warning" | "error"
children: React.ReactNode
} & React.ComponentProps<"div">) => (
-
+
{children}
)
diff --git a/src/app/components/page-title.tsx b/src/app/components/page-title.tsx
new file mode 100644
index 00000000..24437124
--- /dev/null
+++ b/src/app/components/page-title.tsx
@@ -0,0 +1,14 @@
+import { cn } from "@lib/cn"
+import { PropsWithChildren } from "react"
+
+export function PageTitle({
+ children,
+ className,
+ ...props
+}: PropsWithChildren
>) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/app/components/page-wrapper.tsx b/src/app/components/page-wrapper.tsx
new file mode 100644
index 00000000..166627d0
--- /dev/null
+++ b/src/app/components/page-wrapper.tsx
@@ -0,0 +1,14 @@
+import { cn } from "@lib/cn"
+import { PropsWithChildren } from "react"
+
+export function PageWrapper({
+ children,
+ className,
+ ...props
+}: PropsWithChildren>) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/app/components/password-modal/index.tsx b/src/app/components/password-modal/index.tsx
index 952a4ea5..9cf0f8be 100644
--- a/src/app/components/password-modal/index.tsx
+++ b/src/app/components/password-modal/index.tsx
@@ -1,9 +1,17 @@
-import Button from "@components/button"
-import Input from "@components/input"
+import { Input } from "@components/input"
import Note from "@components/note"
-import * as Dialog from "@radix-ui/react-dialog"
-import { useState } from "react"
+import { MouseEventHandler, useState } from "react"
import styles from "./modal.module.css"
+import {
+ AlertDialog,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogDescription,
+ AlertDialogTitle,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogFooter
+} from "@components/alert-dialog"
type Props = {
creating: boolean
@@ -22,7 +30,8 @@ const PasswordModal = ({
const [confirmPassword, setConfirmPassword] = useState("")
const [error, setError] = useState()
- const onSubmit = () => {
+ const onSubmit: MouseEventHandler = (e) => {
+ e.preventDefault()
if (!password || (creating && !confirmPassword)) {
setError("Please enter a password")
return
@@ -39,60 +48,57 @@ const PasswordModal = ({
return (
<>
{
- {
if (!open) onClose()
}}
>
-
-
-
-
+ {/* */}
+
+
+
{creating ? "Add a password" : "Enter password"}
-
-
+
+
{creating
? "Enter a password to protect your post"
: "Enter the password to access the post"}
-
-
- {!error && creating && (
-
- This doesn't protect your post from the server
- administrator.
-
- )}
- {error && {error} }
+
+
+
+ {!error && creating && (
+
+ This doesn't protect your post from the server
+ administrator.
+
+ )}
+ {error && {error} }
+ setPassword(e.currentTarget.value)}
+ />
+ {creating && (
setPassword(e.currentTarget.value)}
+ placeholder="Confirm Password"
+ value={confirmPassword}
+ onChange={(e) => setConfirmPassword(e.currentTarget.value)}
/>
- {creating && (
- setConfirmPassword(e.currentTarget.value)}
- />
- )}
-
-
-
-
-
+ )}
+
+
+ Cancel
+ Submit
+
+
+
}
>
)
diff --git a/src/app/components/popover/index.tsx b/src/app/components/popover/index.tsx
index 7f7f2f90..e1ab533b 100644
--- a/src/app/components/popover/index.tsx
+++ b/src/app/components/popover/index.tsx
@@ -1,35 +1,31 @@
-// largely from https://github.com/shadcn/taxonomy
+"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
-import clsx from "clsx"
-import styles from "./popover.module.css"
-type PopoverProps = PopoverPrimitive.PopoverProps
+import { cn } from "@lib/cn"
-export function Popover({ ...props }: PopoverProps) {
- return
-}
+const Popover = PopoverPrimitive.Root
-Popover.Trigger = React.forwardRef<
- HTMLButtonElement,
- PopoverPrimitive.PopoverTriggerProps
->(function PopoverTrigger({ ...props }, ref) {
- return
-})
+const PopoverTrigger = PopoverPrimitive.Trigger
-Popover.Portal = PopoverPrimitive.Portal
-
-Popover.Content = React.forwardRef<
- HTMLDivElement,
- PopoverPrimitive.PopoverContentProps
->(function PopoverContent({ className, ...props }, ref) {
- return (
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
- )
-})
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/src/app/components/post-list/index.tsx b/src/app/components/post-list/index.tsx
index 8de064a7..82ba368f 100644
--- a/src/app/components/post-list/index.tsx
+++ b/src/app/components/post-list/index.tsx
@@ -4,7 +4,7 @@ import styles from "./post-list.module.css"
import ListItem from "./list-item"
import { ChangeEvent, useCallback, useState } from "react"
import type { PostWithFiles } from "@lib/server/prisma"
-import Input from "@components/input"
+import { Input } from "@components/input"
import { useToasts } from "@components/toasts"
import { ListItemSkeleton } from "./list-item-skeleton"
import Link from "@components/link"
@@ -83,7 +83,10 @@ const PostList = ({
})
if (!res?.ok) {
- console.error(res)
+ setToast({
+ message: "Failed to delete post",
+ type: "error"
+ })
return
} else {
setPosts((posts) => posts?.filter((post) => post.id !== postId))
@@ -103,7 +106,7 @@ const PostList = ({
(
- {/* TODO: this is a bad way to do skeletons and is onlya ccurate on desktop */}
-
-
- {/* title */}
-
-
+ {/* TODO: this is a bad way to do skeletons and is only accurate on desktop */}
+
+
+
+ {/* title */}
+
+
-
-
-
+
+
+
+
)
diff --git a/src/app/components/post-list/list-item.module.css b/src/app/components/post-list/list-item.module.css
index ff8dc82b..ffadde8a 100644
--- a/src/app/components/post-list/list-item.module.css
+++ b/src/app/components/post-list/list-item.module.css
@@ -1,8 +1,3 @@
-.title {
- display: flex;
- justify-content: space-between;
-}
-
.titleText {
display: flex;
gap: var(--gap-half);
@@ -15,11 +10,6 @@
flex-wrap: wrap;
}
-.buttons {
- display: flex;
- gap: var(--gap-half);
-}
-
.oneline {
white-space: nowrap;
overflow: hidden;
@@ -50,17 +40,10 @@
li {
display: flex;
align-items: center;
- gap: var(--gap);
+ gap: var(--gap-half);
padding: var(--gap-quarter);
}
- li a {
- display: flex;
- align-items: center;
- gap: var(--gap);
- color: var(--darker-gray);
- }
-
li a:hover {
color: var(--link);
text-decoration: none;
diff --git a/src/app/components/post-list/list-item.tsx b/src/app/components/post-list/list-item.tsx
index c67a7f06..e5065c0b 100644
--- a/src/app/components/post-list/list-item.tsx
+++ b/src/app/components/post-list/list-item.tsx
@@ -6,20 +6,31 @@ import { useRouter } from "next/navigation"
import styles from "./list-item.module.css"
import Link from "@components/link"
import type { PostWithFiles } from "@lib/server/prisma"
-import Tooltip from "@components/tooltip"
-import Badge from "@components/badges/badge"
-import Card from "@components/card"
-import Button from "@components/button"
+import { Badge } from "@components/badges/badge"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle
+} from "@components/card"
import {
ArrowUpCircle,
Code,
Database,
Edit,
FileText,
+ MoreVertical,
Terminal,
Trash
} from "react-feather"
import { codeFileExtensions } from "@lib/constants"
+import {
+ DropdownMenu,
+ DropdownMenuItem,
+ DropdownMenuTrigger
+} from "@components/dropdown-menu"
+import { DropdownMenuContent } from "@radix-ui/react-dropdown-menu"
// TODO: isOwner should default to false so this can be used generically
const ListItem = ({
@@ -67,9 +78,9 @@ const ListItem = ({
return (
-
- <>
-
+
+
+
-
+
{post.files?.length === 1
? "1 file"
: `${post.files?.length || 0} files`}
@@ -92,62 +103,72 @@ const ListItem = ({
{!hideActions ? (
-
- {post.parentId && (
-
- }
- onClick={viewParentClick}
- // TODO: not perfect on mobile
- height={38}
- />
-
- )}
-
- }
- onClick={editACopy}
- height={38}
- />
-
- {isOwner && (
-
- }
- onClick={deletePost}
- height={38}
- />
-
- )}
+
+
+
+
+
+
+ {
+ editACopy()
+ }}
+ className="cursor-pointer bg-background"
+ >
+ Edit a copy
+
+ {isOwner && (
+ {
+ deletePost()
+ }}
+ className="cursor-pointer bg-background"
+ >
+
+ Delete
+
+ )}
+ {post.parentId && (
+ {
+ viewParentClick()
+ }}
+ >
+
+ View parent
+
+ )}
+
+
) : null}
-
-
+
{post.description && (
- {post.description}
+
+ {post.description}
+
)}
- >
-
- {post?.files?.map(
- (file: Pick["files"][0]) => {
- return (
-
-
- {getIconFromFilename(file.title)}
- {file.title || "Untitled file"}
-
-
- )
- }
- )}
-
+
+
+
+ {post?.files?.map(
+ (file: Pick["files"][0]) => {
+ return (
+
+
+ {getIconFromFilename(file.title)}
+ {file.title || "Untitled file"}
+
+
+ )
+ }
+ )}
+
+
)
diff --git a/src/app/components/scroll-to-top/index.tsx b/src/app/components/scroll-to-top/index.tsx
index 4ac0b0bb..63c3d707 100644
--- a/src/app/components/scroll-to-top/index.tsx
+++ b/src/app/components/scroll-to-top/index.tsx
@@ -1,7 +1,7 @@
"use client"
-import Button from "@components/button"
-import Tooltip from "@components/tooltip"
+import { Button } from "@components/button"
+import { Tooltip } from "@components/tooltip"
import { useEffect, useState } from "react"
import { ChevronUp } from "react-feather"
import styles from "./scroll.module.css"
@@ -39,8 +39,10 @@ const ScrollToTop = () => {
}
- />
+ variant={"secondary"}
+ >
+
+
)
diff --git a/src/app/components/settings-group/index.tsx b/src/app/components/settings-group/index.tsx
index 022ebd5d..0b0e3f3b 100644
--- a/src/app/components/settings-group/index.tsx
+++ b/src/app/components/settings-group/index.tsx
@@ -1,4 +1,4 @@
-import Card from "@components/card"
+import { Card, CardContent, CardHeader, CardTitle } from "@components/card"
import styles from "./settings-group.module.css"
type Props =
@@ -26,9 +26,13 @@ const SettingsGroup = ({ title, children, skeleton }: Props) => {
return (
- {title}
-
- {children}
+
+ {title}
+
+
+
+ {children}
+
)
}
diff --git a/src/app/components/skeleton/index.tsx b/src/app/components/skeleton/index.tsx
index 3a2a87c6..e792f5ec 100644
--- a/src/app/components/skeleton/index.tsx
+++ b/src/app/components/skeleton/index.tsx
@@ -1,4 +1,4 @@
-import styles from "./skeleton.module.css"
+import { cn } from "@lib/cn"
export default function Skeleton({
width = 100,
@@ -13,8 +13,13 @@ export default function Skeleton({
}) {
return (
)
}
diff --git a/src/app/components/skeleton/skeleton.module.css b/src/app/components/skeleton/skeleton.module.css
deleted file mode 100644
index d86eb857..00000000
--- a/src/app/components/skeleton/skeleton.module.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.skeleton {
- background-color: var(--lighter-gray);
- border-radius: var(--radius);
-}
diff --git a/src/app/components/spinner/index.tsx b/src/app/components/spinner/index.tsx
index d35c2e41..60e0e548 100644
--- a/src/app/components/spinner/index.tsx
+++ b/src/app/components/spinner/index.tsx
@@ -1,3 +1,6 @@
+import { cn } from "@lib/cn"
import styles from "./spinner.module.css"
-export const Spinner = () =>
+export const Spinner = ({ className }: { className?: string }) => (
+
+)
diff --git a/src/app/components/tabs.tsx b/src/app/components/tabs.tsx
new file mode 100644
index 00000000..3fc02fe3
--- /dev/null
+++ b/src/app/components/tabs.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@lib/cn"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/src/app/components/textarea.tsx b/src/app/components/textarea.tsx
new file mode 100644
index 00000000..eeff8f78
--- /dev/null
+++ b/src/app/components/textarea.tsx
@@ -0,0 +1,23 @@
+import * as React from "react"
+
+import { cn } from "@lib/cn"
+
+export type TextareaProps = React.TextareaHTMLAttributes
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/src/app/components/tooltip/index.tsx b/src/app/components/tooltip/index.tsx
index b8fd6635..2f90682e 100644
--- a/src/app/components/tooltip/index.tsx
+++ b/src/app/components/tooltip/index.tsx
@@ -1,9 +1,11 @@
"use client"
-import * as RadixTooltip from "@radix-ui/react-tooltip"
-import styles from "./tooltip.module.css"
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
-const Tooltip = ({
+import { cn } from "@lib/cn"
+
+const TooltipWrapper = ({
children,
content,
className,
@@ -12,18 +14,42 @@ const Tooltip = ({
children: React.ReactNode
content: React.ReactNode
className?: string
-} & RadixTooltip.TooltipProps) => {
+} & TooltipPrimitive.TooltipProps) => {
return (
-
-
- {children}
-
+
+
+
+ {children}
+
-
- {content}
-
-
+ {content}
+
+
)
}
-export default Tooltip
+// export TooltipWrapper as Tooltip
+export { TooltipWrapper as Tooltip }
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+))
+
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
diff --git a/src/app/components/typography/index.tsx b/src/app/components/typography/index.tsx
new file mode 100644
index 00000000..a53c85d9
--- /dev/null
+++ b/src/app/components/typography/index.tsx
@@ -0,0 +1,227 @@
+import { PropsWithChildren } from "react"
+import { cn } from "@lib/cn"
+
+type ClassProp = { className?: string }
+
+export function TypographyH1({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyH2({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyH3({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyH4({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyP({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyBlockquote({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyTable({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ )
+}
+
+export function TableHead({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TableBody({
+ children,
+ className
+}: PropsWithChildren) {
+ return {children}
+}
+
+export function TableHeader({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TableRow({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TableCell({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyList({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+ li]:mt-2", className)}>
+ {children}
+
+ )
+}
+
+export function TypographyInlineCode({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyLead({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+ {children}
+ )
+}
+
+export function TypographyLarge({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+ {children}
+ )
+}
+
+export function TypographySmall({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function TypographyMuted({
+ children,
+ className
+}: PropsWithChildren) {
+ return (
+ {children}
+ )
+}
diff --git a/src/app/lib/fetch-with-user.ts b/src/app/lib/fetch-with-user.ts
index 81094c03..30ebf02b 100644
--- a/src/app/lib/fetch-with-user.ts
+++ b/src/app/lib/fetch-with-user.ts
@@ -7,6 +7,6 @@ export async function fetchWithUser(url: string, options: RequestInit = {}) {
// TODO: figure out if this extra network call hurts performance
const session = await getSession()
const newUrl = new URL(url, process.env.NEXT_PUBLIC_DRIFT_URL)
- newUrl.searchParams.append("userId", session?.user.id || "")
+ newUrl.searchParams.append("userId", session?.user?.id || "")
return fetch(newUrl.toString(), options)
}
diff --git a/src/app/pages/[fileId]/[fileTitle]/layout.tsx b/src/app/pages/[fileId]/[fileTitle]/layout.tsx
index 3af56aa1..073af03f 100644
--- a/src/app/pages/[fileId]/[fileTitle]/layout.tsx
+++ b/src/app/pages/[fileId]/[fileTitle]/layout.tsx
@@ -1,5 +1,4 @@
import "@styles/globals.css"
-import "@styles/markdown.css"
import Layout from "@components/layout"
import { Inter } from "next/font/google"
diff --git a/src/app/pages/[fileId]/[fileTitle]/page.tsx b/src/app/pages/[fileId]/[fileTitle]/page.tsx
index 18b83837..ff577e74 100644
--- a/src/app/pages/[fileId]/[fileTitle]/page.tsx
+++ b/src/app/pages/[fileId]/[fileTitle]/page.tsx
@@ -28,13 +28,14 @@ export default async function FilePage({
}
return (
- <>
- {file.title}
-
+
+ {file.title}
+
- >
+
)
}
diff --git a/src/app/pages/[fileId]/[fileTitle]/theme-provider.tsx b/src/app/pages/[fileId]/[fileTitle]/theme-provider.tsx
index 0ad822fb..e561625d 100644
--- a/src/app/pages/[fileId]/[fileTitle]/theme-provider.tsx
+++ b/src/app/pages/[fileId]/[fileTitle]/theme-provider.tsx
@@ -7,7 +7,12 @@ export default function ThemeProvider({
children
}: PropsWithChildren) {
return (
-
+
{children}
)
diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css
index eaf51623..20aed326 100644
--- a/src/app/styles/globals.css
+++ b/src/app/styles/globals.css
@@ -1,217 +1,139 @@
-:root {
- /* Spacing */
- --gap-quarter: 0.25rem;
- --gap-half: 0.5rem;
- --gap: 1rem;
- --gap-double: 2rem;
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
- --small-gap: 4rem;
- --big-gap: 4rem;
- --main-content: 50rem;
- --radius: 8px;
- --inline-radius: 5px;
-
- --gap-negative: calc(-1 * var(--gap));
- --gap-half-negative: calc(-1 * var(--gap-half));
- --gap-quarter-negative: calc(-1 * var(--gap-quarter));
-
- /* Typography */
- --font-sans: var(--inter-font), -apple-system, BlinkMacSystemFont, Segoe UI,
- Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
- sans-serif;
- --font-mono: ui-monospace, "SFMono-Regular", "Consolas", "Liberation Mono",
- "Menlo", monospace;
-
- /* Transitions */
- --transition: 0.1s ease-in-out;
- --transition-slow: 0.3s ease-in-out;
-
- --token: #999;
- --comment: #999;
- --keyword: #fff;
- --name: #fff;
- --highlight: #2e2e2e;
-
- /* Dark Mode Colors */
- --bg: #000;
- --fg: #fafbfc;
- --gray: #666;
- --light-gray: #444;
- --lighter-gray: #222;
- --lightest-gray: #1a1a1a;
- --darker-gray: #b4b4b4;
- --darkest-gray: #efefef;
- --article-color: #eaeaea;
- --header-bg: rgba(19, 20, 21, 0.45);
- --gray-alpha: rgba(19, 20, 21, 0.6);
- --selection: rgba(255, 255, 255, 0.99);
- --border: var(--light-gray);
- --warning: #ff6700;
- --link: #3291ff;
- color-scheme: dark;
-}
-
-[data-theme="light"] {
- --token: #666;
- --comment: #999;
- --keyword: #000;
- --name: #333;
- --highlight: #eaeaea;
-
- --bg: #fff;
- --fg: #000;
- --gray: #888;
-
- --light-gray: #dedede;
- --lighter-gray: #f2f2f2;
- --lightest-gray: #fafafa;
- --darker-gray: #555;
- --darkest-gray: #222;
- --article-color: #212121;
- --header-bg: rgba(255, 255, 255, 0.8);
- --gray-alpha: rgba(19, 20, 21, 0.5);
- --selection: var(0, 0, 0, 0.6);
- color-scheme: light;
-}
-
-* {
- box-sizing: border-box;
-}
-
-::selection {
- text-shadow: none;
- background: var(--fg);
- color: var(--bg);
-}
-
-html,
-body {
- padding: 0;
- margin: 0;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- min-height: 100vh;
- font-family: var(--font-sans);
- display: flex;
- flex-direction: column;
- background: var(--bg);
-}
-
-p {
- overflow-wrap: break-word;
- hyphens: auto;
-}
-
-input,
-button,
-textarea,
-select {
- border: none;
- font-size: 1rem;
- background: var(--bg);
-}
-
-blockquote {
- font-style: italic;
- margin: 0;
- padding-left: 1rem;
- border-left: 3px solid var(--light-gray);
-}
-
-a.reset {
- outline: none;
- text-decoration: none;
-}
-
-pre,
-code {
- font-family: var(--font-mono);
-}
-
-hr {
- border: 0;
- border-bottom: 1px solid var(--light-gray);
- margin: var(--gap) 0;
-}
-
-@media print {
+@layer base {
:root {
- --bg: #fff;
- --fg: #000;
- --gray: #888;
- --light-gray: #dedede;
- --lighter-gray: #f5f5f5;
- --lightest-gray: #fafafa;
- --article-color: #212121;
- --header-bg: rgba(255, 255, 255, 0.8);
- --gray-alpha: rgba(19, 20, 21, 0.5);
- --selection: rgba(0, 0, 0, 0.99);
+ /* Spacing */
+ --gap-quarter: 0.25rem;
+ --gap-half: 0.5rem;
+ --gap: 1rem;
+ --gap-double: 2rem;
- --token: #666;
+ --small-gap: 4rem;
+ --big-gap: 4rem;
+ --main-content: min(55rem, 100vw);
+ --radius: 8px;
+ --inline-radius: 5px;
+
+ --gap-negative: calc(-1 * var(--gap));
+ --gap-half-negative: calc(-1 * var(--gap-half));
+ --gap-quarter-negative: calc(-1 * var(--gap-quarter));
+
+ /* Typography */
+ --font-sans: var(--inter-font), -apple-system, BlinkMacSystemFont, Segoe UI,
+ Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
+ sans-serif;
+ --font-mono: ui-monospace, "SFMono-Regular", "Consolas", "Liberation Mono",
+ "Menlo", monospace;
+
+ /* Transitions */
+ --transition: 0.1s ease-in-out;
+ --transition-slow: 0.3s ease-in-out;
+
+ --token: #999;
--comment: #999;
- --keyword: #000;
- --name: #333;
- --highlight: #eaeaea;
+ --keyword: #fff;
+ --name: #fff;
+ --highlight: #2e2e2e;
+
+ /* Dark Mode Colors */
+ --bg: #000;
+ --fg: #fafbfc;
+ --gray: #666;
+
+ --light-gray: #dedede;
+ --lighter-gray: #f2f2f2;
+ --lightest-gray: #fafafa;
+ --darker-gray: #555;
+ --darkest-gray: #222;
+ --article-color: #eaeaea;
+
+ --header-bg: rgba(19, 20, 21, 0.45);
+ --gray-alpha: rgba(19, 20, 21, 0.6);
+ --selection: rgba(255, 255, 255, 0.99);
+ --warning: #ff6700;
+ --link: #3291ff;
+ color-scheme: dark;
+
+ --background: 0 0% 100%;
+ --foreground: #fafbfc;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 47.4% 11.2%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 47.4% 11.2%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 100% 50%;
+ --destructive-foreground: 210 40% 98%;
+
+ --ring: 215 20.2% 65.1%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 224 71% 3%;
+ --foreground: 213 31% 88%;
+
+ --muted: 223 47% 11%;
+ --muted-foreground: 215.4 16.3% 56.9%;
+
+ --popover: 224 71% 4%;
+ --popover-foreground: 215 20.2% 65.1%;
+
+ --card: 224 71% 4%;
+ --card-foreground: 213 31% 88%;
+
+ --border: 216 34% 17%;
+ --input: 216 34% 17%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 1.2%;
+
+ --secondary: 222.2 47.4% 11.2%;
+ --secondary-foreground: 210 40% 98%;
+
+ --accent: 216 34% 17%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 63% 31%;
+ --destructive-foreground: 210 40% 98%;
+
+ --ring: 216 34% 17%;
+
+ --radius: 0.5rem;
+
+ --light-gray: #444;
+ --lighter-gray: #222;
+ --lightest-gray: #1a1a1a;
+ --darker-gray: #b4b4b4;
+ --darkest-gray: #efefef;
}
}
-#root,
-#__next {
- isolation: isolate;
-}
-
-textarea {
- resize: vertical;
- border: 1px solid var(--border);
- font-family: var(--font-sans);
- padding: var(--gap-half);
- border-radius: var(--radius);
-}
-
-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;
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ font-feature-settings: "rlig" 1, "calt" 1;
+ }
}
diff --git a/src/app/styles/markdown.css b/src/app/styles/markdown.css
index f92e8ccf..28273fd6 100644
--- a/src/app/styles/markdown.css
+++ b/src/app/styles/markdown.css
@@ -49,7 +49,7 @@ article ul li.reset .check {
/* Checkbox */
-input[type="checkbox"] {
+article input[type="checkbox"] {
vertical-align: middle;
appearance: none;
display: inline-block;
@@ -64,7 +64,7 @@ input[type="checkbox"] {
border-radius: 3px;
}
-input[type="checkbox"]:checked {
+article input[type="checkbox"]:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
@@ -73,26 +73,21 @@ input[type="checkbox"]:checked {
background-repeat: no-repeat;
}
-html[data-theme="light"] input[type="checkbox"]:checked {
+html[data-theme="light"] article input[type="checkbox"]:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
}
-input[type="checkbox"]:focus {
+article input[type="checkbox"]:focus {
outline: none;
border-color: var(--fg);
}
/* Code Snippets */
-.token-line:not(:last-child) {
+article .token-line:not(:last-child) {
min-height: 1.4rem;
}
-article *:not(pre) > code {
- font-weight: 500;
- font-family: var(--font-sans);
-}
-
article li > p {
display: inline-block;
padding: 0;
@@ -112,16 +107,16 @@ article pre {
background-color: var(--lighter-gray);
}
-table {
+article table {
border-collapse: collapse;
}
-table th {
+article table th {
font-weight: 500;
}
-table th,
-table td {
+article table th,
+article table td {
padding: 0.35rem 0.75rem;
border: 1px solid var(--light-gray);
}
diff --git a/src/lib/cn.ts b/src/lib/cn.ts
new file mode 100644
index 00000000..9a7122c3
--- /dev/null
+++ b/src/lib/cn.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/src/lib/server/prisma.ts b/src/lib/server/prisma.ts
index 03ac0643..83ec83af 100644
--- a/src/lib/server/prisma.ts
+++ b/src/lib/server/prisma.ts
@@ -179,6 +179,7 @@ export async function getPostsByUser(
createdAt: true,
updatedAt: true,
authorId: true,
+ expiresAt: true,
visibility: true,
...(withFiles && {
files: {
@@ -252,10 +253,7 @@ export const createUser = async (
}
// all of prisma.post.findUnique
-type GetPostByIdOptions = Pick<
- Prisma.PostFindUniqueArgs,
- "include" | "rejectOnNotFound" | "select"
->
+type GetPostByIdOptions = Pick
export const getPostById = async (
postId: ServerPost["id"],
diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts
deleted file mode 100644
index 44fccc86..00000000
--- a/src/pages/api/auth/[...nextauth].ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { authOptions } from "@lib/server/auth"
-import NextAuth from "next-auth"
-
-export default NextAuth(authOptions)
diff --git a/src/pages/api/auth/requires-passcode.ts b/src/pages/api/auth/requires-passcode.ts
deleted file mode 100644
index 1ec1e758..00000000
--- a/src/pages/api/auth/requires-passcode.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import config from "@lib/config"
-import { NextApiRequest, NextApiResponse } from "next"
-
-export const getRequiresPasscode = async () => {
- const requiresPasscode = Boolean(config.registration_password)
- return requiresPasscode
-}
-
-const handleRequiresPasscode = async (
- _: NextApiRequest,
- res: NextApiResponse
-) => {
- return res.json({ requiresPasscode: await getRequiresPasscode() })
-}
-
-export default async function requiresPasscode(
- req: NextApiRequest,
- res: NextApiResponse
-) {
- const { slug } = req.query
-
- if (!slug || Array.isArray(slug)) {
- return res.status(400).json({ error: "Missing param" })
- }
-
- switch (req.method) {
- case "GET":
- if (slug === "requires-passcode") {
- return handleRequiresPasscode(req, res)
- }
-
- return res.status(404).json({ error: "Not found" })
- default:
- return res.status(405).json({ error: "Method not allowed" })
- }
-}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 00000000..bbc72254
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,73 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: ["class"],
+ content: [
+ './src/**/*.{ts,tsx}',
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
+}
\ No newline at end of file