fixed editor missing peaces
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 0s
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 0s
This commit is contained in:
parent
d9ed115e56
commit
1597d4f113
@ -7,13 +7,4 @@ jobs:
|
||||
runs-on: linux
|
||||
steps:
|
||||
- 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: 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 }}."
|
||||
- run: Test "🎉 job completed! 🚀"
|
||||
|
||||
@ -67,6 +67,7 @@
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiptap-extension-resize-image": "^1.2.1",
|
||||
"use-debounce": "^10.0.4",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -149,6 +149,9 @@ importers:
|
||||
tailwindcss-animate:
|
||||
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)))
|
||||
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:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4(react@18.3.1)
|
||||
@ -4095,6 +4098,13 @@ packages:
|
||||
tiptap-extension-global-drag-handle@0.1.18:
|
||||
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:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@ -8492,6 +8502,12 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
BIN
public/uploads/upload-1742077219929-person-3.jpg.png
Normal file
BIN
public/uploads/upload-1742077219929-person-3.jpg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 569 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@ -1,5 +1,6 @@
|
||||
import RenderArticle from "@/components/article/render-article";
|
||||
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 { appRoutes } from "@/config";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
@ -21,8 +22,9 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<BreadNavigator
|
||||
className="w-full"
|
||||
links={[
|
||||
...(article?.category
|
||||
? [
|
||||
@ -38,7 +40,14 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
/>
|
||||
</div>
|
||||
{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"}>
|
||||
<Link href={appRoutes.editArticle(article.slug)}>
|
||||
<Edit className="size-4" />
|
||||
@ -52,10 +61,7 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold">{article.title}</h1>
|
||||
|
||||
{article?.content?.length ? (
|
||||
<RenderArticle content={article.content} />
|
||||
) : null}
|
||||
{article.content && <RenderContent content={article.content} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,14 +13,17 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import Avatar from "../avatar";
|
||||
import { Icons } from "../icons";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
function ArticleCard({
|
||||
title,
|
||||
slug,
|
||||
author,
|
||||
published,
|
||||
createdAt,
|
||||
}: Pick<Article, "title" | "slug" | "createdAt" | "author">) {
|
||||
}: Pick<Article, "title" | "slug" | "createdAt" | "author" | "published">) {
|
||||
const authorName = author?.name ?? `${appConfig.name} Team`;
|
||||
|
||||
return (
|
||||
<Link href={appRoutes.article(slug)}>
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{createdAt.toLocaleDateString("de-DE", {
|
||||
dateStyle: "long",
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
{typeof published === "boolean" && (
|
||||
<Badge
|
||||
className="size-max px-2 py-px text-xs"
|
||||
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>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@ -85,6 +85,7 @@ function ArticleFilterBar({
|
||||
|
||||
<CategorySelect
|
||||
className="w-full"
|
||||
initialValue={filter.category}
|
||||
onSelect={(category) => {
|
||||
onFilterChange({
|
||||
category: category?.length ? category : undefined,
|
||||
|
||||
@ -26,6 +26,7 @@ import { CheckCircle, XCircle } from "lucide-react";
|
||||
import PublishArticleAlertDialog from "./publish-article-alert-dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import dynamic from "next/dynamic";
|
||||
import { appRoutes } from "@/config";
|
||||
const Editor = dynamic(() => import("../editor"), { ssr: false });
|
||||
|
||||
export default ({ server_article }: { server_article: Article }) => {
|
||||
@ -78,7 +79,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
<FormControl>
|
||||
<TextareaAutosize
|
||||
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}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
@ -117,7 +118,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
// 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
|
||||
className={cn(
|
||||
"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>
|
||||
</div>
|
||||
<Link
|
||||
href={"/editoren-hilfe"}
|
||||
href={appRoutes.article(server_article.slug)}
|
||||
target="_blank"
|
||||
className="size-max scale-90 p-0 text-xs text-muted-foreground"
|
||||
>
|
||||
<span>? Hilfe</span>
|
||||
<span>Ansehen</span>
|
||||
</Link>
|
||||
</div>
|
||||
<FormField
|
||||
@ -188,6 +189,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<CategorySelect
|
||||
className="w-full"
|
||||
initialValue={field.value}
|
||||
onSelect={(categoryId) => {
|
||||
field.onChange(categoryId);
|
||||
|
||||
@ -7,6 +7,7 @@ import ArticleCard from "../article-card";
|
||||
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
|
||||
import { Article } from "@/server/db/schema";
|
||||
|
||||
function InfiniteArticlesGrid() {
|
||||
const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
|
||||
@ -19,7 +20,7 @@ function InfiniteArticlesGrid() {
|
||||
},
|
||||
{
|
||||
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
|
||||
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
|
||||
},
|
||||
@ -46,7 +47,7 @@ function InfiniteArticlesGrid() {
|
||||
{data?.pages?.length
|
||||
? allItems.map((article, idx) => (
|
||||
<li key={`article-${idx}`}>
|
||||
<ArticleCard {...article} />
|
||||
<ArticleCard {...(article as Article)} />
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
|
||||
@ -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;
|
||||
@ -16,7 +16,8 @@ function CategorySelect(props: Partial<ComboboxProps>) {
|
||||
empty: "Keine Kategorien gefunden",
|
||||
}}
|
||||
data={
|
||||
categories?.map(({ name, id }) => ({ label: name!, value: id! })) ?? []
|
||||
categories?.map(({ name, slug }) => ({ label: name, value: slug })) ??
|
||||
[]
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -50,6 +50,7 @@ export function Combobox({
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState(initialValue ?? "");
|
||||
const selectedItem = data.find((item) => item.value === initialValue)!;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@ -18,15 +18,15 @@ import {
|
||||
Youtube,
|
||||
} from "novel";
|
||||
import LinkPreview from "./link-preview";
|
||||
import { Heading } from "@tiptap/extension-heading";
|
||||
import { cx } from "class-variance-authority";
|
||||
import { slashCommand } from "./slash-commands";
|
||||
import ImageResize from "tiptap-extension-resize-image";
|
||||
|
||||
const placeholder = Placeholder;
|
||||
const tiptapLink = TiptapLink.configure({
|
||||
HTMLAttributes: {
|
||||
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({
|
||||
HTMLAttributes: {
|
||||
class: cx("rounded-lg border border-muted"),
|
||||
@ -108,6 +114,7 @@ export const defaultExtensions = [
|
||||
tiptapLink,
|
||||
tiptapImage,
|
||||
updatedImage,
|
||||
resizeImage,
|
||||
taskList,
|
||||
taskItem,
|
||||
horizontalRule,
|
||||
|
||||
@ -28,7 +28,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 " autoFocus>
|
||||
<form onSubmit={handleSubmit} className="flex gap-2" autoFocus>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
@ -36,7 +36,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
|
||||
placeholder="Enter a link"
|
||||
value={link}
|
||||
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
|
||||
type="submit"
|
||||
@ -63,26 +63,28 @@ const Preview = ({
|
||||
|
||||
return (
|
||||
<a href={href} target="_blank">
|
||||
<div className=" flex gap-4 flex-col-reverse md:flex-row">
|
||||
<div className="w-full space-y-2">
|
||||
<h2
|
||||
className="text-xl"
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "1.5rem",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-sm mt-2">{description}</p>
|
||||
<span className="text-xs text-muted-foreground ">{href}</span>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||
<div className="flex w-full flex-col justify-between">
|
||||
<div className="w-full space-y-2">
|
||||
<h2
|
||||
className="text-xl"
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "1.5rem",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm">{description}</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{href}</span>
|
||||
</div>
|
||||
|
||||
{image?.length ? (
|
||||
<img
|
||||
src={image}
|
||||
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" />
|
||||
@ -98,13 +100,13 @@ export const LinkPreviewComponent: React.FC<NodeViewProps> = ({
|
||||
extension,
|
||||
}) => {
|
||||
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);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className="p-4 rounded-md bg-background border ">
|
||||
<NodeViewWrapper as="div" className="rounded-md border bg-background p-4">
|
||||
{loading ? (
|
||||
<Skeleton className="h-8 w-full rounded" />
|
||||
) : preview ? (
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { Command, renderItems, createSuggestionItems } from "novel";
|
||||
import { selectionItems } from "../../selector/selection-items";
|
||||
import { uploadFile } from "@/server/actions/image";
|
||||
|
||||
// const items = selectionItems.filter((item) => !item.inline);
|
||||
// const defaultSuggestionItems = items.map((item) => (
|
||||
@ -148,10 +149,19 @@ export const suggestionItems = createSuggestionItems([
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = async () => {
|
||||
if (input.files?.length) {
|
||||
if (input.files?.[0]) {
|
||||
const file = input.files[0];
|
||||
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();
|
||||
|
||||
@ -14,23 +14,26 @@ import { MenuBar } from "./menu/menu-bar";
|
||||
const Editor = ({
|
||||
onContentChange,
|
||||
initialContent,
|
||||
readOnly,
|
||||
}: {
|
||||
initialContent: JSONContent | null;
|
||||
onContentChange: (content: JSONContent) => void;
|
||||
onContentChange?: (content: JSONContent) => void;
|
||||
readOnly?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<EditorRoot>
|
||||
<EditorContent
|
||||
slotBefore={<MenuBar />}
|
||||
slotBefore={!readOnly && <MenuBar />}
|
||||
extensions={defaultExtensions}
|
||||
editorProps={{
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => handleCommandNavigation(event),
|
||||
},
|
||||
}}
|
||||
editable={!readOnly}
|
||||
initialContent={initialContent ?? { type: "doc" }}
|
||||
onUpdate={({ editor }) => {
|
||||
onContentChange(editor.getJSON());
|
||||
onContentChange?.(editor.getJSON());
|
||||
}}
|
||||
>
|
||||
<SlashCommandComponent />
|
||||
|
||||
11
src/components/editor/render-content.tsx
Normal file
11
src/components/editor/render-content.tsx
Normal 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;
|
||||
@ -15,80 +15,43 @@ export interface BubbleColorMenuItem {
|
||||
const TEXT_COLORS: BubbleColorMenuItem[] = [
|
||||
{
|
||||
name: "Default",
|
||||
color: "var(--novel-black)",
|
||||
color: "var(--foreground)",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
color: "#9333EA",
|
||||
color: "var(--color-purple-600)",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
color: "#E00000",
|
||||
color: "var(--color-red-600)",
|
||||
},
|
||||
{
|
||||
name: "Yellow",
|
||||
color: "#EAB308",
|
||||
color: "var(--color-yellow-600)",
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
color: "#2563EB",
|
||||
color: "var(--color-blue-600)",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
color: "#008A00",
|
||||
color: "var(--color-emerald-600)",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
color: "#FFA500",
|
||||
color: "var(--color-orange-600)",
|
||||
},
|
||||
{
|
||||
name: "Pink",
|
||||
color: "#BA4081",
|
||||
color: "var(--color-pink-600)",
|
||||
},
|
||||
{
|
||||
name: "Gray",
|
||||
color: "#A8A29E",
|
||||
color: "var(--color-gray-600)",
|
||||
},
|
||||
];
|
||||
|
||||
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
|
||||
{
|
||||
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)",
|
||||
},
|
||||
];
|
||||
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = TEXT_COLORS;
|
||||
|
||||
interface ColorSelectorProps {
|
||||
open: boolean;
|
||||
@ -100,11 +63,11 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
|
||||
|
||||
if (!editor) return null;
|
||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||
editor.isActive("textStyle", { color })
|
||||
editor.isActive("textStyle", { color }),
|
||||
);
|
||||
|
||||
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
|
||||
editor.isActive("highlight", { color })
|
||||
editor.isActive("highlight", { color }),
|
||||
);
|
||||
|
||||
return (
|
||||
@ -126,7 +89,7 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
|
||||
|
||||
<PopoverContent
|
||||
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"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -9,12 +9,11 @@ import {
|
||||
Heading3,
|
||||
Heading4,
|
||||
ListOrdered,
|
||||
TextIcon,
|
||||
TextQuote,
|
||||
SeparatorHorizontalIcon,
|
||||
Heading5,
|
||||
Heading6,
|
||||
QuoteIcon,
|
||||
TypeIcon,
|
||||
} from "lucide-react";
|
||||
import { EditorInstance } from "novel";
|
||||
|
||||
@ -64,7 +63,7 @@ export const selectionItems: SelectorItem[] = [
|
||||
// blocks
|
||||
{
|
||||
name: "Text",
|
||||
icon: TextIcon,
|
||||
icon: TypeIcon,
|
||||
command: (editor) => editor.chain().focus().clearNodes().run(),
|
||||
isActive: (editor) => editor.isActive("paragraph"), //node?.type?.name === "paragraph",
|
||||
},
|
||||
|
||||
@ -8,9 +8,6 @@ const items = selectionItems.filter((item) => item.inline);
|
||||
export const TextButtons = () => {
|
||||
const { editor } = useEditor();
|
||||
if (!editor) return null;
|
||||
editor.on("selectionUpdate", () => {
|
||||
editor.view.dispatch(editor.state.tr);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
|
||||
@ -72,36 +72,40 @@
|
||||
}
|
||||
|
||||
.tiptap hr {
|
||||
@apply border-border;
|
||||
/* border-color: var(--border); */
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #5abbf7;
|
||||
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.menu-bar button {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.menu-bar .is-active {
|
||||
border-color: var(--primary-color);
|
||||
img {
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
/* 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"] {
|
||||
@apply size-4 border border-border bg-muted;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
border: 2px solid var(--border);
|
||||
margin-right: 0.3rem;
|
||||
|
||||
display: grid;
|
||||
border-radius: 0.25rem;
|
||||
place-content: center;
|
||||
@ -126,7 +130,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
color: var(--muted-foreground);
|
||||
@apply text-foreground/75;
|
||||
text-decoration: line-through;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ export async function updateArticle(
|
||||
article: { ...article, content: JSON.parse(article.content) },
|
||||
articleId,
|
||||
});
|
||||
revalidatePath("/");
|
||||
|
||||
// if (!result[0]?.id?.length) return false;
|
||||
// return revalidatePath(`/artikel/${result[0]?.slug}/edit`);
|
||||
}
|
||||
@ -35,5 +37,7 @@ export async function deleteArticle(articleId: string) {
|
||||
const result = await api.article.delete({
|
||||
articleId,
|
||||
});
|
||||
revalidatePath("/");
|
||||
|
||||
// if (!result[0]?.id?.length) return false;
|
||||
}
|
||||
|
||||
@ -6,16 +6,25 @@ import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
|
||||
export async function uploadFile(formData: FormData) {
|
||||
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 arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = new Uint8Array(arrayBuffer);
|
||||
const filename = `upload-${Date.now()}-${file.name}.png`;
|
||||
console.log("Filename", filename);
|
||||
|
||||
try {
|
||||
await fs.writeFile(`./public/uploads/${filename}`, buffer);
|
||||
console.log("File uploaded successfully");
|
||||
|
||||
revalidatePath("/");
|
||||
|
||||
return `/uploads/${filename}`;
|
||||
} catch (e) {
|
||||
console.error("Error uploading file:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
import { articles as articlesTable } from "@/server/db/schema";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
getSidebarMain: publicProcedure.query(async ({ ctx }) => {
|
||||
@ -11,6 +13,8 @@ export const appRouter = createTRPCRouter({
|
||||
});
|
||||
const articles = await ctx.db.query.articles.findMany({
|
||||
limit: 3,
|
||||
where: eq(articlesTable.published, true),
|
||||
orderBy: desc(articlesTable.createdAt),
|
||||
columns: {
|
||||
slug: true,
|
||||
title: true,
|
||||
|
||||
@ -71,8 +71,11 @@ export const articleRouter = createTRPCRouter({
|
||||
get: publicProcedure
|
||||
.input(z.object({ slug: z.string() }))
|
||||
.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({
|
||||
where: eq(articles.slug, input.slug),
|
||||
where: and(eq(articles.slug, input.slug), publishedArg),
|
||||
with: { category: true },
|
||||
})) as Article;
|
||||
}),
|
||||
@ -112,20 +115,19 @@ export const articleRouter = createTRPCRouter({
|
||||
sortConfig,
|
||||
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({
|
||||
where: and(
|
||||
cursorArg,
|
||||
categoryArg,
|
||||
queryFilterArg,
|
||||
eq(articles.published, true),
|
||||
),
|
||||
where: and(cursorArg, categoryArg, queryFilterArg, publishedArg),
|
||||
limit: limit + 1,
|
||||
orderBy,
|
||||
columns: {
|
||||
title: true,
|
||||
slug: true,
|
||||
createdAt: true,
|
||||
published: isEditor ? true : false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -89,6 +89,7 @@ const timingMiddleware = t.middleware(async ({ next, path }) => {
|
||||
|
||||
if (t._config.isDev) {
|
||||
// artificial delay in dev
|
||||
// const waitMs = 1000 * 5; // 5 seconds
|
||||
const waitMs = Math.floor(Math.random() * 400) + 100;
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted: 240 3.7% 5.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
@ -96,7 +96,7 @@
|
||||
transform: scaleX(1); /* Slight overshoot for bounce effect */
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(0.2);
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user