diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx index 2cf0992..f4b4d15 100644 --- a/resources/js/components/app-sidebar-header.tsx +++ b/resources/js/components/app-sidebar-header.tsx @@ -1,5 +1,5 @@ -import { NavUser } from '@/components/nav-user'; import { Breadcrumbs } from '@/components/breadcrumbs'; +import { NavUser } from '@/components/nav-user'; import { SidebarTrigger } from '@/components/ui/sidebar'; import type { BreadcrumbItem as BreadcrumbItemType } from '@/types'; diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index e5dbaa1..07ae19c 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -13,14 +13,14 @@ import { import { dashboard } from '@/routes'; import category from '@/routes/category'; -import type { NavItem } from '@/types'; -import product from '@/routes/product'; import expense from '@/routes/expense'; -import payroll from '@/routes/payroll'; -import user from '@/routes/user'; -import system from '@/routes/system'; -import purchase from '@/routes/purchase'; import order from '@/routes/order'; +import payroll from '@/routes/payroll'; +import product from '@/routes/product'; +import purchase from '@/routes/purchase'; +import system from '@/routes/system'; +import user from '@/routes/user'; +import type { NavItem } from '@/types'; const mainNavItems: NavItem[] = [ { diff --git a/resources/js/components/cards/stat-card.tsx b/resources/js/components/cards/stat-card.tsx index 6b8e4c3..f3a9fd1 100644 --- a/resources/js/components/cards/stat-card.tsx +++ b/resources/js/components/cards/stat-card.tsx @@ -1,9 +1,9 @@ -import { cn } from '@/lib/utils'; -import { formatCurrency, formatNumber } from '@/lib/formatters'; -import { StatItem } from '@/types'; import { TrendingUp, TrendingDown } from 'lucide-react'; -import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card'; +import { formatCurrency, formatNumber } from '@/lib/formatters'; +import { cn } from '@/lib/utils'; +import type { StatItem } from '@/types'; export interface StatCardProps { title: string; diff --git a/resources/js/components/cards/welcome-card.tsx b/resources/js/components/cards/welcome-card.tsx index 04ac820..2264407 100644 --- a/resources/js/components/cards/welcome-card.tsx +++ b/resources/js/components/cards/welcome-card.tsx @@ -1,5 +1,5 @@ -import { formatTime, formatDate } from '@/lib/formatters'; import { Sun, Moon, Cloud, Trees, Stars, Bird } from 'lucide-react'; +import { formatTime, formatDate } from '@/lib/formatters'; const DynamicScene = ({ hour }: { hour: number }) => { if (hour >= 5 && hour < 11) { @@ -11,6 +11,7 @@ const DynamicScene = ({ hour }: { hour: number }) => { ); } + if (hour >= 11 && hour < 15) { return (
@@ -20,6 +21,7 @@ const DynamicScene = ({ hour }: { hour: number }) => {
); } + if (hour >= 15 && hour < 19) { return (
@@ -29,6 +31,7 @@ const DynamicScene = ({ hour }: { hour: number }) => {
); } + return (
diff --git a/resources/js/components/charts/bar-chart.tsx b/resources/js/components/charts/bar-chart.tsx index d69266e..df42eef 100644 --- a/resources/js/components/charts/bar-chart.tsx +++ b/resources/js/components/charts/bar-chart.tsx @@ -13,9 +13,10 @@ import { import { ChartContainer, ChartTooltip, - ChartTooltipContent, - type ChartConfig, + ChartTooltipContent + } from "@/components/ui/chart" +import type {ChartConfig} from "@/components/ui/chart"; export const CustomBarChart = React.memo(function CustomBarChart({ title, @@ -44,6 +45,7 @@ export const CustomBarChart = React.memo(function CustomBarChart({ label: String(item.name), color: `hsl(${hue}, 60%, 65%)`, } + return { ...item, fill: `var(--color-${key})`, @@ -51,11 +53,15 @@ export const CustomBarChart = React.memo(function CustomBarChart({ total: Number(item.total) } }) + return { config: cfg, chartData: formattedData } }, [data, colorOffset]) const formatValue = (val: any) => { - if (!isCurrency) return val; + if (!isCurrency) { +return val; +} + return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(val); }; diff --git a/resources/js/components/charts/pie-chart.tsx b/resources/js/components/charts/pie-chart.tsx index a1b8fa6..2664738 100644 --- a/resources/js/components/charts/pie-chart.tsx +++ b/resources/js/components/charts/pie-chart.tsx @@ -13,9 +13,10 @@ import { import { ChartContainer, ChartTooltip, - ChartTooltipContent, - type ChartConfig, + ChartTooltipContent + } from "@/components/ui/chart" +import type {ChartConfig} from "@/components/ui/chart"; export const CustomPieChart = React.memo(function CustomPieChart({ title, @@ -44,12 +45,14 @@ export const CustomPieChart = React.memo(function CustomPieChart({ label: String(item.name), color: `hsl(${hue}, 70%, 50%)`, } + return { ...item, fill: `var(--color-${key})`, name: String(item.name), } }) + return { config: cfg, chartData: formattedData } }, [data, colorOffset]) diff --git a/resources/js/components/charts/sales-by-hour-chart.tsx b/resources/js/components/charts/sales-by-hour-chart.tsx index a13087a..2c505d2 100644 --- a/resources/js/components/charts/sales-by-hour-chart.tsx +++ b/resources/js/components/charts/sales-by-hour-chart.tsx @@ -14,9 +14,10 @@ import { ChartLegend, ChartLegendContent, ChartTooltip, - ChartTooltipContent, - type ChartConfig, + ChartTooltipContent + } from "@/components/ui/chart" +import type {ChartConfig} from "@/components/ui/chart"; export const description = "Menampilkan aktivitas toko berdasarkan pesanan yang masuk." diff --git a/resources/js/components/data-table-column-header.tsx b/resources/js/components/data-table-column-header.tsx index ce7a614..53f5bbf 100644 --- a/resources/js/components/data-table-column-header.tsx +++ b/resources/js/components/data-table-column-header.tsx @@ -1,8 +1,8 @@ -import { type Column } from "@tanstack/react-table" +import type {Column} from "@tanstack/react-table"; import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react" -import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" interface DataTableColumnHeaderProps extends React.HTMLAttributes { diff --git a/resources/js/components/data-table.tsx b/resources/js/components/data-table.tsx index e40234a..6e177af 100644 --- a/resources/js/components/data-table.tsx +++ b/resources/js/components/data-table.tsx @@ -1,26 +1,20 @@ -import { +import type { ColumnDef, + SortingState, + ColumnFiltersState, + VisibilityState} from "@tanstack/react-table"; +import { flexRender, getCoreRowModel, useReactTable, getPaginationRowModel, - SortingState, getSortedRowModel, - ColumnFiltersState, - getFilteredRowModel, - VisibilityState, + getFilteredRowModel } from "@tanstack/react-table"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { Settings2, ChevronDown, ListFilter, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, X } from "lucide-react"; +import React from "react"; import { Checkbox } from "@/components/ui/checkbox"; -import { Button } from "./ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -33,16 +27,11 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import React from "react"; -import { Input } from "./ui/input"; -import { Settings2, ChevronDown, ListFilter, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, List, X } from "lucide-react"; import { Empty, - EmptyContent, EmptyDescription, EmptyHeader, - EmptyMedia, EmptyTitle, } from "@/components/ui/empty" import { @@ -52,7 +41,17 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Badge } from "./ui/badge"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; interface DataTableFilterOption { columnId: string; @@ -245,7 +244,10 @@ export function DataTable({ {filters.map((filter) => { const column = table.getColumn(filter.columnId); - if (!column) return null; + + if (!column) { +return null; +} return ( @@ -265,9 +267,13 @@ export function DataTable({ key={option.value} onSelect={(e) => e.preventDefault()} onClick={() => { - if (option.value === "true") column.setFilterValue(true); - else if (option.value === "false") column.setFilterValue(false); - else column.setFilterValue(option.value); + if (option.value === "true") { +column.setFilterValue(true); +} else if (option.value === "false") { +column.setFilterValue(false); +} else { +column.setFilterValue(option.value); +} }} > {option.label} diff --git a/resources/js/components/input-error.tsx b/resources/js/components/input-error.tsx index 278672c..425ee4b 100644 --- a/resources/js/components/input-error.tsx +++ b/resources/js/components/input-error.tsx @@ -1,6 +1,6 @@ import type { HTMLAttributes } from 'react'; -import { cn } from '@/lib/utils'; import { useMemo } from 'react'; +import { cn } from '@/lib/utils'; interface InputErrorProps extends HTMLAttributes { message?: string; @@ -14,8 +14,13 @@ export default function InputError({ ...props }: InputErrorProps) { const formattedError = useMemo(() => { - if (!message) return null; - if (!label) return message; + if (!message) { +return null; +} + + if (!label) { +return message; +} // Preserve all-caps labels (like NIK), otherwise capitalize first letter and lowercase the rest const isAllOptionsCaps = label === label.toUpperCase() && label.length > 1; @@ -33,6 +38,7 @@ export default function InputError({ // Find the first occurrence of a verb let verbIndex = -1; + for (let i = 0; i < words.length; i++) { if (verbs.includes(words[i].toLowerCase())) { verbIndex = i; diff --git a/resources/js/components/modal/delete-confirmation.tsx b/resources/js/components/modal/delete-confirmation.tsx index ae6de14..04d573b 100644 --- a/resources/js/components/modal/delete-confirmation.tsx +++ b/resources/js/components/modal/delete-confirmation.tsx @@ -1,4 +1,5 @@ import { Trash2, X } from 'lucide-react'; +import React from 'react'; import { AlertDialog, AlertDialogAction, @@ -10,7 +11,6 @@ import { AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import React from 'react'; interface DeleteConfirmationProps { isOpen: boolean; diff --git a/resources/js/components/modal/image-preview.tsx b/resources/js/components/modal/image-preview.tsx index 6876af0..99bf8c5 100644 --- a/resources/js/components/modal/image-preview.tsx +++ b/resources/js/components/modal/image-preview.tsx @@ -2,9 +2,9 @@ interface ImagePreviewDialogProps { imageUrl: string | null; onClose: () => void; } -import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { VisuallyHidden } from "@radix-ui/react-visually-hidden" import { X } from 'lucide-react'; +import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog" export function ImagePreviewDialog({ imageUrl, onClose }: ImagePreviewDialogProps) { return ( diff --git a/resources/js/components/tiptap-extension/node-background-extension.ts b/resources/js/components/tiptap-extension/node-background-extension.ts index e26901c..a701512 100644 --- a/resources/js/components/tiptap-extension/node-background-extension.ts +++ b/resources/js/components/tiptap-extension/node-background-extension.ts @@ -34,10 +34,13 @@ function getToggleColor( targets: NodeWithPos[], inputColor: string ): string | null { - if (targets.length === 0) return null + if (targets.length === 0) { +return null +} for (const target of targets) { const currentColor = target.node.attrs?.backgroundColor ?? null + if (currentColor !== inputColor) { return inputColor } @@ -75,15 +78,22 @@ export const NodeBackground = Extension.create({ parseHTML: (element: HTMLElement) => { const styleColor = element.style?.backgroundColor - if (styleColor) return styleColor + + if (styleColor) { +return styleColor +} const dataColor = element.getAttribute("data-background-color") + return dataColor || null }, renderHTML: (attributes) => { const color = attributes.backgroundColor as string | null - if (!color) return {} + + if (!color) { +return {} +} if (this.options.useStyle) { return { @@ -118,7 +128,9 @@ export const NodeBackground = Extension.create({ this.options.types ) - if (targets.length === 0) return false + if (targets.length === 0) { +return false +} const targetColor = getTargetColor(targets, inputColor) diff --git a/resources/js/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts b/resources/js/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts index de28208..1c5d2bb 100644 --- a/resources/js/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +++ b/resources/js/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts @@ -1,5 +1,5 @@ -import { mergeAttributes } from "@tiptap/react" import TiptapHorizontalRule from "@tiptap/extension-horizontal-rule" +import { mergeAttributes } from "@tiptap/react" export const HorizontalRule = TiptapHorizontalRule.extend({ renderHTML() { diff --git a/resources/js/components/tiptap-node/image-upload-node/image-upload-node-extension.ts b/resources/js/components/tiptap-node/image-upload-node/image-upload-node-extension.ts index c282b1b..dcc4d2a 100644 --- a/resources/js/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +++ b/resources/js/components/tiptap-node/image-upload-node/image-upload-node-extension.ts @@ -1,7 +1,7 @@ +import type { NodeType } from "@tiptap/pm/model" import { mergeAttributes, Node } from "@tiptap/react" import { ReactNodeViewRenderer } from "@tiptap/react" import { ImageUploadNode as ImageUploadNodeComponent } from "@/components/tiptap-node/image-upload-node/image-upload-node" -import type { NodeType } from "@tiptap/pm/model" export type UploadFunction = ( file: File, @@ -47,7 +47,7 @@ export interface ImageUploadNodeOptions { * @default {} * @example { class: 'foo' } */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any + HTMLAttributes: Record } @@ -144,15 +144,19 @@ export const ImageUploadNode = Node.create({ editor.isActive("imageUpload") ) { const nodeEl = editor.view.nodeDOM(selection.$from.pos) + if (nodeEl && nodeEl instanceof HTMLElement) { // Since NodeViewWrapper is wrapped with a div, we need to click the first child const firstChild = nodeEl.firstChild + if (firstChild && firstChild instanceof HTMLElement) { firstChild.click() + return true } } } + return false }, } diff --git a/resources/js/components/tiptap-node/image-upload-node/image-upload-node.tsx b/resources/js/components/tiptap-node/image-upload-node/image-upload-node.tsx index e92b1bc..6caa2bb 100644 --- a/resources/js/components/tiptap-node/image-upload-node/image-upload-node.tsx +++ b/resources/js/components/tiptap-node/image-upload-node/image-upload-node.tsx @@ -1,10 +1,10 @@ "use client" -import { useRef, useState } from "react" import type { NodeViewProps } from "@tiptap/react" import { NodeViewWrapper } from "@tiptap/react" -import { Button } from "@/components/tiptap-ui-primitive/button" +import { useRef, useState } from "react" import { CloseIcon } from "@/components/tiptap-icons/close-icon" +import { Button } from "@/components/tiptap-ui-primitive/button" import "@/components/tiptap-node/image-upload-node/image-upload-node.scss" import { focusNextNode, isValidPosition } from "@/lib/tiptap-utils" @@ -91,6 +91,7 @@ function useFileUpload(options: UploadOptions) { `File size exceeds maximum allowed (${options.maxSize / 1024 / 1024}MB)` ) options.onError?.(error) + return null } @@ -124,7 +125,9 @@ function useFileUpload(options: UploadOptions) { abortController.signal ) - if (!url) throw new Error("Upload failed: No URL returned") + if (!url) { +throw new Error("Upload failed: No URL returned") +} if (!abortController.signal.aborted) { setFileItems((prev) => @@ -135,6 +138,7 @@ function useFileUpload(options: UploadOptions) { ) ) options.onSuccess?.(url) + return url } @@ -152,6 +156,7 @@ function useFileUpload(options: UploadOptions) { error instanceof Error ? error : new Error("Upload failed") ) } + return null } } @@ -159,6 +164,7 @@ function useFileUpload(options: UploadOptions) { const uploadFiles = async (files: File[]): Promise => { if (!files || files.length === 0) { options.onError?.(new Error("No files to upload")) + return [] } @@ -168,6 +174,7 @@ function useFileUpload(options: UploadOptions) { `Maximum ${options.limit} file${options.limit === 1 ? "" : "s"} allowed` ) ) + return [] } @@ -182,12 +189,15 @@ function useFileUpload(options: UploadOptions) { const removeFileItem = (fileId: string) => { setFileItems((prev) => { const fileToRemove = prev.find((item) => item.id === fileId) + if (fileToRemove?.abortController) { fileToRemove.abortController.abort() } + if (fileToRemove?.url) { URL.revokeObjectURL(fileToRemove.url) } + return prev.filter((item) => item.id !== fileId) }) } @@ -197,6 +207,7 @@ function useFileUpload(options: UploadOptions) { if (item.abortController) { item.abortController.abort() } + if (item.url) { URL.revokeObjectURL(item.url) } @@ -300,6 +311,7 @@ const ImageUploadDragArea: React.FC = ({ const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() + if (!e.currentTarget.contains(e.relatedTarget as Node)) { setIsDragActive(false) setIsDragOver(false) @@ -319,6 +331,7 @@ const ImageUploadDragArea: React.FC = ({ setIsDragOver(false) const files = Array.from(e.dataTransfer.files) + if (files.length > 0) { onFile(files) } @@ -356,10 +369,14 @@ const ImageUploadPreview: React.FC = ({ onRemove, }) => { const formatFileSize = (bytes: number) => { - if (bytes === 0) return "0 Bytes" + if (bytes === 0) { +return "0 Bytes" +} + const k = 1024 const sizes = ["Bytes", "KB", "MB", "GB"] const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}` } @@ -460,6 +477,7 @@ export const ImageUploadNode: React.FC = (props) => { const imageNodes = urls.map((url, index) => { const filename = files[index]?.name.replace(/\.[^/.]+$/, "") || "unknown" + return { type: extension.options.type, attrs: { @@ -485,10 +503,13 @@ export const ImageUploadNode: React.FC = (props) => { const handleChange = (e: React.ChangeEvent) => { const files = e.target.files + if (!files || files.length === 0) { extension.options.onError?.(new Error("No file selected")) + return } + handleUpload(Array.from(files)) } diff --git a/resources/js/components/tiptap-templates/simple/simple-editor.tsx b/resources/js/components/tiptap-templates/simple/simple-editor.tsx index 0449d56..23b14bb 100644 --- a/resources/js/components/tiptap-templates/simple/simple-editor.tsx +++ b/resources/js/components/tiptap-templates/simple/simple-editor.tsx @@ -1,18 +1,18 @@ "use client" -import { useEffect, useRef, useState } from "react" -import { EditorContent, EditorContext, useEditor } from "@tiptap/react" // --- Tiptap Core Extensions --- -import { StarterKit } from "@tiptap/starter-kit" +import { Highlight } from "@tiptap/extension-highlight" import { Image } from "@tiptap/extension-image" import { TaskItem, TaskList } from "@tiptap/extension-list" -import { TextAlign } from "@tiptap/extension-text-align" -import { Typography } from "@tiptap/extension-typography" -import { Highlight } from "@tiptap/extension-highlight" import { Subscript } from "@tiptap/extension-subscript" import { Superscript } from "@tiptap/extension-superscript" +import { TextAlign } from "@tiptap/extension-text-align" +import { Typography } from "@tiptap/extension-typography" import { Placeholder, Selection } from "@tiptap/extensions" +import { EditorContent, EditorContext, useEditor } from "@tiptap/react" +import { StarterKit } from "@tiptap/starter-kit" +import { useEffect, useRef, useState } from "react" // --- UI Primitives --- import { Button } from "@/components/tiptap-ui-primitive/button" @@ -35,9 +35,11 @@ import "@/components/tiptap-node/heading-node/heading-node.scss" import "@/components/tiptap-node/paragraph-node/paragraph-node.scss" // --- Tiptap UI --- -import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" -import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button" -import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" + +// --- Icons --- +import { ArrowLeftIcon } from "@/components/tiptap-icons/arrow-left-icon" +import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" +import { LinkIcon } from "@/components/tiptap-icons/link-icon" import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button" import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button" import { @@ -45,20 +47,18 @@ import { ColorHighlightPopoverContent, ColorHighlightPopoverButton, } from "@/components/tiptap-ui/color-highlight-popover" +import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu" +import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button" import { LinkPopover, LinkContent, LinkButton, } from "@/components/tiptap-ui/link-popover" +import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu" import { MarkButton } from "@/components/tiptap-ui/mark-button" import { TextAlignButton } from "@/components/tiptap-ui/text-align-button" import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button" -// --- Icons --- -import { ArrowLeftIcon } from "@/components/tiptap-icons/arrow-left-icon" -import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" -import { LinkIcon } from "@/components/tiptap-icons/link-icon" - // --- Hooks --- import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" diff --git a/resources/js/components/tiptap-templates/simple/theme-toggle.tsx b/resources/js/components/tiptap-templates/simple/theme-toggle.tsx index 45355b8..8c79303 100644 --- a/resources/js/components/tiptap-templates/simple/theme-toggle.tsx +++ b/resources/js/components/tiptap-templates/simple/theme-toggle.tsx @@ -1,9 +1,9 @@ +import { useEffect, useState } from "react" +import { MoonStarIcon } from "@/components/tiptap-icons/moon-star-icon" +import { SunIcon } from "@/components/tiptap-icons/sun-icon" import { Button } from "@/components/tiptap-ui-primitive/button" // --- Icons --- -import { MoonStarIcon } from "@/components/tiptap-icons/moon-star-icon" -import { SunIcon } from "@/components/tiptap-icons/sun-icon" -import { useEffect, useState } from "react" export function ThemeToggle() { const [isDarkMode, setIsDarkMode] = useState(false) @@ -12,6 +12,7 @@ export function ThemeToggle() { const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const handleChange = () => setIsDarkMode(mediaQuery.matches) mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) }, []) diff --git a/resources/js/components/tiptap-ui-primitive/button-group/button-group.tsx b/resources/js/components/tiptap-ui-primitive/button-group/button-group.tsx index ebb013e..5480bcf 100644 --- a/resources/js/components/tiptap-ui-primitive/button-group/button-group.tsx +++ b/resources/js/components/tiptap-ui-primitive/button-group/button-group.tsx @@ -1,8 +1,9 @@ import { mergeProps } from "@base-ui/react/merge-props" import { useRender } from "@base-ui/react/use-render" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/tiptap-utils" +import { cva } from "class-variance-authority" +import type {VariantProps} from "class-variance-authority"; import { Separator } from "@/components/tiptap-ui-primitive/separator" +import { cn } from "@/lib/tiptap-utils" import "./button-group.scss" const buttonGroupVariants = cva("tiptap-button-group", { diff --git a/resources/js/components/tiptap-ui-primitive/button/button.tsx b/resources/js/components/tiptap-ui-primitive/button/button.tsx index 02be4fe..d61aeb9 100644 --- a/resources/js/components/tiptap-ui-primitive/button/button.tsx +++ b/resources/js/components/tiptap-ui-primitive/button/button.tsx @@ -27,7 +27,9 @@ export interface ButtonProps extends React.ButtonHTMLAttributes = ({ shortcuts, }) => { - if (shortcuts.length === 0) return null + if (shortcuts.length === 0) { +return null +} return (
diff --git a/resources/js/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx b/resources/js/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx index 6851d7b..242d64a 100644 --- a/resources/js/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +++ b/resources/js/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx @@ -1,6 +1,6 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { cn } from "@/lib/tiptap-utils" import { CheckIcon } from "@/components/tiptap-icons/check-icon" +import { cn } from "@/lib/tiptap-utils" import "@/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss" diff --git a/resources/js/components/tiptap-ui-primitive/toolbar/toolbar.tsx b/resources/js/components/tiptap-ui-primitive/toolbar/toolbar.tsx index b6de1f4..491071b 100644 --- a/resources/js/components/tiptap-ui-primitive/toolbar/toolbar.tsx +++ b/resources/js/components/tiptap-ui-primitive/toolbar/toolbar.tsx @@ -1,9 +1,9 @@ import { forwardRef, useCallback, useEffect, useRef, useState } from "react" import { Separator } from "@/components/tiptap-ui-primitive/separator" import "@/components/tiptap-ui-primitive/toolbar/toolbar.scss" -import { cn } from "@/lib/tiptap-utils" -import { useMenuNavigation } from "@/hooks/use-menu-navigation" import { useComposedRef } from "@/hooks/use-composed-ref" +import { useMenuNavigation } from "@/hooks/use-menu-navigation" +import { cn } from "@/lib/tiptap-utils" type BaseProps = React.HTMLAttributes @@ -17,7 +17,10 @@ const useToolbarNavigation = ( const [items, setItems] = useState([]) const collectItems = useCallback(() => { - if (!toolbarRef.current) return [] + if (!toolbarRef.current) { +return [] +} + return Array.from( toolbarRef.current.querySelectorAll( 'button:not([disabled]), [role="button"]:not([disabled]), [tabindex="0"]:not([disabled])' @@ -27,7 +30,10 @@ const useToolbarNavigation = ( useEffect(() => { const toolbar = toolbarRef.current - if (!toolbar) return + + if (!toolbar) { +return +} const updateItems = () => setItems(collectItems()) @@ -48,17 +54,25 @@ const useToolbarNavigation = ( useEffect(() => { const toolbar = toolbarRef.current - if (!toolbar) return + + if (!toolbar) { +return +} const handleFocus = (e: FocusEvent) => { const target = e.target as HTMLElement - if (toolbar.contains(target)) - target.setAttribute("data-focus-visible", "true") + + if (toolbar.contains(target)) { +target.setAttribute("data-focus-visible", "true") +} } const handleBlur = (e: FocusEvent) => { const target = e.target as HTMLElement - if (toolbar.contains(target)) target.removeAttribute("data-focus-visible") + + if (toolbar.contains(target)) { +target.removeAttribute("data-focus-visible") +} } toolbar.addEventListener("focus", handleFocus, true) diff --git a/resources/js/components/tiptap-ui-primitive/tooltip/tooltip.tsx b/resources/js/components/tiptap-ui-primitive/tooltip/tooltip.tsx index 59ecc44..cce140a 100644 --- a/resources/js/components/tiptap-ui-primitive/tooltip/tooltip.tsx +++ b/resources/js/components/tiptap-ui-primitive/tooltip/tooltip.tsx @@ -1,15 +1,5 @@ "use client" -import { - cloneElement, - createContext, - forwardRef, - isValidElement, - useContext, - useMemo, - useState, - version, -} from "react" import { useFloating, autoUpdate, @@ -23,11 +13,22 @@ import { useInteractions, useMergeRefs, FloatingPortal, - type Placement, - type UseFloatingReturn, - type ReferenceType, - FloatingDelayGroup, + + + + FloatingDelayGroup } from "@floating-ui/react" +import type {Placement, UseFloatingReturn, ReferenceType} from "@floating-ui/react"; +import { + cloneElement, + createContext, + forwardRef, + isValidElement, + useContext, + useMemo, + useState, + version, +} from "react" import "@/components/tiptap-ui-primitive/tooltip/tooltip.scss" interface TooltipProviderProps { @@ -165,9 +166,9 @@ export const TooltipTrigger = forwardRef( const context = useTooltipContext() const childrenRef = isValidElement(children) ? parseInt(version, 10) >= 19 - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + ? (children as { props: { ref?: React.Ref } }).props.ref - : // eslint-disable-next-line @typescript-eslint/no-explicit-any + : (children as any).ref : undefined const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]) @@ -208,7 +209,9 @@ export const TooltipContent = forwardRef( const context = useTooltipContext() const ref = useMergeRefs([context.refs.setFloating, propRef]) - if (!context.open) return null + if (!context.open) { +return null +} const content = (
, UseBlockquoteConfig { @@ -79,7 +79,11 @@ export const BlockquoteButton = forwardRef< const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleToggle() }, [handleToggle, onClick] diff --git a/resources/js/components/tiptap-ui/blockquote-button/use-blockquote.ts b/resources/js/components/tiptap-ui/blockquote-button/use-blockquote.ts index 7e4f816..d783793 100644 --- a/resources/js/components/tiptap-ui/blockquote-button/use-blockquote.ts +++ b/resources/js/components/tiptap-ui/blockquote-button/use-blockquote.ts @@ -1,14 +1,14 @@ "use client" -import { useCallback, useEffect, useState } from "react" -import type { Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" +import type { Editor } from "@tiptap/react" +import { useCallback, useEffect, useState } from "react" // --- Hooks --- +import { BlockquoteIcon } from "@/components/tiptap-icons/blockquote-icon" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- -import { BlockquoteIcon } from "@/components/tiptap-icons/blockquote-icon" // --- UI Utils --- import { @@ -48,12 +48,16 @@ export function canToggleBlockquote( editor: Editor | null, turnInto: boolean = true ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + if ( !isNodeInSchema("blockquote", editor) || isNodeTypeSelected(editor, ["image"]) - ) - return false + ) { +return false +} if (!turnInto) { return editor.can().toggleWrap("blockquote") @@ -70,8 +74,9 @@ export function canToggleBlockquote( "blockquote", "codeBlock", ]) - ) - return false + ) { +return false +} // Either we can wrap in blockquote directly on the selection, // or we can clear formatting/nodes to arrive at a blockquote. @@ -82,8 +87,13 @@ export function canToggleBlockquote( * Toggles blockquote formatting for a specific node or the current selection */ export function toggleBlockquote(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false - if (!canToggleBlockquote(editor)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canToggleBlockquote(editor)) { +return false +} try { const view = editor.view @@ -115,7 +125,10 @@ export function toggleBlockquote(editor: Editor | null): boolean { editor, node: state.selection.$anchor.node(1), })?.pos - if (!isValidPosition(pos)) return false + + if (!isValidPosition(pos)) { +return false +} tr = tr.setSelection(NodeSelection.create(state.doc, pos)) view.dispatch(tr) @@ -170,15 +183,21 @@ export function shouldShowButton(props: { }): boolean { const { editor, hideWhenUnavailable } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} - if (!isNodeInSchema("blockquote", editor)) return false + if (!isNodeInSchema("blockquote", editor)) { +return false +} if (!editor.isActive("code")) { return canToggleBlockquote(editor) @@ -236,7 +255,9 @@ export function useBlockquote(config?: UseBlockquoteConfig) { const isActive = editor?.isActive("blockquote") || false useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) @@ -252,12 +273,16 @@ export function useBlockquote(config?: UseBlockquoteConfig) { }, [editor, hideWhenUnavailable]) const handleToggle = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = toggleBlockquote(editor) + if (success) { onToggled?.() } + return success }, [editor, onToggled]) diff --git a/resources/js/components/tiptap-ui/code-block-button/code-block-button.tsx b/resources/js/components/tiptap-ui/code-block-button/code-block-button.tsx index c1e9b79..be77b60 100644 --- a/resources/js/components/tiptap-ui/code-block-button/code-block-button.tsx +++ b/resources/js/components/tiptap-ui/code-block-button/code-block-button.tsx @@ -1,10 +1,8 @@ import { forwardRef, useCallback } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Tiptap UI --- import type { UseCodeBlockConfig } from "@/components/tiptap-ui/code-block-button" @@ -14,9 +12,11 @@ import { } from "@/components/tiptap-ui/code-block-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" export interface CodeBlockButtonProps extends Omit, UseCodeBlockConfig { @@ -79,7 +79,11 @@ export const CodeBlockButton = forwardRef< const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleToggle() }, [handleToggle, onClick] diff --git a/resources/js/components/tiptap-ui/code-block-button/use-code-block.ts b/resources/js/components/tiptap-ui/code-block-button/use-code-block.ts index 8801286..b003fac 100644 --- a/resources/js/components/tiptap-ui/code-block-button/use-code-block.ts +++ b/resources/js/components/tiptap-ui/code-block-button/use-code-block.ts @@ -1,10 +1,11 @@ "use client" -import { useCallback, useEffect, useState } from "react" -import { type Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" +import type {Editor} from "@tiptap/react"; +import { useCallback, useEffect, useState } from "react" // --- Hooks --- +import { CodeBlockIcon } from "@/components/tiptap-icons/code-block-icon" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- @@ -18,7 +19,6 @@ import { } from "@/lib/tiptap-utils" // --- Icons --- -import { CodeBlockIcon } from "@/components/tiptap-icons/code-block-icon" export const CODE_BLOCK_SHORTCUT_KEY = "mod+alt+c" @@ -48,12 +48,16 @@ export function canToggle( editor: Editor | null, turnInto: boolean = true ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + if ( !isNodeInSchema("codeBlock", editor) || isNodeTypeSelected(editor, ["image"]) - ) - return false + ) { +return false +} if (!turnInto) { return editor.can().toggleNode("codeBlock", "paragraph") @@ -70,8 +74,9 @@ export function canToggle( "blockquote", "codeBlock", ]) - ) - return false + ) { +return false +} // Either we can toggle code block directly on the selection, // or we can clear formatting/nodes to arrive at a code block. @@ -85,8 +90,13 @@ export function canToggle( * Toggles code block in the editor */ export function toggleCodeBlock(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false - if (!canToggle(editor)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canToggle(editor)) { +return false +} try { const view = editor.view @@ -118,7 +128,10 @@ export function toggleCodeBlock(editor: Editor | null): boolean { editor, node: state.selection.$anchor.node(1), })?.pos - if (!isValidPosition(pos)) return false + + if (!isValidPosition(pos)) { +return false +} tr = tr.setSelection(NodeSelection.create(state.doc, pos)) view.dispatch(tr) @@ -173,15 +186,21 @@ export function shouldShowButton(props: { }): boolean { const { editor, hideWhenUnavailable } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} - if (!isNodeInSchema("codeBlock", editor)) return false + if (!isNodeInSchema("codeBlock", editor)) { +return false +} if (!editor.isActive("code")) { return canToggle(editor) @@ -246,7 +265,9 @@ export function useCodeBlock(config?: UseCodeBlockConfig) { const isActive = editor?.isActive("codeBlock") || false useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) @@ -262,12 +283,16 @@ export function useCodeBlock(config?: UseCodeBlockConfig) { }, [editor, hideWhenUnavailable]) const handleToggle = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = toggleCodeBlock(editor) + if (success) { onToggled?.() } + return success }, [editor, onToggled]) diff --git a/resources/js/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx b/resources/js/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx index 76fbeb4..fa53fa0 100644 --- a/resources/js/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +++ b/resources/js/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx @@ -1,10 +1,8 @@ import { forwardRef, useCallback, useMemo } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Tiptap UI --- import type { UseColorHighlightConfig } from "@/components/tiptap-ui/color-highlight-button" @@ -14,9 +12,11 @@ import { } from "@/components/tiptap-ui/color-highlight-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Styles --- import "@/components/tiptap-ui/color-highlight-button/color-highlight-button.scss" @@ -112,7 +112,11 @@ export const ColorHighlightButton = forwardRef< const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleColorHighlight() }, [handleColorHighlight, onClick] diff --git a/resources/js/components/tiptap-ui/color-highlight-button/use-color-highlight.ts b/resources/js/components/tiptap-ui/color-highlight-button/use-color-highlight.ts index 4c7cfce..df0e966 100644 --- a/resources/js/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +++ b/resources/js/components/tiptap-ui/color-highlight-button/use-color-highlight.ts @@ -1,12 +1,13 @@ "use client" +import type {Editor} from "@tiptap/react"; import { useCallback, useEffect, useState } from "react" -import { type Editor } from "@tiptap/react" import { useHotkeys } from "react-hotkeys-hook" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- import { @@ -16,7 +17,6 @@ import { } from "@/lib/tiptap-utils" // --- Icons --- -import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" export const COLOR_HIGHLIGHT_SHORTCUT_KEY = "mod+shift+h" export const HIGHLIGHT_COLORS = [ @@ -136,6 +136,7 @@ export function pickHighlightColorsByValue(values: string[]) { const colorMap = new Map( HIGHLIGHT_COLORS.map((color) => [color.value, color]) ) + return values .map((value) => colorMap.get(value)) .filter((color): color is (typeof HIGHLIGHT_COLORS)[number] => !!color) @@ -148,11 +149,14 @@ export function getHighlightColorValue( color: string, useColorValue: boolean = false ): string { - if (!useColorValue) return color + if (!useColorValue) { +return color +} const colorItem = HIGHLIGHT_COLORS.find( (c) => c.value === color || c.colorValue === color ) + return colorItem?.colorValue || color } @@ -163,18 +167,23 @@ export function canColorHighlight( editor: Editor | null, mode: HighlightMode = "mark" ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} if (mode === "mark") { if ( !isMarkInSchema("highlight", editor) || isNodeTypeSelected(editor, ["image"]) - ) - return false + ) { +return false +} return editor.can().setMark("highlight") } else { - if (!isExtensionAvailable(editor, ["nodeBackground"])) return false + if (!isExtensionAvailable(editor, ["nodeBackground"])) { +return false +} try { return editor.can().toggleNodeBackgroundColor("test") @@ -192,26 +201,33 @@ export function isColorHighlightActive( highlightColor?: string, mode: HighlightMode = "mark" ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} if (mode === "mark") { return highlightColor ? editor.isActive("highlight", { color: highlightColor }) : editor.isActive("highlight") } else { - if (!highlightColor) return false + if (!highlightColor) { +return false +} try { const { state } = editor const { selection } = state const $pos = selection.$anchor + for (let depth = $pos.depth; depth >= 0; depth--) { const node = $pos.node(depth) + if (node && node.attrs?.backgroundColor === highlightColor) { return true } } + return false } catch { return false @@ -226,8 +242,13 @@ export function removeHighlight( editor: Editor | null, mode: HighlightMode = "mark" ): boolean { - if (!editor || !editor.isEditable) return false - if (!canColorHighlight(editor, mode)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canColorHighlight(editor, mode)) { +return false +} if (mode === "mark") { return editor.chain().focus().unsetMark("highlight").run() @@ -246,19 +267,27 @@ export function shouldShowButton(props: { }): boolean { const { editor, hideWhenUnavailable, mode } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} // hideWhenUnavailable=true: check schema/extension availability if (mode === "mark") { - if (!isMarkInSchema("highlight", editor)) return false + if (!isMarkInSchema("highlight", editor)) { +return false +} } else { - if (!isExtensionAvailable(editor, ["nodeBackground"])) return false + if (!isExtensionAvailable(editor, ["nodeBackground"])) { +return false +} } if (!editor.isActive("code")) { @@ -289,7 +318,9 @@ export function useColorHighlight(config: UseColorHighlightConfig) { const isActive = isColorHighlightActive(editor, actualColor, mode) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, mode })) @@ -305,12 +336,14 @@ export function useColorHighlight(config: UseColorHighlightConfig) { }, [editor, hideWhenUnavailable, mode]) const handleColorHighlight = useCallback(() => { - if (!editor || !canColorHighlightState || !actualColor || !label) - return false + if (!editor || !canColorHighlightState || !actualColor || !label) { +return false +} if (mode === "mark") { if (editor.state.storedMarks) { const highlightMarkType = editor.schema.marks.highlight + if (highlightMarkType) { editor.view.dispatch( editor.state.tr.removeStoredMark(highlightMarkType) @@ -324,9 +357,11 @@ export function useColorHighlight(config: UseColorHighlightConfig) { .focus() .toggleHighlight({ color: actualColor }) .run() + if (success) { onApplied?.({ color: actualColor, label, mode }) } + return success }, 0) @@ -341,15 +376,18 @@ export function useColorHighlight(config: UseColorHighlightConfig) { if (success) { onApplied?.({ color: actualColor, label, mode }) } + return success } }, [canColorHighlightState, actualColor, editor, label, onApplied, mode]) const handleRemoveHighlight = useCallback(() => { const success = removeHighlight(editor, mode) + if (success) { onApplied?.({ color: "", label: "Remove highlight", mode }) } + return success }, [editor, onApplied, mode]) diff --git a/resources/js/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx b/resources/js/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx index 5a92f2c..02edacc 100644 --- a/resources/js/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +++ b/resources/js/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx @@ -1,31 +1,13 @@ +import type {Editor} from "@tiptap/react"; import { forwardRef, useMemo, useRef, useState } from "react" -import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useMenuNavigation } from "@/hooks/use-menu-navigation" -import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { BanIcon } from "@/components/tiptap-icons/ban-icon" import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { - Popover, - PopoverTrigger, - PopoverContent, -} from "@/components/tiptap-ui-primitive/popover" -import { Separator } from "@/components/tiptap-ui-primitive/separator" -import { - Card, - CardBody, - CardItemGroup, -} from "@/components/tiptap-ui-primitive/card" - -// --- Tiptap UI --- import type { HighlightColor, UseColorHighlightConfig, @@ -35,7 +17,25 @@ import { pickHighlightColorsByValue, useColorHighlight, } from "@/components/tiptap-ui/color-highlight-button" +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" import { ButtonGroup } from "@/components/tiptap-ui-primitive/button-group" +import { + Card, + CardBody, + CardItemGroup, +} from "@/components/tiptap-ui-primitive/card" +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/tiptap-ui-primitive/popover" +import { Separator } from "@/components/tiptap-ui-primitive/separator" + +// --- Tiptap UI --- +import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" +import { useMenuNavigation } from "@/hooks/use-menu-navigation" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" export interface ColorHighlightPopoverContentProps { /** @@ -120,12 +120,22 @@ export function ColorHighlightPopoverContent({ items: menuItems, orientation: "both", onSelect: (item) => { - if (!containerRef.current) return false + if (!containerRef.current) { +return false +} + const highlightedElement = containerRef.current.querySelector( '[data-highlighted="true"]' ) as HTMLElement - if (highlightedElement) highlightedElement.click() - if (item.value === "none") handleRemoveHighlight() + + if (highlightedElement) { +highlightedElement.click() +} + + if (item.value === "none") { +handleRemoveHighlight() +} + return true }, autoSelectFirstItem: false, @@ -200,7 +210,9 @@ export function ColorHighlightPopover({ onApplied, }) - if (!isVisible) return null + if (!isVisible) { +return null +} return ( diff --git a/resources/js/components/tiptap-ui/heading-button/heading-button.tsx b/resources/js/components/tiptap-ui/heading-button/heading-button.tsx index b0b5394..7a5bfdb 100644 --- a/resources/js/components/tiptap-ui/heading-button/heading-button.tsx +++ b/resources/js/components/tiptap-ui/heading-button/heading-button.tsx @@ -1,7 +1,6 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Tiptap UI --- import type { @@ -14,10 +13,11 @@ import { } from "@/components/tiptap-ui/heading-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" export interface HeadingButtonProps extends Omit, UseHeadingConfig { @@ -81,7 +81,11 @@ export const HeadingButton = forwardRef( const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleToggle() }, [handleToggle, onClick] diff --git a/resources/js/components/tiptap-ui/heading-button/use-heading.ts b/resources/js/components/tiptap-ui/heading-button/use-heading.ts index 7d76cdc..3a284f0 100644 --- a/resources/js/components/tiptap-ui/heading-button/use-heading.ts +++ b/resources/js/components/tiptap-ui/heading-button/use-heading.ts @@ -1,10 +1,16 @@ "use client" -import { useCallback, useEffect, useState } from "react" -import { type Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" +import type {Editor} from "@tiptap/react"; +import { useCallback, useEffect, useState } from "react" // --- Hooks --- +import { HeadingFiveIcon } from "@/components/tiptap-icons/heading-five-icon" +import { HeadingFourIcon } from "@/components/tiptap-icons/heading-four-icon" +import { HeadingOneIcon } from "@/components/tiptap-icons/heading-one-icon" +import { HeadingSixIcon } from "@/components/tiptap-icons/heading-six-icon" +import { HeadingThreeIcon } from "@/components/tiptap-icons/heading-three-icon" +import { HeadingTwoIcon } from "@/components/tiptap-icons/heading-two-icon" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- @@ -18,12 +24,6 @@ import { } from "@/lib/tiptap-utils" // --- Icons --- -import { HeadingOneIcon } from "@/components/tiptap-icons/heading-one-icon" -import { HeadingTwoIcon } from "@/components/tiptap-icons/heading-two-icon" -import { HeadingThreeIcon } from "@/components/tiptap-icons/heading-three-icon" -import { HeadingFourIcon } from "@/components/tiptap-icons/heading-four-icon" -import { HeadingFiveIcon } from "@/components/tiptap-icons/heading-five-icon" -import { HeadingSixIcon } from "@/components/tiptap-icons/heading-six-icon" export type Level = 1 | 2 | 3 | 4 | 5 | 6 @@ -76,12 +76,16 @@ export function canToggle( level?: Level, turnInto: boolean = true ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + if ( !isNodeInSchema("heading", editor) || isNodeTypeSelected(editor, ["image"]) - ) - return false + ) { +return false +} if (!turnInto) { return level @@ -100,8 +104,9 @@ export function canToggle( "blockquote", "codeBlock", ]) - ) - return false + ) { +return false +} // Either we can set heading directly on the selection, // or we can clear formatting/nodes to arrive at a heading. @@ -117,7 +122,9 @@ export function isHeadingActive( editor: Editor | null, level?: Level | Level[] ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} if (Array.isArray(level)) { return level.some((l) => editor.isActive("heading", { level: l })) @@ -135,12 +142,16 @@ export function toggleHeading( editor: Editor | null, level: Level | Level[] ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} const levels = Array.isArray(level) ? level : [level] const toggleLevel = levels.find((l) => canToggle(editor, l)) - if (!toggleLevel) return false + if (!toggleLevel) { +return false +} try { const view = editor.view @@ -172,7 +183,10 @@ export function toggleHeading( editor, node: state.selection.$anchor.node(1), })?.pos - if (!isValidPosition(pos)) return false + + if (!isValidPosition(pos)) { +return false +} tr = tr.setSelection(NodeSelection.create(state.doc, pos)) view.dispatch(tr) @@ -231,20 +245,27 @@ export function shouldShowButton(props: { }): boolean { const { editor, level, hideWhenUnavailable } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} - if (!isNodeInSchema("heading", editor)) return false + if (!isNodeInSchema("heading", editor)) { +return false +} if (!editor.isActive("code")) { if (Array.isArray(level)) { return level.some((l) => canToggle(editor, l)) } + return canToggle(editor, level) } @@ -311,7 +332,9 @@ export function useHeading(config: UseHeadingConfig) { const isActive = isHeadingActive(editor, level) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, level, hideWhenUnavailable })) @@ -327,12 +350,16 @@ export function useHeading(config: UseHeadingConfig) { }, [editor, level, hideWhenUnavailable]) const handleToggle = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = toggleHeading(editor, level) + if (success) { onToggled?.() } + return success }, [editor, level, onToggled]) diff --git a/resources/js/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx b/resources/js/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx index 8a43836..4d8697c 100644 --- a/resources/js/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +++ b/resources/js/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx @@ -4,7 +4,6 @@ import { forwardRef, useCallback, useState } from "react" import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Tiptap UI --- import { HeadingButton } from "@/components/tiptap-ui/heading-button" @@ -21,6 +20,7 @@ import { DropdownMenuItem, DropdownMenuGroup, } from "@/components/tiptap-ui-primitive/dropdown-menu" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" export interface HeadingDropdownMenuProps extends Omit, UseHeadingDropdownMenuConfig { @@ -65,7 +65,10 @@ export const HeadingDropdownMenu = forwardRef< const handleOpenChange = useCallback( (open: boolean) => { - if (!editor || !canToggle) return + if (!editor || !canToggle) { +return +} + setIsOpen(open) onOpenChange?.(open) }, diff --git a/resources/js/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts b/resources/js/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts index b25ce63..f3a2f9e 100644 --- a/resources/js/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +++ b/resources/js/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts @@ -1,10 +1,9 @@ "use client" -import { useEffect, useState } from "react" import type { Editor } from "@tiptap/react" +import { useEffect, useState } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { HeadingIcon } from "@/components/tiptap-icons/heading-icon" @@ -12,11 +11,13 @@ import { HeadingIcon } from "@/components/tiptap-icons/heading-icon" // --- Tiptap UI --- import { headingIcons, - type Level, + isHeadingActive, canToggle, - shouldShowButton, + shouldShowButton } from "@/components/tiptap-ui/heading-button" +import type {Level} from "@/components/tiptap-ui/heading-button"; +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" /** * Configuration for the heading dropdown menu functionality @@ -45,7 +46,10 @@ export function getActiveHeadingLevel( editor: Editor | null, levels: Level[] = [1, 2, 3, 4, 5, 6] ): Level | undefined { - if (!editor || !editor.isEditable) return undefined + if (!editor || !editor.isEditable) { +return undefined +} + return levels.find((level) => isHeadingActive(editor, level)) } @@ -103,7 +107,9 @@ export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) { const canToggleState = canToggle(editor) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible( diff --git a/resources/js/components/tiptap-ui/image-upload-button/image-upload-button.tsx b/resources/js/components/tiptap-ui/image-upload-button/image-upload-button.tsx index 3ee2ef4..757890a 100644 --- a/resources/js/components/tiptap-ui/image-upload-button/image-upload-button.tsx +++ b/resources/js/components/tiptap-ui/image-upload-button/image-upload-button.tsx @@ -1,10 +1,8 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Tiptap UI --- import type { UseImageUploadConfig } from "@/components/tiptap-ui/image-upload-button" @@ -14,9 +12,11 @@ import { } from "@/components/tiptap-ui/image-upload-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" type IconProps = React.SVGProps type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement @@ -87,7 +87,11 @@ export const ImageUploadButton = forwardRef< const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleImage() }, [handleImage, onClick] diff --git a/resources/js/components/tiptap-ui/image-upload-button/use-image-upload.ts b/resources/js/components/tiptap-ui/image-upload-button/use-image-upload.ts index f956ab1..821f8ef 100644 --- a/resources/js/components/tiptap-ui/image-upload-button/use-image-upload.ts +++ b/resources/js/components/tiptap-ui/image-upload-button/use-image-upload.ts @@ -1,18 +1,18 @@ "use client" +import type {Editor} from "@tiptap/react"; import { useCallback, useEffect, useState } from "react" import { useHotkeys } from "react-hotkeys-hook" -import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { ImagePlusIcon } from "@/components/tiptap-icons/image-plus-icon" import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- import { isExtensionAvailable } from "@/lib/tiptap-utils" // --- Icons --- -import { ImagePlusIcon } from "@/components/tiptap-icons/image-plus-icon" export const IMAGE_UPLOAD_SHORTCUT_KEY = "mod+shift+i" @@ -39,8 +39,13 @@ export interface UseImageUploadConfig { * Checks if image can be inserted in the current editor state */ export function canInsertImage(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false - if (!isExtensionAvailable(editor, "imageUpload")) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!isExtensionAvailable(editor, "imageUpload")) { +return false +} return editor.can().insertContent({ type: "imageUpload" }) } @@ -49,7 +54,10 @@ export function canInsertImage(editor: Editor | null): boolean { * Checks if image is currently active */ export function isImageActive(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + return editor.isActive("imageUpload") } @@ -57,8 +65,13 @@ export function isImageActive(editor: Editor | null): boolean { * Inserts an image in the editor */ export function insertImage(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false - if (!canInsertImage(editor)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canInsertImage(editor)) { +return false +} try { return editor @@ -82,13 +95,17 @@ export function shouldShowButton(props: { }): boolean { const { editor, hideWhenUnavailable } = props - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} if (!hideWhenUnavailable) { return true } - if (!isExtensionAvailable(editor, "imageUpload")) return false + if (!isExtensionAvailable(editor, "imageUpload")) { +return false +} if (!editor.isActive("code")) { return canInsertImage(editor) @@ -147,7 +164,9 @@ export function useImageUpload(config?: UseImageUploadConfig) { const isActive = isImageActive(editor) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) @@ -163,12 +182,16 @@ export function useImageUpload(config?: UseImageUploadConfig) { }, [editor, hideWhenUnavailable]) const handleImage = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = insertImage(editor) + if (success) { onInserted?.() } + return success }, [editor, onInserted]) diff --git a/resources/js/components/tiptap-ui/link-popover/link-popover.tsx b/resources/js/components/tiptap-ui/link-popover/link-popover.tsx index 65d0233..e1c4d36 100644 --- a/resources/js/components/tiptap-ui/link-popover/link-popover.tsx +++ b/resources/js/components/tiptap-ui/link-popover/link-popover.tsx @@ -1,11 +1,9 @@ "use client" -import { forwardRef, useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" +import { forwardRef, useCallback, useEffect, useState } from "react" // --- Hooks --- -import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { CornerDownLeftIcon } from "@/components/tiptap-icons/corner-down-left-icon" @@ -20,19 +18,21 @@ import { useLinkPopover } from "@/components/tiptap-ui/link-popover" // --- UI Primitives --- import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/tiptap-ui-primitive/popover" -import { Separator } from "@/components/tiptap-ui-primitive/separator" +import { ButtonGroup } from "@/components/tiptap-ui-primitive/button-group" import { Card, CardBody, CardItemGroup, } from "@/components/tiptap-ui-primitive/card" import { Input } from "@/components/tiptap-ui-primitive/input" -import { ButtonGroup } from "@/components/tiptap-ui-primitive/button-group" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/tiptap-ui-primitive/popover" +import { Separator } from "@/components/tiptap-ui-primitive/separator" +import { useIsBreakpoint } from "@/hooks/use-is-breakpoint" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" import "./link-popover.scss" @@ -259,7 +259,11 @@ export const LinkPopover = forwardRef( const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + setIsOpen(!isOpen) }, [onClick, isOpen] diff --git a/resources/js/components/tiptap-ui/link-popover/use-link-popover.ts b/resources/js/components/tiptap-ui/link-popover/use-link-popover.ts index 31aef60..19ce441 100644 --- a/resources/js/components/tiptap-ui/link-popover/use-link-popover.ts +++ b/resources/js/components/tiptap-ui/link-popover/use-link-popover.ts @@ -1,11 +1,11 @@ -import { useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" +import { useCallback, useEffect, useState } from "react" // --- Hooks --- +import { LinkIcon } from "@/components/tiptap-icons/link-icon" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- -import { LinkIcon } from "@/components/tiptap-icons/link-icon" // --- Lib --- import { @@ -51,11 +51,16 @@ export interface LinkHandlerProps { * Checks if a link can be set in the current editor state */ export function canSetLink(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} // The third argument 'true' checks whether the current selection is inside an image caption, and prevents setting a link there // If the selection is inside an image caption, we can't set a link - if (isNodeTypeSelected(editor, ["image"], true)) return false + if (isNodeTypeSelected(editor, ["image"], true)) { +return false +} + try { return editor.can().setMark("link") } catch { @@ -67,7 +72,10 @@ export function canSetLink(editor: Editor | null): boolean { * Checks if a link is currently active in the editor */ export function isLinkActive(editor: Editor | null): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + return editor.isActive("link") } @@ -80,7 +88,9 @@ export function shouldShowLinkButton(props: { }): boolean { const { editor, hideWhenUnavailable } = props - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} const linkInSchema = isMarkInSchema("link", editor) @@ -110,7 +120,9 @@ export function useLinkHandler(props: LinkHandlerProps) { const [url, setUrl] = useState(null) useEffect(() => { - if (!editor) return + if (!editor) { +return +} // Get URL immediately on mount const { href } = editor.getAttributes("link") @@ -121,7 +133,9 @@ export function useLinkHandler(props: LinkHandlerProps) { }, [editor, url]) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const updateLinkState = () => { const { href } = editor.getAttributes("link") @@ -129,13 +143,16 @@ export function useLinkHandler(props: LinkHandlerProps) { } editor.on("selectionUpdate", updateLinkState) + return () => { editor.off("selectionUpdate", updateLinkState) } }, [editor]) const setLink = useCallback(() => { - if (!url || !editor) return + if (!url || !editor) { +return +} const { selection } = editor.state const isEmpty = selection.empty @@ -156,7 +173,10 @@ export function useLinkHandler(props: LinkHandlerProps) { }, [editor, onSetLink, url]) const removeLink = useCallback(() => { - if (!editor) return + if (!editor) { +return +} + editor .chain() .focus() @@ -169,9 +189,12 @@ export function useLinkHandler(props: LinkHandlerProps) { const openLink = useCallback( (target: string = "_blank", features: string = "noopener,noreferrer") => { - if (!url) return + if (!url) { +return +} const safeUrl = sanitizeUrl(url, window.location.href) + if (safeUrl !== "#") { window.open(safeUrl, target, features) } @@ -203,7 +226,9 @@ export function useLinkState(props: { const [isVisible, setIsVisible] = useState(true) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible( diff --git a/resources/js/components/tiptap-ui/list-button/list-button.tsx b/resources/js/components/tiptap-ui/list-button/list-button.tsx index 13a3909..e922d96 100644 --- a/resources/js/components/tiptap-ui/list-button/list-button.tsx +++ b/resources/js/components/tiptap-ui/list-button/list-button.tsx @@ -1,19 +1,19 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- UI Primitives --- -import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" -import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" - -// --- Tiptap UI --- import type { ListType, UseListConfig } from "@/components/tiptap-ui/list-button" import { LIST_SHORTCUT_KEYS, useList } from "@/components/tiptap-ui/list-button" +import { Badge } from "@/components/tiptap-ui-primitive/badge" +import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" +import { Button } from "@/components/tiptap-ui-primitive/button" + +// --- Tiptap UI --- +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" export interface ListButtonProps extends Omit, UseListConfig { @@ -77,7 +77,11 @@ export const ListButton = forwardRef( const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleToggle() }, [handleToggle, onClick] diff --git a/resources/js/components/tiptap-ui/list-button/use-list.ts b/resources/js/components/tiptap-ui/list-button/use-list.ts index b07f440..8d2554a 100644 --- a/resources/js/components/tiptap-ui/list-button/use-list.ts +++ b/resources/js/components/tiptap-ui/list-button/use-list.ts @@ -1,16 +1,16 @@ "use client" -import { useCallback, useEffect, useState } from "react" -import { type Editor } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" +import type {Editor} from "@tiptap/react"; +import { useCallback, useEffect, useState } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { ListIcon } from "@/components/tiptap-icons/list-icon" import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon" import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- import { @@ -73,9 +73,13 @@ export function canToggleList( type: ListType, turnInto: boolean = true ): boolean { - if (!editor || !editor.isEditable) return false - if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) - return false + if (!editor || !editor.isEditable) { +return false +} + + if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) { +return false +} if (!turnInto) { switch (type) { @@ -101,8 +105,9 @@ export function canToggleList( "blockquote", "codeBlock", ]) - ) - return false + ) { +return false +} // Either we can set list directly on the selection, // or we can clear formatting/nodes to arrive at a list. @@ -125,7 +130,9 @@ export function canToggleList( * Checks if list is currently active */ export function isListActive(editor: Editor | null, type: ListType): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} switch (type) { case "bulletList": @@ -143,8 +150,13 @@ export function isListActive(editor: Editor | null, type: ListType): boolean { * Toggles list in the editor */ export function toggleList(editor: Editor | null, type: ListType): boolean { - if (!editor || !editor.isEditable) return false - if (!canToggleList(editor, type)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canToggleList(editor, type)) { +return false +} try { const view = editor.view @@ -176,7 +188,10 @@ export function toggleList(editor: Editor | null, type: ListType): boolean { editor, node: state.selection.$anchor.node(1), })?.pos - if (!isValidPosition(pos)) return false + + if (!isValidPosition(pos)) { +return false +} tr = tr.setSelection(NodeSelection.create(state.doc, pos)) view.dispatch(tr) @@ -225,7 +240,10 @@ export function toggleList(editor: Editor | null, type: ListType): boolean { } const toggle = toggleMap[type] - if (!toggle) return false + + if (!toggle) { +return false +} toggle().run() } @@ -248,15 +266,21 @@ export function shouldShowButton(props: { }): boolean { const { editor, type, hideWhenUnavailable } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} - if (!isNodeInSchema(type, editor)) return false + if (!isNodeInSchema(type, editor)) { +return false +} if (!editor.isActive("code")) { return canToggleList(editor, type) @@ -316,7 +340,9 @@ export function useList(config: UseListConfig) { const isActive = isListActive(editor, type) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable })) @@ -332,12 +358,16 @@ export function useList(config: UseListConfig) { }, [editor, type, hideWhenUnavailable]) const handleToggle = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = toggleList(editor, type) + if (success) { onToggled?.() } + return success }, [editor, type, onToggled]) diff --git a/resources/js/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx b/resources/js/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx index 0c0dc5e..268b69a 100644 --- a/resources/js/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +++ b/resources/js/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx @@ -1,14 +1,14 @@ +import type {Editor} from "@tiptap/react"; import { useCallback, useState } from "react" -import { type Editor } from "@tiptap/react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon" // --- Tiptap UI --- -import { ListButton, type ListType } from "@/components/tiptap-ui/list-button" +import { ListButton } from "@/components/tiptap-ui/list-button" +import type {ListType} from "@/components/tiptap-ui/list-button"; import { useListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu" @@ -22,6 +22,7 @@ import { DropdownMenuItem, DropdownMenuGroup, } from "@/components/tiptap-ui-primitive/dropdown-menu" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" export interface ListDropdownMenuProps extends Omit { /** diff --git a/resources/js/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts b/resources/js/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts index 35299a0..daa73a8 100644 --- a/resources/js/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +++ b/resources/js/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts @@ -1,10 +1,9 @@ "use client" -import { useEffect, useMemo, useState } from "react" import type { Editor } from "@tiptap/react" +import { useEffect, useMemo, useState } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Icons --- import { ListIcon } from "@/components/tiptap-icons/list-icon" @@ -12,15 +11,17 @@ import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon" import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon" // --- Lib --- -import { isNodeInSchema } from "@/lib/tiptap-utils" // --- Tiptap UI --- import { canToggleList, isListActive, - listIcons, - type ListType, + listIcons + } from "@/components/tiptap-ui/list-button" +import type {ListType} from "@/components/tiptap-ui/list-button"; +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { isNodeInSchema } from "@/lib/tiptap-utils" /** * Configuration for the list dropdown menu functionality @@ -70,7 +71,10 @@ export function canToggleAnyList( editor: Editor | null, listTypes: ListType[] ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + return listTypes.some((type) => canToggleList(editor, type)) } @@ -78,7 +82,10 @@ export function isAnyListActive( editor: Editor | null, listTypes: ListType[] ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + return listTypes.some((type) => isListActive(editor, type)) } @@ -99,13 +106,17 @@ export function shouldShowListDropdown(params: { }): boolean { const { editor, hideWhenUnavailable, listInSchema, canToggleAny } = params - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!listInSchema) return false + if (!listInSchema) { +return false +} if (!editor.isActive("code")) { return canToggleAny @@ -121,7 +132,10 @@ export function getActiveListType( editor: Editor | null, availableTypes: ListType[] ): ListType | undefined { - if (!editor || !editor.isEditable) return undefined + if (!editor || !editor.isEditable) { +return undefined +} + return availableTypes.find((type) => isListActive(editor, type)) } @@ -184,7 +198,9 @@ export function useListDropdownMenu(config?: UseListDropdownMenuConfig) { const activeList = filteredLists.find((option) => option.type === activeType) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible( diff --git a/resources/js/components/tiptap-ui/mark-button/mark-button.tsx b/resources/js/components/tiptap-ui/mark-button/mark-button.tsx index 551b46d..f806ce4 100644 --- a/resources/js/components/tiptap-ui/mark-button/mark-button.tsx +++ b/resources/js/components/tiptap-ui/mark-button/mark-button.tsx @@ -3,19 +3,19 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Tiptap UI --- import type { Mark, UseMarkConfig } from "@/components/tiptap-ui/mark-button" import { MARK_SHORTCUT_KEYS, useMark } from "@/components/tiptap-ui/mark-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" export interface MarkButtonProps extends Omit, UseMarkConfig { @@ -79,7 +79,11 @@ export const MarkButton = forwardRef( const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleMark() }, [handleMark, onClick] diff --git a/resources/js/components/tiptap-ui/mark-button/use-mark.ts b/resources/js/components/tiptap-ui/mark-button/use-mark.ts index 35bc7c4..551c73c 100644 --- a/resources/js/components/tiptap-ui/mark-button/use-mark.ts +++ b/resources/js/components/tiptap-ui/mark-button/use-mark.ts @@ -1,11 +1,9 @@ -import { useCallback, useEffect, useState } from "react" import type { Editor } from "@tiptap/react" +import { useCallback, useEffect, useState } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- -import { isMarkInSchema, isNodeTypeSelected } from "@/lib/tiptap-utils" // --- Icons --- import { BoldIcon } from "@/components/tiptap-icons/bold-icon" @@ -15,6 +13,8 @@ import { StrikeIcon } from "@/components/tiptap-icons/strike-icon" import { SubscriptIcon } from "@/components/tiptap-icons/subscript-icon" import { SuperscriptIcon } from "@/components/tiptap-icons/superscript-icon" import { UnderlineIcon } from "@/components/tiptap-icons/underline-icon" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { isMarkInSchema, isNodeTypeSelected } from "@/lib/tiptap-utils" export type Mark = | "bold" @@ -72,9 +72,13 @@ export const MARK_SHORTCUT_KEYS: Record = { * Checks if a mark can be toggled in the current editor state */ export function canToggleMark(editor: Editor | null, type: Mark): boolean { - if (!editor || !editor.isEditable) return false - if (!isMarkInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) - return false + if (!editor || !editor.isEditable) { +return false +} + + if (!isMarkInSchema(type, editor) || isNodeTypeSelected(editor, ["image"])) { +return false +} return editor.can().toggleMark(type) } @@ -83,7 +87,10 @@ export function canToggleMark(editor: Editor | null, type: Mark): boolean { * Checks if a mark is currently active */ export function isMarkActive(editor: Editor | null, type: Mark): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + return editor.isActive(type) } @@ -91,8 +98,13 @@ export function isMarkActive(editor: Editor | null, type: Mark): boolean { * Toggles a mark in the editor */ export function toggleMark(editor: Editor | null, type: Mark): boolean { - if (!editor || !editor.isEditable) return false - if (!canToggleMark(editor, type)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canToggleMark(editor, type)) { +return false +} return editor.chain().focus().toggleMark(type).run() } @@ -107,15 +119,21 @@ export function shouldShowButton(props: { }): boolean { const { editor, type, hideWhenUnavailable } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} - if (!isMarkInSchema(type, editor)) return false + if (!isMarkInSchema(type, editor)) { +return false +} if (!editor.isActive("code")) { return canToggleMark(editor, type) @@ -182,7 +200,9 @@ export function useMark(config: UseMarkConfig) { const isActive = isMarkActive(editor, type) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable })) @@ -198,12 +218,16 @@ export function useMark(config: UseMarkConfig) { }, [editor, type, hideWhenUnavailable]) const handleMark = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = toggleMark(editor, type) + if (success) { onToggled?.() } + return success }, [editor, type, onToggled]) diff --git a/resources/js/components/tiptap-ui/text-align-button/text-align-button.tsx b/resources/js/components/tiptap-ui/text-align-button/text-align-button.tsx index 6e362c5..8af630a 100644 --- a/resources/js/components/tiptap-ui/text-align-button/text-align-button.tsx +++ b/resources/js/components/tiptap-ui/text-align-button/text-align-button.tsx @@ -3,10 +3,8 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Tiptap UI --- import type { @@ -19,9 +17,11 @@ import { } from "@/components/tiptap-ui/text-align-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" type IconProps = React.SVGProps type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement @@ -96,7 +96,11 @@ export const TextAlignButton = forwardRef< const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleTextAlign() }, [handleTextAlign, onClick] diff --git a/resources/js/components/tiptap-ui/text-align-button/use-text-align.ts b/resources/js/components/tiptap-ui/text-align-button/use-text-align.ts index b6b229a..96813a9 100644 --- a/resources/js/components/tiptap-ui/text-align-button/use-text-align.ts +++ b/resources/js/components/tiptap-ui/text-align-button/use-text-align.ts @@ -1,21 +1,21 @@ -import { useCallback, useEffect, useState } from "react" import type { ChainedCommands } from "@tiptap/react" -import { type Editor } from "@tiptap/react" +import type {Editor} from "@tiptap/react"; +import { useCallback, useEffect, useState } from "react" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- -import { - isExtensionAvailable, - isNodeTypeSelected, -} from "@/lib/tiptap-utils" // --- Icons --- import { AlignCenterIcon } from "@/components/tiptap-icons/align-center-icon" import { AlignJustifyIcon } from "@/components/tiptap-icons/align-justify-icon" import { AlignLeftIcon } from "@/components/tiptap-icons/align-left-icon" import { AlignRightIcon } from "@/components/tiptap-icons/align-right-icon" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { + isExtensionAvailable, + isNodeTypeSelected, +} from "@/lib/tiptap-utils" export type TextAlign = "left" | "center" | "right" | "justify" @@ -70,12 +70,16 @@ export function canSetTextAlign( editor: Editor | null, align: TextAlign ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + if ( !isExtensionAvailable(editor, "textAlign") || isNodeTypeSelected(editor, ["image", "horizontalRule"]) - ) - return false + ) { +return false +} return editor.can().setTextAlign(align) } @@ -95,7 +99,10 @@ export function isTextAlignActive( editor: Editor | null, align: TextAlign ): boolean { - if (!editor || !editor.isEditable) return false + if (!editor || !editor.isEditable) { +return false +} + return editor.isActive({ textAlign: align }) } @@ -103,10 +110,16 @@ export function isTextAlignActive( * Sets text alignment in the editor */ export function setTextAlign(editor: Editor | null, align: TextAlign): boolean { - if (!editor || !editor.isEditable) return false - if (!canSetTextAlign(editor, align)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canSetTextAlign(editor, align)) { +return false +} const chain = editor.chain().focus() + if (hasSetTextAlign(chain)) { return chain.setTextAlign(align).run() } @@ -124,15 +137,21 @@ export function shouldShowButton(props: { }): boolean { const { editor, hideWhenUnavailable, align } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} - if (!isExtensionAvailable(editor, "textAlign")) return false + if (!isExtensionAvailable(editor, "textAlign")) { +return false +} if (!editor.isActive("code")) { return canSetTextAlign(editor, align) @@ -192,7 +211,9 @@ export function useTextAlign(config: UseTextAlignConfig) { const isActive = isTextAlignActive(editor, align) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleSelectionUpdate = () => { setIsVisible(shouldShowButton({ editor, align, hideWhenUnavailable })) @@ -208,12 +229,16 @@ export function useTextAlign(config: UseTextAlignConfig) { }, [editor, hideWhenUnavailable, align]) const handleTextAlign = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = setTextAlign(editor, align) + if (success) { onAligned?.() } + return success }, [editor, align, onAligned]) diff --git a/resources/js/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx b/resources/js/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx index 84c2506..ea27675 100644 --- a/resources/js/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +++ b/resources/js/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx @@ -3,10 +3,8 @@ import { forwardRef, useCallback } from "react" // --- Lib --- -import { parseShortcutKeys } from "@/lib/tiptap-utils" // --- Hooks --- -import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Tiptap UI --- import type { @@ -19,9 +17,11 @@ import { } from "@/components/tiptap-ui/undo-redo-button" // --- UI Primitives --- +import { Badge } from "@/components/tiptap-ui-primitive/badge" import type { ButtonProps } from "@/components/tiptap-ui-primitive/button" import { Button } from "@/components/tiptap-ui-primitive/button" -import { Badge } from "@/components/tiptap-ui-primitive/badge" +import { useTiptapEditor } from "@/hooks/use-tiptap-editor" +import { parseShortcutKeys } from "@/lib/tiptap-utils" export interface UndoRedoButtonProps extends Omit, UseUndoRedoConfig { @@ -81,7 +81,11 @@ export const UndoRedoButton = forwardRef< const handleClick = useCallback( (event: React.MouseEvent) => { onClick?.(event) - if (event.defaultPrevented) return + + if (event.defaultPrevented) { +return +} + handleAction() }, [handleAction, onClick] diff --git a/resources/js/components/tiptap-ui/undo-redo-button/use-undo-redo.ts b/resources/js/components/tiptap-ui/undo-redo-button/use-undo-redo.ts index 507b17b..ba4d82d 100644 --- a/resources/js/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +++ b/resources/js/components/tiptap-ui/undo-redo-button/use-undo-redo.ts @@ -1,15 +1,15 @@ +import type {Editor} from "@tiptap/react"; import { useCallback, useEffect, useState } from "react" -import { type Editor } from "@tiptap/react" // --- Hooks --- +import { Redo2Icon } from "@/components/tiptap-icons/redo2-icon" +import { Undo2Icon } from "@/components/tiptap-icons/undo2-icon" import { useTiptapEditor } from "@/hooks/use-tiptap-editor" // --- Lib --- import { isNodeTypeSelected } from "@/lib/tiptap-utils" // --- Icons --- -import { Redo2Icon } from "@/components/tiptap-icons/redo2-icon" -import { Undo2Icon } from "@/components/tiptap-icons/undo2-icon" export type UndoRedoAction = "undo" | "redo" @@ -58,8 +58,13 @@ export function canExecuteUndoRedoAction( editor: Editor | null, action: UndoRedoAction ): boolean { - if (!editor || !editor.isEditable) return false - if (isNodeTypeSelected(editor, ["image"])) return false + if (!editor || !editor.isEditable) { +return false +} + + if (isNodeTypeSelected(editor, ["image"])) { +return false +} return action === "undo" ? editor.can().undo() : editor.can().redo() } @@ -71,10 +76,16 @@ export function executeUndoRedoAction( editor: Editor | null, action: UndoRedoAction ): boolean { - if (!editor || !editor.isEditable) return false - if (!canExecuteUndoRedoAction(editor, action)) return false + if (!editor || !editor.isEditable) { +return false +} + + if (!canExecuteUndoRedoAction(editor, action)) { +return false +} const chain = editor.chain().focus() + return action === "undo" ? chain.undo().run() : chain.redo().run() } @@ -88,13 +99,17 @@ export function shouldShowButton(props: { }): boolean { const { editor, hideWhenUnavailable, action } = props - if (!editor) return false + if (!editor) { +return false +} if (!hideWhenUnavailable) { return true } - if (!editor.isEditable) return false + if (!editor.isEditable) { +return false +} if (!editor.isActive("code")) { return canExecuteUndoRedoAction(editor, action) @@ -152,7 +167,9 @@ export function useUndoRedo(config: UseUndoRedoConfig) { const canExecute = canExecuteUndoRedoAction(editor, action) useEffect(() => { - if (!editor) return + if (!editor) { +return +} const handleUpdate = () => { setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, action })) @@ -168,12 +185,16 @@ export function useUndoRedo(config: UseUndoRedoConfig) { }, [editor, hideWhenUnavailable, action]) const handleAction = useCallback(() => { - if (!editor) return false + if (!editor) { +return false +} const success = executeUndoRedoAction(editor, action) + if (success) { onExecuted?.() } + return success }, [editor, action, onExecuted]) diff --git a/resources/js/hooks/use-current-url.ts b/resources/js/hooks/use-current-url.ts index c67d3cc..a2795f8 100644 --- a/resources/js/hooks/use-current-url.ts +++ b/resources/js/hooks/use-current-url.ts @@ -44,9 +44,13 @@ export function useCurrentUrl(): UseCurrentUrlReturn { const currentPath = currentUrl ?? currentUrlPath; const clean = (p: string) => { - if (!p) return '/'; + if (!p) { +return '/'; +} + // Remove query and hash, then trailing slash let path = p.split('?')[0].split('#')[0]; + if (path.startsWith('http')) { try { path = new URL(path).pathname; @@ -54,6 +58,7 @@ export function useCurrentUrl(): UseCurrentUrlReturn { // Ignore } } + return path.replace(/\/+$/, '') || '/'; }; @@ -64,6 +69,7 @@ export function useCurrentUrl(): UseCurrentUrlReturn { if (normCheck === '/') { return normCurrent === '/'; } + return normCurrent.startsWith(normCheck); } diff --git a/resources/js/hooks/use-cursor-visibility.ts b/resources/js/hooks/use-cursor-visibility.ts index 19668d8..4eec1a0 100644 --- a/resources/js/hooks/use-cursor-visibility.ts +++ b/resources/js/hooks/use-cursor-visibility.ts @@ -1,7 +1,7 @@ import type { Editor } from "@tiptap/react" -import { useWindowSize } from "@/hooks/use-window-size" -import { useBodyRect } from "@/hooks/use-element-rect" import { useEffect } from "react" +import { useBodyRect } from "@/hooks/use-element-rect" +import { useWindowSize } from "@/hooks/use-window-size" export interface CursorVisibilityOptions { /** @@ -35,10 +35,15 @@ export function useCursorVisibility({ useEffect(() => { const ensureCursorVisibility = () => { - if (!editor) return + if (!editor) { +return +} const { state, view } = editor - if (!view.hasFocus()) return + + if (!view.hasFocus()) { +return +} // Get current cursor position coordinates const { from } = state.selection diff --git a/resources/js/hooks/use-element-rect.ts b/resources/js/hooks/use-element-rect.ts index 55ba9bd..d3a6645 100644 --- a/resources/js/hooks/use-element-rect.ts +++ b/resources/js/hooks/use-element-rect.ts @@ -59,7 +59,9 @@ export function useElementRect({ const [rect, setRect] = useState(initialRect) const getTargetElement = useCallback((): Element | null => { - if (!enabled || !isClientSide()) return null + if (!enabled || !isClientSide()) { +return null +} if (!element) { return document.body @@ -78,11 +80,15 @@ export function useElementRect({ const updateRect = useThrottledCallback( () => { - if (!enabled || !isClientSide()) return + if (!enabled || !isClientSide()) { +return +} const targetElement = getTargetElement() + if (!targetElement) { setRect(initialRect) + return } @@ -106,11 +112,15 @@ export function useElementRect({ useEffect(() => { if (!enabled || !isClientSide()) { setRect(initialRect) + return } const targetElement = getTargetElement() - if (!targetElement) return + + if (!targetElement) { +return +} updateRect() diff --git a/resources/js/hooks/use-is-breakpoint.ts b/resources/js/hooks/use-is-breakpoint.ts index 50f1874..9a30a5b 100644 --- a/resources/js/hooks/use-is-breakpoint.ts +++ b/resources/js/hooks/use-is-breakpoint.ts @@ -28,6 +28,7 @@ export function useIsBreakpoint( // Add listener mql.addEventListener("change", onChange) + return () => mql.removeEventListener("change", onChange) }, [mode, breakpoint]) diff --git a/resources/js/hooks/use-menu-navigation.ts b/resources/js/hooks/use-menu-navigation.ts index 58dfd4b..4b81019 100644 --- a/resources/js/hooks/use-menu-navigation.ts +++ b/resources/js/hooks/use-menu-navigation.ts @@ -65,83 +65,117 @@ export function useMenuNavigation({ useEffect(() => { const handleKeyboardNavigation = (event: KeyboardEvent) => { - if (!items.length) return false + if (!items.length) { +return false +} const moveNext = () => setSelectedIndex((currentIndex) => { - if (currentIndex === -1) return 0 + if (currentIndex === -1) { +return 0 +} + return (currentIndex + 1) % items.length }) const movePrev = () => setSelectedIndex((currentIndex) => { - if (currentIndex === -1) return items.length - 1 + if (currentIndex === -1) { +return items.length - 1 +} + return (currentIndex - 1 + items.length) % items.length }) switch (event.key) { case "ArrowUp": { - if (orientation === "horizontal") return false + if (orientation === "horizontal") { +return false +} + event.preventDefault() movePrev() + return true } case "ArrowDown": { - if (orientation === "horizontal") return false + if (orientation === "horizontal") { +return false +} + event.preventDefault() moveNext() + return true } case "ArrowLeft": { - if (orientation === "vertical") return false + if (orientation === "vertical") { +return false +} + event.preventDefault() movePrev() + return true } case "ArrowRight": { - if (orientation === "vertical") return false + if (orientation === "vertical") { +return false +} + event.preventDefault() moveNext() + return true } case "Tab": { event.preventDefault() + if (event.shiftKey) { movePrev() } else { moveNext() } + return true } case "Home": { event.preventDefault() setSelectedIndex(0) + return true } case "End": { event.preventDefault() setSelectedIndex(items.length - 1) + return true } case "Enter": { - if (event.isComposing) return false + if (event.isComposing) { +return false +} + event.preventDefault() + if (selectedIndex !== -1 && items[selectedIndex]) { onSelect?.(items[selectedIndex]) } + return true } case "Escape": { event.preventDefault() onClose?.() + return true } diff --git a/resources/js/hooks/use-mobile.ts b/resources/js/hooks/use-mobile.ts index 2b0fe1d..77ab35a 100644 --- a/resources/js/hooks/use-mobile.ts +++ b/resources/js/hooks/use-mobile.ts @@ -12,6 +12,7 @@ export function useIsMobile() { } mql.addEventListener("change", onChange) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener("change", onChange) }, []) diff --git a/resources/js/hooks/use-printer.ts b/resources/js/hooks/use-printer.ts index c4039ae..a6444cd 100644 --- a/resources/js/hooks/use-printer.ts +++ b/resources/js/hooks/use-printer.ts @@ -3,7 +3,7 @@ import { toast } from 'sonner'; // Type definitions for Web Bluetooth and Web USB // This is to satisfy TypeScript if the global types are missing -/* eslint-disable @typescript-eslint/no-explicit-any */ + export type PrinterType = 'bluetooth' | 'usb' | null; export type PaperSize = '58' | '80'; @@ -32,8 +32,10 @@ export function usePrinter() { const connectBluetooth = useCallback(async () => { try { const nav = navigator as any; + if (!nav.bluetooth) { toast.error('Bluetooth tidak didukung di browser ini'); + return; } @@ -70,6 +72,7 @@ export function usePrinter() { } catch (error: any) { console.error('Bluetooth connection error:', error); + if (error.name !== 'NotFoundError') { toast.error(error.message || 'Gagal menghubungkan printer bluetooth'); } @@ -79,8 +82,10 @@ export function usePrinter() { const connectUsb = useCallback(async () => { try { const nav = navigator as any; + if (!nav.usb) { toast.error('USB tidak didukung di browser ini'); + return; } @@ -89,6 +94,7 @@ export function usePrinter() { }); await device.open(); + if (device.configuration === null) { await device.selectConfiguration(1); } @@ -108,6 +114,7 @@ export function usePrinter() { } catch (error: any) { console.error('USB connection error:', error); + if (error.name !== 'NotFoundError') { toast.error(error.message || 'Gagal menghubungkan printer USB'); } @@ -121,6 +128,7 @@ export function usePrinter() { const sendData = useCallback(async (data: Uint8Array) => { if (!state.isConnected || !state.device) { toast.error('Printer belum terhubung'); + return; } @@ -134,20 +142,26 @@ export function usePrinter() { const server = await device.gatt?.connect(); const services = await server.getPrimaryServices(); const writeChar = await findWriteCharacteristic(services); + if (writeChar) { setCharacteristic(writeChar); toast.success('Printer terhubung kembali', { id: 'printer-reconnect' }); } else { toast.error('Gagal menghubungkan kembali printer', { id: 'printer-reconnect' }); + return; } } - if (!characteristic) throw new Error('Characteristic tidak ditemukan'); + if (!characteristic) { +throw new Error('Characteristic tidak ditemukan'); +} const chunkSize = 20; + for (let i = 0; i < data.length; i += chunkSize) { const chunk = data.slice(i, i + chunkSize); + // Use writeValueWithoutResponse for better stability if supported if (characteristic.writeValueWithoutResponse) { await characteristic.writeValueWithoutResponse(chunk); @@ -158,7 +172,10 @@ export function usePrinter() { } else if (state.type === 'usb') { const device = state.device as any; const endpoint = device.configuration.interfaces[0].alternates[0].endpoints.find((e: any) => e.direction === 'out' && e.type === 'bulk'); - if (!endpoint) throw new Error('Tidak dapat menemukan USB endpoint'); + + if (!endpoint) { +throw new Error('Tidak dapat menemukan USB endpoint'); +} await device.transferOut(endpoint.endpointNumber, data); } @@ -195,11 +212,13 @@ export function usePrinter() { async function findWriteCharacteristic(services: any[]) { for (const service of services) { const characteristics = await service.getCharacteristics(); + for (const char of characteristics) { if (char.properties.write || char.properties.writeWithoutResponse) { return char; } } } + return null; } diff --git a/resources/js/hooks/use-scrolling.ts b/resources/js/hooks/use-scrolling.ts index 8a5fb35..a8536ec 100644 --- a/resources/js/hooks/use-scrolling.ts +++ b/resources/js/hooks/use-scrolling.ts @@ -47,7 +47,9 @@ export function useScrolling( const supportsScrollEnd = element === window && "onscrollend" in window const handleScroll: EventListener = () => { - if (!isScrolling) setIsScrolling(true) + if (!isScrolling) { +setIsScrolling(true) +} if (!supportsScrollEnd) { clearTimeout(timeout) @@ -58,15 +60,18 @@ export function useScrolling( const handleScrollEnd: EventListener = () => setIsScrolling(false) on(eventTarget, "scroll", handleScroll) + if (supportsScrollEnd) { on(eventTarget, "scrollend", handleScrollEnd) } return () => { off(eventTarget, "scroll", handleScroll) + if (supportsScrollEnd) { off(eventTarget, "scrollend", handleScrollEnd) } + clearTimeout(timeout) } }, [target, debounce, fallbackToDocument, isScrolling]) diff --git a/resources/js/hooks/use-throttled-callback.ts b/resources/js/hooks/use-throttled-callback.ts index 54894cf..f71e38c 100644 --- a/resources/js/hooks/use-throttled-callback.ts +++ b/resources/js/hooks/use-throttled-callback.ts @@ -1,7 +1,7 @@ import throttle from "lodash.throttle" -import { useUnmount } from "@/hooks/use-unmount" import { useMemo } from "react" +import { useUnmount } from "@/hooks/use-unmount" interface ThrottleSettings { leading?: boolean | undefined @@ -21,7 +21,7 @@ const defaultOptions: ThrottleSettings = { * @param dependencies The dependencies to watch for changes * @param options The throttle options */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any + export function useThrottledCallback any>( fn: T, wait = 250, diff --git a/resources/js/hooks/use-tiptap-editor.ts b/resources/js/hooks/use-tiptap-editor.ts index ef9ca7b..81552ec 100644 --- a/resources/js/hooks/use-tiptap-editor.ts +++ b/resources/js/hooks/use-tiptap-editor.ts @@ -5,7 +5,11 @@ import { useEffect, useState } from "react" function getActivePageEditor(editor: Editor): Editor | null { const storage = editor.storage as unknown as Record const pages = storage.pages as { activeEditor?: Editor | null } | undefined - if (!pages || !("activeEditor" in pages)) return null + + if (!pages || !("activeEditor" in pages)) { +return null +} + return pages.activeEditor ?? null } @@ -22,6 +26,7 @@ export function useTiptapEditor(providedEditor?: Editor | null): { useEffect(() => { if (!mainEditor) { setStorageEditor(null) + return } @@ -40,11 +45,14 @@ export function useTiptapEditor(providedEditor?: Editor | null): { }, [mainEditor]) useEffect(() => { - if (!storageEditor) return + if (!storageEditor) { +return +} const handleDestroy = () => setStorageEditor(null) storageEditor.on("destroy", handleDestroy) + return () => { storageEditor.off("destroy", handleDestroy) } diff --git a/resources/js/hooks/use-unmount.ts b/resources/js/hooks/use-unmount.ts index 91a22e7..457e665 100644 --- a/resources/js/hooks/use-unmount.ts +++ b/resources/js/hooks/use-unmount.ts @@ -5,7 +5,7 @@ import { useRef, useEffect } from "react" * * @param callback Function to be called on component unmount */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any + export const useUnmount = (callback: (...args: Array) => any) => { const ref = useRef(callback) ref.current = callback diff --git a/resources/js/hooks/use-window-size.ts b/resources/js/hooks/use-window-size.ts index dd211d9..8d77876 100644 --- a/resources/js/hooks/use-window-size.ts +++ b/resources/js/hooks/use-window-size.ts @@ -48,10 +48,15 @@ export function useWindowSize(): WindowSizeState { }) const handleViewportChange = useThrottledCallback(() => { - if (typeof window === "undefined") return + if (typeof window === "undefined") { +return +} const vp = window.visualViewport - if (!vp) return + + if (!vp) { +return +} const { width = 0, @@ -78,7 +83,10 @@ export function useWindowSize(): WindowSizeState { useEffect(() => { const visualViewport = window.visualViewport - if (!visualViewport) return + + if (!visualViewport) { +return +} visualViewport.addEventListener("resize", handleViewportChange) diff --git a/resources/js/lib/esc-pos-encoder.ts b/resources/js/lib/esc-pos-encoder.ts index 10eeb19..07147ab 100644 --- a/resources/js/lib/esc-pos-encoder.ts +++ b/resources/js/lib/esc-pos-encoder.ts @@ -12,6 +12,7 @@ export class EscPosEncoder { */ initialize(): this { this.buffer.push(0x1b, 0x40); + return this; } @@ -21,6 +22,7 @@ export class EscPosEncoder { text(value: string): this { const bytes = this.encoder.encode(value); this.buffer.push(...Array.from(bytes)); + return this; } @@ -29,6 +31,7 @@ export class EscPosEncoder { */ line(value: string = ''): this { this.text(value + '\n'); + return this; } @@ -38,6 +41,7 @@ export class EscPosEncoder { */ align(value: 0 | 1 | 2): this { this.buffer.push(0x1b, 0x61, value); + return this; } @@ -46,6 +50,7 @@ export class EscPosEncoder { */ bold(value: boolean): this { this.buffer.push(0x1b, 0x45, value ? 1 : 0); + return this; } @@ -55,10 +60,21 @@ export class EscPosEncoder { */ size(value: 0 | 1 | 2 | 3): this { let size = 0; - if (value === 1) size = 0x01; - if (value === 2) size = 0x10; - if (value === 3) size = 0x11; + + if (value === 1) { +size = 0x01; +} + + if (value === 2) { +size = 0x10; +} + + if (value === 3) { +size = 0x11; +} + this.buffer.push(0x1d, 0x21, size); + return this; } @@ -67,6 +83,7 @@ export class EscPosEncoder { */ cut(): this { this.buffer.push(0x1d, 0x56, 0x00); + return this; } @@ -75,6 +92,7 @@ export class EscPosEncoder { */ feed(lines: number = 1): this { this.buffer.push(0x1b, 0x64, lines); + return this; } diff --git a/resources/js/lib/formatters.ts b/resources/js/lib/formatters.ts index e02c5aa..a99fe9f 100644 --- a/resources/js/lib/formatters.ts +++ b/resources/js/lib/formatters.ts @@ -7,11 +7,15 @@ export const formatTime = (date: Date) => { }; export const formatDate = (date: Date | string | undefined) => { - if (!date) return '-'; + if (!date) { +return '-'; +} const parsedDate = date instanceof Date ? date : new Date(date); - if (isNaN(parsedDate.getTime())) return '-'; + if (isNaN(parsedDate.getTime())) { +return '-'; +} return parsedDate.toLocaleDateString('id-ID', { weekday: 'long', diff --git a/resources/js/lib/tiptap-utils.ts b/resources/js/lib/tiptap-utils.ts index 79961b1..7f2e069 100644 --- a/resources/js/lib/tiptap-utils.ts +++ b/resources/js/lib/tiptap-utils.ts @@ -8,10 +8,11 @@ import { } from "@tiptap/pm/state" import { cellAround, CellSelection } from "@tiptap/pm/tables" import { - findParentNodeClosestToPos, - type Editor, - type NodeWithPos, + findParentNodeClosestToPos + + } from "@tiptap/react" +import type {Editor, NodeWithPos} from "@tiptap/react"; export const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB @@ -74,6 +75,7 @@ export const formatShortcutKey = ( ) => { if (isMac) { const lowerKey = key.toLowerCase() + return MAC_SYMBOLS[lowerKey] || (capitalize ? key.toUpperCase() : key) } @@ -94,7 +96,9 @@ export const parseShortcutKeys = (props: { }) => { const { shortcutKeys, delimiter = "+", capitalize = true } = props - if (!shortcutKeys) return [] + if (!shortcutKeys) { +return [] +} return shortcutKeys .split(delimiter) @@ -112,7 +116,10 @@ export const isMarkInSchema = ( markName: string, editor: Editor | null ): boolean => { - if (!editor?.schema) return false + if (!editor?.schema) { +return false +} + return editor.schema.spec.marks.get(markName) !== undefined } @@ -126,7 +133,10 @@ export const isNodeInSchema = ( nodeName: string, editor: Editor | null ): boolean => { - if (!editor?.schema) return false + if (!editor?.schema) { +return false +} + return editor.schema.spec.nodes.get(nodeName) !== undefined } @@ -140,14 +150,18 @@ export function focusNextNode(editor: Editor) { const { doc, selection } = state const nextSel = Selection.findFrom(selection.$to, 1, true) + if (nextSel) { view.dispatch(state.tr.setSelection(nextSel).scrollIntoView()) + return true } const paragraphType = state.schema.nodes.paragraph + if (!paragraphType) { console.warn("No paragraph node type found in schema.") + return false } @@ -159,6 +173,7 @@ export function focusNextNode(editor: Editor) { const $inside = tr.doc.resolve(end + 1) tr = tr.setSelection(TextSelection.near($inside)).scrollIntoView() view.dispatch(tr) + return true } @@ -181,7 +196,9 @@ export function isExtensionAvailable( editor: Editor | null, extensionNames: string | string[] ): boolean { - if (!editor) return false + if (!editor) { +return false +} const names = Array.isArray(extensionNames) ? extensionNames @@ -209,13 +226,17 @@ export function isExtensionAvailable( export function findNodeAtPosition(editor: Editor, position: number) { try { const node = editor.state.doc.nodeAt(position) + if (!node) { console.warn(`No node found at position ${position}`) + return null } + return node } catch (error) { console.error(`Error getting node at position ${position}:`, error) + return null } } @@ -235,7 +256,9 @@ export function findNodePosition(props: { }): { pos: number; node: PMNode } | null { const { editor, node, nodePos } = props - if (!editor || !editor.state?.doc) return null + if (!editor || !editor.state?.doc) { +return null +} // Zero is valid position const hasValidNode = node !== undefined && node !== null @@ -256,8 +279,10 @@ export function findNodePosition(props: { if (currentNode === node) { foundPos = pos foundNode = currentNode + return false } + return true }) @@ -269,6 +294,7 @@ export function findNodePosition(props: { // If we have a valid position, use findNodeAtPosition if (hasValidPos) { const nodeAtPos = findNodeAtPosition(editor, nodePos!) + if (nodeAtPos) { return { pos: nodePos!, node: nodeAtPos } } @@ -289,22 +315,30 @@ export function isNodeTypeSelected( nodeTypeNames: string[] = [], checkAncestorNodes: boolean = false ): boolean { - if (!editor || !editor.state.selection) return false + if (!editor || !editor.state.selection) { +return false +} const { selection } = editor.state - if (selection.empty) return false + + if (selection.empty) { +return false +} // Direct node selection check if (selection instanceof NodeSelection) { const selectedNode = selection.node + return selectedNode ? nodeTypeNames.includes(selectedNode.type.name) : false } // Depth-based ancestor node check if (checkAncestorNodes) { const { $from } = selection + for (let depth = $from.depth; depth > 0; depth--) { const ancestorNode = $from.node(depth) + if (nodeTypeNames.includes(ancestorNode.type.name)) { return true } @@ -325,7 +359,9 @@ export function selectionWithinConvertibleTypes( editor: Editor, types: string[] = [] ): boolean { - if (!editor || types.length === 0) return false + if (!editor || types.length === 0) { +return false +} const { state } = editor const { selection } = state @@ -333,6 +369,7 @@ export function selectionWithinConvertibleTypes( if (selection instanceof NodeSelection) { const nodeType = selection.node?.type?.name + return !!nodeType && allowed.has(nodeType) } @@ -341,10 +378,13 @@ export function selectionWithinConvertibleTypes( state.doc.nodesBetween(selection.from, selection.to, (node) => { if (node.isTextblock && !allowed.has(node.type.name)) { valid = false + return false // stop early } + return valid }) + return valid } @@ -380,6 +420,7 @@ export const handleImageUpload = async ( if (abortSignal?.aborted) { throw new Error("Upload cancelled") } + await new Promise((resolve) => setTimeout(resolve, 500)) onProgress?.({ progress }) } @@ -464,6 +505,7 @@ export function sanitizeUrl( } catch { // If URL creation fails, it's considered invalid } + return "#" } @@ -483,14 +525,19 @@ export function updateNodesAttr( attrName: A, next: V | ((prev: V | undefined) => V | undefined) ): boolean { - if (!targets.length) return false + if (!targets.length) { +return false +} let changed = false for (const { pos } of targets) { // Always re-read from the transaction's current doc const currentNode = tr.doc.nodeAt(pos) - if (!currentNode) continue + + if (!currentNode) { +continue +} const prevValue = (currentNode.attrs as Record)[ attrName @@ -500,9 +547,12 @@ export function updateNodesAttr( ? (next as (p: V | undefined) => V | undefined)(prevValue) : next - if (prevValue === resolvedNext) continue + if (prevValue === resolvedNext) { +continue +} const nextAttrs: Record = { ...currentNode.attrs } + if (resolvedNext === undefined) { // Remove the key entirely instead of setting null delete nextAttrs[attrName] @@ -525,7 +575,9 @@ export function updateNodesAttr( export function selectCurrentBlockContent(editor: Editor) { const { selection, doc } = editor.state - if (!selection.empty) return + if (!selection.empty) { +return +} const $pos = selection.$from let blockNode = null @@ -577,14 +629,17 @@ export function getSelectedNodesOfType( results.push({ node, pos }) } }) + return results } if (selection instanceof NodeSelection) { const { node, from: pos } = selection + if (node && allowed.has(node.type.name)) { results.push({ node, pos }) } + return results } @@ -593,8 +648,10 @@ export function getSelectedNodesOfType( if (cell) { const cellNode = selection.$anchor.doc.nodeAt(cell.pos) + if (cellNode && allowed.has(cellNode.type.name)) { results.push({ node: cellNode, pos: cell.pos }) + return results } } @@ -626,7 +683,9 @@ export function getSelectedBlockNodes(editor: Editor): PMNode[] { const seen = new Set() doc.nodesBetween(from, to, (node, pos) => { - if (!node.isBlock) return + if (!node.isBlock) { +return +} if (!seen.has(pos)) { seen.add(pos) diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts index db028d6..30a2ead 100644 --- a/resources/js/lib/utils.ts +++ b/resources/js/lib/utils.ts @@ -8,8 +8,17 @@ export function cn(...inputs: ClassValue[]) { } export function toUrl(url: InertiaLinkProps['href']): string { - if (!url) return ''; - if (typeof url === 'string') return url; - if (typeof url === 'object' && url !== null && 'url' in url) return url.url as string; + if (!url) { +return ''; +} + + if (typeof url === 'string') { +return url; +} + + if (typeof url === 'object' && url !== null && 'url' in url) { +return url.url as string; +} + return String(url); } diff --git a/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts b/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts index 207a71d..343d423 100644 --- a/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts +++ b/resources/js/pages/admin/finance/expense/hooks/use-expense-index.ts @@ -1,8 +1,8 @@ -import { useState } from 'react'; -import { Expense } from '@/types'; import { router } from '@inertiajs/react'; -import expenseRoutes from '@/routes/expense'; +import { useState } from 'react'; import { toast } from 'sonner'; +import expenseRoutes from '@/routes/expense'; +import type { Expense } from '@/types'; export function useExpenseIndex() { const [isFormOpen, setIsFormOpen] = useState(false); diff --git a/resources/js/pages/admin/finance/expense/index.tsx b/resources/js/pages/admin/finance/expense/index.tsx index 7fab709..7a09fb4 100644 --- a/resources/js/pages/admin/finance/expense/index.tsx +++ b/resources/js/pages/admin/finance/expense/index.tsx @@ -1,15 +1,15 @@ import { Head } from '@inertiajs/react'; -import type { Expense } from '@/types'; -import { Card, CardContent } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Trash2, X, Plus } from 'lucide-react'; +import { Trash2, Plus } from 'lucide-react'; import { DataTable } from '@/components/data-table'; +import { DeleteConfirmation } from '@/components/modal/delete-confirmation'; +import { ImagePreviewDialog } from '@/components/modal/image-preview'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import type { Expense } from '@/types'; import { useExpenseIndex } from './hooks/use-expense-index'; import { getColumns } from './partials/columns'; import { ExpenseFormModal } from './partials/expense-form-modal'; -import { DeleteConfirmation } from '@/components/modal/delete-confirmation'; -import { ImagePreviewDialog } from '@/components/modal/image-preview'; export default function ExpenseIndex({ expenses }: { expenses: Expense[] }) { const { diff --git a/resources/js/pages/admin/finance/expense/partials/columns.tsx b/resources/js/pages/admin/finance/expense/partials/columns.tsx index ca73fa7..2f74497 100644 --- a/resources/js/pages/admin/finance/expense/partials/columns.tsx +++ b/resources/js/pages/admin/finance/expense/partials/columns.tsx @@ -1,9 +1,9 @@ -import { ColumnDef } from '@tanstack/react-table'; -import { Expense } from '@/types'; -import { DataTableColumnHeader } from '@/components/data-table-column-header'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { Button } from '@/components/ui/button'; +import type { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash2, ImagePlus } from 'lucide-react'; +import { DataTableColumnHeader } from '@/components/data-table-column-header'; +import { Button } from '@/components/ui/button'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import type { Expense } from '@/types'; interface ColumnProps { onEdit: (expense: Expense) => void; @@ -25,6 +25,7 @@ export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): C header: "Bukti", cell: ({ row }) => { const url = row.original.proof_url; + return url ? (