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
|
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 }}."
|
|
||||||
|
|||||||
@ -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
16
pnpm-lock.yaml
generated
@ -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
|
||||||
|
|||||||
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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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",
|
empty: "Keine Kategorien gefunden",
|
||||||
}}
|
}}
|
||||||
data={
|
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 [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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
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[] = [
|
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">
|
||||||
|
|||||||
@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user