refactor: reorganize imports across multiple components for improved clarity and consistency

This commit is contained in:
Yoga Pangestu 2026-04-23 15:02:59 +07:00
parent 24da9d5333
commit 221f796696
109 changed files with 1528 additions and 765 deletions

View File

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

View File

@ -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[] = [
{

View File

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

View File

@ -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 }) => {
</div>
);
}
if (hour >= 11 && hour < 15) {
return (
<div className="relative h-24 w-32 overflow-hidden">
@ -20,6 +21,7 @@ const DynamicScene = ({ hour }: { hour: number }) => {
</div>
);
}
if (hour >= 15 && hour < 19) {
return (
<div className="relative h-24 w-32 overflow-hidden">
@ -29,6 +31,7 @@ const DynamicScene = ({ hour }: { hour: number }) => {
</div>
);
}
return (
<div className="relative h-24 w-32 overflow-hidden">
<Moon className="absolute top-2 left-6 h-14 w-14 rotate-12 text-indigo-200" fill="currentColor" />

View File

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

View File

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

View File

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

View File

@ -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<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {

View File

@ -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<TData, TValue>({
<DropdownMenuSeparator />
{filters.map((filter) => {
const column = table.getColumn(filter.columnId);
if (!column) return null;
if (!column) {
return null;
}
return (
<DropdownMenuSub key={filter.columnId}>
@ -265,9 +267,13 @@ export function DataTable<TData, TValue>({
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}

View File

@ -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<HTMLParagraphElement> {
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;

View File

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

View File

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

View File

@ -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<NodeBackgroundOptions>({
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<NodeBackgroundOptions>({
this.options.types
)
if (targets.length === 0) return false
if (targets.length === 0) {
return false
}
const targetColor = getTargetColor(targets, inputColor)

View File

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

View File

@ -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<string, any>
}
@ -144,15 +144,19 @@ export const ImageUploadNode = Node.create<ImageUploadNodeOptions>({
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
},
}

View File

@ -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<string[]> => {
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<ImageUploadDragAreaProps> = ({
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<ImageUploadDragAreaProps> = ({
setIsDragOver(false)
const files = Array.from(e.dataTransfer.files)
if (files.length > 0) {
onFile(files)
}
@ -356,10 +369,14 @@ const ImageUploadPreview: React.FC<ImageUploadPreviewProps> = ({
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<NodeViewProps> = (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<NodeViewProps> = (props) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) {
extension.options.onError?.(new Error("No file selected"))
return
}
handleUpload(Array.from(files))
}

View File

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

View File

@ -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<boolean>(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)
}, [])

View File

@ -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", {

View File

@ -27,7 +27,9 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
export const ShortcutDisplay: React.FC<{ shortcuts: string[] }> = ({
shortcuts,
}) => {
if (shortcuts.length === 0) return null
if (shortcuts.length === 0) {
return null
}
return (
<div>

View File

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

View File

@ -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<HTMLDivElement>
@ -17,7 +17,10 @@ const useToolbarNavigation = (
const [items, setItems] = useState<HTMLElement[]>([])
const collectItems = useCallback(() => {
if (!toolbarRef.current) return []
if (!toolbarRef.current) {
return []
}
return Array.from(
toolbarRef.current.querySelectorAll<HTMLElement>(
'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)

View File

@ -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<HTMLElement, TooltipTriggerProps>(
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<any> } }).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<HTMLDivElement, TooltipContentProps>(
const context = useTooltipContext()
const ref = useMergeRefs([context.refs.setFloating, propRef])
if (!context.open) return null
if (!context.open) {
return null
}
const content = (
<div

View File

@ -8,15 +8,15 @@ import {
} from "@/components/tiptap-ui/blockquote-button"
// --- Hooks ---
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 { useTiptapEditor } from "@/hooks/use-tiptap-editor"
// --- Lib ---
import { parseShortcutKeys } from "@/lib/tiptap-utils"
// --- 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"
export interface BlockquoteButtonProps
extends Omit<ButtonProps, "type">, UseBlockquoteConfig {
@ -79,7 +79,11 @@ export const BlockquoteButton = forwardRef<
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleToggle()
},
[handleToggle, onClick]

View File

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

View File

@ -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<ButtonProps, "type">, UseCodeBlockConfig {
@ -79,7 +79,11 @@ export const CodeBlockButton = forwardRef<
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleToggle()
},
[handleToggle, onClick]

View File

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

View File

@ -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<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleColorHighlight()
},
[handleColorHighlight, onClick]

View File

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

View File

@ -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 (
<Popover open={isOpen} onOpenChange={setIsOpen}>

View File

@ -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<ButtonProps, "type">, UseHeadingConfig {
@ -81,7 +81,11 @@ export const HeadingButton = forwardRef<HTMLButtonElement, HeadingButtonProps>(
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleToggle()
},
[handleToggle, onClick]

View File

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

View File

@ -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<ButtonProps, "type">, 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)
},

View File

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

View File

@ -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<SVGSVGElement>
type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement
@ -87,7 +87,11 @@ export const ImageUploadButton = forwardRef<
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleImage()
},
[handleImage, onClick]

View File

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

View File

@ -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<HTMLButtonElement, LinkPopoverProps>(
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
setIsOpen(!isOpen)
},
[onClick, isOpen]

View File

@ -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<string | null>(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(

View File

@ -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<ButtonProps, "type">, UseListConfig {
@ -77,7 +77,11 @@ export const ListButton = forwardRef<HTMLButtonElement, ListButtonProps>(
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleToggle()
},
[handleToggle, onClick]

View File

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

View File

@ -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<ButtonProps, "type"> {
/**

View File

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

View File

@ -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<ButtonProps, "type">, UseMarkConfig {
@ -79,7 +79,11 @@ export const MarkButton = forwardRef<HTMLButtonElement, MarkButtonProps>(
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleMark()
},
[handleMark, onClick]

View File

@ -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<Mark, string> = {
* 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])

View File

@ -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<SVGSVGElement>
type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement
@ -96,7 +96,11 @@ export const TextAlignButton = forwardRef<
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleTextAlign()
},
[handleTextAlign, onClick]

View File

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

View File

@ -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<ButtonProps, "type">, UseUndoRedoConfig {
@ -81,7 +81,11 @@ export const UndoRedoButton = forwardRef<
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event)
if (event.defaultPrevented) return
if (event.defaultPrevented) {
return
}
handleAction()
},
[handleAction, onClick]

View File

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

View File

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

View File

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

View File

@ -59,7 +59,9 @@ export function useElementRect({
const [rect, setRect] = useState<RectState>(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()

View File

@ -28,6 +28,7 @@ export function useIsBreakpoint(
// Add listener
mql.addEventListener("change", onChange)
return () => mql.removeEventListener("change", onChange)
}, [mode, breakpoint])

View File

@ -65,83 +65,117 @@ export function useMenuNavigation<T>({
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
}

View File

@ -12,6 +12,7 @@ export function useIsMobile() {
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])

View File

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

View File

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

View File

@ -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<T extends (...args: any[]) => any>(
fn: T,
wait = 250,

View File

@ -5,7 +5,11 @@ import { useEffect, useState } from "react"
function getActivePageEditor(editor: Editor): Editor | null {
const storage = editor.storage as unknown as Record<string, unknown>
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)
}

View File

@ -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>) => any) => {
const ref = useRef(callback)
ref.current = callback

View File

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

View File

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

View File

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

View File

@ -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<A extends string = string, V = unknown>(
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<string, unknown>)[
attrName
@ -500,9 +547,12 @@ export function updateNodesAttr<A extends string = string, V = unknown>(
? (next as (p: V | undefined) => V | undefined)(prevValue)
: next
if (prevValue === resolvedNext) continue
if (prevValue === resolvedNext) {
continue
}
const nextAttrs: Record<string, unknown> = { ...currentNode.attrs }
if (resolvedNext === undefined) {
// Remove the key entirely instead of setting null
delete nextAttrs[attrName]
@ -525,7 +575,9 @@ export function updateNodesAttr<A extends string = string, V = unknown>(
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<number>()
doc.nodesBetween(from, to, (node, pos) => {
if (!node.isBlock) return
if (!node.isBlock) {
return
}
if (!seen.has(pos)) {
seen.add(pos)

View File

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

View File

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

View File

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

View File

@ -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 ? (
<button onClick={() => onPreviewImage(url)} className="block w-fit">
<img src={url} alt="Proof" className="h-10 w-10 object-cover rounded-md border hover:opacity-80 transition-opacity" />
@ -77,6 +78,7 @@ export const getColumns = ({ onEdit, onDelete, onPreviewImage }: ColumnProps): C
header: "Aksi",
cell: ({ row }) => {
const expense = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,3 +1,8 @@
import { useForm, router } from '@inertiajs/react';
import { ImagePlus, X, Save, Loader } from 'lucide-react';
import { useEffect, useState, useRef } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -9,13 +14,8 @@ import {
import { Field, FieldError, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Expense } from '@/types';
import { useForm, router } from '@inertiajs/react';
import { useEffect, useState, useRef } from 'react';
import expenseRoutes from '@/routes/expense';
import { toast } from 'sonner';
import { NumericFormat } from 'react-number-format';
import { ImagePlus, X, Save, Loader } from 'lucide-react';
import type { Expense } from '@/types';
interface ExpenseFormModalProps {
isOpen: boolean;
@ -63,6 +63,7 @@ export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalP
const onImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setData('image', file);
const reader = new FileReader();
@ -76,6 +77,7 @@ export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalP
const removeImage = () => {
setData('image', null);
setImagePreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
@ -83,6 +85,7 @@ export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalP
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isEditing && expense) {
router.post(expenseRoutes.update(expense.id).url, {
...data,

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Payroll } from '@/types';
import { router } from '@inertiajs/react';
import payrollRoutes from '@/routes/payroll';
import { useState } from 'react';
import { toast } from 'sonner';
import payrollRoutes from '@/routes/payroll';
import type { Payroll } from '@/types';
export function usePayrollIndex() {
const [isFormOpen, setIsFormOpen] = useState(false);

View File

@ -1,26 +1,17 @@
import { Head } from '@inertiajs/react';
import type { Payroll } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, Sparkles, Plus, X } from 'lucide-react';
import { Trash2, Sparkles } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import type { Payroll } from '@/types';
import { usePayrollIndex } from './hooks/use-payroll-index';
import { Badge } from '@/components/ui/badge';
import { getColumns } from './partials/columns';
import { PayrollFormModal } from './partials/payroll-form-modal';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
export default function PayrollIndex({
payrolls,

View File

@ -1,11 +1,11 @@
import { ColumnDef } from '@tanstack/react-table';
import { Payroll } from '@/types';
import type { ColumnDef } from '@tanstack/react-table';
import { PencilRuler, Trash2 } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { PencilRuler, Trash2 } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import type { Payroll } from '@/types';
interface ColumnProps {
onEdit: (payroll: Payroll) => void;
@ -37,6 +37,7 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
),
cell: ({ row }) => {
const payroll = row.original;
return (
<div className="flex flex-col gap-1 py-1 min-w-[200px]">
<div className="flex justify-between text-[10px] text-muted-foreground uppercase tracking-tighter">
@ -71,6 +72,7 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
header: "Status",
cell: ({ row }) => {
const payroll = row.original;
return (
<div className="flex items-center gap-2">
<Switch
@ -93,6 +95,7 @@ export const getColumns = ({ onEdit, onDelete, onTogglePaid }: ColumnProps): Col
header: "Aksi",
cell: ({ row }) => {
const payroll = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,3 +1,9 @@
import { useForm } from '@inertiajs/react';
import { Loader, Plus, Trash2 } from 'lucide-react';
import { X, Save } from 'lucide-react';
import { useEffect } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -6,21 +12,14 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldGroup } from "@/components/ui/field"
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import { FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Payroll } from '@/types';
import { useForm } from '@inertiajs/react';
import { useEffect } from 'react';
import payrollRoutes from '@/routes/payroll';
import { toast } from 'sonner';
import { Loader, Plus, Trash2 } from 'lucide-react';
import { NumericFormat } from 'react-number-format';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import { X, Save } from 'lucide-react';
import { SelectTrigger } from '@/components/ui/select';
import payrollRoutes from '@/routes/payroll';
import type { Payroll } from '@/types';
interface PayrollFormModalProps {
isOpen: boolean;
@ -76,6 +75,7 @@ export function PayrollFormModal({ isOpen, onClose, payroll }: PayrollFormModalP
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (payroll) {
patch(payrollRoutes.update(payroll.id).url, {
onSuccess: (response: any) => {

View File

@ -1,19 +1,22 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Head, Link, router, useForm } from '@inertiajs/react';
import { ArrowLeft, Check, ImagePlus, Loader, Minus, Plus, Save, Search, ShoppingCart, Tag, Trash2, X } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import * as orderRoutes from '@/routes/order';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Product, ProductPrice } from '@/types';
import { OrderItem } from '@/types/order';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Tag, ArrowLeft, Check, Loader, Save } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
@ -22,21 +25,15 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import {
Sheet,
SheetContent,
SheetTrigger,
SheetClose,
SheetTrigger
} from "@/components/ui/sheet";
import { NumericFormat } from 'react-number-format';
import { cn } from '@/lib/utils';
import * as orderRoutes from '@/routes/order';
import type { Product } from '@/types';
import type { OrderItem } from '@/types/order';
type CartItem = OrderItem & { id: number };
type EnumOption = { value: string, label: string };
@ -60,6 +57,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
const categories = useMemo(() => {
const map = new Map<number, string>();
products.forEach(p => p.categories?.forEach(c => map.set(c.id, c.name)));
return Array.from(map.entries()).map(([id, name]) => ({ id, name }));
}, [products]);
@ -77,6 +75,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
const filteredProducts = products.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesCategory = selectedCategory === 'all' || p.categories?.some(c => String(c.id) === selectedCategory);
return matchesSearch && matchesCategory;
});
@ -130,7 +129,10 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
};
const updateCartQuantity = (itemId: number, qty: number) => {
if (qty < 1) return;
if (qty < 1) {
return;
}
router.patch(orderRoutes.updateCartItem(itemId).url, {
qty
}, {
@ -146,16 +148,20 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
const confirmQty = () => {
const val = parseInt(qtyInputValue);
if (!isNaN(val) && val >= 1 && qtyDialogIndex !== null) {
updateCartQuantity(qtyDialogIndex, val);
}
setQtyDialogIndex(null);
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (cartItems.length === 0) {
toast.error('Pilih minimal satu produk');
return;
}
@ -187,6 +193,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
const PriceTypeLabel = ({ type }: { type: string }) => {
const option = priceTypes.find(opt => opt.value === type);
return option ? option.label : type;
};
@ -526,7 +533,10 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
cartItem ? "text-foreground hover:text-primary cursor-pointer" : "text-muted-foreground/40 cursor-default"
)}
onClick={(e) => {
if (!cartItem) return;
if (!cartItem) {
return;
}
openQtyDialog(cartItem, e);
}}
>
@ -600,7 +610,11 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
</div>
{/* Quantity Input Dialog */}
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => { if (!open) setQtyDialogIndex(null); }}>
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => {
if (!open) {
setQtyDialogIndex(null);
}
}}>
<DialogContent className="max-w-xs">
<DialogHeader>
<DialogTitle>Ubah Jumlah</DialogTitle>
@ -614,7 +628,11 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
min="1"
value={qtyInputValue}
onChange={e => setQtyInputValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirmQty(); }}
onKeyDown={e => {
if (e.key === 'Enter') {
confirmQty();
}
}}
className="text-lg font-bold"
autoFocus
/>

View File

@ -1,19 +1,21 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Head, Link, useForm } from '@inertiajs/react';
import { ArrowLeft, Check, ImagePlus, Loader, Minus, Plus, Save, Search, ShoppingCart, Tag, Trash2, X } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import * as orderRoutes from '@/routes/order';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Product, ProductPrice } from '@/types';
import { Order, OrderItem } from '@/types/order';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Tag, ArrowLeft, Check, Save, Loader } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { format } from 'date-fns';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
@ -22,21 +24,15 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import {
Sheet,
SheetContent,
SheetTrigger,
SheetClose,
SheetTrigger
} from "@/components/ui/sheet";
import { NumericFormat } from 'react-number-format';
import { cn } from '@/lib/utils';
import * as orderRoutes from '@/routes/order';
import type { Product } from '@/types';
import type { Order } from '@/types/order';
type EnumOption = { value: string, label: string };
@ -57,6 +53,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
const categories = useMemo(() => {
const map = new Map<number, string>();
products.forEach(p => p.categories?.forEach(c => map.set(c.id, c.name)));
return Array.from(map.entries()).map(([id, name]) => ({ id, name }));
}, [products]);
@ -81,6 +78,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
const filteredProducts = products.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesCategory = selectedCategory === 'all' || p.categories?.some(c => String(c.id) === selectedCategory);
return matchesSearch && matchesCategory;
});
@ -118,22 +116,30 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
const decreaseQuantity = (productId: number, priceType: string, e: React.MouseEvent) => {
e.stopPropagation();
const index = findItemIndex(productId, priceType);
if (index === -1) return;
if (index === -1) {
return;
}
const newItems = [...data.items];
if (newItems[index].qty <= 1) {
newItems.splice(index, 1);
} else {
newItems[index].qty -= 1;
newItems[index].total = newItems[index].qty * newItems[index].price;
}
setData('items', newItems);
};
const increaseQuantity = (productId: number, priceType: string, e: React.MouseEvent) => {
e.stopPropagation();
const product = products.find(p => p.id === productId);
if (product) addToCart(product, priceType);
if (product) {
addToCart(product, priceType);
}
};
const removeFromCart = (productId: number, priceType: string) => {
@ -142,8 +148,12 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
};
const updateCartQuantity = (productId: number, priceType: string, qty: number) => {
if (qty < 1) return;
if (qty < 1) {
return;
}
const index = findItemIndex(productId, priceType);
if (index > -1) {
const newItems = [...data.items];
newItems[index].qty = qty;
@ -160,16 +170,20 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
const confirmQty = () => {
const val = parseInt(qtyInputValue);
if (!isNaN(val) && val >= 1 && qtyDialogIndex) {
updateCartQuantity(qtyDialogIndex.product_id, qtyDialogIndex.price_type, val);
}
setQtyDialogIndex(null);
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (data.items.length === 0) {
toast.error('Pilih minimal satu produk');
return;
}
@ -190,6 +204,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
const PriceTypeLabel = ({ type }: { type: string }) => {
const option = priceTypes.find(opt => opt.value === type);
return option ? option.label : type;
};
@ -522,7 +537,10 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
cartItem ? "text-foreground hover:text-primary cursor-pointer" : "text-muted-foreground/40 cursor-default"
)}
onClick={(e) => {
if (!cartItem) return;
if (!cartItem) {
return;
}
openQtyDialog(cartItem, e);
}}
>
@ -586,7 +604,11 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
</div>
{/* Quantity Input Dialog */}
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => { if (!open) setQtyDialogIndex(null); }}>
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => {
if (!open) {
setQtyDialogIndex(null);
}
}}>
<DialogContent className="max-w-xs">
<DialogHeader>
<DialogTitle>Ubah Jumlah</DialogTitle>
@ -600,7 +622,11 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
min="1"
value={qtyInputValue}
onChange={e => setQtyInputValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirmQty(); }}
onKeyDown={e => {
if (e.key === 'Enter') {
confirmQty();
}
}}
className="text-lg font-bold"
autoFocus
/>

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Order } from '@/types/order';
import { router } from '@inertiajs/react';
import * as orderRoutes from '@/routes/order';
import { useState } from 'react';
import { toast } from 'sonner';
import * as orderRoutes from '@/routes/order';
import type { Order } from '@/types/order';
export function useOrderIndex() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

View File

@ -1,44 +1,33 @@
import { Head, Link } from '@inertiajs/react';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { Bluetooth, Plus, Printer, Trash2, Unplug, Usb } from 'lucide-react';
import { useCallback } from 'react';
import type { Order } from '@/types/order';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, ShoppingBag, Printer, Bluetooth, Usb, Unplug, Plus, X, ArrowLeft } from 'lucide-react';
import { toast } from 'sonner';
import { DataTable } from '@/components/data-table';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from "@/components/ui/dropdown-menu"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import * as orderRoutes from '@/routes/order';
import { useOrderIndex } from './hooks/use-order-index';
import { getColumns } from './partials/columns';
import { usePrinter } from '@/hooks/use-printer';
import { EscPosEncoder } from '@/lib/esc-pos-encoder';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { toast } from 'sonner';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import * as orderRoutes from '@/routes/order';
import type { Order } from '@/types/order';
import { useOrderIndex } from './hooks/use-order-index';
import { getColumns } from './partials/columns';
export default function OrderIndex({ orders }: { orders: Order[] }) {
const {
@ -70,6 +59,7 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
const handlePrint = useCallback(async (order: Order) => {
if (!isConnected) {
toast.error('Hubungkan printer terlebih dahulu');
return;
}
@ -93,7 +83,10 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
.line(line);
order.items?.forEach((item, index) => {
if (index > 0) result.line();
if (index > 0) {
result.line();
}
result.line(item.product?.name || 'Produk');
const qtyPrice = `${item.qty} x ${item.price_formatted}`;
const subtotal = item.total_formatted;
@ -104,6 +97,7 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
result.line(line);
const discountAmount = Number(order.discount || 0);
if (discountAmount > 0) {
const discountLabel = 'Diskon:';
const discountVal = `- Rp ${discountAmount.toLocaleString('id-ID')}`;

View File

@ -1,14 +1,14 @@
import { ColumnDef } from '@tanstack/react-table';
import { Order } from '@/types/order';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Pencil, Trash2, ShoppingBag, Printer } from 'lucide-react';
import { Link } from '@inertiajs/react';
import * as orderRoutes from '@/routes/order';
import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { Pencil, Trash2, Printer } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import * as orderRoutes from '@/routes/order';
import type { Order } from '@/types/order';
interface ColumnProps {
onDelete: (order: Order) => void;
@ -100,6 +100,7 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
header: "Aksi",
cell: ({ row }) => {
const order = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,19 +1,26 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { format } from 'date-fns';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Loader, Save, ArrowLeft } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import purchaseRoutes from '@/routes/purchase';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Product, ProductPrice } from '@/types';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Loader, Save, ArrowLeft } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Calendar } from '@/components/ui/calendar';
import { format } from 'date-fns';
import type { Product, ProductPrice } from '@/types';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
@ -22,20 +29,13 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import {
Sheet,
SheetContent,
SheetTrigger,
SheetClose,
} from "@/components/ui/sheet";
import purchaseRoutes from '@/routes/purchase';
type CartItem = { product_id: number; quantity: number; unit_price: number; product: Product };
@ -57,6 +57,7 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
const categories = useMemo(() => {
const map = new Map<number, string>();
products.forEach(p => p.categories?.forEach(c => map.set(c.id, c.name)));
return Array.from(map.entries()).map(([id, name]) => ({ id, name }));
}, [products]);
@ -69,6 +70,7 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
const filteredProducts = products.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesCategory = selectedCategory === 'all' || p.categories?.some(c => String(c.id) === selectedCategory);
return matchesSearch && matchesCategory;
});
@ -89,7 +91,10 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
const decreaseQuantity = (product: Product, e: React.MouseEvent) => {
e.stopPropagation();
const item = getCartItem(product.id);
if (!item) return;
if (!item) {
return;
}
router.post(purchaseRoutes.addToCart().url, {
product_id: product.id,
@ -112,7 +117,10 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
};
const updateCartQuantity = (itemId: number, quantity: number) => {
if (quantity < 1) return;
if (quantity < 1) {
return;
}
router.patch(purchaseRoutes.updateCartItem(itemId).url, {
quantity
}, {
@ -128,16 +136,20 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
const confirmQty = () => {
const val = parseInt(qtyInputValue);
if (!isNaN(val) && val >= 1 && qtyDialogIndex !== null) {
updateCartQuantity(qtyDialogIndex, val);
}
setQtyDialogIndex(null);
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (cartItems.length === 0) {
toast.error('Pilih minimal satu produk');
return;
}
@ -267,6 +279,7 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
} else {
setData('purchase_date', '');
}
setIsCalendarOpen(false);
}}
/>
@ -429,7 +442,10 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
cartItem ? "text-foreground hover:text-primary cursor-pointer" : "text-muted-foreground/40 cursor-default"
)}
onClick={(e) => {
if (!cartItem) return;
if (!cartItem) {
return;
}
openQtyDialog(cartItem, e);
}}
>
@ -569,6 +585,7 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
} else {
setData('purchase_date', '');
}
setIsCalendarOpen(false);
}}
/>
@ -786,7 +803,11 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
</div>
{/* Quantity Input Dialog */}
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => { if (!open) setQtyDialogIndex(null); }}>
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => {
if (!open) {
setQtyDialogIndex(null);
}
}}>
<DialogContent className="max-w-xs">
<DialogHeader>
<DialogTitle>Ubah Jumlah</DialogTitle>
@ -800,7 +821,11 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
min="1"
value={qtyInputValue}
onChange={e => setQtyInputValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirmQty(); }}
onKeyDown={e => {
if (e.key === 'Enter') {
confirmQty();
}
}}
className="text-lg font-bold"
autoFocus
/>

View File

@ -1,19 +1,26 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Head, Link, useForm } from '@inertiajs/react';
import { format } from 'date-fns';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Loader, Save, ArrowLeft } from 'lucide-react';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import purchaseRoutes from '@/routes/purchase';
import React, { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Product, ProductPrice, Purchase } from '@/types';
import { Plus, Trash2, CalendarIcon, Minus, ShoppingCart, Search, ImagePlus, X, Loader, Save, ArrowLeft } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Calendar } from '@/components/ui/calendar';
import { format } from 'date-fns';
import type { Product, ProductPrice, Purchase } from '@/types';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
@ -22,22 +29,13 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
SheetClose,
} from "@/components/ui/sheet";
import purchaseRoutes from '@/routes/purchase';
type CartItem = { product_id: number; quantity: number; unit_price: number; product: Product };
@ -59,6 +57,7 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
const categories = useMemo(() => {
const map = new Map<number, string>();
products.forEach(p => p.categories?.forEach(c => map.set(c.id, c.name)));
return Array.from(map.entries()).map(([id, name]) => ({ id, name }));
}, [products]);
@ -76,6 +75,7 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
const filteredProducts = products.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesCategory = selectedCategory === 'all' || p.categories?.some(c => String(c.id) === selectedCategory);
return matchesSearch && matchesCategory;
});
@ -101,14 +101,19 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
const decreaseQuantity = (product: Product, e: React.MouseEvent) => {
e.stopPropagation();
const existingIndex = data.items.findIndex(i => i.product_id === product.id);
if (existingIndex === -1) return;
if (existingIndex === -1) {
return;
}
const newItems = [...data.items];
if (newItems[existingIndex].quantity <= 1) {
newItems.splice(existingIndex, 1);
} else {
newItems[existingIndex].quantity -= 1;
}
setData('items', newItems);
};
@ -123,9 +128,13 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
};
const updateCartQuantity = (productId: number, quantity: number) => {
if (quantity < 1) return;
if (quantity < 1) {
return;
}
const newItems = [...data.items];
const index = newItems.findIndex(i => i.product_id === productId);
if (index > -1) {
newItems[index].quantity = quantity;
setData('items', newItems);
@ -141,16 +150,20 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
const confirmQty = () => {
const val = parseInt(qtyInputValue);
if (!isNaN(val) && val >= 1 && qtyDialogIndex !== null) {
updateCartQuantity(qtyDialogIndex, val);
}
setQtyDialogIndex(null);
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (data.items.length === 0) {
toast.error('Pilih minimal satu produk');
return;
}
@ -271,6 +284,7 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
} else {
setData('purchase_date', '');
}
setIsCalendarOpen(false);
}}
/>
@ -433,7 +447,10 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
cartItem ? "text-foreground hover:text-primary cursor-pointer" : "text-muted-foreground/40 cursor-default"
)}
onClick={(e) => {
if (!cartItem) return;
if (!cartItem) {
return;
}
openQtyDialog(cartItem, e);
}}
>
@ -573,6 +590,7 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
} else {
setData('purchase_date', '');
}
setIsCalendarOpen(false);
}}
/>
@ -790,7 +808,11 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
</div>
</div>
{/* Quantity Input Dialog */}
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => { if (!open) setQtyDialogIndex(null); }}>
<Dialog open={qtyDialogIndex !== null} onOpenChange={(open) => {
if (!open) {
setQtyDialogIndex(null);
}
}}>
<DialogContent className="max-w-xs">
<DialogHeader>
<DialogTitle>Ubah Jumlah</DialogTitle>
@ -804,7 +826,11 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
min="1"
value={qtyInputValue}
onChange={e => setQtyInputValue(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirmQty(); }}
onKeyDown={e => {
if (e.key === 'Enter') {
confirmQty();
}
}}
className="text-lg font-bold"
autoFocus
/>

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Purchase } from '@/types';
import { router } from '@inertiajs/react';
import purchaseRoutes from '@/routes/purchase';
import { useState } from 'react';
import { toast } from 'sonner';
import purchaseRoutes from '@/routes/purchase';
import type { Purchase } from '@/types';
export function usePurchaseIndex() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

View File

@ -1,28 +1,18 @@
import { Head, Link } from '@inertiajs/react';
import type { Purchase } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Plus, Trash2 } from 'lucide-react';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { DataTable } from '@/components/data-table';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import {
AlertDialogMedia,
} from "@/components/ui/alert-dialog"
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { formatDate } from '@/lib/formatters';
import purchaseRoutes from '@/routes/purchase';
import type { Purchase } from '@/types';
import { usePurchaseIndex } from './hooks/use-purchase-index';
import { getColumns } from './partials/columns';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { formatDate } from '@/lib/formatters';
export default function PurchaseIndex({ purchases }: { purchases: Purchase[] }) {
const {

View File

@ -1,13 +1,13 @@
import { ColumnDef } from '@tanstack/react-table';
import { Purchase } 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 { Pencil, Trash2 } from 'lucide-react';
import { Link } from '@inertiajs/react';
import purchaseRoutes from '@/routes/purchase';
import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { Pencil, Trash2 } 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 purchaseRoutes from '@/routes/purchase';
import type { Purchase } from '@/types';
interface ColumnProps {
onDelete: (purchase: Purchase) => void;
@ -21,6 +21,7 @@ export const getColumns = ({ onDelete }: ColumnProps): ColumnDef<Purchase>[] =>
),
cell: ({ row }) => {
const date = row.original.purchase_date;
return format(new Date(date), 'dd MMMM yyyy', { locale: id });
},
meta: { title: "Tanggal" },
@ -80,6 +81,7 @@ export const getColumns = ({ onDelete }: ColumnProps): ColumnDef<Purchase>[] =>
header: "Aksi",
cell: ({ row }) => {
const purchase = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Category } from '@/types';
import { router } from '@inertiajs/react';
import categoryRoutes from '@/routes/category';
import { useState } from 'react';
import { toast } from 'sonner';
import categoryRoutes from '@/routes/category';
import type { Category } from '@/types';
export function useCategoryIndex() {
const [isFormOpen, setIsFormOpen] = useState(false);

View File

@ -1,14 +1,14 @@
import { Head } from '@inertiajs/react';
import type { Category } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, Plus, X } from 'lucide-react';
import { Trash2, Plus } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import type { Category } from '@/types';
import { useCategoryIndex } from './hooks/use-category-index';
import { getColumns } from './partials/columns';
import { CategoryFormModal } from './partials/category-form-modal';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { getColumns } from './partials/columns';
export default function CategoryIndex({ categories }: { categories: Category[] }) {
const {

View File

@ -1,3 +1,7 @@
import { useForm } from '@inertiajs/react';
import { X, Save, Loader } from 'lucide-react';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -9,12 +13,8 @@ import {
import { Field, FieldError, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Category } from '@/types';
import { useForm } from '@inertiajs/react';
import { useEffect } from 'react';
import categoryRoutes from '@/routes/category';
import { toast } from 'sonner';
import { X, Save, Loader } from 'lucide-react';
import type { Category } from '@/types';
interface CategoryFormModalProps {
isOpen: boolean;
@ -49,6 +49,7 @@ export function CategoryFormModal({ isOpen, onClose, category }: CategoryFormMod
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isEditing && category) {
patch(categoryRoutes.update(category.id).url, {
onSuccess: (response: any) => {

View File

@ -1,10 +1,10 @@
import { ColumnDef } from '@tanstack/react-table';
import { Category } from '@/types';
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Pencil, Trash2 } from 'lucide-react';
import type { Category } from '@/types';
interface ColumnProps {
onEdit: (category: Category) => void;
@ -26,6 +26,7 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C
meta: { title: "Status" },
cell: ({ row }) => {
const category = row.original;
return (
<Switch
checked={category.is_active}
@ -39,6 +40,7 @@ export const getColumns = ({ onEdit, onDelete, onToggleStatus }: ColumnProps): C
header: "Aksi",
cell: ({ row }) => {
const category = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,15 +1,12 @@
import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import productRoutes from '@/routes/product';
import { ImagePlus, X, Upload, ArrowLeft, Save, Loader } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { SimpleEditor } from '@/components/tiptap-templates/simple/simple-editor'
import { Category } from '@/types/category';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Combobox,
ComboboxContent,
@ -22,8 +19,11 @@ import {
ComboboxValue,
useComboboxAnchor,
} from "@/components/ui/combobox"
import { NumericFormat } from 'react-number-format';
import { ImagePlus, X, Upload, ArrowLeft, Save, Loader } from 'lucide-react';
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import productRoutes from '@/routes/product';
import type { Category } from '@/types/category';
export default function ProductCreate({ categories }: { categories: Category[] }) {
const { data, setData, post, processing, errors } = useForm<{
@ -63,7 +63,11 @@ export default function ProductCreate({ categories }: { categories: Category[] }
const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file) {
return;
}
setData('thumbnail', file);
const reader = new FileReader();
reader.onloadend = () => setThumbnailPreview(reader.result as string);
@ -73,12 +77,18 @@ export default function ProductCreate({ categories }: { categories: Category[] }
const removeThumbnail = () => {
setData('thumbnail', null);
setThumbnailPreview(null);
if (thumbnailInputRef.current) thumbnailInputRef.current.value = '';
if (thumbnailInputRef.current) {
thumbnailInputRef.current.value = '';
}
};
const handleImagesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? []);
if (!files.length) return;
if (!files.length) {
return;
}
const newPreviews = files.map(file => ({
file,
@ -87,7 +97,10 @@ export default function ProductCreate({ categories }: { categories: Category[] }
setImagePreviews(prev => [...prev, ...newPreviews]);
setData('images', [...data.images, ...files]);
if (imagesInputRef.current) imagesInputRef.current.value = '';
if (imagesInputRef.current) {
imagesInputRef.current.value = '';
}
};
const removeImage = (index: number) => {

View File

@ -1,15 +1,11 @@
import { Head, useForm, Link } from '@inertiajs/react';
import type { Product, ProductPrice } from '@/types';
import { Category } from '@/types/category';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from 'sonner';
import productRoutes from '@/routes/product';
import { ImagePlus, X, Upload, ArrowLeft, Save, Loader } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { SimpleEditor } from '@/components/tiptap-templates/simple/simple-editor';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Combobox,
ComboboxContent,
@ -22,8 +18,12 @@ import {
ComboboxValue,
useComboboxAnchor,
} from "@/components/ui/combobox"
import { NumericFormat } from 'react-number-format';
import { ImagePlus, X, Upload, ArrowLeft, Save, Loader } from 'lucide-react';
import { Field, FieldError } from "@/components/ui/field"
import { Category } from '@/types/category';
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import productRoutes from '@/routes/product';
import type { Product, ProductPrice } from '@/types';
interface ExistingImage {
kind: 'existing';
@ -42,6 +42,7 @@ type GalleryItem = ExistingImage | NewImage;
export default function ProductEdit({ product, categories }: { product: Product, categories: Category[] }) {
const getPrice = (type: string) => {
const priceObj = product.prices?.find((p: ProductPrice) => p.price_type === type);
return priceObj ? priceObj.price.toString() : '';
};
@ -98,7 +99,11 @@ export default function ProductEdit({ product, categories }: { product: Product,
const handleThumbnailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file) {
return;
}
setData('thumbnail', file);
setThumbnailCleared(false);
const reader = new FileReader();
@ -110,12 +115,18 @@ export default function ProductEdit({ product, categories }: { product: Product,
setData('thumbnail', null);
setThumbnailPreview(null);
setThumbnailCleared(true);
if (thumbnailInputRef.current) thumbnailInputRef.current.value = '';
if (thumbnailInputRef.current) {
thumbnailInputRef.current.value = '';
}
};
const handleImagesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? []);
if (!files.length) return;
if (!files.length) {
return;
}
const newItems: NewImage[] = files.map(file => ({
kind: 'new',
@ -125,7 +136,10 @@ export default function ProductEdit({ product, categories }: { product: Product,
setGallery(prev => [...prev, ...newItems]);
setData('images', [...data.images, ...files]);
if (imagesInputRef.current) imagesInputRef.current.value = '';
if (imagesInputRef.current) {
imagesInputRef.current.value = '';
}
};
const removeGalleryItem = (index: number) => {

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { Product } from '@/types';
import { router } from '@inertiajs/react';
import productRoutes from '@/routes/product';
import { useState } from 'react';
import { toast } from 'sonner';
import productRoutes from '@/routes/product';
import type { Product } from '@/types';
export function useProductIndex() {
const [selectedImage, setSelectedImage] = useState<string | null>(null);

View File

@ -1,15 +1,15 @@
import { Head, Link } from '@inertiajs/react';
import type { Product, Category } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, Plus } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import productRoutes from '@/routes/product';
import type { Product, Category } from '@/types';
import { ImagePreviewDialog } from '../../../../components/modal/image-preview';
import { useProductIndex } from './hooks/use-product-index';
import { getColumns } from './partials/columns';
import { ImagePreviewDialog } from '../../../../components/modal/image-preview';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
export default function ProductIndex({ products, categories }: { products: Product[], categories: Category[] }) {
const {

View File

@ -1,13 +1,13 @@
import { ColumnDef } from '@tanstack/react-table';
import { Product, Category } from '@/types';
import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2, ImagePlus } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Pencil, Trash2, ImagePlus } from 'lucide-react';
import { Link } from '@inertiajs/react';
import productRoutes from '@/routes/product';
import type { Product, Category } from '@/types';
interface ColumnProps {
onEdit?: (product: Product) => void;
@ -23,6 +23,7 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
meta: { title: "Thumbnail" },
cell: ({ row }) => {
const url = row.original.thumbnail_url;
return url ? (
<button onClick={() => onPreviewImage(url)} className="block w-fit">
<img src={url} alt="Thumbnail" className="h-10 w-10 object-cover rounded-md border hover:opacity-80 transition-opacity" />
@ -49,11 +50,16 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
meta: { title: "Kategori" },
filterFn: (row, id, value) => {
const categories = row.getValue(id) as Category[];
if (!categories) return false;
if (!categories) {
return false;
}
return categories.some(cat => String(cat.id) === String(value));
},
cell: ({ row }) => {
const product = row.original;
return (
<div className="flex flex-wrap gap-1">
{product.categories?.map((category) => (
@ -118,6 +124,7 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
meta: { title: "Status" },
cell: ({ row }) => {
const product = row.original;
return (
<Switch
checked={product.is_active}
@ -131,6 +138,7 @@ export const getColumns = ({ onDelete, onToggleStatus, onPreviewImage }: ColumnP
header: "Aksi",
cell: ({ row }) => {
const product = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,22 +1,22 @@
import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, Loader, Save } from 'lucide-react';
import React from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Calendar } from "@/components/ui/calendar"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import userRoutes from '@/routes/user';
import React from 'react';
import { toast } from 'sonner';
import { Textarea } from '@/components/ui/textarea';
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { NumericFormat } from 'react-number-format';
import { ArrowLeft, Loader, Save } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea';
import userRoutes from '@/routes/user';
export default function UserCreate() {
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
@ -170,6 +170,7 @@ export default function UserCreate() {
} else {
setData('birth_date', '');
}
setIsCalendarOpen(false);
}}
/>

View File

@ -1,22 +1,22 @@
import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, Loader, Save } from 'lucide-react';
import React from 'react';
import { NumericFormat } from 'react-number-format';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Calendar } from "@/components/ui/calendar"
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import userRoutes from '@/routes/user';
import React from 'react';
import { toast } from 'sonner';
import { Textarea } from '@/components/ui/textarea';
import { Calendar } from "@/components/ui/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { NumericFormat } from 'react-number-format';
import { ArrowLeft, Loader, Save } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea';
import userRoutes from '@/routes/user';
export default function UserEdit({ user }: { user: any }) {
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
@ -171,6 +171,7 @@ export default function UserEdit({ user }: { user: any }) {
} else {
setData('birth_date', '');
}
setIsCalendarOpen(false);
}}
/>

View File

@ -1,8 +1,8 @@
import { useState } from 'react';
import { User } from '@/types';
import { router } from '@inertiajs/react';
import userRoutes from '@/routes/user';
import { useState } from 'react';
import { toast } from 'sonner';
import userRoutes from '@/routes/user';
import type { User } from '@/types';
export function useUserIndex() {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

View File

@ -1,9 +1,7 @@
import { Head, Link } from '@inertiajs/react';
import type { User } from '@/types';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, KeyRound, Plus, X } from 'lucide-react';
import { DataTable } from '@/components/data-table';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import {
AlertDialog,
AlertDialogAction,
@ -15,11 +13,13 @@ import {
AlertDialogMedia,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import userRoutes from '@/routes/user';
import type { User } from '@/types';
import { useUserIndex } from './hooks/use-user-index';
import { getColumns } from './partials/columns';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
export default function UserIndex({ users, defaultPassword }: { users: User[], defaultPassword: string }) {
const {

View File

@ -1,12 +1,12 @@
import { ColumnDef } from '@tanstack/react-table';
import { User } from '@/types';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { UserInfo } from '@/components/user-info';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { KeyRound, Pencil, Trash2 } from 'lucide-react';
import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { KeyRound, Pencil, Trash2 } 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 { UserInfo } from '@/components/user-info';
import userRoutes from '@/routes/user';
import type { User } from '@/types';
interface ColumnProps {
onResetPassword: (user: User) => void;
@ -22,6 +22,7 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
meta: { title: "Nama dan Alamat Surel" },
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center gap-3">
<UserInfo user={user} showEmail={true} />
@ -49,6 +50,7 @@ export const getColumns = ({ onResetPassword, onDelete }: ColumnProps): ColumnDe
header: "Aksi",
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

View File

@ -1,8 +1,8 @@
import { Head } from '@inertiajs/react';
import { Card, CardContent } from '@/components/ui/card';
import { DataTable } from '@/components/data-table';
import { Card, CardContent } from '@/components/ui/card';
import type { Activity } from '@/types';
import { getColumns } from './partials/columns';
import { Activity } from '@/types';
interface Props {
activities: Activity[];

View File

@ -1,6 +1,6 @@
import { ColumnDef } from '@tanstack/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import { Badge } from '@/components/ui/badge';
import { Activity } from '@/types';
import type { Activity } from '@/types';
export function getColumns(): ColumnDef<Activity>[] {
return [
@ -66,7 +66,9 @@ export function getColumns(): ColumnDef<Activity>[] {
const attributes = row.original.properties?.attributes;
const old = row.original.properties?.old;
if (!attributes) return <span className="text-muted-foreground text-xs italic">-</span>;
if (!attributes) {
return <span className="text-muted-foreground text-xs italic">-</span>;
}
return (
<div className="flex flex-col gap-1 py-1 max-w-[400px]">

View File

@ -1,10 +1,7 @@
import { Head, router } from '@inertiajs/react';
import { Card, CardContent } from '@/components/ui/card';
import { DataTable } from '@/components/data-table';
import { useState } from 'react';
import { getColumns } from './partials/columns';
import { SystemLog } from '@/types';
import system from '@/routes/system';
import { DataTable } from '@/components/data-table';
import { Card, CardContent } from '@/components/ui/card';
import {
Select,
SelectContent,
@ -12,6 +9,9 @@ import {
SelectTrigger,
SelectValue
} from "@/components/ui/select";
import system from '@/routes/system';
import type { SystemLog } from '@/types';
import { getColumns } from './partials/columns';
import { LogDetailModal } from './partials/log-detail-modal';
interface Props {

View File

@ -1,11 +1,11 @@
import { ColumnDef } from '@tanstack/react-table';
import type { ColumnDef } from '@tanstack/react-table';
import { Eye } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Badge } from '@/components/ui/badge';
import { Eye } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { SystemLog } from '@/types';
import { getLevelColor } from '@/lib/log-helpers';
import type { SystemLog } from '@/types';
interface ColumnProps {
onView: (log: SystemLog) => void;
@ -27,6 +27,7 @@ export const getColumns = ({ onView }: ColumnProps): ColumnDef<SystemLog>[] => [
),
cell: ({ row }) => {
const level = row.original.level;
return (
<Badge variant={getLevelColor(level) as any}>
{level}
@ -42,6 +43,7 @@ export const getColumns = ({ onView }: ColumnProps): ColumnDef<SystemLog>[] => [
),
cell: ({ row }) => {
const message = row.original.message;
return (
<div className="max-w-[400px] lg:max-w-[600px] truncate font-sans text-xs" title={message}>
{message}
@ -55,6 +57,7 @@ export const getColumns = ({ onView }: ColumnProps): ColumnDef<SystemLog>[] => [
header: "Aksi",
cell: ({ row }) => {
const log = row.original;
return (
<div className="flex items-center gap-2">
<Tooltip>

Some files were not shown because too many files have changed in this diff Show More