enabled react-compiler; added color picker; menu bar sorted

This commit is contained in:
shrt 2025-03-08 15:20:27 +01:00
parent 66aadfd04d
commit 96b989799a
14 changed files with 361 additions and 122 deletions

View File

@ -5,6 +5,10 @@
import "./src/env.js";
/** @type {import("next").NextConfig} */
const config = {};
const config = {
experimental: {
reactCompiler: true,
},
};
export default config;

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>
);
}

View 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;
}

View 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;

View File

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

View File

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

View File

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