Compare commits
2 commits
refactor
...
max/dep-up
Author | SHA1 | Date | |
---|---|---|---|
|
77533aa4c8 | ||
|
11ac185f86 |
125 changed files with 4068 additions and 5639 deletions
|
@ -23,7 +23,8 @@ GITHUB_CLIENT_SECRET=
|
||||||
# Optional: if you want Keycloak oauth. Currently incompatible with the registration password
|
# Optional: if you want Keycloak oauth. Currently incompatible with the registration password
|
||||||
KEYCLOAK_ID=
|
KEYCLOAK_ID=
|
||||||
KEYCLOAK_SECRET=
|
KEYCLOAK_SECRET=
|
||||||
KEYCLOAK_ISSUER= # keycloak path including realm
|
# keycloak path including realm
|
||||||
|
KEYCLOAK_ISSUER=
|
||||||
KEYCLOAK_NAME=
|
KEYCLOAK_NAME=
|
||||||
|
|
||||||
# Optional: if you want to support credential auth (username/password, supports registration password)
|
# Optional: if you want to support credential auth (username/password, supports registration password)
|
||||||
|
@ -31,5 +32,5 @@ KEYCLOAK_NAME=
|
||||||
CREDENTIAL_AUTH=true
|
CREDENTIAL_AUTH=true
|
||||||
|
|
||||||
# Optional:
|
# Optional:
|
||||||
WELCOME_CONTENT=
|
WELCOME_CONTENT="## Drift is a self-hostable clone of GitHub Gist. \nIt is a simple way to share code and text snippets. It supportsthe following:\n \n - Render GitHub Extended Markdown (including images)\n - User authentication\n - Private, public, and password protected posts\n - Markdown is rendered and stored on the server\n - Syntax highlighting and automatic language detection\n - Drag-and-drop file uploading\n\n If you want to signup, you can join at [/signup](/signup) as long as you have a passcode provided by the administrator (which you don\'t need for this demo). **This demo is on a memory-only database, so accounts and pastes can be deleted at any time.** \n\nYou can find the source code on [GitHub](https://github.com/MaxLeiter/drift)."
|
||||||
WELCOME_TITLE=
|
WELCOME_TITLE="Welcome to Drift"
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||||
"ignorePatterns": [
|
"ignorePatterns": ["node_modules/", "__tests__/"],
|
||||||
"node_modules/",
|
|
||||||
"__tests__/",
|
|
||||||
"coverage/",
|
|
||||||
".next/",
|
|
||||||
"public"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error"
|
"@typescript-eslint/no-explicit-any": "error"
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,7 +4,6 @@
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
analyze
|
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
|
@ -3,6 +3,5 @@
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"useTabs": true,
|
"useTabs": true
|
||||||
"plugins": ["prettier-plugin-tailwindcss"]
|
|
||||||
}
|
}
|
||||||
|
|
52
README.md
52
README.md
|
@ -8,8 +8,6 @@ You can try a demo at https://drift.lol. The demo is built on main but has no da
|
||||||
|
|
||||||
If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
|
If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
|
||||||
|
|
||||||
Drift is built with Next.js 13, React Server Components, [shadcn/ui](https://github.com/shadcn/ui), and [Prisma](https://prisma.io/).
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
**Contents:**
|
**Contents:**
|
||||||
|
@ -50,7 +48,6 @@ You can change these to your liking.
|
||||||
- `NODE_ENV`: defaults to development, can be `production`
|
- `NODE_ENV`: defaults to development, can be `production`
|
||||||
|
|
||||||
#### Auth environment variables
|
#### Auth environment variables
|
||||||
|
|
||||||
**Note:** Only credential auth currently supports the registration password, so if you want to secure registration, you must use only credential auth.
|
**Note:** Only credential auth currently supports the registration password, so if you want to secure registration, you must use only credential auth.
|
||||||
|
|
||||||
- `GITHUB_CLIENT_ID`: the client ID for GitHub OAuth.
|
- `GITHUB_CLIENT_ID`: the client ID for GitHub OAuth.
|
||||||
|
@ -71,55 +68,6 @@ Refer to pm2's docs or `pm2 help` for more information.
|
||||||
|
|
||||||
## Running with Docker
|
## Running with Docker
|
||||||
|
|
||||||
## Running with systemd
|
|
||||||
|
|
||||||
_**NOTE:** We assume that you know how to enable user lingering if you don't want to use the systemd unit as root_
|
|
||||||
|
|
||||||
- As root
|
|
||||||
- Place the following systemd unit in ___/etc/systemd/system___ and name it _drift.service_
|
|
||||||
- Replace any occurrence of ___`$USERNAME`___ with the shell username of the user that will be running the Drift server
|
|
||||||
|
|
||||||
```
|
|
||||||
##########
|
|
||||||
# Drift Systemd Unit (Global)
|
|
||||||
##########
|
|
||||||
[Unit]
|
|
||||||
Description=Drift Server (Global)
|
|
||||||
After=default.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=$USERNAME
|
|
||||||
Group=$USERNAME
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=/home/$USERNAME/Drift
|
|
||||||
ExecStart=/usr/bin/pnpm start
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
```
|
|
||||||
- As a nomal user
|
|
||||||
- Place the following systemd unit inside ___/home/user/.config/systemd/user___ and name it _drift_user.service_
|
|
||||||
- Replace any occurrence of ___`$USERNAME`___ with the shell username of the user that will be running the Drift server
|
|
||||||
|
|
||||||
```
|
|
||||||
##########
|
|
||||||
# Drift Systemd Unit (User)
|
|
||||||
##########
|
|
||||||
[Unit]
|
|
||||||
Description=Drift Server (User)
|
|
||||||
After=default.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=/home/$USERNAME/Drift
|
|
||||||
ExecStart=/usr/bin/pnpm start
|
|
||||||
Restart=on-failure
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
```
|
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
Drift is a work in progress. Below is a (rough) list of completed and envisioned features. If you want to help address any of them, please let me know regardless of your experience and I'll be happy to assist.
|
Drift is a work in progress. Below is a (rough) list of completed and envisioned features. If you want to help address any of them, please let me know regardless of your experience and I'll be happy to assist.
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "default",
|
|
||||||
"rsc": true,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "tailwind.config.js",
|
|
||||||
"css": "app/globals.css",
|
|
||||||
"baseColor": "slate",
|
|
||||||
"cssVariables": true
|
|
||||||
},
|
|
||||||
"aliases": {
|
|
||||||
"components": "@components",
|
|
||||||
"utils": "@utils"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,9 @@ import bundleAnalyzer from "@next/bundle-analyzer"
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
experimental: {
|
experimental: {
|
||||||
appDir: true
|
// esmExternals: true,
|
||||||
|
appDir: true,
|
||||||
|
serverComponentsExternalPackages: ["prisma", "@prisma/client"],
|
||||||
},
|
},
|
||||||
rewrites() {
|
rewrites() {
|
||||||
return [
|
return [
|
||||||
|
@ -12,35 +14,16 @@ const nextConfig = {
|
||||||
source: "/file/raw/:id",
|
source: "/file/raw/:id",
|
||||||
destination: `/api/raw/:id`
|
destination: `/api/raw/:id`
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: "/signout",
|
|
||||||
destination: `/api/auth/signout`
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ["avatars.githubusercontent.com"]
|
domains: ["avatars.githubusercontent.com"]
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
NEXT_PUBLIC_DRIFT_URL:
|
NEXT_PUBLIC_DRIFT_URL: process.env.DRIFT_URL
|
||||||
process.env.DRIFT_URL ||
|
|
||||||
(process.env.VERCEL_URL
|
|
||||||
? `https://${process.env.VERCEL_URL}`
|
|
||||||
: "http://localhost:3000")
|
|
||||||
},
|
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: process.env.VERCEL_ENV !== "production"
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
ignoreBuildErrors: process.env.VERCEL_ENV !== "production"
|
|
||||||
},
|
|
||||||
modularizeImports: {
|
|
||||||
"react-feather": {
|
|
||||||
transform: "react-feather/dist/icons/{{kebabCase member}}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default process.env.ANALYZE === "true"
|
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
|
||||||
? bundleAnalyzer({ enabled: true })(nextConfig)
|
nextConfig
|
||||||
: nextConfig
|
)
|
||||||
|
|
46
package.json
46
package.json
|
@ -13,48 +13,41 @@
|
||||||
"jest": "jest"
|
"jest": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@next/eslint-plugin-next": "13.4.11-canary.0",
|
"@next/eslint-plugin-next": "13.3.1-canary.6",
|
||||||
"@prisma/client": "^5.0.0",
|
"@prisma/client": "^4.12.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
|
||||||
"@radix-ui/react-dialog": "^1.0.3",
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
"@radix-ui/react-navigation-menu": "^1.1.3",
|
|
||||||
"@radix-ui/react-popover": "^1.0.5",
|
"@radix-ui/react-popover": "^1.0.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
|
||||||
"@radix-ui/react-tabs": "^1.0.3",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.5",
|
"@radix-ui/react-tooltip": "^1.0.5",
|
||||||
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
|
"@vercel/og": "^0.5.2",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"class-variance-authority": "^0.6.0",
|
|
||||||
"client-only": "^0.0.1",
|
"client-only": "^0.0.1",
|
||||||
"client-zip": "2.3.1",
|
"client-zip": "2.3.1",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^2.30.0",
|
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"next": "13.4.11-canary.1",
|
"next": "13.4.3-canary.2",
|
||||||
"next-auth": "^4.22.3",
|
"next-auth": "^4.22.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-datepicker": "4.10.0",
|
"react-datepicker": "4.11.0",
|
||||||
"react-day-picker": "^8.8.0",
|
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.3",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-hot-toast": "2.4.1",
|
"react-hot-toast": "2.4.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"swr": "^2.2.0",
|
"swr": "^2.1.3",
|
||||||
"tailwind-merge": "^1.13.0",
|
|
||||||
"tailwindcss-animate": "^1.0.5",
|
|
||||||
"textarea-markdown-editor": "1.0.4",
|
"textarea-markdown-editor": "1.0.4",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"uuid": "^9.0.0"
|
"ts-morph": "^18.0.0",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "13.4.11-canary.0",
|
"@next/bundle-analyzer": "13.4.3-canary.2",
|
||||||
"@total-typescript/ts-reset": "^0.4.2",
|
"@total-typescript/ts-reset": "^0.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/git-http-backend": "^1.0.1",
|
"@types/git-http-backend": "^1.0.1",
|
||||||
|
@ -68,25 +61,22 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||||
"@typescript-eslint/parser": "^5.58.0",
|
"@typescript-eslint/parser": "^5.58.0",
|
||||||
"@wcj/markdown-to-html": "^2.2.1",
|
"@wcj/markdown-to-html": "^2.2.1",
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"csstype": "^3.1.2",
|
"csstype": "^3.1.2",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "8.38.0",
|
"eslint": "8.38.0",
|
||||||
"eslint-config-next": "13.4.11-canary.1",
|
"eslint-config-next": "13.4.3-canary.2",
|
||||||
"jest-mock-extended": "^3.0.3",
|
"jest-mock-extended": "^3.0.3",
|
||||||
"next-unused": "0.0.6",
|
"next-unused": "0.0.6",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-hover-media-feature": "^1.0.2",
|
"postcss-hover-media-feature": "^1.0.2",
|
||||||
"postcss-nested": "^6.0.1",
|
"postcss-nested": "^6.0.1",
|
||||||
"postcss-preset-env": "^8.4.1",
|
"postcss-preset-env": "^8.3.1",
|
||||||
"prettier": "2.8.7",
|
"prettier": "2.8.7",
|
||||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
"prisma": "^4.12.0",
|
||||||
"prisma": "^5.0.0",
|
"typescript": "5.0.4",
|
||||||
"tailwindcss": "^3.3.2",
|
|
||||||
"typescript": "5.1.6",
|
|
||||||
"typescript-plugin-css-modules": "5.0.1"
|
"typescript-plugin-css-modules": "5.0.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
4293
pnpm-lock.yaml
4293
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,17 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: [
|
||||||
"@tailwindcss/nesting": {},
|
"postcss-nested",
|
||||||
tailwindcss: {},
|
"postcss-flexbugs-fixes",
|
||||||
autoprefixer: {}
|
"postcss-hover-media-feature",
|
||||||
}
|
[
|
||||||
|
"postcss-preset-env",
|
||||||
|
{
|
||||||
|
stage: 3,
|
||||||
|
features: {
|
||||||
|
"custom-media-queries": true,
|
||||||
|
"custom-properties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
|
.container {
|
||||||
|
padding: 2rem 2rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.formGroup {
|
.formGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
max-width: 300px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formContentSpace {
|
.formContentSpace {
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { startTransition, Suspense, useState } from "react"
|
||||||
import styles from "./auth.module.css"
|
import styles from "./auth.module.css"
|
||||||
import Link from "../../../components/link"
|
import Link from "../../../components/link"
|
||||||
import { signIn } from "next-auth/react"
|
import { signIn } from "next-auth/react"
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { Key, User } from "react-feather"
|
import { GitHub, Key, User } from "react-feather"
|
||||||
// @ts-expect-error - no types
|
|
||||||
import GitHub from "react-feather/dist/icons/github"
|
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
|
@ -34,18 +32,16 @@ function Auth({
|
||||||
const [username, setUsername] = useState("")
|
const [username, setUsername] = useState("")
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
|
|
||||||
const res = await signIn("credentials", {
|
const res = await signIn("credentials", {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
registration_password: serverPassword,
|
registration_password: serverPassword,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
// callbackUrl: "/signin",
|
// callbackUrl: "/signin",
|
||||||
signingIn: signingIn
|
signingIn
|
||||||
})
|
})
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
setToast({
|
setToast({
|
||||||
|
@ -54,7 +50,10 @@ function Auth({
|
||||||
})
|
})
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
} else {
|
} else {
|
||||||
router.refresh()
|
startTransition(() => {
|
||||||
|
router.push("/new")
|
||||||
|
router.refresh()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,10 +73,13 @@ function Auth({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ErrorQueryParamsHandler />
|
{/* Suspense boundary because useSearchParams causes static bailout */}
|
||||||
<div className={"mx-auto w-[300px]"}>
|
<Suspense fallback={null}>
|
||||||
|
<ErrorQueryParamsHandler />
|
||||||
|
</Suspense>
|
||||||
|
<div className={styles.form}>
|
||||||
<div className={styles.formContentSpace}>
|
<div className={styles.formContentSpace}>
|
||||||
<h1 className="text-3xl font-bold">Sign {signText}</h1>
|
<h1>Sign {signText}</h1>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
|
@ -94,6 +96,7 @@ function Auth({
|
||||||
onChange={handleChangeServerPassword}
|
onChange={handleChangeServerPassword}
|
||||||
placeholder="Server Password"
|
placeholder="Server Password"
|
||||||
required={true}
|
required={true}
|
||||||
|
width="100%"
|
||||||
aria-label="Server Password"
|
aria-label="Server Password"
|
||||||
/>
|
/>
|
||||||
<hr style={{ width: "100%" }} />
|
<hr style={{ width: "100%" }} />
|
||||||
|
@ -124,7 +127,7 @@ function Auth({
|
||||||
width="100%"
|
width="100%"
|
||||||
aria-label="Password"
|
aria-label="Password"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" loading={submitting}>
|
<Button width={"100%"} type="submit" loading={submitting}>
|
||||||
Sign {signText}
|
Sign {signText}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
@ -132,27 +135,25 @@ function Auth({
|
||||||
|
|
||||||
{authProviders?.length ? (
|
{authProviders?.length ? (
|
||||||
<>
|
<>
|
||||||
<hr className="w-full" />
|
|
||||||
<p className="mt-2 p-0 text-center">
|
|
||||||
Or sign {signText.toLowerCase()} with one of the following
|
|
||||||
</p>
|
|
||||||
{authProviders?.map((provider) => {
|
{authProviders?.map((provider) => {
|
||||||
return provider.enabled ? (
|
return provider.enabled ? (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
width="100%"
|
||||||
key={provider.id + "-button"}
|
key={provider.id + "-button"}
|
||||||
|
style={{
|
||||||
|
color: "var(--fg)"
|
||||||
|
}}
|
||||||
|
iconLeft={getProviderIcon(provider.id)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
signIn(provider.id, {
|
signIn(provider.id, {
|
||||||
callbackUrl: "/",
|
callbackUrl: "/",
|
||||||
registration_password: serverPassword
|
registration_password: serverPassword
|
||||||
})
|
})
|
||||||
router.refresh()
|
|
||||||
}}
|
}}
|
||||||
className="my-2 flex w-full max-w-[250px] items-center justify-center"
|
|
||||||
>
|
>
|
||||||
{getProviderIcon(provider.id)} Sign{" "}
|
Sign {signText.toLowerCase()} with {provider.public_name}
|
||||||
{signText.toLowerCase()} with {provider.public_name}
|
|
||||||
</Button>
|
</Button>
|
||||||
) : null
|
) : null
|
||||||
})}
|
})}
|
||||||
|
@ -187,10 +188,10 @@ export default Auth
|
||||||
const getProviderIcon = (provider: string) => {
|
const getProviderIcon = (provider: string) => {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case "github":
|
case "github":
|
||||||
return <GitHub className="mr-2 h-5 w-5" />
|
return <GitHub />
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
return <Key className="mr-2 h-5 w-5" />
|
return <Key />
|
||||||
default:
|
default:
|
||||||
return <User className="mr-2 h-5 w-5" />
|
return <User />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { useSearchParams } from "next/navigation"
|
import { useSearchParams } from "next/navigation"
|
||||||
import { Suspense, useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
function InnerErrorQueryParamsHandler() {
|
export function ErrorQueryParamsHandler() {
|
||||||
const queryParams = useSearchParams()
|
const queryParams = useSearchParams()
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
|
@ -19,12 +19,3 @@ function InnerErrorQueryParamsHandler() {
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorQueryParamsHandler() {
|
|
||||||
/* Suspense boundary because useSearchParams causes static bailout */
|
|
||||||
return (
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<InnerErrorQueryParamsHandler />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,17 +2,14 @@ import { getMetadata } from "src/app/lib/metadata"
|
||||||
|
|
||||||
import Auth from "../components"
|
import Auth from "../components"
|
||||||
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
|
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
|
|
||||||
export default function SignInPage() {
|
export default function SignInPage() {
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Auth
|
||||||
<Auth
|
page="signin"
|
||||||
page="signin"
|
credentialAuth={isCredentialEnabled()}
|
||||||
credentialAuth={isCredentialEnabled()}
|
authProviders={getAuthProviders()}
|
||||||
authProviders={getAuthProviders()}
|
/>
|
||||||
/>
|
|
||||||
</PageWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import Auth from "../components"
|
import Auth from "../components"
|
||||||
|
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
|
||||||
import { getMetadata } from "src/app/lib/metadata"
|
import { getMetadata } from "src/app/lib/metadata"
|
||||||
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
|
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
|
||||||
import { getRequiresPasscode } from "src/app/api/auth/requires-passcode/route"
|
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
|
|
||||||
async function getPasscode() {
|
async function getPasscode() {
|
||||||
return getRequiresPasscode()
|
return await getRequiresPasscode()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function SignUpPage() {
|
export default async function SignUpPage() {
|
||||||
const requiresPasscode = await getPasscode()
|
const requiresPasscode = await getPasscode()
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Auth
|
||||||
<Auth
|
page="signup"
|
||||||
page="signup"
|
requiresServerPassword={requiresPasscode}
|
||||||
requiresServerPassword={requiresPasscode}
|
credentialAuth={isCredentialEnabled()}
|
||||||
credentialAuth={isCredentialEnabled()}
|
authProviders={getAuthProviders()}
|
||||||
authProviders={getAuthProviders()}
|
/>
|
||||||
/>
|
|
||||||
</PageWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
.root {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list .formattingIcons {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
|
@ -23,6 +23,36 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content li {
|
||||||
|
transition: var(--transition);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contenthover,
|
||||||
|
.content li:focus {
|
||||||
|
background-color: var(--lighter-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content .listItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--dark-gray);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: var(--gap-quarter) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content li .fileIcon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: var(--gap-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content li .fileTitle {
|
||||||
|
font-size: calc(0.875 * 16px);
|
||||||
|
}
|
||||||
|
|
||||||
.content li::before {
|
.content li::before {
|
||||||
content: "";
|
content: "";
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/popover"
|
import { Popover } from "@components/popover"
|
||||||
import { codeFileExtensions } from "@lib/constants"
|
import { codeFileExtensions } from "@lib/constants"
|
||||||
|
import clsx from "clsx"
|
||||||
import type { PostWithFiles } from "src/lib/server/prisma"
|
import type { PostWithFiles } from "src/lib/server/prisma"
|
||||||
import styles from "./dropdown.module.css"
|
import styles from "./dropdown.module.css"
|
||||||
|
import buttonStyles from "@components/button/button.module.css"
|
||||||
import { ChevronDown, Code, File as FileIcon } from "react-feather"
|
import { ChevronDown, Code, File as FileIcon } from "react-feather"
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { buttonVariants } from "@components/button"
|
|
||||||
|
|
||||||
function FileDropdown({
|
function FileDropdown({
|
||||||
files,
|
files,
|
||||||
|
@ -17,15 +18,11 @@ function FileDropdown({
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<Popover.Trigger className={buttonStyles.button}>
|
||||||
className={buttonVariants({
|
|
||||||
variant: "link"
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div style={{ minWidth: 125 }}>
|
<div style={{ minWidth: 125 }}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</Popover.Trigger>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -35,26 +32,25 @@ function FileDropdown({
|
||||||
if (codeFileExtensions.includes(extension || "")) {
|
if (codeFileExtensions.includes(extension || "")) {
|
||||||
return {
|
return {
|
||||||
...file,
|
...file,
|
||||||
icon: <Code className="h-4 w-4" />
|
icon: <Code />
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...file,
|
...file,
|
||||||
icon: <FileIcon className="h-4 w-4" />
|
icon: <FileIcon />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<ul className="text-sm">
|
<ul className={styles.content}>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<li key={item.id} className="flex">
|
<li key={item.id}>
|
||||||
<Link
|
<Link href={`#${item.title}`} className={styles.listItem}>
|
||||||
href={`#${item.title}`}
|
<span className={styles.fileIcon}>{item.icon}</span>
|
||||||
className="flex w-full items-center gap-3 hover:underline"
|
<span className={styles.fileTitle}>
|
||||||
>
|
{item.title ? item.title : "Untitled"}
|
||||||
{item.icon}
|
</span>
|
||||||
{item.title ? item.title : "Untitled"}
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
@ -63,19 +59,23 @@ function FileDropdown({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<Popover.Trigger
|
||||||
className={buttonVariants({
|
className={buttonStyles.button}
|
||||||
variant: "secondary"
|
style={{ height: 40, padding: 10 }}
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<div className={styles.chevron} style={{ marginRight: 6 }}>
|
<div
|
||||||
|
className={clsx(buttonStyles.icon, styles.chevron)}
|
||||||
|
style={{ marginRight: 6 }}
|
||||||
|
>
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
Jump to {files.length} {files.length === 1 ? "file" : "files"}
|
Jump to {files.length} {files.length === 1 ? "file" : "files"}
|
||||||
</span>
|
</span>
|
||||||
</PopoverTrigger>
|
</Popover.Trigger>
|
||||||
<PopoverContent>{content}</PopoverContent>
|
<Popover.Content className={styles.contentWrapper}>
|
||||||
|
{content}
|
||||||
|
</Popover.Content>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,18 @@
|
||||||
.markdownPreview {
|
.markdownPreview {
|
||||||
padding: var(--gap-quarter);
|
padding: var(--gap-quarter);
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
color: hsl(var(--foreground));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeletonPreview {
|
.skeletonPreview {
|
||||||
padding: var(--gap-half);
|
padding: var(--gap-half);
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
.markdownPreview {
|
|
||||||
padding: var(--gap-quarter);
|
|
||||||
color: hsl(var(--foreground));
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeletonPreview {
|
|
||||||
padding: var(--gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview h1,
|
|
||||||
.markdownPreview h2,
|
|
||||||
.markdownPreview h3,
|
|
||||||
.markdownPreview h4,
|
|
||||||
.markdownPreview h5,
|
|
||||||
.markdownPreview h6 {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview h1 {
|
|
||||||
font-size: 1.775rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview h4 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview h5 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview p {
|
.markdownPreview p {
|
||||||
margin-top: 1.25rem;
|
margin-top: var(--gap);
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: var(--gap);
|
||||||
/*
|
|
||||||
&:not(:first-child) {
|
|
||||||
line-height: 1.75rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownPreview pre {
|
.markdownPreview pre {
|
||||||
|
@ -69,6 +26,31 @@
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdownPreview h1,
|
||||||
|
.markdownPreview h2,
|
||||||
|
.markdownPreview h3,
|
||||||
|
.markdownPreview h4,
|
||||||
|
.markdownPreview h5,
|
||||||
|
.markdownPreview h6 {
|
||||||
|
margin-top: var(--gap);
|
||||||
|
margin-bottom: var(--gap-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview h1 {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview h2 {
|
||||||
|
color: var(--darkest-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview h3,
|
||||||
|
.markdownPreview h4,
|
||||||
|
.markdownPreview h5,
|
||||||
|
.markdownPreview h6 {
|
||||||
|
color: var(--darker-gray);
|
||||||
|
}
|
||||||
|
|
||||||
/* Auto-linked headers */
|
/* Auto-linked headers */
|
||||||
.markdownPreview h1 a,
|
.markdownPreview h1 a,
|
||||||
.markdownPreview h2 a,
|
.markdownPreview h2 a,
|
||||||
|
@ -93,49 +75,44 @@
|
||||||
filter: opacity(0.5);
|
filter: opacity(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownPreview ul {
|
.markdownPreview h1 {
|
||||||
margin-bottom: 1.5rem;
|
font-size: 2rem;
|
||||||
margin-left: 1.5rem;
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownPreview ol {
|
.markdownPreview h2 {
|
||||||
margin-bottom: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin-left: 1.5rem;
|
}
|
||||||
list-style-type: decimal;
|
|
||||||
|
.markdownPreview h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview h5 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview h6 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdownPreview ul {
|
||||||
|
list-style: inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownPreview ul li::before {
|
.markdownPreview ul li::before {
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.markdownPreview code::before,
|
.markdownPreview code::before,
|
||||||
.markdownPreview code::after {
|
.markdownPreview code::after {
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownPreview blockquote {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
font-style: italic;
|
|
||||||
border-left-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview blockquote p {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview table {
|
|
||||||
overflow-y: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdownPreview table th {
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border-top-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
.markdownPreview h1 a::after,
|
.markdownPreview h1 a::after,
|
||||||
.markdownPreview h2 a::after,
|
.markdownPreview h2 a::after,
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import * as RadixTabs from "@radix-ui/react-tabs"
|
||||||
import FormattingIcons from "src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
|
import FormattingIcons from "src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
|
||||||
import {
|
import { ChangeEvent, ClipboardEvent, useRef } from "react"
|
||||||
ChangeEvent,
|
|
||||||
ClipboardEvent,
|
|
||||||
ComponentProps,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from "react"
|
|
||||||
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
|
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||||
import Preview, { StaticPreview } from "../preview"
|
import Preview, { StaticPreview } from "../preview"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/tabs"
|
import styles from "./tabs.module.css"
|
||||||
import { Textarea } from "@components/textarea"
|
|
||||||
|
|
||||||
type Props = ComponentProps<typeof Tabs> & {
|
type Props = RadixTabs.TabsProps & {
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
defaultTab: "preview" | "edit"
|
defaultTab: "preview" | "edit"
|
||||||
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||||
|
@ -34,33 +28,37 @@ export default function DocumentTabs({
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const codeEditorRef = useRef<TextareaMarkdownRef>(null)
|
const codeEditorRef = useRef<TextareaMarkdownRef>(null)
|
||||||
const [activeTab, setActiveTab] = useState<"preview" | "edit">(defaultTab)
|
|
||||||
const handleTabChange = (newTab: string) => {
|
const handleTabChange = (newTab: string) => {
|
||||||
if (newTab === "preview") {
|
if (newTab === "preview") {
|
||||||
codeEditorRef.current?.focus()
|
codeEditorRef.current?.focus()
|
||||||
}
|
}
|
||||||
setActiveTab(newTab as "preview" | "edit")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs {...props} onValueChange={handleTabChange} defaultValue={defaultTab}>
|
<RadixTabs.Root
|
||||||
<TabsList className="flex flex-col items-start justify-start sm:flex-row sm:items-center sm:justify-between">
|
{...props}
|
||||||
<div>
|
onValueChange={handleTabChange}
|
||||||
<TabsTrigger value="edit">{isEditing ? "Edit" : "Raw"}</TabsTrigger>
|
className={styles.root}
|
||||||
<TabsTrigger value="preview">
|
defaultValue={defaultTab}
|
||||||
|
>
|
||||||
|
<RadixTabs.List className={styles.listWrapper}>
|
||||||
|
<div className={styles.list}>
|
||||||
|
<RadixTabs.Trigger value="edit" className={styles.trigger}>
|
||||||
|
{isEditing ? "Edit" : "Raw"}
|
||||||
|
</RadixTabs.Trigger>
|
||||||
|
<RadixTabs.Trigger value="preview" className={styles.trigger}>
|
||||||
{isEditing ? "Preview" : "Rendered"}
|
{isEditing ? "Preview" : "Rendered"}
|
||||||
</TabsTrigger>
|
</RadixTabs.Trigger>
|
||||||
</div>
|
</div>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<FormattingIcons
|
<FormattingIcons
|
||||||
|
className={styles.formattingIcons}
|
||||||
textareaRef={codeEditorRef}
|
textareaRef={codeEditorRef}
|
||||||
className={`ml-auto ${
|
|
||||||
activeTab === "preview" ? "hidden" : "hidden sm:block"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TabsList>
|
</RadixTabs.List>
|
||||||
<TabsContent value="edit">
|
<RadixTabs.Content value="edit">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
|
@ -68,16 +66,8 @@ export default function DocumentTabs({
|
||||||
flexDirection: "column"
|
flexDirection: "column"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormattingIcons
|
|
||||||
textareaRef={codeEditorRef}
|
|
||||||
className={`ml-auto ${
|
|
||||||
activeTab === "preview"
|
|
||||||
? "hidden"
|
|
||||||
: "block text-muted-foreground sm:hidden"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<TextareaMarkdown.Wrapper ref={codeEditorRef}>
|
<TextareaMarkdown.Wrapper ref={codeEditorRef}>
|
||||||
<Textarea
|
<textarea
|
||||||
readOnly={!isEditing}
|
readOnly={!isEditing}
|
||||||
onPaste={onPaste ? onPaste : undefined}
|
onPaste={onPaste ? onPaste : undefined}
|
||||||
ref={codeEditorRef}
|
ref={codeEditorRef}
|
||||||
|
@ -90,8 +80,8 @@ export default function DocumentTabs({
|
||||||
/>
|
/>
|
||||||
</TextareaMarkdown.Wrapper>
|
</TextareaMarkdown.Wrapper>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</RadixTabs.Content>
|
||||||
<TabsContent value="preview">
|
<RadixTabs.Content value="preview">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Preview height={"100%"} title={title}>
|
<Preview height={"100%"} title={title}>
|
||||||
{rawContent}
|
{rawContent}
|
||||||
|
@ -99,7 +89,7 @@ export default function DocumentTabs({
|
||||||
) : (
|
) : (
|
||||||
<StaticPreview height={"100%"}>{preview || ""}</StaticPreview>
|
<StaticPreview height={"100%"}>{preview || ""}</StaticPreview>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</RadixTabs.Content>
|
||||||
</Tabs>
|
</RadixTabs.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
60
src/app/(drift)/(posts)/components/tabs/tabs.module.css
Normal file
60
src/app/(drift)/(posts)/components/tabs/tabs.module.css
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .formattingIcons {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 4px 0;
|
||||||
|
font-family: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.1s ease;
|
||||||
|
margin-bottom: var(--gap-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger:hover {
|
||||||
|
background-color: var(--lighter-gray);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger:last-child {
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger[data-state="active"] {
|
||||||
|
color: var(--darkest-gray);
|
||||||
|
box-shadow: inset 0 -1px 0 0 currentColor, 0 1px 0 0 currentColor;
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
import { ChangeEvent } from "react"
|
import { ChangeEvent } from "react"
|
||||||
|
|
||||||
import styles from "../post.module.css"
|
import styles from "../post.module.css"
|
||||||
import clsx from "clsx"
|
|
||||||
|
|
||||||
type props = {
|
type props = {
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||||
|
@ -11,7 +10,7 @@ type props = {
|
||||||
|
|
||||||
function Description({ onChange, description }: props) {
|
function Description({ onChange, description }: props) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.description, "pb-4")}>
|
<div className={styles.description}>
|
||||||
<Input
|
<Input
|
||||||
value={description || ""}
|
value={description || ""}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
|
@ -8,6 +8,18 @@
|
||||||
margin-top: var(--gap-double);
|
margin-top: var(--gap-double);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropzone {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.dropzone:focus {
|
.dropzone:focus {
|
||||||
border-color: var(--gray);
|
border-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,11 +82,12 @@ function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div {...getRootProps()}>
|
<div {...getRootProps()} className={styles.dropzone}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
{!isDragActive && (
|
{!isDragActive && (
|
||||||
<p className="cursor-pointer select-none rounded-md border-2 border-dashed p-4 text-sm text-muted-foreground">
|
<p style={{ color: "var(--gray)" }}>
|
||||||
Drag and drop files here, or click to select
|
Drag some files here, or <span className={styles.verb} /> to select
|
||||||
|
files
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{isDragActive && <p>Release to drop the files here</p>}
|
{isDragActive && <p>Release to drop the files here</p>}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.card {
|
.card {
|
||||||
padding: var(--gap);
|
padding: var(--gap);
|
||||||
|
border: 1px solid var(--lighter-gray);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,9 @@ import {
|
||||||
import { RefObject, useMemo } from "react"
|
import { RefObject, useMemo } from "react"
|
||||||
import styles from "./formatting-icons.module.css"
|
import styles from "./formatting-icons.module.css"
|
||||||
import { TextareaMarkdownRef } from "textarea-markdown-editor"
|
import { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||||
import { Tooltip } from "@components/tooltip"
|
import Tooltip from "@components/tooltip"
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import React from "react"
|
|
||||||
// TODO: clean up
|
// TODO: clean up
|
||||||
|
|
||||||
function FormattingIcons({
|
function FormattingIcons({
|
||||||
|
@ -70,18 +69,22 @@ function FormattingIcons({
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={name[0].toUpperCase() + name.slice(1).replace("-", " ")}
|
content={name[0].toUpperCase() + name.slice(1).replace("-", " ")}
|
||||||
key={name}
|
key={name}
|
||||||
delayDuration={100}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
height={32}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
borderRight: "none",
|
||||||
|
borderLeft: "none",
|
||||||
|
borderTop: "none",
|
||||||
|
borderBottom: "none"
|
||||||
|
}}
|
||||||
aria-label={name}
|
aria-label={name}
|
||||||
|
iconRight={icon}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={action}
|
onClick={action}
|
||||||
variant="ghost"
|
buttonType="secondary"
|
||||||
>
|
/>
|
||||||
{React.cloneElement(icon, {
|
|
||||||
className: "h-4 w-4"
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { ChangeEvent, ClipboardEvent, useCallback } from "react"
|
import { ChangeEvent, ClipboardEvent, useCallback } from "react"
|
||||||
import styles from "./document.module.css"
|
import styles from "./document.module.css"
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
|
import DocumentTabs from "src/app/(drift)/(posts)/components/tabs"
|
||||||
import { Trash } from "react-feather"
|
import { Trash } from "react-feather"
|
||||||
import { Card, CardContent, CardHeader } from "@components/card"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
|
@ -50,8 +49,8 @@ function Document({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="min-h-[512px]">
|
<>
|
||||||
<CardHeader>
|
<div className={styles.card}>
|
||||||
<div className={styles.fileNameContainer}>
|
<div className={styles.fileNameContainer}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="MyFile.md"
|
placeholder="MyFile.md"
|
||||||
|
@ -66,31 +65,35 @@ function Document({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{remove && (
|
{remove && (
|
||||||
// no left border
|
|
||||||
<Button
|
<Button
|
||||||
|
iconLeft={<Trash />}
|
||||||
|
height={"39px"}
|
||||||
|
width={"48px"}
|
||||||
|
padding={0}
|
||||||
|
margin={0}
|
||||||
onClick={() => removeFile(remove)}
|
onClick={() => removeFile(remove)}
|
||||||
variant="outline"
|
style={{
|
||||||
className="border-color-[var(--border)] rounded-l-none border-l-0"
|
borderTopLeftRadius: 0,
|
||||||
>
|
borderBottomLeftRadius: 0
|
||||||
<Trash height={18} />
|
}}
|
||||||
</Button>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
<div className={styles.documentContainer}>
|
||||||
<CardContent>
|
<DocumentTabs
|
||||||
<DocumentTabs
|
isEditing={true}
|
||||||
isEditing={true}
|
defaultTab={defaultTab}
|
||||||
defaultTab={defaultTab}
|
handleOnContentChange={handleOnContentChange}
|
||||||
handleOnContentChange={handleOnContentChange}
|
// TODO: solve types
|
||||||
// TODO: solve types
|
// @ts-expect-error Type 'HTMLDivElement' is missing the following properties from type 'HTMLTextAreaElement': autocomplete, cols, defaultValue, dirName, and 26 more
|
||||||
// @ts-expect-error Type 'HTMLDivElement' is missing the following properties from type 'HTMLTextAreaElement': autocomplete, cols, defaultValue, dirName, and 26 more
|
onPaste={onPaste}
|
||||||
onPaste={onPaste}
|
title={title}
|
||||||
title={title}
|
>
|
||||||
>
|
{content}
|
||||||
{content}
|
</DocumentTabs>
|
||||||
</DocumentTabs>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,42 +3,23 @@
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useCallback, useState, ClipboardEvent } from "react"
|
import { useCallback, useState, ClipboardEvent } from "react"
|
||||||
import generateUUID from "@lib/generate-uuid"
|
import generateUUID from "@lib/generate-uuid"
|
||||||
|
import styles from "./post.module.css"
|
||||||
import EditDocumentList from "./edit-document-list"
|
import EditDocumentList from "./edit-document-list"
|
||||||
import { ChangeEvent } from "react"
|
import { ChangeEvent } from "react"
|
||||||
import getTitleForPostCopy from "src/app/lib/get-title-for-post-copy"
|
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 { PostWithFiles } from "@lib/server/prisma"
|
||||||
import PasswordModal from "../../../../components/password-modal"
|
import PasswordModal from "../../../../components/password-modal"
|
||||||
import Title from "./title"
|
import Title from "./title"
|
||||||
import FileDropzone from "./drag-and-drop"
|
import FileDropzone from "./drag-and-drop"
|
||||||
import { Button, buttonVariants } from "@components/button"
|
import Button from "@components/button"
|
||||||
|
import Input from "@components/input"
|
||||||
|
import ButtonDropdown from "@components/button-dropdown"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
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 dynamic from "next/dynamic"
|
||||||
() => import("@components/date-picker").then((m) => m.DatePicker),
|
const DatePicker = dynamic(() => import("react-datepicker"))
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<Button
|
|
||||||
variant={"outline"}
|
|
||||||
className={cn(
|
|
||||||
"w-[280px] justify-start text-left font-normal",
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="w-4 h-4 mr-2" />
|
|
||||||
<span>Won't expire</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const emptyDoc = {
|
const emptyDoc = {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -64,9 +45,7 @@ function Post({
|
||||||
const [title, setTitle] = useState(
|
const [title, setTitle] = useState(
|
||||||
getTitleForPostCopy(initialPost?.title) || ""
|
getTitleForPostCopy(initialPost?.title) || ""
|
||||||
)
|
)
|
||||||
const [description /*, setDescription */] = useState(
|
const [description, setDescription] = useState(initialPost?.description || "")
|
||||||
initialPost?.description || ""
|
|
||||||
)
|
|
||||||
const [expiresAt, setExpiresAt] = useState<Date>()
|
const [expiresAt, setExpiresAt] = useState<Date>()
|
||||||
|
|
||||||
const defaultDocs: Document[] = initialPost
|
const defaultDocs: Document[] = initialPost
|
||||||
|
@ -149,7 +128,7 @@ function Post({
|
||||||
|
|
||||||
if (!docs.length) {
|
if (!docs.length) {
|
||||||
setToast({
|
setToast({
|
||||||
message: "Please add at least one file",
|
message: "Please add at least one document",
|
||||||
type: "error"
|
type: "error"
|
||||||
})
|
})
|
||||||
hasErrored = true
|
hasErrored = true
|
||||||
|
@ -188,13 +167,13 @@ function Post({
|
||||||
setTitle(e.target.value)
|
setTitle(e.target.value)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// const onChangeDescription = useCallback(
|
const onChangeDescription = useCallback(
|
||||||
// (e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
// e.preventDefault()
|
e.preventDefault()
|
||||||
// setDescription(e.target.value)
|
setDescription(e.target.value)
|
||||||
// },
|
},
|
||||||
// []
|
[]
|
||||||
// )
|
)
|
||||||
|
|
||||||
function onClosePasswordModal() {
|
function onClosePasswordModal() {
|
||||||
setPasswordModalVisible(false)
|
setPasswordModalVisible(false)
|
||||||
|
@ -205,6 +184,10 @@ function Post({
|
||||||
return onSubmit("protected", password)
|
return onSubmit("protected", password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangeExpiration(date: Date) {
|
||||||
|
return setExpiresAt(date)
|
||||||
|
}
|
||||||
|
|
||||||
function updateDocTitle(i: number) {
|
function updateDocTitle(i: number) {
|
||||||
return (title: string) => {
|
return (title: string) => {
|
||||||
setDocs((docs) =>
|
setDocs((docs) =>
|
||||||
|
@ -255,9 +238,10 @@ function Post({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 gap-4">
|
<div className={styles.root}>
|
||||||
<Title title={title} onChange={onChangeTitle} className="py-4" />
|
<Title title={title} onChange={onChangeTitle} />
|
||||||
{/* <Description description={description} onChange={onChangeDescription} /> */}
|
<Description description={description} onChange={onChangeDescription} />
|
||||||
|
<FileDropzone setDocs={uploadDocs} />
|
||||||
<EditDocumentList
|
<EditDocumentList
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
docs={docs}
|
docs={docs}
|
||||||
|
@ -265,60 +249,68 @@ function Post({
|
||||||
updateDocContent={updateDocContent}
|
updateDocContent={updateDocContent}
|
||||||
removeDoc={removeDoc}
|
removeDoc={removeDoc}
|
||||||
/>
|
/>
|
||||||
<FileDropzone setDocs={uploadDocs} />
|
<div className={styles.buttons}>
|
||||||
|
<Button
|
||||||
<div className="flex flex-col items-end justify-between gap-4 mt-4 sm:flex-row sm:items-center">
|
onClick={() => {
|
||||||
<span className="flex flex-1 gap-2">
|
setDocs([
|
||||||
<Button
|
...docs,
|
||||||
onClick={() => {
|
{
|
||||||
setDocs([
|
title: "",
|
||||||
...docs,
|
content: "",
|
||||||
{
|
id: generateUUID()
|
||||||
title: "",
|
}
|
||||||
content: "",
|
])
|
||||||
id: generateUUID()
|
}}
|
||||||
}
|
style={{
|
||||||
])
|
flex: 1,
|
||||||
}}
|
minWidth: 120
|
||||||
className="min-w-[120px] max-w-[200px] flex-1"
|
}}
|
||||||
variant={"secondary"}
|
>
|
||||||
>
|
Add a File
|
||||||
Add a File
|
</Button>
|
||||||
</Button>
|
<div className={styles.rightButtons}>
|
||||||
<DatePicker setExpiresAt={setExpiresAt} expiresAt={expiresAt} />
|
<DatePicker
|
||||||
</span>
|
onChange={onChangeExpiration}
|
||||||
<ButtonDropdown>
|
customInput={
|
||||||
<span
|
<Input label="Expires at" width="100%" height="40px" />
|
||||||
className={clsx(
|
}
|
||||||
"w-full cursor-pointer rounded-br-none rounded-tr-none",
|
placeholderText="Won't expire"
|
||||||
buttonVariants({
|
selected={expiresAt}
|
||||||
variant: "default"
|
showTimeInput={true}
|
||||||
})
|
// @ts-expect-error fix time input type
|
||||||
)}
|
customTimeInput={<CustomTimeInput />}
|
||||||
onClick={() => onSubmit("unlisted")}
|
timeInputLabel="Time:"
|
||||||
>
|
dateFormat="MM/dd/yyyy h:mm aa"
|
||||||
{isSubmitting ? <Spinner className="mr-2" /> : null}
|
className={styles.datePicker}
|
||||||
Create Unlisted
|
clearButtonTitle={"Clear"}
|
||||||
</span>
|
// TODO: investigate why this causes margin shift if true
|
||||||
<span
|
enableTabLoop={false}
|
||||||
className={clsx("w-full cursor-pointer")}
|
minDate={new Date()}
|
||||||
onClick={() => onSubmit("private")}
|
/>
|
||||||
>
|
<ButtonDropdown>
|
||||||
Create Private
|
<Button
|
||||||
</span>
|
height={40}
|
||||||
<span
|
width={251}
|
||||||
className={clsx("w-full cursor-pointer")}
|
onClick={() => onSubmit("unlisted")}
|
||||||
onClick={() => onSubmit("public")}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
Create Public
|
Create Unlisted
|
||||||
</span>
|
</Button>
|
||||||
<span
|
<Button height={40} width={300} onClick={() => onSubmit("private")}>
|
||||||
className={clsx("w-full cursor-pointer")}
|
Create Private
|
||||||
onClick={() => onSubmit("protected")}
|
</Button>
|
||||||
>
|
<Button height={40} width={300} onClick={() => onSubmit("public")}>
|
||||||
Create with Password
|
Create Public
|
||||||
</span>
|
</Button>
|
||||||
</ButtonDropdown>
|
<Button
|
||||||
|
height={40}
|
||||||
|
width={300}
|
||||||
|
onClick={() => onSubmit("protected")}
|
||||||
|
>
|
||||||
|
Create with Password
|
||||||
|
</Button>
|
||||||
|
</ButtonDropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PasswordModal
|
<PasswordModal
|
||||||
creating={true}
|
creating={true}
|
||||||
|
@ -332,30 +324,30 @@ function Post({
|
||||||
|
|
||||||
export default Post
|
export default Post
|
||||||
|
|
||||||
// function CustomTimeInput({
|
function CustomTimeInput({
|
||||||
// date,
|
date,
|
||||||
// value,
|
value,
|
||||||
// onChange
|
onChange
|
||||||
// }: {
|
}: {
|
||||||
// date: Date
|
date: Date
|
||||||
// value: string
|
value: string
|
||||||
// onChange: (date: string) => void
|
onChange: (date: string) => void
|
||||||
// }) {
|
}) {
|
||||||
// return (
|
return (
|
||||||
// <input
|
<input
|
||||||
// type="time"
|
type="time"
|
||||||
// value={value}
|
value={value}
|
||||||
// onChange={(e) => {
|
onChange={(e) => {
|
||||||
// if (!isNaN(date.getTime())) {
|
if (!isNaN(date.getTime())) {
|
||||||
// onChange(e.target.value || date.toISOString().slice(11, 16))
|
onChange(e.target.value || date.toISOString().slice(11, 16))
|
||||||
// }
|
}
|
||||||
// }}
|
}}
|
||||||
// style={{
|
style={{
|
||||||
// backgroundColor: "var(--bg)",
|
backgroundColor: "var(--bg)",
|
||||||
// border: "1px solid var(--light-gray)",
|
border: "1px solid var(--light-gray)",
|
||||||
// borderRadius: "var(--radius)"
|
borderRadius: "var(--radius)"
|
||||||
// }}
|
}}
|
||||||
// required
|
required
|
||||||
// />
|
/>
|
||||||
// )
|
)
|
||||||
// }
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
padding-bottom: 200px;
|
padding-bottom: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-half);
|
gap: var(--gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
|
@ -19,6 +19,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.datePicker {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ChangeEvent, memo } from "react"
|
import { ChangeEvent, memo } from "react"
|
||||||
|
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
|
import styles from "./title.module.css"
|
||||||
|
|
||||||
const titlePlaceholders = [
|
const titlePlaceholders = [
|
||||||
"How to...",
|
"How to...",
|
||||||
|
@ -17,17 +18,20 @@ const placeholder = titlePlaceholders[3]
|
||||||
type props = {
|
type props = {
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||||
title?: string
|
title?: string
|
||||||
className?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Title({ onChange, title, className }: props) {
|
function Title({ onChange, title }: props) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={styles.title}>
|
||||||
|
<h1 style={{ margin: 0, padding: 0 }}>Drift</h1>
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label="Title"
|
label="Title"
|
||||||
|
className={styles.labelAndInput}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
labelClassName={styles.labelAndInput}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
import { getMetadata } from "src/app/lib/metadata"
|
import { getMetadata } from "src/app/lib/metadata"
|
||||||
import NewPost from "src/app/(drift)/(posts)/new/components/new"
|
import NewPost from "src/app/(drift)/(posts)/new/components/new"
|
||||||
import { PageTitle } from "@components/page-title"
|
import "./react-datepicker.css"
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
|
|
||||||
export default function New() {
|
export default function New() {
|
||||||
return (
|
return <NewPost />
|
||||||
<>
|
|
||||||
<PageTitle>New Post</PageTitle>
|
|
||||||
<PageWrapper>
|
|
||||||
<NewPost />
|
|
||||||
</PageWrapper>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = getMetadata({
|
export const metadata = getMetadata({
|
||||||
|
|
372
src/app/(drift)/(posts)/new/react-datepicker.css
Normal file
372
src/app/(drift)/(posts)/new/react-datepicker.css
Normal file
|
@ -0,0 +1,372 @@
|
||||||
|
.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;
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import ButtonGroup from "@components/button-group"
|
import ButtonGroup from "@components/button-group"
|
||||||
import FileDropdown from "src/app/(drift)/(posts)/components/file-dropdown"
|
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 styles from "./post-buttons.module.css"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { PostWithFiles } from "@lib/server/prisma"
|
import { PostWithFiles } from "@lib/server/prisma"
|
||||||
|
@ -47,19 +48,15 @@ export const PostButtons = ({
|
||||||
return (
|
return (
|
||||||
<span className={styles.buttons}>
|
<span className={styles.buttons}>
|
||||||
<ButtonGroup verticalIfMobile>
|
<ButtonGroup verticalIfMobile>
|
||||||
<Button variant={"secondary"} onClick={editACopy} className="border-r">
|
<Button iconLeft={<Edit />} onClick={editACopy}>
|
||||||
Edit a Copy
|
Edit a Copy
|
||||||
</Button>
|
</Button>
|
||||||
{parentId && (
|
{parentId && (
|
||||||
<Button variant={"secondary"} onClick={viewParentClick}>
|
<Button iconLeft={<ArrowUpCircle />} onClick={viewParentClick}>
|
||||||
View Parent
|
View Parent
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button onClick={downloadClick} iconLeft={<Archive />}>
|
||||||
variant={"secondary"}
|
|
||||||
onClick={downloadClick}
|
|
||||||
className="border-r"
|
|
||||||
>
|
|
||||||
Download as ZIP Archive
|
Download as ZIP Archive
|
||||||
</Button>
|
</Button>
|
||||||
<FileDropdown loading={loading} files={files || []} />
|
<FileDropdown loading={loading} files={files || []} />
|
||||||
|
|
|
@ -17,9 +17,13 @@ export const PostTitle = ({ post, loading }: TitleProps) => {
|
||||||
const displayName = author?.displayName
|
const displayName = author?.displayName
|
||||||
return (
|
return (
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
<h1 className="text-3xl font-bold">
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: "1.175rem"
|
||||||
|
}}
|
||||||
|
>
|
||||||
{title}{" "}
|
{title}{" "}
|
||||||
<span className="text-2xl text-muted-foreground">
|
<span style={{ color: "var(--gray)" }}>
|
||||||
by {/* <Link colored href={`/author/${authorId}`}> */}
|
by {/* <Link colored href={`/author/${authorId}`}> */}
|
||||||
{displayName || "anonymous"}
|
{displayName || "anonymous"}
|
||||||
{/* </Link> */}
|
{/* </Link> */}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* .card header {
|
.card header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
border: 1px solid var(--lighter-gray);
|
border: 1px solid var(--lighter-gray);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0px 0px 8px 8px;
|
border-radius: 0px 0px 8px 8px;
|
||||||
} */
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import ButtonGroup from "@components/button-group"
|
import ButtonGroup from "@components/button-group"
|
||||||
import Skeleton from "@components/skeleton"
|
import Skeleton from "@components/skeleton"
|
||||||
import { Tooltip } from "@components/tooltip"
|
import Tooltip from "@components/tooltip"
|
||||||
import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
|
import DocumentTabs from "src/app/(drift)/(posts)/components/tabs"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { Download, ExternalLink, Globe } from "react-feather"
|
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 { getURLFriendlyTitle } from "src/app/lib/get-url-friendly-title"
|
||||||
import { PostWithFiles, ServerPost } from "@lib/server/prisma"
|
import { PostWithFiles, ServerPost } from "@lib/server/prisma"
|
||||||
import { isAllowedVisibilityForWebpage } from "@lib/constants"
|
import { isAllowedVisibilityForWebpage } from "@lib/constants"
|
||||||
import { Card, CardContent, CardHeader } from "@components/card"
|
|
||||||
type SharedProps = {
|
type SharedProps = {
|
||||||
initialTab: "edit" | "preview"
|
initialTab: "edit" | "preview"
|
||||||
file?: PostWithFiles["files"][0]
|
file?: PostWithFiles["files"][0]
|
||||||
|
@ -36,50 +36,38 @@ const DownloadButtons = ({
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Tooltip content="Download" delayDuration={200}>
|
<Tooltip content="Download">
|
||||||
<Link
|
<Link
|
||||||
href={`${rawLink}?download=true`}
|
href={`${rawLink}?download=true`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
iconRight={<Download color="var(--fg)" />}
|
||||||
aria-label="Download"
|
aria-label="Download"
|
||||||
size="sm"
|
style={{ border: "none", background: "transparent" }}
|
||||||
className="bg-transparent border-none"
|
/>
|
||||||
variant={"ghost"}
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 " />
|
|
||||||
<span className="sr-only">Download</span>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{rawLink ? (
|
{rawLink ? (
|
||||||
<Tooltip content="Open raw in new tab" delayDuration={200}>
|
<Tooltip content="Open raw in new tab">
|
||||||
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
|
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
|
||||||
<Button
|
<Button
|
||||||
|
iconLeft={<ExternalLink color="var(--fg)" />}
|
||||||
aria-label="Open raw file in new tab"
|
aria-label="Open raw file in new tab"
|
||||||
className="bg-transparent border-none"
|
style={{ border: "none", background: "transparent" }}
|
||||||
size="sm"
|
/>
|
||||||
variant={"ghost"}
|
|
||||||
>
|
|
||||||
<ExternalLink className="w-4 h-4" />
|
|
||||||
<span className="sr-only">Open raw file in new tab</span>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
{siteLink ? (
|
{siteLink ? (
|
||||||
<Tooltip content="Open as webpage" delayDuration={200}>
|
<Tooltip content="Open as webpage">
|
||||||
<Link href={siteLink || ""} target="_blank" rel="noopener noreferrer">
|
<Link href={siteLink || ""} target="_blank" rel="noopener noreferrer">
|
||||||
<Button
|
<Button
|
||||||
|
iconLeft={<Globe color="var(--fg)" />}
|
||||||
aria-label="Open as webpage"
|
aria-label="Open as webpage"
|
||||||
className="bg-transparent border-none"
|
style={{ border: "none", background: "transparent" }}
|
||||||
size="sm"
|
/>
|
||||||
variant={"ghost"}
|
|
||||||
>
|
|
||||||
<Globe className="w-4 h-4" />
|
|
||||||
<span className="sr-only">Open as webpage</span>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -121,39 +109,18 @@ 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="border-gray-200 dark:border-gray-900">
|
<div className={styles.card}>
|
||||||
<CardHeader
|
<header id={file?.title}>
|
||||||
id={file?.title}
|
|
||||||
className="flex flex-row items-center justify-between py-1 bg-gray-200 dark:bg-gray-900"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={`#${file?.title}`}
|
href={`#${file?.title}`}
|
||||||
aria-label="File"
|
aria-label="File"
|
||||||
// show an # when hovered avia :after
|
style={{
|
||||||
className="text-gray-900 hover:after:ml-1 hover:after:content-[#] dark:text-gray-100"
|
textDecoration: "none",
|
||||||
|
color: "var(--fg)"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{file?.title}
|
{file?.title}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -167,8 +134,8 @@ const Document = ({ skeleton, ...props }: Props) => {
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</CardHeader>
|
</header>
|
||||||
<CardContent className="flex flex-col h-full pt-2">
|
<div className={styles.documentContainer}>
|
||||||
<DocumentTabs
|
<DocumentTabs
|
||||||
defaultTab={props.initialTab}
|
defaultTab={props.initialTab}
|
||||||
staticPreview={file?.html}
|
staticPreview={file?.html}
|
||||||
|
@ -176,8 +143,8 @@ const Document = ({ skeleton, ...props }: Props) => {
|
||||||
>
|
>
|
||||||
{file?.content || ""}
|
{file?.content || ""}
|
||||||
</DocumentTabs>
|
</DocumentTabs>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,11 @@ export default async function PostLayout({
|
||||||
{post.visibility !== "protected" && <PostButtons post={clientPost} />}
|
{post.visibility !== "protected" && <PostButtons post={clientPost} />}
|
||||||
{post.visibility !== "protected" && <PostTitle post={clientPost} />}
|
{post.visibility !== "protected" && <PostTitle post={clientPost} />}
|
||||||
</div>
|
</div>
|
||||||
{/* {post.description && <p className="pb-4 text-lg">{post.description}</p>} */}
|
{post.description && (
|
||||||
|
<div>
|
||||||
|
<p>{post.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,13 +21,11 @@ export default async function PostPage({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostFiles post={clientPost} />
|
<PostFiles post={clientPost} />
|
||||||
<div className="mx-auto mb-4 mt-4">
|
<VisibilityControl
|
||||||
<VisibilityControl
|
authorId={post.authorId}
|
||||||
authorId={post.authorId}
|
postId={post.id}
|
||||||
postId={post.id}
|
visibility={post.visibility}
|
||||||
visibility={post.visibility}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,13 @@
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.id {
|
.id {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { ServerPostWithFilesAndAuthor, UserWithPosts } from "@lib/server/prisma"
|
import { ServerPostWithFilesAndAuthor, UserWithPosts } from "@lib/server/prisma"
|
||||||
|
@ -18,7 +18,7 @@ export function UserTable({
|
||||||
id: string
|
id: string
|
||||||
email: string | null
|
email: string | null
|
||||||
role: string | null
|
role: string | null
|
||||||
username: string | null
|
displayName: string | null
|
||||||
}[]
|
}[]
|
||||||
}) {
|
}) {
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
@ -26,14 +26,6 @@ export function UserTable({
|
||||||
|
|
||||||
const deleteUser = async (id: string) => {
|
const deleteUser = async (id: string) => {
|
||||||
try {
|
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", {
|
const res = await fetchWithUser("/api/admin?action=delete-user", {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -61,8 +53,8 @@ export function UserTable({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="w-full overflow-x-auto">
|
<table className={styles.table}>
|
||||||
<thead className="text-left">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
|
@ -81,20 +73,14 @@ export function UserTable({
|
||||||
) : null}
|
) : null}
|
||||||
{users?.map((user) => (
|
{users?.map((user) => (
|
||||||
<tr key={user.id}>
|
<tr key={user.id}>
|
||||||
<td>{user.username ? user.username : "no name"}</td>
|
<td>{user.displayName ? user.displayName : "no name"}</td>
|
||||||
<td>{user.email}</td>
|
<td>{user.email}</td>
|
||||||
<td>{user.role}</td>
|
<td>{user.role}</td>
|
||||||
<td className={styles.id} title={user.id}>
|
<td className={styles.id} title={user.id}>
|
||||||
{user.id}
|
{user.id}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Button
|
<Button onClick={() => deleteUser(user.id)}>Delete</Button>
|
||||||
variant={"destructive"}
|
|
||||||
onClick={() => deleteUser(user.id)}
|
|
||||||
size={"sm"}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
@ -104,7 +90,7 @@ export function UserTable({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PostTable({
|
export function PostTable({
|
||||||
posts: initialPosts
|
posts
|
||||||
}: {
|
}: {
|
||||||
posts?: {
|
posts?: {
|
||||||
createdAt: string
|
createdAt: string
|
||||||
|
@ -114,54 +100,15 @@ export function PostTable({
|
||||||
visibility: string
|
visibility: string
|
||||||
}[]
|
}[]
|
||||||
}) {
|
}) {
|
||||||
const [posts, setPosts] = useState<typeof initialPosts>(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 (
|
return (
|
||||||
<table className="w-full overflow-x-auto">
|
<table className={styles.table}>
|
||||||
<thead className="text-left">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Author</th>
|
<th>Author</th>
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th>Visibility</th>
|
<th>Visibility</th>
|
||||||
<th className={styles.id}>Post ID</th>
|
<th className={styles.id}>Post ID</th>
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -183,15 +130,6 @@ export function PostTable({
|
||||||
<td>{new Date(post.createdAt).toLocaleDateString()}</td>
|
<td>{new Date(post.createdAt).toLocaleDateString()}</td>
|
||||||
<td>{post.visibility}</td>
|
<td>{post.visibility}</td>
|
||||||
<td>{post.id}</td>
|
<td>{post.id}</td>
|
||||||
<td>
|
|
||||||
<Button
|
|
||||||
variant={"destructive"}
|
|
||||||
size={"sm"}
|
|
||||||
onClick={() => deletePost(post.id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { TypographyH1, TypographyH2 } from "@components/typography"
|
|
||||||
import { PostTable, UserTable } from "./components/tables"
|
import { PostTable, UserTable } from "./components/tables"
|
||||||
|
|
||||||
export default function AdminLoading() {
|
export default function AdminLoading() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TypographyH1>Admin</TypographyH1>
|
<h1>Admin</h1>
|
||||||
<TypographyH2>Users</TypographyH2>
|
<h2>Users</h2>
|
||||||
<UserTable />
|
<UserTable />
|
||||||
<TypographyH2>Posts</TypographyH2>
|
<h2>Posts</h2>
|
||||||
<PostTable />
|
<PostTable />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,20 +6,15 @@ import {
|
||||||
ServerPostWithFiles
|
ServerPostWithFiles
|
||||||
} from "@lib/server/prisma"
|
} from "@lib/server/prisma"
|
||||||
import { PostTable, UserTable } from "./components/tables"
|
import { PostTable, UserTable } from "./components/tables"
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
import { PageTitle } from "@components/page-title"
|
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const usersPromise = getAllUsers({
|
const usersPromise = getAllUsers({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
name: true,
|
||||||
email: true,
|
createdAt: true
|
||||||
role: true,
|
|
||||||
username: true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const postsPromise = getAllPosts({
|
const postsPromise = getAllPosts({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -48,15 +43,13 @@ export default async function AdminPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<PageTitle>Admin</PageTitle>
|
<h1>Admin</h1>
|
||||||
<PageWrapper>
|
<h2>Users</h2>
|
||||||
<h2 className="mb-4 mt-4 text-2xl font-bold">Users</h2>
|
{/* @ts-expect-error Type 'unknown' is not assignable to type */}
|
||||||
{/* @ts-expect-error Type 'unknown' is not assignable to type */}
|
<UserTable users={serializedUsers as unknown} />
|
||||||
<UserTable users={serializedUsers as unknown} />
|
<h2>Posts</h2>
|
||||||
<h2 className="mb-4 mt-4 text-2xl font-bold">Posts</h2>
|
<PostTable posts={serializedPosts} />
|
||||||
<PostTable posts={serializedPosts} />
|
</div>
|
||||||
</PageWrapper>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import PostList from "@components/post-list"
|
import PostList from "@components/post-list"
|
||||||
import { TypographyH1 } from "@components/typography"
|
|
||||||
import {
|
import {
|
||||||
getPostsByUser,
|
getPostsByUser,
|
||||||
getUserById,
|
getUserById,
|
||||||
|
@ -60,9 +59,7 @@ export default async function UserPage({
|
||||||
justifyContent: "space-between"
|
justifyContent: "space-between"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TypographyH1>
|
<h1>Public posts by {user?.displayName || "Anonymous"}</h1>
|
||||||
Public posts by {user?.displayName || "Anonymous"}
|
|
||||||
</TypographyH1>
|
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<PostList hideSearch skeleton initialPosts={[]} />}>
|
<Suspense fallback={<PostList hideSearch skeleton initialPosts={[]} />}>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import HomePage from "../page"
|
import HomePage from "../page"
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
||||||
|
|
||||||
export const revalidate = 300
|
|
||||||
|
|
|
@ -6,9 +6,10 @@ import Header from "@components/header"
|
||||||
import { Inter } from "next/font/google"
|
import { Inter } from "next/font/google"
|
||||||
import { getMetadata } from "src/app/lib/metadata"
|
import { getMetadata } from "src/app/lib/metadata"
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import clsx from "clsx"
|
import { cookies } from "next/headers"
|
||||||
import Link from "@components/link"
|
|
||||||
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
|
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
|
||||||
|
import { THEME_COOKIE, DEFAULT_THEME, SIGNED_IN_COOKIE } from "@lib/constants"
|
||||||
|
import { Suspense } from "react"
|
||||||
|
|
||||||
const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false })
|
const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false })
|
||||||
|
|
||||||
|
@ -17,20 +18,22 @@ export default async function RootLayout({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const cookiesList = cookies()
|
||||||
|
const theme = cookiesList.get(THEME_COOKIE)?.value || DEFAULT_THEME
|
||||||
|
const isAuthenticated = Boolean(cookiesList.get(SIGNED_IN_COOKIE)?.value)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// suppressHydrationWarning is required because of next-themes
|
// suppressHydrationWarning is required because of next-themes
|
||||||
<html
|
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
||||||
lang="en"
|
|
||||||
className={clsx(inter.variable, "mx-auto w-[var(--main-content)]")}
|
|
||||||
suppressHydrationWarning
|
|
||||||
>
|
|
||||||
<body>
|
<body>
|
||||||
<Toasts />
|
<Toasts />
|
||||||
<Providers>
|
<Providers>
|
||||||
<Layout>
|
<Layout>
|
||||||
<CmdK />
|
<CmdK />
|
||||||
<Header />
|
<Suspense fallback={<>Loading...</>}>
|
||||||
<main>{children}</main>
|
<Header theme={theme} isAuthenticated={isAuthenticated} />
|
||||||
|
</Suspense>
|
||||||
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { PageTitle } from "@components/page-title"
|
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
import PostList from "@components/post-list"
|
import PostList from "@components/post-list"
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return <PostList skeleton={true} initialPosts={[]} />
|
||||||
<>
|
|
||||||
<PageTitle>Your Posts</PageTitle>
|
|
||||||
<PageWrapper></PageWrapper>
|
|
||||||
<PostList skeleton={true} initialPosts={[]} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { Suspense } from "react"
|
||||||
import ErrorBoundary from "@components/error/fallback"
|
import ErrorBoundary from "@components/error/fallback"
|
||||||
import { getMetadata } from "src/app/lib/metadata"
|
import { getMetadata } from "src/app/lib/metadata"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
import { PageTitle } from "@components/page-title"
|
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
|
|
||||||
export default async function Mine() {
|
export default async function Mine() {
|
||||||
const userId = (await getCurrentUser())?.id
|
const userId = (await getCurrentUser())?.id
|
||||||
|
@ -18,21 +16,16 @@ export default async function Mine() {
|
||||||
|
|
||||||
const posts = (await getPostsByUser(userId, true)).map(serverPostToClientPost)
|
const posts = (await getPostsByUser(userId, true)).map(serverPostToClientPost)
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorBoundary>
|
||||||
<PageTitle>Your Posts</PageTitle>
|
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
|
||||||
<PageWrapper>
|
<PostList
|
||||||
<ErrorBoundary>
|
userId={userId}
|
||||||
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
|
initialPosts={posts}
|
||||||
<PostList
|
isOwner={true}
|
||||||
userId={userId}
|
hideSearch={false}
|
||||||
initialPosts={posts}
|
/>
|
||||||
isOwner={true}
|
</Suspense>
|
||||||
hideSearch={false}
|
</ErrorBoundary>
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</PageWrapper>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Card, CardContent } from "@components/card"
|
import Image from "next/image"
|
||||||
|
import Card from "@components/card"
|
||||||
import { getWelcomeContent } from "src/pages/api/welcome"
|
import { getWelcomeContent } from "src/pages/api/welcome"
|
||||||
|
import DocumentTabs from "./(posts)/components/tabs"
|
||||||
import {
|
import {
|
||||||
getAllPosts,
|
getAllPosts,
|
||||||
serverPostToClientPost,
|
serverPostToClientPost,
|
||||||
|
@ -8,9 +10,7 @@ import {
|
||||||
import PostList, { NoPostsFound } from "@components/post-list"
|
import PostList, { NoPostsFound } from "@components/post-list"
|
||||||
import { cache, Suspense } from "react"
|
import { cache, Suspense } from "react"
|
||||||
import ErrorBoundary from "@components/error/fallback"
|
import ErrorBoundary from "@components/error/fallback"
|
||||||
import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
|
import { Stack } from "@components/stack"
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
export const revalidate = 300
|
|
||||||
|
|
||||||
const getWelcomeData = cache(async () => {
|
const getWelcomeData = cache(async () => {
|
||||||
const welcomeContent = await getWelcomeContent()
|
const welcomeContent = await getWelcomeContent()
|
||||||
|
@ -18,11 +18,23 @@ const getWelcomeData = cache(async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
const { title } = await getWelcomeData()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<Stack direction="column">
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<Image
|
||||||
|
src={"/assets/logo.svg"}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
alt=""
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<h1 style={{ marginLeft: "var(--gap)" }}>{title}</h1>
|
||||||
|
</Stack>
|
||||||
{/* @ts-expect-error because of async RSC */}
|
{/* @ts-expect-error because of async RSC */}
|
||||||
<WelcomePost />
|
<WelcomePost />
|
||||||
<h2 className="mt-4 text-2xl font-bold">Recent Public Posts</h2>
|
<h2>Recent public posts</h2>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
|
@ -33,24 +45,22 @@ export default async function Page() {
|
||||||
<PublicPostList />
|
<PublicPostList />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</PageWrapper>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function WelcomePost() {
|
async function WelcomePost() {
|
||||||
const { content, rendered, title } = await getWelcomeData()
|
const { content, rendered, title } = await getWelcomeData()
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card>
|
||||||
<CardContent>
|
<DocumentTabs
|
||||||
<DocumentTabs
|
defaultTab="preview"
|
||||||
defaultTab="preview"
|
isEditing={false}
|
||||||
isEditing={false}
|
staticPreview={rendered as string}
|
||||||
staticPreview={rendered as string}
|
title={title}
|
||||||
title={title}
|
>
|
||||||
>
|
{content}
|
||||||
{content}
|
</DocumentTabs>
|
||||||
</DocumentTabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +77,6 @@ async function PublicPostList() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visibility: true,
|
visibility: true,
|
||||||
expiresAt: true,
|
|
||||||
files: {
|
files: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|
|
@ -10,12 +10,7 @@ export function Providers({ children }: PropsWithChildren<unknown>) {
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<RadixTooltip.Provider delayDuration={200}>
|
<RadixTooltip.Provider delayDuration={200}>
|
||||||
<ThemeProvider
|
<ThemeProvider enableSystem defaultTheme="dark">
|
||||||
attribute="class"
|
|
||||||
enableColorScheme
|
|
||||||
enableSystem
|
|
||||||
defaultTheme="dark"
|
|
||||||
>
|
|
||||||
<SWRProvider>{children}</SWRProvider>
|
<SWRProvider>{children}</SWRProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</RadixTooltip.Provider>
|
</RadixTooltip.Provider>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
|
@ -13,7 +13,6 @@ import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import styles from "./api-keys.module.css"
|
import styles from "./api-keys.module.css"
|
||||||
import { useSessionSWR } from "@lib/use-session-swr"
|
import { useSessionSWR } from "@lib/use-session-swr"
|
||||||
import { TypographyH4 } from "@components/typography"
|
|
||||||
|
|
||||||
// need to pass in the accessToken
|
// need to pass in the accessToken
|
||||||
const APIKeys = ({
|
const APIKeys = ({
|
||||||
|
@ -76,7 +75,7 @@ const APIKeys = ({
|
||||||
)}
|
)}
|
||||||
{hasError && <Note type="error">{error?.message}</Note>}
|
{hasError && <Note type="error">{error?.message}</Note>}
|
||||||
<form className={styles.form}>
|
<form className={styles.form}>
|
||||||
<TypographyH4>Create new</TypographyH4>
|
<h5>Create new</h5>
|
||||||
<fieldset className={styles.fieldset}>
|
<fieldset className={styles.fieldset}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -86,9 +85,10 @@ const APIKeys = ({
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={onCreateTokenClick}
|
onClick={onCreateTokenClick}
|
||||||
disabled={!newToken}
|
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
|
disabled={!newToken}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -121,9 +121,7 @@ const APIKeys = ({
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<p className="p-4 text-center text-muted-foreground">
|
<p>You have no API keys.</p>
|
||||||
No API keys found.
|
|
||||||
</p>
|
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div style={{ marginTop: "var(--gap-quarter)" }}>
|
<div style={{ marginTop: "var(--gap-quarter)" }}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { useSessionSWR } from "@lib/use-session-swr"
|
import { useSessionSWR } from "@lib/use-session-swr"
|
||||||
|
@ -142,7 +142,8 @@ function Profile() {
|
||||||
</div>
|
</div>
|
||||||
</TooltipComponent>
|
</TooltipComponent>
|
||||||
</div> */}
|
</div> */}
|
||||||
<Button type="submit" disabled={!name} loading={submitting}>
|
|
||||||
|
<Button type="submit" loading={submitting}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
22
src/app/(drift)/settings/layout.tsx
Normal file
22
src/app/(drift)/settings/layout.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export default function SettingsLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "var(--gap)",
|
||||||
|
marginBottom: "var(--gap)",
|
||||||
|
marginTop: "var(--gap)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,15 +1,5 @@
|
||||||
import { PageTitle } from "@components/page-title"
|
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
import SettingsGroup from "@components/settings-group"
|
import SettingsGroup from "@components/settings-group"
|
||||||
|
|
||||||
export default function SettingsLoading() {
|
export default function SettingsLoading() {
|
||||||
return (
|
return <SettingsGroup skeleton />
|
||||||
<>
|
|
||||||
<PageTitle>Settings</PageTitle>
|
|
||||||
<PageWrapper>
|
|
||||||
<SettingsGroup skeleton />
|
|
||||||
<SettingsGroup skeleton />
|
|
||||||
</PageWrapper>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,16 @@ import { getMetadata } from "src/app/lib/metadata"
|
||||||
import SettingsGroup from "../../components/settings-group"
|
import SettingsGroup from "../../components/settings-group"
|
||||||
import APIKeys from "./components/sections/api-keys"
|
import APIKeys from "./components/sections/api-keys"
|
||||||
import Profile from "./components/sections/profile"
|
import Profile from "./components/sections/profile"
|
||||||
import { PageTitle } from "@components/page-title"
|
|
||||||
import { PageWrapper } from "@components/page-wrapper"
|
|
||||||
|
|
||||||
export default async function SettingsPage() {
|
export default async function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle>Settings</PageTitle>
|
<SettingsGroup title="Profile">
|
||||||
<PageWrapper>
|
<Profile />
|
||||||
<SettingsGroup title="Profile">
|
</SettingsGroup>
|
||||||
<Profile />
|
<SettingsGroup title="API Keys">
|
||||||
</SettingsGroup>
|
<APIKeys />
|
||||||
<SettingsGroup title="API Keys">
|
</SettingsGroup>
|
||||||
<APIKeys />
|
|
||||||
</SettingsGroup>
|
|
||||||
</PageWrapper>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { authOptions } from "@lib/server/auth"
|
|
||||||
import NextAuth from "next-auth"
|
|
||||||
|
|
||||||
const handler = NextAuth(authOptions)
|
|
||||||
|
|
||||||
export { handler as GET, handler as POST }
|
|
|
@ -1,41 +0,0 @@
|
||||||
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"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
"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) => (
|
|
||||||
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</AlertDialogPrimitive.Portal>
|
|
||||||
)
|
|
||||||
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
|
|
||||||
|
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Overlay
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const AlertDialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay />
|
|
||||||
<AlertDialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
))
|
|
||||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const AlertDialogHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
|
||||||
|
|
||||||
const AlertDialogFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
|
||||||
|
|
||||||
const AlertDialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const AlertDialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogDescription.displayName =
|
|
||||||
AlertDialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
const AlertDialogAction = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Action
|
|
||||||
ref={ref}
|
|
||||||
className={cn(buttonVariants(), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
||||||
|
|
||||||
const AlertDialogCancel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<AlertDialogPrimitive.Cancel
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"mt-2 sm:mt-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel
|
|
||||||
}
|
|
|
@ -1,35 +1,21 @@
|
||||||
import * as React from "react"
|
import React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import styles from "./badge.module.css"
|
||||||
import { cn } from "@lib/cn"
|
type BadgeProps = {
|
||||||
|
type: "primary" | "secondary" | "error" | "warning"
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
"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",
|
({ type, children, ...rest }: BadgeProps, ref) => {
|
||||||
{
|
return (
|
||||||
variants: {
|
<div className={styles.container} {...rest}>
|
||||||
variant: {
|
<div className={`${styles.badge} ${styles[type]}`} ref={ref}>
|
||||||
default:
|
{children}
|
||||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
</div>
|
||||||
secondary:
|
</div>
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
Badge.displayName = "Badge"
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
export default Badge
|
||||||
return (
|
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
"use client"
|
"use client"
|
||||||
import { useToasts } from "@components/toasts"
|
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 { copyToClipboard } from "src/app/lib/copy-to-clipboard"
|
||||||
import { timeAgo } from "src/app/lib/time-ago"
|
import { timeAgo } from "src/app/lib/time-ago"
|
||||||
import { useMemo, useState, useEffect } from "react"
|
import { useMemo, useState, useEffect } from "react"
|
||||||
import { Badge } from "../badge"
|
import Badge from "../badge"
|
||||||
|
|
||||||
const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
|
const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
|
||||||
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
|
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
|
||||||
|
@ -30,7 +30,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
|
||||||
return (
|
return (
|
||||||
// TODO: investigate tooltip not showing
|
// TODO: investigate tooltip not showing
|
||||||
<Tooltip content={formattedTime}>
|
<Tooltip content={formattedTime}>
|
||||||
<Badge onClick={onClick} variant={"outline"} suppressHydrationWarning>
|
<Badge type="secondary" onClick={onClick}>
|
||||||
{" "}
|
{" "}
|
||||||
<>{time}</>
|
<>{time}</>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Tooltip } from "@components/tooltip"
|
import Tooltip from "@components/tooltip"
|
||||||
import { timeUntil } from "src/app/lib/time-ago"
|
import { timeUntil } from "src/app/lib/time-ago"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { Badge } from "../badge"
|
import Badge from "../badge"
|
||||||
|
|
||||||
const ExpirationBadge = ({
|
const ExpirationBadge = ({
|
||||||
postExpirationDate
|
postExpirationDate
|
||||||
|
@ -43,7 +43,7 @@ const ExpirationBadge = ({
|
||||||
const isExpired = expirationDate < new Date()
|
const isExpired = expirationDate < new Date()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant={isExpired ? "destructive" : "outline"}>
|
<Badge type={isExpired ? "error" : "warning"}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}
|
content={`${expirationDate.toLocaleDateString()} ${expirationDate.toLocaleTimeString()}`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Badge } from "../badge"
|
import Badge from "../badge"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visibility: string
|
visibility: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const VisibilityBadge = ({ visibility }: Props) => {
|
const VisibilityBadge = ({ visibility }: Props) => {
|
||||||
return <Badge variant={"outline"}>{visibility}</Badge>
|
return <Badge type={"primary"}>{visibility}</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VisibilityBadge
|
export default VisibilityBadge
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
import PasswordModal from "@components/password-modal"
|
import PasswordModal from "@components/password-modal"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import ButtonGroup from "@components/button-group"
|
import ButtonGroup from "@components/button-group"
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
|
import { Spinner } from "@components/spinner"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useSessionSWR } from "@lib/use-session-swr"
|
import { useSessionSWR } from "@lib/use-session-swr"
|
||||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||||
|
@ -88,40 +89,39 @@ function VisibilityControl({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FadeIn className="mt-8">
|
<FadeIn>
|
||||||
<ButtonGroup>
|
<ButtonGroup
|
||||||
|
style={{
|
||||||
|
maxWidth: 600,
|
||||||
|
margin: "var(--gap) auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
disabled={visibility === "private"}
|
disabled={visibility === "private"}
|
||||||
variant={"outline"}
|
|
||||||
onClick={() => onSubmit("private")}
|
onClick={() => onSubmit("private")}
|
||||||
loading={isSubmitting === "private"}
|
|
||||||
>
|
>
|
||||||
Make Private
|
{isSubmitting === "private" ? <Spinner /> : "Make Private"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={visibility === "public"}
|
disabled={visibility === "public"}
|
||||||
variant={"outline"}
|
|
||||||
onClick={() => onSubmit("public")}
|
onClick={() => onSubmit("public")}
|
||||||
loading={isSubmitting === "public"}
|
|
||||||
>
|
>
|
||||||
Make Public
|
{isSubmitting === "public" ? <Spinner /> : "Make Public"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={visibility === "unlisted"}
|
disabled={visibility === "unlisted"}
|
||||||
variant={"outline"}
|
|
||||||
onClick={() => onSubmit("unlisted")}
|
onClick={() => onSubmit("unlisted")}
|
||||||
loading={isSubmitting === "unlisted"}
|
|
||||||
>
|
>
|
||||||
Make Unlisted
|
{isSubmitting === "unlisted" ? <Spinner /> : "Make Unlisted"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={() => onSubmit("protected")}>
|
||||||
onClick={() => onSubmit("protected")}
|
{isSubmitting === "protected" ? (
|
||||||
variant={"outline"}
|
<Spinner />
|
||||||
loading={isSubmitting === "protected"}
|
) : visibility === "protected" ? (
|
||||||
>
|
"Change Password"
|
||||||
{visibility === "protected"
|
) : (
|
||||||
? "Change Password"
|
"Protect with Password"
|
||||||
: "Protect with Password"}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<PasswordModal
|
<PasswordModal
|
||||||
|
|
|
@ -1,44 +1,52 @@
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import React, { ComponentProps, ReactNode } from "react"
|
import React, { ReactNode } from "react"
|
||||||
import styles from "./dropdown.module.css"
|
import styles from "./dropdown.module.css"
|
||||||
import {
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem
|
|
||||||
} from "@components/dropdown-menu"
|
|
||||||
import { ArrowDown } from "react-feather"
|
import { ArrowDown } from "react-feather"
|
||||||
|
type Props = {
|
||||||
|
type?: "primary" | "secondary"
|
||||||
|
height?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
type ButtonDropdownProps = ComponentProps<typeof DropdownMenu>
|
type Attrs = Omit<React.HTMLAttributes<HTMLDivElement>, keyof Props>
|
||||||
|
type ButtonDropdownProps = Props & Attrs
|
||||||
|
|
||||||
const ButtonDropdown: React.FC<React.PropsWithChildren<ButtonDropdownProps>> = (
|
const ButtonDropdown: React.FC<
|
||||||
props
|
React.PropsWithChildren<ButtonDropdownProps>
|
||||||
) => {
|
> = ({ type, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu.Root>
|
||||||
<div className={styles.dropdown}>
|
<div className={styles.dropdown} style={{ height: props.height }}>
|
||||||
<>
|
<>
|
||||||
{Array.isArray(props.children) ? props.children[0] : props.children}
|
{Array.isArray(props.children) ? props.children[0] : props.children}
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenu.Trigger
|
||||||
<Button>
|
style={{
|
||||||
<ArrowDown height={20} />
|
display: "flex",
|
||||||
</Button>
|
flexDirection: "row",
|
||||||
</DropdownMenuTrigger>
|
justifyContent: "flex-end"
|
||||||
|
}}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
iconLeft={<ArrowDown />}
|
||||||
|
buttonType={type}
|
||||||
|
className={styles.icon}
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
{Array.isArray(props.children) ? (
|
{Array.isArray(props.children) ? (
|
||||||
<DropdownMenuPortal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenu.Content align="end">
|
||||||
{(props.children as ReactNode[])
|
{(props.children as ReactNode[])
|
||||||
?.slice(1)
|
?.slice(1)
|
||||||
.map((child, index) => (
|
.map((child, index) => (
|
||||||
<DropdownMenuItem key={index}>{child}</DropdownMenuItem>
|
<DropdownMenu.Item key={index}>{child}</DropdownMenu.Item>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenuPortal>
|
</DropdownMenu.Portal>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-top: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group > * {
|
.button-group > * {
|
||||||
|
|
56
src/app/components/button/button.module.css
Normal file
56
src/app/components/button/button.module.css
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
.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);
|
||||||
|
}
|
|
@ -1,65 +1,86 @@
|
||||||
import * as React from "react"
|
import styles from "./button.module.css"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { forwardRef } from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import clsx from "clsx"
|
||||||
|
|
||||||
import { cn } from "@lib/cn"
|
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
type Props = React.DetailedHTMLProps<
|
||||||
"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",
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
{
|
HTMLButtonElement
|
||||||
variants: {
|
> & {
|
||||||
variant: {
|
children?: React.ReactNode
|
||||||
default: "bg-primary/80 text-primary-foreground/80 hover:bg-primary/70",
|
buttonType?: "primary" | "secondary"
|
||||||
destructive:
|
className?: string
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||||
outline:
|
iconRight?: React.ReactNode
|
||||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
iconLeft?: React.ReactNode
|
||||||
secondary:
|
height?: string | number
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
width?: string | number
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
padding?: string | number
|
||||||
link: "underline-offset-4 hover:underline text-primary"
|
margin?: string | number
|
||||||
},
|
|
||||||
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<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean
|
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
// eslint-disable-next-line react/display-name
|
||||||
|
const Button = forwardRef<HTMLButtonElement, Props>(
|
||||||
(
|
(
|
||||||
{ className, variant, size, loading, children, asChild = false, ...props },
|
{
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
buttonType = "secondary",
|
||||||
|
disabled = false,
|
||||||
|
iconRight,
|
||||||
|
iconLeft,
|
||||||
|
height = 40,
|
||||||
|
width,
|
||||||
|
padding = 10,
|
||||||
|
margin,
|
||||||
|
loading,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<button
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
className={clsx(styles.button, className, {
|
||||||
|
[styles.primary]: buttonType === "primary",
|
||||||
|
[styles.secondary]: buttonType === "secondary"
|
||||||
|
})}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{ height, width, margin, padding, ...style }}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{loading ? <Spinner className="mr-2" /> : null}
|
{children && iconLeft && (
|
||||||
{children}
|
<span className={clsx(styles.icon, styles.iconLeft)}>{iconLeft}</span>
|
||||||
</Comp>
|
)}
|
||||||
|
{!loading &&
|
||||||
|
(children ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<span className={styles.icon}>{iconLeft || iconRight}</span>
|
||||||
|
))}
|
||||||
|
{loading && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{children && iconRight && (
|
||||||
|
<span className={clsx(styles.icon, styles.iconRight)}>
|
||||||
|
{iconRight}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Button.displayName = "Button"
|
export default Button
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
"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<typeof DayPicker>
|
|
||||||
|
|
||||||
function Calendar({
|
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
...props
|
|
||||||
}: CalendarProps) {
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn("p-3", className)}
|
|
||||||
classNames={{
|
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
|
||||||
month: "space-y-4",
|
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
|
||||||
caption_label: "text-sm font-medium",
|
|
||||||
nav: "space-x-1 flex items-center",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
|
||||||
),
|
|
||||||
nav_button_previous: "absolute left-1",
|
|
||||||
nav_button_next: "absolute right-1",
|
|
||||||
table: "w-full border-collapse space-y-1",
|
|
||||||
head_row: "flex",
|
|
||||||
head_cell:
|
|
||||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
|
||||||
row: "flex w-full mt-2",
|
|
||||||
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
|
||||||
day: cn(
|
|
||||||
buttonVariants({ variant: "ghost" }),
|
|
||||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
|
||||||
),
|
|
||||||
day_selected:
|
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
||||||
day_today: "bg-accent text-accent-foreground",
|
|
||||||
day_outside: "text-muted-foreground opacity-50",
|
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
|
||||||
day_range_middle:
|
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
||||||
day_hidden: "invisible",
|
|
||||||
...classNames
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
IconLeft: ({}) => <ChevronLeft className="h-4 w-4" />,
|
|
||||||
IconRight: ({}) => <ChevronRight className="h-4 w-4" />
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Calendar.displayName = "Calendar"
|
|
||||||
|
|
||||||
export { Calendar }
|
|
|
@ -1,78 +1,16 @@
|
||||||
import * as React from "react"
|
import styles from "./card.module.css"
|
||||||
import { cn } from "@lib/cn"
|
|
||||||
|
|
||||||
const Card = React.forwardRef<
|
export default function Card({
|
||||||
HTMLDivElement,
|
children,
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
className,
|
||||||
>(({ className, ...props }, ref) => (
|
...props
|
||||||
<div
|
}: {
|
||||||
ref={ref}
|
children?: React.ReactNode
|
||||||
className={cn(
|
className?: string
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
} & React.ComponentProps<"div">) {
|
||||||
className
|
return (
|
||||||
)}
|
<div className={`${styles.card} ${className || ""}`} {...props}>
|
||||||
{...props}
|
<div className={styles.content}>{children}</div>
|
||||||
/>
|
</div>
|
||||||
))
|
)
|
||||||
Card.displayName = "Card"
|
}
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardHeader.displayName = "CardHeader"
|
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h3
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardTitle.displayName = "CardTitle"
|
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<p
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardDescription.displayName = "CardDescription"
|
|
||||||
|
|
||||||
const CardContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
||||||
))
|
|
||||||
CardContent.displayName = "CardContent"
|
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(" flex items-center p-6 pt-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CardFooter.displayName = "CardFooter"
|
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
||||||
|
|
165
src/app/components/cmdk/cmdk.module.css
Normal file
165
src/app/components/cmdk/cmdk.module.css
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
/** 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,163 +0,0 @@
|
||||||
"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<typeof CommandPrimitive>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Command.displayName = CommandPrimitive.displayName
|
|
||||||
|
|
||||||
type CommandDialogProps = DialogProps
|
|
||||||
|
|
||||||
const CommandDialog = React.forwardRef<
|
|
||||||
React.ElementRef<typeof Dialog>,
|
|
||||||
CommandDialogProps
|
|
||||||
>(({ children, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<Dialog {...props}>
|
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
|
||||||
<Command
|
|
||||||
ref={ref}
|
|
||||||
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Command>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
CommandDialog.displayName = Dialog.displayName
|
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => (
|
|
||||||
<CommandPrimitive.Empty
|
|
||||||
ref={ref}
|
|
||||||
className="py-6 text-center text-sm"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const CommandShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandShortcut,
|
|
||||||
CommandSeparator
|
|
||||||
}
|
|
|
@ -1,3 +1,16 @@
|
||||||
body [cmdk-dialog] {
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { CommandDialog, CommandList, CommandInput, CommandEmpty } from "./cmdk"
|
import { Command } from "cmdk"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import styles from "./cmdk.module.css"
|
||||||
import "./dialog.css"
|
import "./dialog.css"
|
||||||
import HomePage from "./pages/home"
|
import HomePage from "./pages/home"
|
||||||
import PostsPage from "./pages/posts"
|
import PostsPage from "./pages/posts"
|
||||||
|
@ -9,7 +10,7 @@ import PostsPage from "./pages/posts"
|
||||||
export type CmdKPage = "home" | "posts"
|
export type CmdKPage = "home" | "posts"
|
||||||
export default function CmdK() {
|
export default function CmdK() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement>()
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
const [page, setPage] = useState<CmdKPage>("home")
|
const [page, setPage] = useState<CmdKPage>("home")
|
||||||
|
|
||||||
// Toggle the menu when ⌘K is pressed
|
// Toggle the menu when ⌘K is pressed
|
||||||
|
@ -52,15 +53,21 @@ export default function CmdK() {
|
||||||
}, [page])
|
}, [page])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<Command.Dialog
|
||||||
<CommandList>
|
open={open}
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
onOpenChange={setOpen}
|
||||||
|
label="Global Command Menu"
|
||||||
|
className={styles.cmdk}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Command.List>
|
||||||
|
<Command.Empty>No results found.</Command.Empty>
|
||||||
{page === "home" ? (
|
{page === "home" ? (
|
||||||
<HomePage setPage={setPage} setOpen={setOpen} />
|
<HomePage setPage={setPage} setOpen={setOpen} />
|
||||||
) : null}
|
) : null}
|
||||||
{page === "posts" ? <PostsPage setOpen={setOpen} /> : null}
|
{page === "posts" ? <PostsPage setOpen={setOpen} /> : null}
|
||||||
</CommandList>
|
</Command.List>
|
||||||
<CommandInput />
|
<Command.Input />
|
||||||
</CommandDialog>
|
</Command.Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CommandItem } from "@components/cmdk/cmdk"
|
import { Command } from "cmdk"
|
||||||
|
|
||||||
export default function Item({
|
export default function Item({
|
||||||
children,
|
children,
|
||||||
|
@ -12,7 +12,7 @@ export default function Item({
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<CommandItem onSelect={onSelect}>
|
<Command.Item onSelect={onSelect}>
|
||||||
{icon}
|
{icon}
|
||||||
{children}
|
{children}
|
||||||
{shortcut ? (
|
{shortcut ? (
|
||||||
|
@ -22,6 +22,6 @@ export default function Item({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</CommandItem>
|
</Command.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { Command } from "cmdk"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { FilePlus, Moon, Search, Settings, Sun } from "react-feather"
|
import { FilePlus, Moon, Search, Settings, Sun } from "react-feather"
|
||||||
|
import { setDriftTheme } from "src/app/lib/set-theme"
|
||||||
import { CmdKPage } from ".."
|
import { CmdKPage } from ".."
|
||||||
import Item from "../item"
|
import Item from "../item"
|
||||||
import { CommandGroup } from "@components/cmdk/cmdk"
|
|
||||||
|
|
||||||
export default function HomePage({
|
export default function HomePage({
|
||||||
setOpen,
|
setOpen,
|
||||||
|
@ -16,7 +17,7 @@ export default function HomePage({
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
const { setTheme, resolvedTheme } = useTheme()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommandGroup heading="Posts">
|
<Command.Group heading="Posts">
|
||||||
<Item
|
<Item
|
||||||
shortcut="R P"
|
shortcut="R P"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
|
@ -36,12 +37,12 @@ export default function HomePage({
|
||||||
>
|
>
|
||||||
New Post
|
New Post
|
||||||
</Item>
|
</Item>
|
||||||
</CommandGroup>
|
</Command.Group>
|
||||||
<CommandGroup heading="Settings">
|
<Command.Group heading="Settings">
|
||||||
<Item
|
<Item
|
||||||
shortcut="T"
|
shortcut="T"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
setDriftTheme(resolvedTheme === "dark" ? "light" : "dark", setTheme)
|
||||||
}}
|
}}
|
||||||
icon={resolvedTheme === "dark" ? <Sun /> : <Moon />}
|
icon={resolvedTheme === "dark" ? <Sun /> : <Moon />}
|
||||||
>
|
>
|
||||||
|
@ -57,7 +58,7 @@ export default function HomePage({
|
||||||
>
|
>
|
||||||
Go to Settings
|
Go to Settings
|
||||||
</Item>
|
</Item>
|
||||||
</CommandGroup>
|
</Command.Group>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
"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<React.SetStateAction<Date | undefined>>
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={"outline"}
|
|
||||||
className={cn(
|
|
||||||
"w-[280px] justify-start text-left font-normal",
|
|
||||||
!expiresAt && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="w-4 h-4 mr-2" />
|
|
||||||
{expiresAt ? (
|
|
||||||
format(expiresAt, "PPP")
|
|
||||||
) : (
|
|
||||||
<span>Won't expire</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0">
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={expiresAt}
|
|
||||||
onSelect={(date) => {
|
|
||||||
setExpiresAt(date)
|
|
||||||
}}
|
|
||||||
initialFocus
|
|
||||||
fromDate={new Date()}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
"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) => (
|
|
||||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</DialogPrimitive.Portal>
|
|
||||||
)
|
|
||||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName
|
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
))
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DialogHeader = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
DialogHeader.displayName = "DialogHeader"
|
|
||||||
|
|
||||||
const DialogFooter = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
DialogFooter.displayName = "DialogFooter"
|
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"text-lg font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
"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<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
))
|
|
||||||
DropdownMenuSubTrigger.displayName =
|
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSubContent.displayName =
|
|
||||||
DropdownMenuPrimitive.SubContent.displayName
|
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
))
|
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
))
|
|
||||||
DropdownMenuCheckboxItem.displayName =
|
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
))
|
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuRadioGroup
|
|
||||||
}
|
|
|
@ -1,9 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import Link from "@components/link"
|
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { TypographyH3 } from "@components/typography"
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
// an error fallback for react-error-boundary
|
// an error fallback for react-error-boundary
|
||||||
|
|
||||||
|
@ -16,14 +14,8 @@ import {
|
||||||
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||||
return (
|
return (
|
||||||
<Note type="error" style={{ width: "100%" }}>
|
<Note type="error" style={{ width: "100%" }}>
|
||||||
<TypographyH3>Something went wrong:</TypographyH3>
|
<h3>Something went wrong:</h3>
|
||||||
<pre>{error.message}</pre>
|
<pre>{error.message}</pre>
|
||||||
<Link
|
|
||||||
href="https://github.com/MaxLeiter/Drift/issues/new"
|
|
||||||
className="mr-2"
|
|
||||||
>
|
|
||||||
<Button>Report an issue</Button>
|
|
||||||
</Link>
|
|
||||||
<Button onClick={resetErrorBoundary}>Try again</Button>
|
<Button onClick={resetErrorBoundary}>Try again</Button>
|
||||||
</Note>
|
</Note>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// https://www.joshwcomeau.com/snippets/react-components/fade-in/
|
// https://www.joshwcomeau.com/snippets/react-components/fade-in/
|
||||||
import React from "react"
|
|
||||||
import styles from "./fade.module.css"
|
import styles from "./fade.module.css"
|
||||||
|
|
||||||
function FadeIn({
|
function FadeIn({
|
||||||
|
@ -12,18 +11,8 @@ function FadeIn({
|
||||||
duration?: number
|
duration?: number
|
||||||
delay?: number
|
delay?: number
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
as?: React.ElementType | JSX.Element
|
as?: React.ElementType
|
||||||
} & React.HTMLAttributes<HTMLElement>) {
|
} & React.HTMLAttributes<HTMLElement>) {
|
||||||
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"
|
const Element = as || "div"
|
||||||
return (
|
return (
|
||||||
<Element
|
<Element
|
||||||
|
|
3
src/app/components/header/buttons.module.css
Normal file
3
src/app/components/header/buttons.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.active {
|
||||||
|
color: var(--fg) !important;
|
||||||
|
}
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import { useSelectedLayoutSegments } from "next/navigation"
|
import { useSelectedLayoutSegments } from "next/navigation"
|
||||||
import FadeIn from "@components/fade-in"
|
import FadeIn from "@components/fade-in"
|
||||||
|
import { setDriftTheme } from "src/app/lib/set-theme"
|
||||||
import {
|
import {
|
||||||
Circle,
|
|
||||||
Home,
|
Home,
|
||||||
Moon,
|
Moon,
|
||||||
PlusCircle,
|
PlusCircle,
|
||||||
|
@ -13,13 +13,11 @@ import {
|
||||||
UserX
|
UserX
|
||||||
} from "react-feather"
|
} from "react-feather"
|
||||||
import { signOut } from "next-auth/react"
|
import { signOut } from "next-auth/react"
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import Link from "@components/link"
|
import Link from "@components/link"
|
||||||
import { useSessionSWR } from "@lib/use-session-swr"
|
import { useSessionSWR } from "@lib/use-session-swr"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import styles from "./buttons.module.css"
|
import styles from "./buttons.module.css"
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
import { cn } from "@lib/cn"
|
|
||||||
|
|
||||||
// constant width for sign in / sign out buttons to avoid CLS
|
// constant width for sign in / sign out buttons to avoid CLS
|
||||||
const SIGN_IN_WIDTH = 110
|
const SIGN_IN_WIDTH = 110
|
||||||
|
@ -40,37 +38,50 @@ type Tab = {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function NavButton({ className, ...tab }: Tab & { className?: string }) {
|
export function HeaderButtons({
|
||||||
|
isAuthenticated,
|
||||||
|
theme: initialTheme
|
||||||
|
}: {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
theme: string
|
||||||
|
}) {
|
||||||
|
const { isAdmin, userId } = useSessionSWR()
|
||||||
|
const { resolvedTheme } = useTheme()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{getButtons({
|
||||||
|
isAuthenticated,
|
||||||
|
theme: resolvedTheme ? resolvedTheme : initialTheme,
|
||||||
|
isAdmin,
|
||||||
|
userId
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavButton(tab: Tab) {
|
||||||
const segment = useSelectedLayoutSegments().slice(-1)[0]
|
const segment = useSelectedLayoutSegments().slice(-1)[0]
|
||||||
const isActive = segment === tab.value.toLowerCase()
|
const isActive = segment === tab.value.toLowerCase()
|
||||||
const activeStyle = isActive ? "text-primary-500" : "text-gray-600"
|
const activeStyle = isActive ? styles.active : undefined
|
||||||
if (tab.onClick) {
|
if (tab.onClick) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
|
iconLeft={tab.icon}
|
||||||
onClick={tab.onClick}
|
onClick={tab.onClick}
|
||||||
className={cn(activeStyle, "w-full md:w-auto", className)}
|
className={activeStyle}
|
||||||
aria-label={tab.name}
|
aria-label={tab.name}
|
||||||
aria-current={isActive ? "page" : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
data-tab={tab.value}
|
data-tab={tab.value}
|
||||||
variant={"ghost"}
|
width={tab.width}
|
||||||
>
|
>
|
||||||
{tab.name ? tab.name : undefined}
|
{tab.name ? tab.name : undefined}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link key={tab.value} href={tab.href} data-tab={tab.value}>
|
||||||
key={tab.value}
|
<Button className={activeStyle} iconLeft={tab.icon} width={tab.width}>
|
||||||
href={tab.href}
|
|
||||||
data-tab={tab.value}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={cn(activeStyle, "w-full md:w-auto", className)}
|
|
||||||
aria-label={tab.name}
|
|
||||||
variant={"ghost"}
|
|
||||||
>
|
|
||||||
{tab.name ? tab.name : undefined}
|
{tab.name ? tab.name : undefined}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -78,126 +89,99 @@ function NavButton({ className, ...tab }: Tab & { className?: string }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ThemeButton() {
|
function ThemeButton({ theme }: { theme: string }) {
|
||||||
const { setTheme, resolvedTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
const [mounted, setMounted] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<NavButton
|
||||||
{!mounted && (
|
name="Theme"
|
||||||
<NavButton
|
icon={theme === "dark" ? <Sun /> : <Moon />}
|
||||||
name="Theme"
|
value="dark"
|
||||||
icon={<Circle opacity={0.3} />}
|
onClick={() => {
|
||||||
value="dark"
|
setDriftTheme(theme === "dark" ? "light" : "dark", setTheme)
|
||||||
href=""
|
}}
|
||||||
key="theme"
|
key="theme"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{mounted && (
|
|
||||||
<NavButton
|
|
||||||
name="Theme"
|
|
||||||
icon={
|
|
||||||
<FadeIn>{resolvedTheme === "dark" ? <Sun /> : <Moon />}</FadeIn>
|
|
||||||
}
|
|
||||||
value="dark"
|
|
||||||
onClick={() => {
|
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
|
||||||
}}
|
|
||||||
key="theme"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderButtons(): JSX.Element {
|
/** For use by mobile */
|
||||||
const { isAdmin, isAuthenticated, userId } = useSessionSWR()
|
export function getButtons({
|
||||||
|
isAuthenticated,
|
||||||
useEffect(() => {
|
theme,
|
||||||
if (isAuthenticated && !userId) {
|
// mutate: mutateSession,
|
||||||
signOut()
|
isAdmin,
|
||||||
}
|
userId
|
||||||
}, [isAuthenticated, userId])
|
}: {
|
||||||
|
isAuthenticated: boolean
|
||||||
return (
|
theme: string
|
||||||
<>
|
// mutate: KeyedMutator<Session>
|
||||||
|
isAdmin?: boolean
|
||||||
|
userId?: string
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
<NavButton
|
||||||
|
key="home"
|
||||||
|
name="Home"
|
||||||
|
icon={<Home />}
|
||||||
|
value="home"
|
||||||
|
href="/home"
|
||||||
|
/>,
|
||||||
|
<NavButton
|
||||||
|
key="new"
|
||||||
|
name="New"
|
||||||
|
icon={<PlusCircle />}
|
||||||
|
value="new"
|
||||||
|
href="/new"
|
||||||
|
/>,
|
||||||
|
<NavButton
|
||||||
|
key="yours"
|
||||||
|
name="Yours"
|
||||||
|
icon={<User />}
|
||||||
|
value="mine"
|
||||||
|
href="/mine"
|
||||||
|
/>,
|
||||||
|
<NavButton
|
||||||
|
name="Settings"
|
||||||
|
icon={<Settings />}
|
||||||
|
value="settings"
|
||||||
|
href="/settings"
|
||||||
|
key="settings"
|
||||||
|
/>,
|
||||||
|
<ThemeButton key="theme-button" theme={theme} />,
|
||||||
|
isAuthenticated === true ? (
|
||||||
<NavButton
|
<NavButton
|
||||||
key="home"
|
name="Sign Out"
|
||||||
name="Home"
|
key="signout"
|
||||||
icon={<Home />}
|
icon={<UserX />}
|
||||||
value="home"
|
value="signout"
|
||||||
href="/home"
|
onClick={() => {
|
||||||
|
signOut({
|
||||||
|
callbackUrl: `/signedout${userId ? "?userId=" + userId : ""}`
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
width={SIGN_IN_WIDTH}
|
||||||
/>
|
/>
|
||||||
|
) : undefined,
|
||||||
|
isAuthenticated === false ? (
|
||||||
<NavButton
|
<NavButton
|
||||||
key="new"
|
name="Sign In"
|
||||||
name="New"
|
key="signin"
|
||||||
icon={<PlusCircle />}
|
|
||||||
value="new"
|
|
||||||
href="/new"
|
|
||||||
/>
|
|
||||||
<NavButton
|
|
||||||
key="yours"
|
|
||||||
name="Yours"
|
|
||||||
icon={<User />}
|
icon={<User />}
|
||||||
value="mine"
|
value="signin"
|
||||||
href="/mine"
|
href="/signin"
|
||||||
|
width={SIGN_IN_WIDTH}
|
||||||
/>
|
/>
|
||||||
<NavButton
|
) : undefined,
|
||||||
name="Settings"
|
isAdmin ? (
|
||||||
icon={<Settings />}
|
<FadeIn>
|
||||||
value="settings"
|
|
||||||
href="/settings"
|
|
||||||
key="settings"
|
|
||||||
/>
|
|
||||||
<ThemeButton key="theme-button" />
|
|
||||||
{isAdmin && (
|
|
||||||
<NavButton
|
<NavButton
|
||||||
name="Admin"
|
name="Admin"
|
||||||
key="admin"
|
key="admin"
|
||||||
icon={<Settings />}
|
icon={<Settings />}
|
||||||
value="admin"
|
value="admin"
|
||||||
href="/admin"
|
href="/admin"
|
||||||
className="transition-opacity duration-500"
|
|
||||||
/>
|
/>
|
||||||
)}
|
</FadeIn>
|
||||||
{isAuthenticated === true && (
|
) : undefined
|
||||||
<NavButton
|
].filter(Boolean)
|
||||||
name="Sign Out"
|
|
||||||
key="signout"
|
|
||||||
icon={<UserX />}
|
|
||||||
value="signout"
|
|
||||||
onClick={() => {
|
|
||||||
signOut({
|
|
||||||
callbackUrl: `/signedout${userId ? "?userId=" + userId : ""}`
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
width={SIGN_IN_WIDTH}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthenticated === false && (
|
|
||||||
<NavButton
|
|
||||||
name="Sign In"
|
|
||||||
key="signin"
|
|
||||||
icon={<User />}
|
|
||||||
value="signin"
|
|
||||||
href="/signin"
|
|
||||||
width={SIGN_IN_WIDTH}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAuthenticated === undefined && (
|
|
||||||
<NavButton
|
|
||||||
name="Sign"
|
|
||||||
key="signin"
|
|
||||||
icon={<User />}
|
|
||||||
value="signin"
|
|
||||||
href="/signin"
|
|
||||||
width={SIGN_IN_WIDTH}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,120 +1,22 @@
|
||||||
"use client"
|
import styles from "./header.module.css"
|
||||||
|
import { HeaderButtons } from "./buttons"
|
||||||
import Link from "next/link"
|
|
||||||
import { cn } from "@lib/cn"
|
|
||||||
|
|
||||||
import { useSessionSWR } from "@lib/use-session-swr"
|
|
||||||
import { PropsWithChildren, useEffect, useState } from "react"
|
|
||||||
import { useSelectedLayoutSegments } from "next/navigation"
|
|
||||||
import Image from "next/image"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import { Moon, Sun } from "react-feather"
|
|
||||||
import FadeIn from "@components/fade-in"
|
|
||||||
import MobileHeader from "./mobile"
|
import MobileHeader from "./mobile"
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header({
|
||||||
const { isAdmin, isAuthenticated } = useSessionSWR()
|
theme,
|
||||||
const { resolvedTheme, setTheme } = useTheme()
|
isAuthenticated
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
}: {
|
||||||
const toggleTheme = () => {
|
theme: string
|
||||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
isAuthenticated: boolean
|
||||||
}
|
}) {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMounted(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="mt-4 flex h-16 items-center justify-start md:justify-between">
|
<header className={styles.header}>
|
||||||
<span className="hidden items-center md:flex">
|
<div className={styles.tabs}>
|
||||||
<Link href="/" className="mr-4 flex items-center">
|
<div className={styles.buttons}>
|
||||||
<Image
|
<HeaderButtons isAuthenticated={isAuthenticated} theme={theme} />
|
||||||
src={"/assets/logo.svg"}
|
</div>
|
||||||
width={32}
|
</div>
|
||||||
height={32}
|
<MobileHeader isAuthenticated={isAuthenticated} theme={theme} />
|
||||||
alt=""
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<span className="bg-transparent pl-4 text-lg font-bold">Drift</span>
|
|
||||||
</Link>
|
|
||||||
<nav className="flex space-x-4 lg:space-x-6">
|
|
||||||
<ul className="flex justify-center space-x-4">
|
|
||||||
<NavLink href="/home">Home</NavLink>
|
|
||||||
<NavLink href="/new" disabled={!isAuthenticated}>
|
|
||||||
New
|
|
||||||
</NavLink>
|
|
||||||
<NavLink href="/mine" disabled={!isAuthenticated}>
|
|
||||||
Yours
|
|
||||||
</NavLink>
|
|
||||||
<NavLink href="/settings" disabled={!isAuthenticated}>
|
|
||||||
Settings
|
|
||||||
</NavLink>
|
|
||||||
{isAdmin && <NavLink href="/admin">Admin</NavLink>}
|
|
||||||
{isAuthenticated !== undefined && (
|
|
||||||
<>
|
|
||||||
{isAuthenticated === true && (
|
|
||||||
<NavLink href="/signout">Sign Out</NavLink>
|
|
||||||
)}
|
|
||||||
{isAuthenticated === false && (
|
|
||||||
<NavLink href="/signin">Sign In</NavLink>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center justify-center md:hidden">
|
|
||||||
<MobileHeader />
|
|
||||||
</span>
|
|
||||||
{isMounted && (
|
|
||||||
<FadeIn>
|
|
||||||
<button
|
|
||||||
aria-hidden
|
|
||||||
className="ml-4 flex h-8 w-8 cursor-pointer items-center justify-center font-medium text-muted-foreground transition-colors hover:text-primary md:ml-0"
|
|
||||||
onClick={toggleTheme}
|
|
||||||
title="Toggle theme"
|
|
||||||
>
|
|
||||||
{resolvedTheme === "dark" ? (
|
|
||||||
<Sun className="h-[16px] w-[16px]" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-[16px] w-[16px]" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</FadeIn>
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
className={cn(
|
|
||||||
baseClasses,
|
|
||||||
isActive && activeClasses,
|
|
||||||
disabled && disabledClasses
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
53
src/app/components/header/mobile.module.css
Normal file
53
src/app/components/header/mobile.module.css
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,56 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button, buttonVariants } from "@components/button"
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
||||||
|
import buttonStyles from "@components/button/button.module.css"
|
||||||
|
import Button from "@components/button"
|
||||||
import { Menu } from "react-feather"
|
import { Menu } from "react-feather"
|
||||||
import { HeaderButtons } from "./buttons"
|
import clsx from "clsx"
|
||||||
import * as DropdownMenu from "@components/dropdown-menu"
|
import styles from "./mobile.module.css"
|
||||||
import React from "react"
|
import { getButtons } from "./buttons"
|
||||||
|
import { useSessionSWR } from "@lib/use-session-swr"
|
||||||
|
|
||||||
|
export default function MobileHeader({
|
||||||
|
isAuthenticated,
|
||||||
|
theme
|
||||||
|
}: {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
theme: string
|
||||||
|
}) {
|
||||||
|
const { isAdmin } = useSessionSWR()
|
||||||
|
const buttons = getButtons({
|
||||||
|
isAuthenticated,
|
||||||
|
theme,
|
||||||
|
isAdmin
|
||||||
|
})
|
||||||
|
|
||||||
export default function MobileHeader() {
|
|
||||||
// TODO: this is a hack to close the radix ui menu when a next link is clicked
|
// TODO: this is a hack to close the radix ui menu when a next link is clicked
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))
|
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.DropdownMenu>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.DropdownMenuTrigger
|
<DropdownMenu.Trigger
|
||||||
className={buttonVariants({ variant: "ghost" })}
|
className={clsx(buttonStyles.button, styles.mobileTrigger)}
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Button aria-label="Menu" variant={"ghost"}>
|
<Button aria-label="Menu" height="auto">
|
||||||
<Menu />
|
<Menu />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.DropdownMenuTrigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.DropdownMenuPortal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.DropdownMenuContent>
|
<DropdownMenu.Content>
|
||||||
{HeaderButtons().props.children.map((button: JSX.Element) => (
|
{buttons.map((button) => (
|
||||||
<DropdownMenu.DropdownMenuItem
|
<DropdownMenu.Item
|
||||||
key={`mobile-${button?.key}`}
|
key={`mobile-${button?.key}`}
|
||||||
|
className={styles.dropdownItem}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
</DropdownMenu.DropdownMenuItem>
|
</DropdownMenu.Item>
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.DropdownMenuContent>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.DropdownMenuPortal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu.DropdownMenu>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +1,82 @@
|
||||||
import * as React from "react"
|
import clsx from "clsx"
|
||||||
|
import React from "react"
|
||||||
|
import styles from "./input.module.css"
|
||||||
|
|
||||||
import { cn } from "@lib/cn"
|
type Props = React.HTMLProps<HTMLInputElement> & {
|
||||||
|
|
||||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
||||||
label?: string
|
label?: string
|
||||||
hideLabel?: boolean
|
width?: number | string
|
||||||
|
height?: number | string
|
||||||
|
labelClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<Props, "onChange" | "value" | "label" | "aria-label"> &
|
||||||
|
(
|
||||||
|
| {
|
||||||
|
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<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, label, hideLabel, ...props }, ref) => {
|
(
|
||||||
const id = React.useId()
|
{ label, className, required, width, height, labelClassName, ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const labelId = label?.replace(/\s/g, "-").toLowerCase()
|
||||||
return (
|
return (
|
||||||
<span className="flex w-full flex-row items-center">
|
<div
|
||||||
{label && !hideLabel ? (
|
className={styles.wrapper}
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={labelId}
|
||||||
className={cn(
|
className={clsx(styles.label, labelClassName)}
|
||||||
"h-10 rounded-md border border-input bg-transparent px-3 py-2 text-sm font-medium text-muted-foreground",
|
|
||||||
"rounded-br-none rounded-tr-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
) : null}
|
)}
|
||||||
{label && hideLabel ? (
|
|
||||||
<label htmlFor={id} className="sr-only">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
) : null}
|
|
||||||
<input
|
<input
|
||||||
type={type}
|
|
||||||
className={cn(
|
|
||||||
"flex h-10 w-full border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
label && !hideLabel
|
|
||||||
? "rounded-bl-none rounded-tl-none border-l-0"
|
|
||||||
: "rounded-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={labelId}
|
||||||
|
className={clsx(styles.input, label && styles.withLabel, className)}
|
||||||
|
required={required}
|
||||||
{...props}
|
{...props}
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
...(props.style || {})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Input.displayName = "Input"
|
Input.displayName = "Input"
|
||||||
|
|
||||||
export { Input }
|
export default Input
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
import Link from "@components/link"
|
|
||||||
|
|
||||||
export default function Layout({
|
export default function Layout({
|
||||||
children,
|
children,
|
||||||
|
@ -13,22 +12,7 @@ export default function Layout({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.page, forSites && styles.forSites)}>
|
<div className={clsx(styles.page, forSites && styles.forSites)}>
|
||||||
<div className="flex flex-col justify-between h-screen">
|
{children}
|
||||||
<div> {children}</div>
|
|
||||||
<footer className="mx-auto h-4 max-w-[var(--main-content)] text-center text-sm text-gray-500">
|
|
||||||
<p>
|
|
||||||
Drift is an open source project by{" "}
|
|
||||||
<Link colored href="https://twitter.com/Max_Leiter">
|
|
||||||
Max Leiter
|
|
||||||
</Link>
|
|
||||||
. You can view the source code on{" "}
|
|
||||||
<Link colored href="https://github.com/MaxLeiter/Drift">
|
|
||||||
GitHub
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.page {
|
.page {
|
||||||
|
max-width: var(--main-content);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
import { cn } from "@lib/cn"
|
import styles from "./link.module.css"
|
||||||
|
|
||||||
type LinkProps = {
|
type LinkProps = {
|
||||||
colored?: boolean
|
colored?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
} & React.ComponentProps<typeof NextLink>
|
} & React.ComponentProps<typeof NextLink>
|
||||||
|
|
||||||
const Link = ({ colored, className, children, ...props }: LinkProps) => {
|
const Link = ({ colored, children, ...props }: LinkProps) => {
|
||||||
const classes = colored ? "text-blue-500 dark:text-blue-400 hover:underline" : "hover:underline"
|
const className = colored ? `${styles.link} ${styles.color}` : styles.link
|
||||||
return (
|
return (
|
||||||
<NextLink {...props} className={cn(classes, className)}>
|
<NextLink {...props} className={className}>
|
||||||
{children}
|
{children}
|
||||||
</NextLink>
|
</NextLink>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,128 +0,0 @@
|
||||||
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<typeof NavigationMenuPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-10 flex flex-1 items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<NavigationMenuViewport />
|
|
||||||
</NavigationMenuPrimitive.Root>
|
|
||||||
))
|
|
||||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const NavigationMenuList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
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<typeof NavigationMenuPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}{" "}
|
|
||||||
<ChevronDown
|
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</NavigationMenuPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const NavigationMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
|
||||||
|
|
||||||
const NavigationMenuViewport = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
|
||||||
<NavigationMenuPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
NavigationMenuViewport.displayName =
|
|
||||||
NavigationMenuPrimitive.Viewport.displayName
|
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<NavigationMenuPrimitive.Indicator
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
|
||||||
</NavigationMenuPrimitive.Indicator>
|
|
||||||
))
|
|
||||||
NavigationMenuIndicator.displayName =
|
|
||||||
NavigationMenuPrimitive.Indicator.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
navigationMenuTriggerStyle,
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuIndicator,
|
|
||||||
NavigationMenuViewport
|
|
||||||
}
|
|
|
@ -10,10 +10,7 @@ const Note = ({
|
||||||
type: "info" | "warning" | "error"
|
type: "info" | "warning" | "error"
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
} & React.ComponentProps<"div">) => (
|
} & React.ComponentProps<"div">) => (
|
||||||
<div
|
<div className={clsx(className, styles.note, styles[type])} {...props}>
|
||||||
className={clsx(className, styles.note, styles[type], "text-sm")}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { cn } from "@lib/cn"
|
|
||||||
import { PropsWithChildren } from "react"
|
|
||||||
|
|
||||||
export function PageTitle({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PropsWithChildren<React.HTMLProps<HTMLHeadingElement>>) {
|
|
||||||
return (
|
|
||||||
<h1 className={cn("pb-2 pt-2 text-4xl font-bold", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</h1>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { cn } from "@lib/cn"
|
|
||||||
import { PropsWithChildren } from "react"
|
|
||||||
|
|
||||||
export function PageWrapper({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PropsWithChildren<React.HTMLProps<HTMLDivElement>>) {
|
|
||||||
return (
|
|
||||||
<div className={cn("mb-4 mt-4 flex flex-col gap-4", className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,17 +1,9 @@
|
||||||
import { Input } from "@components/input"
|
import Button from "@components/button"
|
||||||
|
import Input from "@components/input"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { MouseEventHandler, useState } from "react"
|
import * as Dialog from "@radix-ui/react-dialog"
|
||||||
|
import { useState } from "react"
|
||||||
import styles from "./modal.module.css"
|
import styles from "./modal.module.css"
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogFooter
|
|
||||||
} from "@components/alert-dialog"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
creating: boolean
|
creating: boolean
|
||||||
|
@ -30,8 +22,7 @@ const PasswordModal = ({
|
||||||
const [confirmPassword, setConfirmPassword] = useState<string>("")
|
const [confirmPassword, setConfirmPassword] = useState<string>("")
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
const onSubmit: MouseEventHandler<HTMLButtonElement> = (e) => {
|
const onSubmit = () => {
|
||||||
e.preventDefault()
|
|
||||||
if (!password || (creating && !confirmPassword)) {
|
if (!password || (creating && !confirmPassword)) {
|
||||||
setError("Please enter a password")
|
setError("Please enter a password")
|
||||||
return
|
return
|
||||||
|
@ -48,57 +39,60 @@ const PasswordModal = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
<AlertDialog
|
<Dialog.Root
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) onClose()
|
if (!open) onClose()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <AlertDialogOverlay className={styles.overlay} /> */}
|
<Dialog.Portal>
|
||||||
<AlertDialogContent onEscapeKeyDown={onClose}>
|
<Dialog.Overlay className={styles.overlay} />
|
||||||
<AlertDialogHeader>
|
<Dialog.Content
|
||||||
<AlertDialogTitle>
|
className={styles.content}
|
||||||
|
onEscapeKeyDown={onClose}
|
||||||
|
>
|
||||||
|
<Dialog.Title>
|
||||||
{creating ? "Add a password" : "Enter password"}
|
{creating ? "Add a password" : "Enter password"}
|
||||||
</AlertDialogTitle>
|
</Dialog.Title>
|
||||||
<AlertDialogDescription>
|
<Dialog.Description>
|
||||||
{creating
|
{creating
|
||||||
? "Enter a password to protect your post"
|
? "Enter a password to protect your post"
|
||||||
: "Enter the password to access the post"}
|
: "Enter the password to access the post"}
|
||||||
</AlertDialogDescription>
|
</Dialog.Description>
|
||||||
</AlertDialogHeader>
|
<fieldset className={styles.fieldset}>
|
||||||
<fieldset className={styles.fieldset}>
|
{!error && creating && (
|
||||||
{!error && creating && (
|
<Note type="warning">
|
||||||
<Note type="warning">
|
This doesn't protect your post from the server
|
||||||
This doesn't protect your post from the server
|
administrator.
|
||||||
administrator.
|
</Note>
|
||||||
</Note>
|
)}
|
||||||
)}
|
{error && <Note type="error">{error}</Note>}
|
||||||
{error && <Note type="error">{error}</Note>}
|
|
||||||
<Input
|
|
||||||
width={"100%"}
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
{creating && (
|
|
||||||
<Input
|
<Input
|
||||||
width={"100%"}
|
width={"100%"}
|
||||||
label="Confirm"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Confirm Password"
|
placeholder="Password"
|
||||||
value={confirmPassword}
|
value={password}
|
||||||
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
|
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
)}
|
{creating && (
|
||||||
</fieldset>
|
<Input
|
||||||
<AlertDialogFooter>
|
width={"100%"}
|
||||||
<AlertDialogCancel onClick={onClose}>Cancel</AlertDialogCancel>
|
label="Confirm"
|
||||||
<AlertDialogAction onClick={onSubmit}>Submit</AlertDialogAction>
|
type="password"
|
||||||
</AlertDialogFooter>
|
placeholder="Confirm Password"
|
||||||
</AlertDialogContent>
|
value={confirmPassword}
|
||||||
</AlertDialog>
|
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</fieldset>
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button onClick={onSubmit}>Submit</Button>
|
||||||
|
</footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,31 +1,35 @@
|
||||||
"use client"
|
// largely from https://github.com/shadcn/taxonomy
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import styles from "./popover.module.css"
|
||||||
|
|
||||||
import { cn } from "@lib/cn"
|
type PopoverProps = PopoverPrimitive.PopoverProps
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
export function Popover({ ...props }: PopoverProps) {
|
||||||
|
return <PopoverPrimitive.Root {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
Popover.Trigger = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
PopoverPrimitive.PopoverTriggerProps
|
||||||
|
>(function PopoverTrigger({ ...props }, ref) {
|
||||||
|
return <PopoverPrimitive.Trigger {...props} ref={ref} />
|
||||||
|
})
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
Popover.Portal = PopoverPrimitive.Portal
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
Popover.Content = React.forwardRef<
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
HTMLDivElement,
|
||||||
<PopoverPrimitive.Portal>
|
PopoverPrimitive.PopoverContentProps
|
||||||
|
>(function PopoverContent({ className, ...props }, ref) {
|
||||||
|
return (
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align="end"
|
||||||
sideOffset={sideOffset}
|
className={clsx(styles.root, className)}
|
||||||
className={cn(
|
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
)
|
||||||
))
|
})
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent }
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styles from "./post-list.module.css"
|
||||||
import ListItem from "./list-item"
|
import ListItem from "./list-item"
|
||||||
import { ChangeEvent, useCallback, useState } from "react"
|
import { ChangeEvent, useCallback, useState } from "react"
|
||||||
import type { PostWithFiles } from "@lib/server/prisma"
|
import type { PostWithFiles } from "@lib/server/prisma"
|
||||||
import { Input } from "@components/input"
|
import Input from "@components/input"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { ListItemSkeleton } from "./list-item-skeleton"
|
import { ListItemSkeleton } from "./list-item-skeleton"
|
||||||
import Link from "@components/link"
|
import Link from "@components/link"
|
||||||
|
@ -83,10 +83,7 @@ const PostList = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!res?.ok) {
|
if (!res?.ok) {
|
||||||
setToast({
|
console.error(res)
|
||||||
message: "Failed to delete post",
|
|
||||||
type: "error"
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
setPosts((posts) => posts?.filter((post) => post.id !== postId))
|
setPosts((posts) => posts?.filter((post) => post.id !== postId))
|
||||||
|
@ -106,7 +103,7 @@ const PostList = ({
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
onChange={onSearchChange}
|
onChange={onSearchChange}
|
||||||
disabled={!posts || posts.length === 0}
|
disabled={!posts}
|
||||||
style={{ maxWidth: 300 }}
|
style={{ maxWidth: 300 }}
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
|
@ -135,7 +132,6 @@ const PostList = ({
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
{!showSkeleton && posts && posts.length === 0 && <NoPostsFound />}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,24 @@
|
||||||
import styles from "./list-item.module.css"
|
import styles from "./list-item.module.css"
|
||||||
import { Card, CardContent, CardHeader } from "@components/card"
|
import Card from "@components/card"
|
||||||
import Skeleton from "@components/skeleton"
|
import Skeleton from "@components/skeleton"
|
||||||
|
|
||||||
export const ListItemSkeleton = () => (
|
export const ListItemSkeleton = () => (
|
||||||
<li>
|
<li>
|
||||||
<Card style={{ overflowY: "scroll" }}>
|
<Card style={{ overflowY: "scroll" }}>
|
||||||
{/* TODO: this is a bad way to do skeletons and is only accurate on desktop */}
|
{/* TODO: this is a bad way to do skeletons and is onlya ccurate on desktop */}
|
||||||
<CardHeader>
|
<div style={{ display: "flex", gap: 16, marginBottom: 14 }}>
|
||||||
<div style={{ display: "flex", gap: 16, marginBottom: 14 }}>
|
<div className={styles.title}>
|
||||||
<div className={styles.title}>
|
{/* title */}
|
||||||
{/* title */}
|
<Skeleton width={80} height={32} />
|
||||||
<Skeleton width={80} height={32} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.badges}>
|
|
||||||
<Skeleton width={60} height={32} />
|
|
||||||
<Skeleton width={60} height={32} />
|
|
||||||
<Skeleton width={60} height={32} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
<div className={styles.badges}>
|
||||||
<Skeleton width={100} height={32} />
|
<Skeleton width={60} height={32} />
|
||||||
</CardContent>
|
<Skeleton width={60} height={32} />
|
||||||
|
<Skeleton width={60} height={32} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Skeleton width={100} height={32} />
|
||||||
</Card>
|
</Card>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.titleText {
|
.titleText {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--gap-half);
|
gap: var(--gap-half);
|
||||||
|
@ -10,6 +15,11 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--gap-half);
|
||||||
|
}
|
||||||
|
|
||||||
.oneline {
|
.oneline {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -40,10 +50,17 @@
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--gap-half);
|
gap: var(--gap);
|
||||||
padding: var(--gap-quarter);
|
padding: var(--gap-quarter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--gap);
|
||||||
|
color: var(--darker-gray);
|
||||||
|
}
|
||||||
|
|
||||||
li a:hover {
|
li a:hover {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -6,31 +6,20 @@ import { useRouter } from "next/navigation"
|
||||||
import styles from "./list-item.module.css"
|
import styles from "./list-item.module.css"
|
||||||
import Link from "@components/link"
|
import Link from "@components/link"
|
||||||
import type { PostWithFiles } from "@lib/server/prisma"
|
import type { PostWithFiles } from "@lib/server/prisma"
|
||||||
import { Badge } from "@components/badges/badge"
|
import Tooltip from "@components/tooltip"
|
||||||
import {
|
import Badge from "@components/badges/badge"
|
||||||
Card,
|
import Card from "@components/card"
|
||||||
CardContent,
|
import Button from "@components/button"
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@components/card"
|
|
||||||
import {
|
import {
|
||||||
ArrowUpCircle,
|
ArrowUpCircle,
|
||||||
Code,
|
Code,
|
||||||
Database,
|
Database,
|
||||||
Edit,
|
Edit,
|
||||||
FileText,
|
FileText,
|
||||||
MoreVertical,
|
|
||||||
Terminal,
|
Terminal,
|
||||||
Trash
|
Trash
|
||||||
} from "react-feather"
|
} from "react-feather"
|
||||||
import { codeFileExtensions } from "@lib/constants"
|
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
|
// TODO: isOwner should default to false so this can be used generically
|
||||||
const ListItem = ({
|
const ListItem = ({
|
||||||
|
@ -77,88 +66,80 @@ const ListItem = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FadeIn key={post.id} as="li">
|
<FadeIn key={post.id}>
|
||||||
<Card className="overflow-y-scroll h-42">
|
<li>
|
||||||
<CardHeader>
|
<Card style={{ overflowY: "scroll" }}>
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
<>
|
||||||
<span className={styles.titleText}>
|
<div className={styles.title}>
|
||||||
<h4 style={{ display: "inline-block", margin: 0 }}>
|
<span className={styles.titleText}>
|
||||||
<Link
|
<h4 style={{ display: "inline-block", margin: 0 }}>
|
||||||
colored
|
<Link
|
||||||
style={{ marginRight: "var(--gap)" }}
|
colored
|
||||||
href={`/post/${post.id}`}
|
style={{ marginRight: "var(--gap)" }}
|
||||||
>
|
href={`/post/${post.id}`}
|
||||||
{post.title}
|
>
|
||||||
</Link>
|
{post.title}
|
||||||
</h4>
|
</Link>
|
||||||
<div className={styles.badges}>
|
</h4>
|
||||||
<VisibilityBadge visibility={post.visibility} />
|
<div className={styles.badges}>
|
||||||
<Badge variant={"outline"}>
|
<VisibilityBadge visibility={post.visibility} />
|
||||||
{post.files?.length === 1
|
<Badge type="secondary">
|
||||||
? "1 file"
|
{post.files?.length === 1
|
||||||
: `${post.files?.length || 0} files`}
|
? "1 file"
|
||||||
</Badge>
|
: `${post.files?.length || 0} files`}
|
||||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
</Badge>
|
||||||
<ExpirationBadge postExpirationDate={post.expiresAt} />
|
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||||
</div>
|
<ExpirationBadge postExpirationDate={post.expiresAt} />
|
||||||
</span>
|
</div>
|
||||||
{!hideActions ? (
|
|
||||||
<span className="flex gap-2">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<MoreVertical className="cursor-pointer" />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="mt-2 border rounded-md shadow-sm border-border bg-background">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
editACopy()
|
|
||||||
}}
|
|
||||||
className="cursor-pointer bg-background"
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4 mr-2" /> Edit a copy
|
|
||||||
</DropdownMenuItem>
|
|
||||||
{isOwner && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
deletePost()
|
|
||||||
}}
|
|
||||||
className="cursor-pointer bg-background"
|
|
||||||
>
|
|
||||||
<Trash className="w-4 h-4 mr-2" />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{post.parentId && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={() => {
|
|
||||||
viewParentClick()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowUpCircle className="w-4 h-4 mr-2" />
|
|
||||||
View parent
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
{!hideActions ? (
|
||||||
</CardTitle>
|
<span className={styles.buttons}>
|
||||||
{post.description && (
|
{post.parentId && (
|
||||||
<CardDescription>
|
<Tooltip content={"View parent"}>
|
||||||
|
<Button
|
||||||
|
iconRight={<ArrowUpCircle />}
|
||||||
|
onClick={viewParentClick}
|
||||||
|
// TODO: not perfect on mobile
|
||||||
|
height={38}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip content={"Make a copy"}>
|
||||||
|
<Button
|
||||||
|
iconRight={<Edit />}
|
||||||
|
onClick={editACopy}
|
||||||
|
height={38}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{isOwner && (
|
||||||
|
<Tooltip content={"Delete"}>
|
||||||
|
<Button
|
||||||
|
iconRight={<Trash />}
|
||||||
|
onClick={deletePost}
|
||||||
|
height={38}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{post.description && (
|
||||||
<p className={styles.oneline}>{post.description}</p>
|
<p className={styles.oneline}>{post.description}</p>
|
||||||
</CardDescription>
|
)}
|
||||||
)}
|
</>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ul className={styles.files}>
|
<ul className={styles.files}>
|
||||||
{post?.files?.map(
|
{post?.files?.map(
|
||||||
(file: Pick<PostWithFiles, "files">["files"][0]) => {
|
(file: Pick<PostWithFiles, "files">["files"][0]) => {
|
||||||
return (
|
return (
|
||||||
<li key={file.id} className="text-black">
|
<li key={file.id}>
|
||||||
<Link
|
<Link
|
||||||
colored
|
colored
|
||||||
href={`/post/${post.id}#${file.title}`}
|
href={`/post/${post.id}#${file.title}`}
|
||||||
className="flex items-center gap-2 font-mono text-sm text-foreground"
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{getIconFromFilename(file.title)}
|
{getIconFromFilename(file.title)}
|
||||||
{file.title || "Untitled file"}
|
{file.title || "Untitled file"}
|
||||||
|
@ -168,8 +149,8 @@ const ListItem = ({
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
</li>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@components/button"
|
import Button from "@components/button"
|
||||||
import { Tooltip } from "@components/tooltip"
|
import Tooltip from "@components/tooltip"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { ChevronUp } from "react-feather"
|
import { ChevronUp } from "react-feather"
|
||||||
import styles from "./scroll.module.css"
|
import styles from "./scroll.module.css"
|
||||||
|
@ -39,10 +39,8 @@ const ScrollToTop = () => {
|
||||||
<Button
|
<Button
|
||||||
aria-label="Scroll to Top"
|
aria-label="Scroll to Top"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={"secondary"}
|
iconLeft={<ChevronUp />}
|
||||||
>
|
/>
|
||||||
<ChevronUp />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@components/card"
|
import Card from "@components/card"
|
||||||
import styles from "./settings-group.module.css"
|
import styles from "./settings-group.module.css"
|
||||||
|
|
||||||
type Props =
|
type Props =
|
||||||
|
@ -26,13 +26,9 @@ const SettingsGroup = ({ title, children, skeleton }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<h4>{title}</h4>
|
||||||
<CardTitle>{title}</CardTitle>
|
<hr />
|
||||||
</CardHeader>
|
<div className={styles.content}>{children}</div>
|
||||||
<hr className="pb-4" />
|
|
||||||
<CardContent>
|
|
||||||
<div className={styles.content}>{children}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue