enabled react-compiler; added color picker; menu bar sorted
This commit is contained in:
parent
66aadfd04d
commit
96b989799a
@ -5,6 +5,10 @@
|
||||
import "./src/env.js";
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {};
|
||||
const config = {
|
||||
experimental: {
|
||||
reactCompiler: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
"@trpc/client": "^11.0.0-rc.446",
|
||||
"@trpc/react-query": "^11.0.0-rc.446",
|
||||
"@trpc/server": "^11.0.0-rc.446",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.33.0",
|
||||
@ -55,6 +56,7 @@
|
||||
"next-themes": "^0.4.4",
|
||||
"postgres": "^3.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
|
||||
78
pnpm-lock.yaml
generated
78
pnpm-lock.yaml
generated
@ -83,6 +83,9 @@ importers:
|
||||
'@trpc/server':
|
||||
specifier: ^11.0.0-rc.446
|
||||
version: 11.0.0-rc.824(typescript@5.8.2)
|
||||
babel-plugin-react-compiler:
|
||||
specifier: 19.0.0-beta-40c6c23-20250301
|
||||
version: 19.0.0-beta-40c6c23-20250301
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@ -94,16 +97,16 @@ importers:
|
||||
version: 0.33.0(@types/react@18.3.18)(postgres@3.4.5)(react@18.3.1)
|
||||
geist:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.1(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
version: 1.3.1(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
|
||||
lucide-react:
|
||||
specifier: ^0.477.0
|
||||
version: 0.477.0(react@18.3.1)
|
||||
next:
|
||||
specifier: ^15.0.1
|
||||
version: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
version: 15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.25
|
||||
version: 5.0.0-beta.25(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 5.0.0-beta.25(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
next-themes:
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -113,6 +116,9 @@ importers:
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
react-colorful:
|
||||
specifier: ^5.6.1
|
||||
version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
@ -233,10 +239,22 @@ packages:
|
||||
'@auth/drizzle-adapter@1.8.0':
|
||||
resolution: {integrity: sha512-cxApE0h5WcyDsgGix9hzmWmCz0qxvmMJexAOQmI6R/YXYxrZ/mKBKu0BlfgQBR6z2XvNWl4wbEGchwSenSCksQ==}
|
||||
|
||||
'@babel/helper-string-parser@7.25.9':
|
||||
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9':
|
||||
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.26.9':
|
||||
resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.26.9':
|
||||
resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
@ -1719,6 +1737,9 @@ packages:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301:
|
||||
resolution: {integrity: sha512-himtjPafvMbA7PYnV2L+jprpB3h4rhx/n5s4L3gC654FOUsmsv5n4p8d6ufvK2zqUQs4kTOjgT2b4wnuDU32CA==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@ -2990,6 +3011,12 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
react-colorful@5.6.1:
|
||||
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
peerDependencies:
|
||||
@ -3525,10 +3552,19 @@ snapshots:
|
||||
- '@simplewebauthn/server'
|
||||
- nodemailer
|
||||
|
||||
'@babel/helper-string-parser@7.25.9': {}
|
||||
|
||||
'@babel/helper-validator-identifier@7.25.9': {}
|
||||
|
||||
'@babel/runtime@7.26.9':
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/types@7.26.9':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
'@babel/helper-validator-identifier': 7.25.9
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
@ -4782,6 +4818,10 @@ snapshots:
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301:
|
||||
dependencies:
|
||||
'@babel/types': 7.26.9
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
@ -5186,8 +5226,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.4(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@ -5206,7 +5246,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
@ -5217,18 +5257,18 @@ snapshots:
|
||||
stable-hash: 0.0.4
|
||||
tinyglobby: 0.2.12
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -5236,7 +5276,7 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@ -5247,7 +5287,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@ -5458,9 +5498,9 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
geist@1.3.1(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
geist@1.3.1(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
|
||||
dependencies:
|
||||
next: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
@ -5832,10 +5872,10 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-auth@5.0.0-beta.25(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
next-auth@5.0.0-beta.25(next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@auth/core': 0.37.2
|
||||
next: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next: 15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
|
||||
next-themes@0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
@ -5843,7 +5883,7 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
next@15.2.1(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@next/env': 15.2.1
|
||||
'@swc/counter': 0.1.3
|
||||
@ -5863,6 +5903,7 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 15.2.1
|
||||
'@next/swc-win32-arm64-msvc': 15.2.1
|
||||
'@next/swc-win32-x64-msvc': 15.2.1
|
||||
babel-plugin-react-compiler: 19.0.0-beta-40c6c23-20250301
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@ -6159,6 +6200,11 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
react-colorful@5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import "./styles.css";
|
||||
import { Article } from "@/server/db/schema";
|
||||
|
||||
import React from "react";
|
||||
@ -18,37 +17,44 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { articleSchema } from "@/lib/validation/zod/article";
|
||||
import { debounce } from "@/lib/utils";
|
||||
import { cn, debounce } from "@/lib/utils";
|
||||
import { updateArticle } from "@/server/actions/article";
|
||||
import Editor from "./editor";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export default ({ server_article }: { server_article: Article }) => {
|
||||
const form = useForm<z.infer<typeof articleSchema>>({
|
||||
resolver: zodResolver(articleSchema),
|
||||
defaultValues: {
|
||||
title: server_article?.title ?? "",
|
||||
content:
|
||||
server_article?.content ??
|
||||
`<h2>
|
||||
const defaultValues = {
|
||||
title: server_article?.title ?? "",
|
||||
content:
|
||||
server_article?.content ??
|
||||
`<h2>
|
||||
Hey bearbeite mich!
|
||||
</h2>`,
|
||||
},
|
||||
};
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const form = useForm<z.infer<typeof articleSchema>>({
|
||||
resolver: zodResolver(articleSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
// 2. Define a submit handler.
|
||||
async function onSubmit(values: z.infer<typeof articleSchema>) {
|
||||
setLoading(true);
|
||||
await updateArticle(values, server_article.id);
|
||||
setLoading(false);
|
||||
form.reset(values);
|
||||
}
|
||||
const debouncedSubmit = React.useCallback(
|
||||
debounce(() => {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}, 1000),
|
||||
}, 3000),
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
@ -56,6 +62,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<TextareaAutosize
|
||||
className="w-full resize-none text-4xl font-bold focus-visible:outline-none"
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
@ -67,28 +74,42 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex w-full gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Editor
|
||||
editorProviderProps={{
|
||||
editorProps: { attributes: { class: "min-h-64" } },
|
||||
content: field.value,
|
||||
onUpdate: (value) => {
|
||||
field.onChange(value.editor.getHTML());
|
||||
debouncedSubmit();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Editor
|
||||
editorProviderProps={{
|
||||
content: field.value,
|
||||
onUpdate: (value) => {
|
||||
field.onChange(value.editor.getHTML());
|
||||
debouncedSubmit();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-4 h-max w-full max-w-md rounded-md border-t-2 bg-muted p-4",
|
||||
loading && "border-t-blue-600",
|
||||
)}
|
||||
>
|
||||
<Badge>
|
||||
{!form.formState.isDirty && !loading
|
||||
? "gespeichert"
|
||||
: "nicht gespeichert"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import "./styles.css";
|
||||
import React from "react";
|
||||
import { EditorProvider, EditorProviderProps } from "@tiptap/react";
|
||||
import { MenuBar } from "./menu-bar";
|
||||
@ -12,12 +14,14 @@ function Editor({
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<EditorProvider
|
||||
immediatelyRender={false}
|
||||
extensions={extensions}
|
||||
slotBefore={!readOnly && <MenuBar />}
|
||||
{...editorProviderProps}
|
||||
/>
|
||||
<div className="rounded-md bg-gradient-to-b from-muted to-background p-2">
|
||||
<EditorProvider
|
||||
immediatelyRender={false}
|
||||
extensions={extensions}
|
||||
slotBefore={!readOnly && <MenuBar />}
|
||||
{...editorProviderProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,9 @@ export const extensions = [
|
||||
/* @ts-ignore */
|
||||
TextStyle.configure({ types: [ListItem.name] }),
|
||||
StarterKit.configure({
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: {},
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import ColorPickerPopover from "@/components/color-picker/color-picker-popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { RGBAToHexA } from "@/lib/utils";
|
||||
import { useCurrentEditor } from "@tiptap/react";
|
||||
import {
|
||||
BoldIcon,
|
||||
@ -17,6 +20,7 @@ import {
|
||||
SeparatorHorizontalIcon,
|
||||
StrikethroughIcon,
|
||||
TextIcon,
|
||||
TypeIcon,
|
||||
UndoIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
@ -28,7 +32,10 @@ export const MenuBar = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="control-group my-4">
|
||||
<div className="menu-bar mb-4 flex items-center justify-between space-y-2">
|
||||
{/* <div className="flex w-full items-center justify-between">
|
||||
|
||||
</div> */}
|
||||
<div className="Button-group flex items-center gap-1">
|
||||
<Button
|
||||
size={"icon"}
|
||||
@ -44,7 +51,9 @@ export const MenuBar = () => {
|
||||
variant={"outline"}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
||||
className={editor.isActive("italic") ? "is-active" : ""}
|
||||
className={
|
||||
editor.isActive("italic") ? "bg-foreground text-background" : ""
|
||||
}
|
||||
>
|
||||
<ItalicIcon className="size-4" />
|
||||
</Button>
|
||||
@ -57,7 +66,7 @@ export const MenuBar = () => {
|
||||
>
|
||||
<StrikethroughIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
@ -65,7 +74,7 @@ export const MenuBar = () => {
|
||||
className={editor.isActive("code") ? "is-active" : ""}
|
||||
>
|
||||
<CodeIcon className="size-4" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
<Button
|
||||
variant={"outline"}
|
||||
@ -73,7 +82,7 @@ export const MenuBar = () => {
|
||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||
className={editor.isActive("paragraph") ? "is-active" : ""}
|
||||
>
|
||||
<TextIcon className="size-4" />
|
||||
<TypeIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@ -152,14 +161,14 @@ export const MenuBar = () => {
|
||||
>
|
||||
<ListOrderedIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
className={editor.isActive("codeBlock") ? "is-active" : ""}
|
||||
>
|
||||
<CodeSquareIcon className="size-4" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
@ -175,35 +184,12 @@ export const MenuBar = () => {
|
||||
>
|
||||
<SeparatorHorizontalIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => editor.chain().focus().setHardBreak().run()}
|
||||
>
|
||||
Hard Break
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => editor.chain().focus().setColor("#958DF1").run()}
|
||||
className={
|
||||
editor.isActive("textStyle", { color: "#958DF1" })
|
||||
? "is-active"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
Purple
|
||||
</Button>
|
||||
{/* <Button
|
||||
variant={"outline"}
|
||||
onClick={() => editor.chain().focus().unsetAllMarks().run()}
|
||||
>
|
||||
Formatierung aufheben
|
||||
</Button>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => editor.chain().focus().clearNodes().run()}
|
||||
>
|
||||
Clear nodes
|
||||
</Button> */}
|
||||
<ColorPickerPopover
|
||||
// initialColor={editor.getAttributes("textStyle").color}
|
||||
onInput={(color) => editor.chain().focus().setColor(color).run()}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
|
||||
@ -7,16 +7,28 @@
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
.tiptap ul,
|
||||
/* .tiptap ul,
|
||||
.tiptap ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
}
|
||||
} */
|
||||
|
||||
.tiptap ul li p,
|
||||
.tiptap ol li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
@apply my-1;
|
||||
}
|
||||
|
||||
.tiptap ul,
|
||||
ol {
|
||||
@apply ml-4 p-1;
|
||||
}
|
||||
|
||||
.tiptap ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
.tiptap ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
@ -26,42 +38,38 @@
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h6 {
|
||||
@apply my-2;
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.tiptap h1,
|
||||
.tiptap h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tiptap h1 {
|
||||
font-size: 1.4rem;
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
.tiptap h2 {
|
||||
font-size: 1.2rem;
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
.tiptap h3 {
|
||||
font-size: 1.1rem;
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
.tiptap h4,
|
||||
.tiptap h5,
|
||||
.tiptap h4 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
.tiptap h5 {
|
||||
@apply text-xl;
|
||||
}
|
||||
.tiptap h6 {
|
||||
font-size: 1rem;
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
.tiptap code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
/* .tiptap code {
|
||||
@apply bg-foreground p-1 text-background;
|
||||
}
|
||||
|
||||
.tiptap pre {
|
||||
@ -78,18 +86,29 @@
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
} */
|
||||
|
||||
/* Blockquote styles */
|
||||
.tiptap blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
@apply m-6 border-l-2 pl-4;
|
||||
/* border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
padding-left: 1rem; */
|
||||
}
|
||||
|
||||
/* Horizontal rule styles */
|
||||
.tiptap hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
@apply my-1;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply rounded-md bg-muted-foreground/25;
|
||||
}
|
||||
|
||||
.menu-bar button {
|
||||
@apply duration-0;
|
||||
}
|
||||
|
||||
.menu-bar .is-active {
|
||||
@apply border-primary;
|
||||
}
|
||||
|
||||
20
src/components/color-picker/color-picker-popover.tsx
Normal file
20
src/components/color-picker/color-picker-popover.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import ColorPicker, { type ColorPickerProps } from ".";
|
||||
import { Button } from "../ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
|
||||
export default function ColorPickerPopover(props: ColorPickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button style={{ backgroundColor: props.initialColor }} className="">
|
||||
color
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<ColorPicker {...props} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
16
src/components/color-picker/color-picker.css
Normal file
16
src/components/color-picker/color-picker.css
Normal file
@ -0,0 +1,16 @@
|
||||
.color-picker .react-colorful {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
.color-picker .react-colorful__saturation {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.color-picker .react-colorful__hue {
|
||||
height: 40px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
.color-picker .react-colorful__hue-pointer {
|
||||
width: 12px;
|
||||
height: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
103
src/components/color-picker/index.tsx
Normal file
103
src/components/color-picker/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
import "./color-picker.css";
|
||||
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { Input } from "../ui/input";
|
||||
import { debounce } from "@/lib/utils";
|
||||
|
||||
const STORAGE_KEY = "savedColors";
|
||||
const MAX_COLORS = 12;
|
||||
const SELECT_DEBOUNCE = 500;
|
||||
|
||||
export type ColorPickerProps = {
|
||||
onInput?: (color: string) => void;
|
||||
initialColor?: string;
|
||||
};
|
||||
|
||||
function ColorPicker({ onInput, initialColor }: ColorPickerProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [customColors, setCustomColors] = useState<string[]>([]);
|
||||
const [color, setColor] = useState(initialColor ?? "#ff0000");
|
||||
|
||||
// Load colors from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (initialColor?.length) setColor(initialColor);
|
||||
const storageColors = localStorage.getItem(STORAGE_KEY);
|
||||
const storedColors = storageColors ? JSON.parse(storageColors) : [];
|
||||
if (storedColors.length) {
|
||||
setCustomColors(storedColors);
|
||||
if (!initialColor?.length) setColor(storedColors[0]);
|
||||
setMounted(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const persistColor = (newColor: string) => {
|
||||
if (newColor.length < 2) return;
|
||||
if (customColors[0] === newColor) return; // Prevent duplicate consecutive colors
|
||||
|
||||
const updatedColors = [
|
||||
newColor,
|
||||
...customColors.filter((c) => c !== newColor),
|
||||
].slice(0, MAX_COLORS);
|
||||
console.log(updatedColors);
|
||||
|
||||
setCustomColors(updatedColors);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedColors));
|
||||
};
|
||||
|
||||
const selectColor = (newColor: string) => {
|
||||
persistColor(newColor);
|
||||
onInput?.(newColor);
|
||||
};
|
||||
|
||||
const selectColorDebounced = debounce((newColor: string) => {
|
||||
selectColor(newColor);
|
||||
}, SELECT_DEBOUNCE);
|
||||
|
||||
const handleColorChange = (newColor: string, skipDebounce = false) => {
|
||||
setColor(newColor);
|
||||
if (skipDebounce)
|
||||
selectColor(newColor); // Delayed save
|
||||
else selectColorDebounced(newColor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="color-picker flex flex-col items-center gap-4">
|
||||
{/* React Colorful Picker */}
|
||||
<HexColorPicker
|
||||
color={color}
|
||||
onChange={handleColorChange}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
{/* Default Input (Native Color Picker) */}
|
||||
<div className="flex w-full gap-2">
|
||||
<div
|
||||
className="size-8 rounded-md border"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<Input
|
||||
className="h-8"
|
||||
value={color}
|
||||
onInput={(e) => handleColorChange(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Recent Colors */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{customColors.map((col) => (
|
||||
<button
|
||||
key={col}
|
||||
className="size-8 rounded border"
|
||||
style={{ backgroundColor: col }}
|
||||
onClick={() => handleColorChange(col, true)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorPicker;
|
||||
@ -20,3 +20,18 @@ export function debounce<T extends (...args: any[]) => void>(
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
export function RGBAToHexA(rgba: string, forceRemoveAlpha = false) {
|
||||
return (
|
||||
"#" +
|
||||
rgba
|
||||
.replace(/^rgba?\(|\s+|\)$/g, "") // Get's rgba / rgb string values
|
||||
.split(",") // splits them at ","
|
||||
.filter((string, index) => !forceRemoveAlpha || index !== 3)
|
||||
.map((string) => parseFloat(string)) // Converts them to numbers
|
||||
.map((number, index) => (index === 3 ? Math.round(number * 255) : number)) // Converts alpha to 255 number
|
||||
.map((number) => number.toString(16)) // Converts numbers to hex
|
||||
.map((string) => (string.length === 1 ? "0" + string : string)) // Adds 0 when length of one number is 1
|
||||
.join("")
|
||||
); // Puts the array to togehter to a string
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export async function updateArticle(
|
||||
articleId,
|
||||
});
|
||||
// if (!result[0]?.id?.length) return false;
|
||||
// return revalidatePath(`/artikel/${result[0].id}/edit`);
|
||||
// return revalidatePath(`/artikel/${result[0]?.slug}/edit`);
|
||||
}
|
||||
export async function deleteArticle(articleId: string) {
|
||||
const result = await api.article.delete({
|
||||
|
||||
@ -86,7 +86,7 @@ export const articleRouter = createTRPCRouter({
|
||||
.set(input.article)
|
||||
.where(eq(articles.id, input.articleId))
|
||||
.returning({
|
||||
id: articles.id,
|
||||
slug: articles.slug,
|
||||
});
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user