fixed editor missing peaces
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 0s

This commit is contained in:
mr-shortman 2025-03-15 23:48:44 +01:00
parent d9ed115e56
commit 1597d4f113
29 changed files with 203 additions and 141 deletions

View File

@ -7,13 +7,4 @@ jobs:
runs-on: linux runs-on: linux
steps: steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" - run: Test "🎉 job completed! 🚀"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@ -67,6 +67,7 @@
"superjson": "^2.2.1", "superjson": "^2.2.1",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tiptap-extension-resize-image": "^1.2.1",
"use-debounce": "^10.0.4", "use-debounce": "^10.0.4",
"winston": "^3.17.0", "winston": "^3.17.0",
"zod": "^3.24.2" "zod": "^3.24.2"

16
pnpm-lock.yaml generated
View File

@ -149,6 +149,9 @@ importers:
tailwindcss-animate: tailwindcss-animate:
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.23)(typescript@5.8.2))) version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.23)(typescript@5.8.2)))
tiptap-extension-resize-image:
specifier: ^1.2.1
version: 1.2.1(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))(@tiptap/pm@2.11.5)
use-debounce: use-debounce:
specifier: ^10.0.4 specifier: ^10.0.4
version: 10.0.4(react@18.3.1) version: 10.0.4(react@18.3.1)
@ -4095,6 +4098,13 @@ packages:
tiptap-extension-global-drag-handle@0.1.18: tiptap-extension-global-drag-handle@0.1.18:
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==} resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
tiptap-extension-resize-image@1.2.1:
resolution: {integrity: sha512-SLMAujDa+0LN/6Iv2HtU4Uk0BL6LMh4b/r85frpdnjFDW2i6pIOfTVG8jzJQ8T1EgYHNn2YG1U2HoVAGuwLc3Q==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/extension-image': ^2.0.0
'@tiptap/pm': ^2.0.0
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -8492,6 +8502,12 @@ snapshots:
tiptap-extension-global-drag-handle@0.1.18: {} tiptap-extension-global-drag-handle@0.1.18: {}
tiptap-extension-resize-image@1.2.1(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))(@tiptap/pm@2.11.5):
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/extension-image': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
'@tiptap/pm': 2.11.5
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,5 +1,6 @@
import RenderArticle from "@/components/article/render-article";
import BreadNavigator from "@/components/bread-navigator"; import BreadNavigator from "@/components/bread-navigator";
import RenderContent from "@/components/editor/render-content";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { appRoutes } from "@/config"; import { appRoutes } from "@/config";
import { hasPermission, Role } from "@/lib/validation/permissions"; import { hasPermission, Role } from "@/lib/validation/permissions";
@ -21,8 +22,9 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<div className="w-full"> <div className="flex w-full items-center gap-4">
<BreadNavigator <BreadNavigator
className="w-full"
links={[ links={[
...(article?.category ...(article?.category
? [ ? [
@ -38,7 +40,14 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
/> />
</div> </div>
{isEditor && ( {isEditor && (
<div className="flex w-full justify-end space-x-2"> <div className="flex w-full items-center justify-end space-x-2">
<Badge
className="size-max"
variant={article.published ? "outline" : "destructive"}
>
{article.published ? "Veröffentlicht" : "Draft"}
</Badge>
<Button asChild variant={"outline"}> <Button asChild variant={"outline"}>
<Link href={appRoutes.editArticle(article.slug)}> <Link href={appRoutes.editArticle(article.slug)}>
<Edit className="size-4" /> <Edit className="size-4" />
@ -52,10 +61,7 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
)} )}
</div> </div>
<h1 className="text-4xl font-bold">{article.title}</h1> <h1 className="text-4xl font-bold">{article.title}</h1>
{article.content && <RenderContent content={article.content} />}
{article?.content?.length ? (
<RenderArticle content={article.content} />
) : null}
</div> </div>
); );
} }

View File

@ -13,14 +13,17 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import Avatar from "../avatar"; import Avatar from "../avatar";
import { Icons } from "../icons"; import { Icons } from "../icons";
import { Badge } from "../ui/badge";
function ArticleCard({ function ArticleCard({
title, title,
slug, slug,
author, author,
published,
createdAt, createdAt,
}: Pick<Article, "title" | "slug" | "createdAt" | "author">) { }: Pick<Article, "title" | "slug" | "createdAt" | "author" | "published">) {
const authorName = author?.name ?? `${appConfig.name} Team`; const authorName = author?.name ?? `${appConfig.name} Team`;
return ( return (
<Link href={appRoutes.article(slug)}> <Link href={appRoutes.article(slug)}>
<Card className="group flex h-full flex-col justify-between"> <Card className="group flex h-full flex-col justify-between">
@ -37,11 +40,21 @@ function ArticleCard({
)} )}
<span className="text-sm text-muted-foreground">{authorName}</span> <span className="text-sm text-muted-foreground">{authorName}</span>
</div> </div>
<p className="text-sm text-muted-foreground"> <div className="flex items-center space-x-2">
{createdAt.toLocaleDateString("de-DE", { {typeof published === "boolean" && (
dateStyle: "long", <Badge
})} className="size-max px-2 py-px text-xs"
</p> variant={published ? "outline" : "destructive"}
>
{published ? "Veröffentlicht" : "Draft"}
</Badge>
)}
<p className="text-sm text-muted-foreground">
{createdAt.toLocaleDateString("de-DE", {
dateStyle: "long",
})}
</p>
</div>
</CardFooter> </CardFooter>
</Card> </Card>
</Link> </Link>

View File

@ -85,6 +85,7 @@ function ArticleFilterBar({
<CategorySelect <CategorySelect
className="w-full" className="w-full"
initialValue={filter.category}
onSelect={(category) => { onSelect={(category) => {
onFilterChange({ onFilterChange({
category: category?.length ? category : undefined, category: category?.length ? category : undefined,

View File

@ -26,6 +26,7 @@ import { CheckCircle, XCircle } from "lucide-react";
import PublishArticleAlertDialog from "./publish-article-alert-dialog"; import PublishArticleAlertDialog from "./publish-article-alert-dialog";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { appRoutes } from "@/config";
const Editor = dynamic(() => import("../editor"), { ssr: false }); const Editor = dynamic(() => import("../editor"), { ssr: false });
export default ({ server_article }: { server_article: Article }) => { export default ({ server_article }: { server_article: Article }) => {
@ -78,7 +79,7 @@ export default ({ server_article }: { server_article: Article }) => {
<FormControl> <FormControl>
<TextareaAutosize <TextareaAutosize
cols={1} cols={1}
className="h-max w-full resize-none text-4xl font-bold focus-visible:outline-none" className="h-max w-full resize-none bg-transparent text-4xl font-bold focus-visible:outline-none"
value={field.value} value={field.value}
onChange={(e) => { onChange={(e) => {
field.onChange(e); field.onChange(e);
@ -117,7 +118,7 @@ export default ({ server_article }: { server_article: Article }) => {
// loading && "border-t-blue-600", // loading && "border-t-blue-600",
)} )}
> >
<div className="relative w-full space-y-4 overflow-hidden rounded-md border-t-2 bg-muted p-4"> <div className="relative w-full space-y-4 overflow-hidden rounded-md border bg-background p-4">
<div <div
className={cn( className={cn(
"saving absolute left-0 right-0 top-0 h-0.5 rounded-full bg-primary", "saving absolute left-0 right-0 top-0 h-0.5 rounded-full bg-primary",
@ -152,11 +153,11 @@ export default ({ server_article }: { server_article: Article }) => {
</span> </span>
</div> </div>
<Link <Link
href={"/editoren-hilfe"} href={appRoutes.article(server_article.slug)}
target="_blank" target="_blank"
className="size-max scale-90 p-0 text-xs text-muted-foreground" className="size-max scale-90 p-0 text-xs text-muted-foreground"
> >
<span>? Hilfe</span> <span>Ansehen</span>
</Link> </Link>
</div> </div>
<FormField <FormField
@ -188,6 +189,7 @@ export default ({ server_article }: { server_article: Article }) => {
<FormItem className="w-full"> <FormItem className="w-full">
<FormControl> <FormControl>
<CategorySelect <CategorySelect
className="w-full"
initialValue={field.value} initialValue={field.value}
onSelect={(categoryId) => { onSelect={(categoryId) => {
field.onChange(categoryId); field.onChange(categoryId);

View File

@ -7,6 +7,7 @@ import ArticleCard from "../article-card";
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook"; import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar"; import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
import { Article } from "@/server/db/schema";
function InfiniteArticlesGrid() { function InfiniteArticlesGrid() {
const [filter, setFilter] = React.useState<ArticleFilter | undefined>( const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
@ -19,7 +20,7 @@ function InfiniteArticlesGrid() {
}, },
{ {
getNextPageParam: (lastPage) => lastPage.nextCursor, getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time // staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs refetchOnWindowFocus: false, // Avoids refetch when switching tabs
}, },
@ -46,7 +47,7 @@ function InfiniteArticlesGrid() {
{data?.pages?.length {data?.pages?.length
? allItems.map((article, idx) => ( ? allItems.map((article, idx) => (
<li key={`article-${idx}`}> <li key={`article-${idx}`}>
<ArticleCard {...article} /> <ArticleCard {...(article as Article)} />
</li> </li>
)) ))
: null} : null}

View File

@ -1,7 +0,0 @@
import { JSONContent } from "novel";
function RenderArticle({ content }: { content: JSONContent }) {
return "render article: in work"; //<Editor readOnly editorProviderProps={{ content: content }} />;
}
export default RenderArticle;

View File

@ -16,7 +16,8 @@ function CategorySelect(props: Partial<ComboboxProps>) {
empty: "Keine Kategorien gefunden", empty: "Keine Kategorien gefunden",
}} }}
data={ data={
categories?.map(({ name, id }) => ({ label: name!, value: id! })) ?? [] categories?.map(({ name, slug }) => ({ label: name, value: slug })) ??
[]
} }
/> />
); );

View File

@ -50,6 +50,7 @@ export function Combobox({
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(initialValue ?? ""); const [value, setValue] = React.useState(initialValue ?? "");
const selectedItem = data.find((item) => item.value === initialValue)!; const selectedItem = data.find((item) => item.value === initialValue)!;
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>

View File

@ -18,15 +18,15 @@ import {
Youtube, Youtube,
} from "novel"; } from "novel";
import LinkPreview from "./link-preview"; import LinkPreview from "./link-preview";
import { Heading } from "@tiptap/extension-heading";
import { cx } from "class-variance-authority"; import { cx } from "class-variance-authority";
import { slashCommand } from "./slash-commands"; import { slashCommand } from "./slash-commands";
import ImageResize from "tiptap-extension-resize-image";
const placeholder = Placeholder; const placeholder = Placeholder;
const tiptapLink = TiptapLink.configure({ const tiptapLink = TiptapLink.configure({
HTMLAttributes: { HTMLAttributes: {
class: cx( class: cx(
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer", "underline text-foreground underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
), ),
}, },
}); });
@ -46,6 +46,12 @@ const tiptapImage = TiptapImage.extend({
}, },
}); });
const resizeImage = ImageResize.configure({
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
},
});
const updatedImage = UpdatedImage.configure({ const updatedImage = UpdatedImage.configure({
HTMLAttributes: { HTMLAttributes: {
class: cx("rounded-lg border border-muted"), class: cx("rounded-lg border border-muted"),
@ -108,6 +114,7 @@ export const defaultExtensions = [
tiptapLink, tiptapLink,
tiptapImage, tiptapImage,
updatedImage, updatedImage,
resizeImage,
taskList, taskList,
taskItem, taskItem,
horizontalRule, horizontalRule,

View File

@ -28,7 +28,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
inputRef.current?.focus(); inputRef.current?.focus();
}, []); }, []);
return ( return (
<form onSubmit={handleSubmit} className="flex gap-2 " autoFocus> <form onSubmit={handleSubmit} className="flex gap-2" autoFocus>
<Input <Input
ref={inputRef} ref={inputRef}
autoFocus autoFocus
@ -36,7 +36,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
placeholder="Enter a link" placeholder="Enter a link"
value={link} value={link}
onChange={(e) => setLink(e.target.value)} onChange={(e) => setLink(e.target.value)}
className="flex-1 focus-visible:ring-transparent border-0" className="flex-1 border-0 focus-visible:ring-transparent"
/> />
<Button <Button
type="submit" type="submit"
@ -60,29 +60,31 @@ const Preview = ({
_description?.length > 100 _description?.length > 100
? `${_description?.slice(0, 150)}...` ? `${_description?.slice(0, 150)}...`
: _description; : _description;
return ( return (
<a href={href} target="_blank"> <a href={href} target="_blank">
<div className=" flex gap-4 flex-col-reverse md:flex-row"> <div className="flex flex-col-reverse gap-4 md:flex-row">
<div className="w-full space-y-2"> <div className="flex w-full flex-col justify-between">
<h2 <div className="w-full space-y-2">
className="text-xl" <h2
style={{ className="text-xl"
margin: 0, style={{
fontSize: "1.5rem", margin: 0,
}} fontSize: "1.5rem",
> }}
{title} >
</h2> {title}
<p className="text-sm mt-2">{description}</p> </h2>
<span className="text-xs text-muted-foreground ">{href}</span> <p className="mt-2 text-sm">{description}</p>
</div>
<span className="text-xs text-muted-foreground">{href}</span>
</div> </div>
{image?.length ? ( {image?.length ? (
<img <img
src={image} src={image}
alt={title} alt={title}
className="w-full max-w-40 rounded-md object-cover " className="w-full max-w-40 rounded-md object-cover"
/> />
) : ( ) : (
<div className="size-20 rounded-md bg-muted" /> <div className="size-20 rounded-md bg-muted" />
@ -98,13 +100,13 @@ export const LinkPreviewComponent: React.FC<NodeViewProps> = ({
extension, extension,
}) => { }) => {
const [preview, setPreview] = React.useState<LinkPreviewData | undefined>( const [preview, setPreview] = React.useState<LinkPreviewData | undefined>(
(node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined (node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined,
); );
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
return ( return (
<NodeViewWrapper as="div" className="p-4 rounded-md bg-background border "> <NodeViewWrapper as="div" className="rounded-md border bg-background p-4">
{loading ? ( {loading ? (
<Skeleton className="h-8 w-full rounded" /> <Skeleton className="h-8 w-full rounded" />
) : preview ? ( ) : preview ? (

View File

@ -14,6 +14,7 @@ import {
import { Command, renderItems, createSuggestionItems } from "novel"; import { Command, renderItems, createSuggestionItems } from "novel";
import { selectionItems } from "../../selector/selection-items"; import { selectionItems } from "../../selector/selection-items";
import { uploadFile } from "@/server/actions/image";
// const items = selectionItems.filter((item) => !item.inline); // const items = selectionItems.filter((item) => !item.inline);
// const defaultSuggestionItems = items.map((item) => ( // const defaultSuggestionItems = items.map((item) => (
@ -148,10 +149,19 @@ export const suggestionItems = createSuggestionItems([
input.type = "file"; input.type = "file";
input.accept = "image/*"; input.accept = "image/*";
input.onchange = async () => { input.onchange = async () => {
if (input.files?.length) { if (input.files?.[0]) {
const file = input.files[0]; const file = input.files[0];
const pos = editor.view.state.selection.from; const pos = editor.view.state.selection.from;
// uploadFn(file, editor.view, pos); const formData = new FormData();
formData.append("file", file);
const url = await uploadFile(formData);
console.log("URL", url);
if (!url) return;
editor
.chain()
.focus()
.setImage({ src: url, alt: file.name, title: file.name })
.run();
} }
}; };
input.click(); input.click();

View File

@ -14,23 +14,26 @@ import { MenuBar } from "./menu/menu-bar";
const Editor = ({ const Editor = ({
onContentChange, onContentChange,
initialContent, initialContent,
readOnly,
}: { }: {
initialContent: JSONContent | null; initialContent: JSONContent | null;
onContentChange: (content: JSONContent) => void; onContentChange?: (content: JSONContent) => void;
readOnly?: boolean;
}) => { }) => {
return ( return (
<EditorRoot> <EditorRoot>
<EditorContent <EditorContent
slotBefore={<MenuBar />} slotBefore={!readOnly && <MenuBar />}
extensions={defaultExtensions} extensions={defaultExtensions}
editorProps={{ editorProps={{
handleDOMEvents: { handleDOMEvents: {
keydown: (_view, event) => handleCommandNavigation(event), keydown: (_view, event) => handleCommandNavigation(event),
}, },
}} }}
editable={!readOnly}
initialContent={initialContent ?? { type: "doc" }} initialContent={initialContent ?? { type: "doc" }}
onUpdate={({ editor }) => { onUpdate={({ editor }) => {
onContentChange(editor.getJSON()); onContentChange?.(editor.getJSON());
}} }}
> >
<SlashCommandComponent /> <SlashCommandComponent />

View File

@ -0,0 +1,11 @@
"use client";
import dynamic from "next/dynamic";
import { JSONContent } from "novel";
import React from "react";
const Editor = dynamic(() => import("."), { ssr: false });
function RenderContent({ content }: { content: JSONContent }) {
return <Editor readOnly initialContent={content} />;
}
export default RenderContent;

View File

@ -15,80 +15,43 @@ export interface BubbleColorMenuItem {
const TEXT_COLORS: BubbleColorMenuItem[] = [ const TEXT_COLORS: BubbleColorMenuItem[] = [
{ {
name: "Default", name: "Default",
color: "var(--novel-black)", color: "var(--foreground)",
}, },
{ {
name: "Purple", name: "Purple",
color: "#9333EA", color: "var(--color-purple-600)",
}, },
{ {
name: "Red", name: "Red",
color: "#E00000", color: "var(--color-red-600)",
}, },
{ {
name: "Yellow", name: "Yellow",
color: "#EAB308", color: "var(--color-yellow-600)",
}, },
{ {
name: "Blue", name: "Blue",
color: "#2563EB", color: "var(--color-blue-600)",
}, },
{ {
name: "Green", name: "Green",
color: "#008A00", color: "var(--color-emerald-600)",
}, },
{ {
name: "Orange", name: "Orange",
color: "#FFA500", color: "var(--color-orange-600)",
}, },
{ {
name: "Pink", name: "Pink",
color: "#BA4081", color: "var(--color-pink-600)",
}, },
{ {
name: "Gray", name: "Gray",
color: "#A8A29E", color: "var(--color-gray-600)",
}, },
]; ];
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [ const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = TEXT_COLORS;
{
name: "Default",
color: "var(--novel-highlight-default)",
},
{
name: "Purple",
color: "var(--novel-highlight-purple)",
},
{
name: "Red",
color: "var(--novel-highlight-red)",
},
{
name: "Yellow",
color: "var(--novel-highlight-yellow)",
},
{
name: "Blue",
color: "var(--novel-highlight-blue)",
},
{
name: "Green",
color: "var(--novel-highlight-green)",
},
{
name: "Orange",
color: "var(--novel-highlight-orange)",
},
{
name: "Pink",
color: "var(--novel-highlight-pink)",
},
{
name: "Gray",
color: "var(--novel-highlight-gray)",
},
];
interface ColorSelectorProps { interface ColorSelectorProps {
open: boolean; open: boolean;
@ -100,11 +63,11 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
if (!editor) return null; if (!editor) return null;
const activeColorItem = TEXT_COLORS.find(({ color }) => const activeColorItem = TEXT_COLORS.find(({ color }) =>
editor.isActive("textStyle", { color }) editor.isActive("textStyle", { color }),
); );
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
editor.isActive("highlight", { color }) editor.isActive("highlight", { color }),
); );
return ( return (
@ -126,7 +89,7 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
<PopoverContent <PopoverContent
sideOffset={5} sideOffset={5}
className="my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl " className="my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl"
align="start" align="start"
> >
<div className="flex flex-col"> <div className="flex flex-col">

View File

@ -9,12 +9,11 @@ import {
Heading3, Heading3,
Heading4, Heading4,
ListOrdered, ListOrdered,
TextIcon,
TextQuote,
SeparatorHorizontalIcon, SeparatorHorizontalIcon,
Heading5, Heading5,
Heading6, Heading6,
QuoteIcon, QuoteIcon,
TypeIcon,
} from "lucide-react"; } from "lucide-react";
import { EditorInstance } from "novel"; import { EditorInstance } from "novel";
@ -64,7 +63,7 @@ export const selectionItems: SelectorItem[] = [
// blocks // blocks
{ {
name: "Text", name: "Text",
icon: TextIcon, icon: TypeIcon,
command: (editor) => editor.chain().focus().clearNodes().run(), command: (editor) => editor.chain().focus().clearNodes().run(),
isActive: (editor) => editor.isActive("paragraph"), //node?.type?.name === "paragraph", isActive: (editor) => editor.isActive("paragraph"), //node?.type?.name === "paragraph",
}, },

View File

@ -8,9 +8,6 @@ const items = selectionItems.filter((item) => item.inline);
export const TextButtons = () => { export const TextButtons = () => {
const { editor } = useEditor(); const { editor } = useEditor();
if (!editor) return null; if (!editor) return null;
editor.on("selectionUpdate", () => {
editor.view.dispatch(editor.state.tr);
});
return ( return (
<div className="flex"> <div className="flex">

View File

@ -72,36 +72,40 @@
} }
.tiptap hr { .tiptap hr {
@apply border-border;
/* border-color: var(--border); */
margin: 0.25rem 0; margin: 0.25rem 0;
} }
::selection { ::selection {
background-color: #5abbf7; background-color: #5abbf7;
border-radius: 0.375rem;
} }
.menu-bar button { .menu-bar button {
transition-duration: 0s; transition-duration: 0s;
} }
.menu-bar .is-active { img {
border-color: var(--primary-color); @apply rounded-md;
} }
/* Task List */ /* Task List */
ul[data-type="taskList"] li {
@apply m-0 my-3 flex items-center;
}
ul[data-type="taskList"] li p {
@apply m-0 my-0 flex items-center;
}
ul[data-type="taskList"] li > label input[type="checkbox"] { ul[data-type="taskList"] li > label input[type="checkbox"] {
@apply size-4 border border-border bg-muted;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
width: 1.2em;
height: 1.2em;
position: relative; position: relative;
top: 5px;
border: 2px solid var(--border);
margin-right: 0.3rem;
display: grid; display: grid;
border-radius: 0.25rem; border-radius: 0.25rem;
place-content: center; place-content: center;
@ -126,7 +130,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before {
transform: scale(1); transform: scale(1);
} }
ul[data-type="taskList"] li[data-checked="true"] > div > p { ul[data-type="taskList"] li[data-checked="true"] > div > p {
color: var(--muted-foreground); @apply text-foreground/75;
text-decoration: line-through; text-decoration: line-through;
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
} }

View File

@ -28,6 +28,8 @@ export async function updateArticle(
article: { ...article, content: JSON.parse(article.content) }, article: { ...article, content: JSON.parse(article.content) },
articleId, articleId,
}); });
revalidatePath("/");
// if (!result[0]?.id?.length) return false; // if (!result[0]?.id?.length) return false;
// return revalidatePath(`/artikel/${result[0]?.slug}/edit`); // return revalidatePath(`/artikel/${result[0]?.slug}/edit`);
} }
@ -35,5 +37,7 @@ export async function deleteArticle(articleId: string) {
const result = await api.article.delete({ const result = await api.article.delete({
articleId, articleId,
}); });
revalidatePath("/");
// if (!result[0]?.id?.length) return false; // if (!result[0]?.id?.length) return false;
} }

View File

@ -6,16 +6,25 @@ import { hasPermission, Role } from "@/lib/validation/permissions";
export async function uploadFile(formData: FormData) { export async function uploadFile(formData: FormData) {
const session = await auth(); const session = await auth();
if (!session || hasPermission(session.user.role, Role.EDITOR)) return false;
if (!session || !hasPermission(session.user.role, Role.EDITOR)) return false;
console.log("Starting upload");
const file = formData.get("file") as File; const file = formData.get("file") as File;
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer); const buffer = new Uint8Array(arrayBuffer);
const filename = `upload-${Date.now()}-${file.name}.png`; const filename = `upload-${Date.now()}-${file.name}.png`;
console.log("Filename", filename);
try { try {
await fs.writeFile(`./public/uploads/${filename}`, buffer); await fs.writeFile(`./public/uploads/${filename}`, buffer);
console.log("File uploaded successfully");
revalidatePath("/"); revalidatePath("/");
return `/uploads/${filename}`; return `/uploads/${filename}`;
} catch (e) { } catch (e) {
console.error("Error uploading file:", e);
return false; return false;
} }
} }

View File

@ -1,4 +1,6 @@
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
import { articles as articlesTable } from "@/server/db/schema";
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
getSidebarMain: publicProcedure.query(async ({ ctx }) => { getSidebarMain: publicProcedure.query(async ({ ctx }) => {
@ -11,6 +13,8 @@ export const appRouter = createTRPCRouter({
}); });
const articles = await ctx.db.query.articles.findMany({ const articles = await ctx.db.query.articles.findMany({
limit: 3, limit: 3,
where: eq(articlesTable.published, true),
orderBy: desc(articlesTable.createdAt),
columns: { columns: {
slug: true, slug: true,
title: true, title: true,

View File

@ -71,8 +71,11 @@ export const articleRouter = createTRPCRouter({
get: publicProcedure get: publicProcedure
.input(z.object({ slug: z.string() })) .input(z.object({ slug: z.string() }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const user = ctx?.session?.user;
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
return (await ctx.db.query.articles.findFirst({ return (await ctx.db.query.articles.findFirst({
where: eq(articles.slug, input.slug), where: and(eq(articles.slug, input.slug), publishedArg),
with: { category: true }, with: { category: true },
})) as Article; })) as Article;
}), }),
@ -112,20 +115,19 @@ export const articleRouter = createTRPCRouter({
sortConfig, sortConfig,
cursorObj, cursorObj,
); );
const user = ctx?.session?.user;
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
const items = await ctx.db.query.articles.findMany({ const items = await ctx.db.query.articles.findMany({
where: and( where: and(cursorArg, categoryArg, queryFilterArg, publishedArg),
cursorArg,
categoryArg,
queryFilterArg,
eq(articles.published, true),
),
limit: limit + 1, limit: limit + 1,
orderBy, orderBy,
columns: { columns: {
title: true, title: true,
slug: true, slug: true,
createdAt: true, createdAt: true,
published: isEditor ? true : false,
}, },
}); });

View File

@ -89,6 +89,7 @@ const timingMiddleware = t.middleware(async ({ next, path }) => {
if (t._config.isDev) { if (t._config.isDev) {
// artificial delay in dev // artificial delay in dev
// const waitMs = 1000 * 5; // 5 seconds
const waitMs = Math.floor(Math.random() * 400) + 100; const waitMs = Math.floor(Math.random() * 400) + 100;
await new Promise((resolve) => setTimeout(resolve, waitMs)); await new Promise((resolve) => setTimeout(resolve, waitMs));
} }

View File

@ -50,7 +50,7 @@
--primary-foreground: 240 5.9% 10%; --primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%; --secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%; --muted: 240 3.7% 5.9%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%; --accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
@ -96,7 +96,7 @@
transform: scaleX(1); /* Slight overshoot for bounce effect */ transform: scaleX(1); /* Slight overshoot for bounce effect */
} }
100% { 100% {
transform: scaleX(0.2); transform: scaleX(0);
} }
} }

View File

@ -69,5 +69,25 @@ export default {
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [
require("tailwindcss-animate"),
function ({ addBase, theme }: any) {
function extractColorVars(colorObj: any, colorGroup = "") {
return Object.keys(colorObj).reduce((vars: any, colorKey: any) => {
const value = colorObj[colorKey];
const newVars: any =
typeof value === "string"
? { [`--color${colorGroup}-${colorKey}`]: value }
: extractColorVars(value, `-${colorKey}`);
return { ...vars, ...newVars };
}, {});
}
addBase({
":root": extractColorVars(theme("colors")),
});
},
],
} satisfies Config; } satisfies Config;