
diff --git a/src/components/loading/loading.module.css b/src/components/page-loading/loading.module.css
similarity index 100%
rename from src/components/loading/loading.module.css
rename to src/components/page-loading/loading.module.css
diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx
new file mode 100644
index 0000000..2415bd8
--- /dev/null
+++ b/src/components/ui/combobox.tsx
@@ -0,0 +1,310 @@
+"use client"
+
+import * as React from "react"
+import { Combobox as ComboboxPrimitive } from "@base-ui/react"
+import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupInput,
+} from "@/components/ui/input-group"
+
+const Combobox = ComboboxPrimitive.Root
+
+function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
+ return
+}
+
+function ComboboxTrigger({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Trigger.Props) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
+ return (
+
}
+ className={cn(className)}
+ {...props}
+ >
+
+
+ )
+}
+
+function ComboboxInput({
+ className,
+ children,
+ disabled = false,
+ showTrigger = true,
+ showClear = false,
+ ...props
+}: ComboboxPrimitive.Input.Props & {
+ showTrigger?: boolean
+ showClear?: boolean
+}) {
+ return (
+
+ }
+ {...props}
+ />
+
+ {showTrigger && (
+
+
+
+ )}
+ {showClear && }
+
+ {children}
+
+ )
+}
+
+function ComboboxContent({
+ className,
+ side = "bottom",
+ sideOffset = 6,
+ align = "start",
+ alignOffset = 0,
+ anchor,
+ ...props
+}: ComboboxPrimitive.Popup.Props &
+ Pick<
+ ComboboxPrimitive.Positioner.Props,
+ "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
+ >) {
+ return (
+
+
+
+
+
+ )
+}
+
+function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
+ return (
+
+ )
+}
+
+function ComboboxItem({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Item.Props) {
+ return (
+
+ {children}
+
+ }
+ >
+
+
+
+ )
+}
+
+function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
+ return (
+
+ )
+}
+
+function ComboboxLabel({
+ className,
+ ...props
+}: ComboboxPrimitive.GroupLabel.Props) {
+ return (
+
+ )
+}
+
+function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
+ return (
+
+ )
+}
+
+function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
+ return (
+
+ )
+}
+
+function ComboboxSeparator({
+ className,
+ ...props
+}: ComboboxPrimitive.Separator.Props) {
+ return (
+
+ )
+}
+
+function ComboboxChips({
+ className,
+ ...props
+}: React.ComponentPropsWithRef
&
+ ComboboxPrimitive.Chips.Props) {
+ return (
+
+ )
+}
+
+function ComboboxChip({
+ className,
+ children,
+ showRemove = true,
+ ...props
+}: ComboboxPrimitive.Chip.Props & {
+ showRemove?: boolean
+}) {
+ return (
+
+ {children}
+ {showRemove && (
+ }
+ className="-ml-1 opacity-50 hover:opacity-100"
+ data-slot="combobox-chip-remove"
+ >
+
+
+ )}
+
+ )
+}
+
+function ComboboxChipsInput({
+ className,
+ children,
+ ...props
+}: ComboboxPrimitive.Input.Props) {
+ return (
+
+ )
+}
+
+function useComboboxAnchor() {
+ return React.useRef(null)
+}
+
+export {
+ Combobox,
+ ComboboxInput,
+ ComboboxContent,
+ ComboboxList,
+ ComboboxItem,
+ ComboboxGroup,
+ ComboboxLabel,
+ ComboboxCollection,
+ ComboboxEmpty,
+ ComboboxSeparator,
+ ComboboxChips,
+ ComboboxChip,
+ ComboboxChipsInput,
+ ComboboxTrigger,
+ ComboboxValue,
+ useComboboxAnchor,
+}
diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx
index 6db1784..7f78fec 100644
--- a/src/components/ui/input-group.tsx
+++ b/src/components/ui/input-group.tsx
@@ -12,7 +12,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
- "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
+ "group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
@@ -22,10 +22,10 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
- "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
// Error state.
- "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
+ "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className
)}
@@ -35,7 +35,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
}
const inputGroupAddonVariants = cva(
- "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
@@ -44,9 +44,9 @@ const inputGroupAddonVariants = cva(
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
- "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
+ "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
"block-end":
- "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
+ "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
},
},
defaultVariants: {
@@ -78,12 +78,12 @@ function InputGroupAddon({
}
const inputGroupButtonVariants = cva(
- "text-sm shadow-none flex gap-2 items-center",
+ "flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
- xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
- sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
+ sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
@@ -118,7 +118,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
) {
type={type}
data-slot="input"
className={cn(
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
diff --git a/src/enum/menu_icon.ts b/src/enum/menu_icon.ts
index 78d8247..5e37527 100644
--- a/src/enum/menu_icon.ts
+++ b/src/enum/menu_icon.ts
@@ -8,6 +8,7 @@ export const MenuIconKey = {
POST: 7,
POST_EDIT: 8,
CATEGORY: 9,
+ TAG: 10,
} as const
export type MenuIconKeyValues = (typeof MenuIconKey)[keyof typeof MenuIconKey]
@@ -22,6 +23,6 @@ export const MenuIconOptions: MenuIconOption[] = Object.entries(MenuIconKey).map
([key, value]) => ({
label: key as keyof typeof MenuIconKey,
value,
- selectValue: value.toString() as `${MenuIconKeyValues}`,
+ selectValue: value?.toString() as `${MenuIconKeyValues}`,
}),
)
diff --git a/src/enum/post_status.tsx b/src/enum/post_status.ts
similarity index 81%
rename from src/enum/post_status.tsx
rename to src/enum/post_status.ts
index b74de22..ed6666e 100644
--- a/src/enum/post_status.tsx
+++ b/src/enum/post_status.ts
@@ -18,19 +18,19 @@ export const postStatusOptions = [
{
label: postStatusDict[PostStatus.draft],
value: String(PostStatus.draft),
- icon: ,
+ icon: ClipboardPenLine,
className: 'bg-gray-200 text-gray-600',
},
{
label: postStatusDict[PostStatus.published],
value: String(PostStatus.published),
- icon: ,
- className: 'bg-lime-100 text-emerald-700',
+ icon: Globe,
+ className: 'bg-blue-400 text-white',
},
{
label: postStatusDict[PostStatus.offline],
value: String(PostStatus.offline),
- icon: ,
- className: 'bg-gray-100 text-gray-300',
+ icon: EyeOff,
+ className: 'bg-gray-100 text-gray-400',
},
]
diff --git a/src/layout/components/app-header.tsx b/src/layout/components/app-header.tsx
index de0d5f7..7aafe08 100644
--- a/src/layout/components/app-header.tsx
+++ b/src/layout/components/app-header.tsx
@@ -26,13 +26,13 @@ import { generateBreadcrumbs } from '@/lib'
import { useStore } from '@/store'
export const AppHeader = () => {
- const location = useLocation()
+ const pathname = useLocation({ select: (location) => location.pathname })
const userInfo = useStore((state) => state.userInfo)
const logout = useStore((state) => state.logout)
const breadcrumbs = useMemo(() => {
- return generateBreadcrumbs(userInfo?.menus || [], location.pathname)
- }, [userInfo?.menus, location.pathname])
+ return generateBreadcrumbs(userInfo?.menus || [], pathname)
+ }, [userInfo?.menus, pathname])
return (
diff --git a/src/layout/components/app-sidebar.tsx b/src/layout/components/app-sidebar.tsx
index c939189..1974b2e 100644
--- a/src/layout/components/app-sidebar.tsx
+++ b/src/layout/components/app-sidebar.tsx
@@ -19,6 +19,7 @@ import {
import { MenuType } from '@/enum'
import { generateMenus } from '@/lib'
import type { SysMenuTree } from '@/schemas'
+import { siteConfig } from '@/siteConfig.ts'
import { useStore } from '@/store'
const isMenuActive = (menu: SysMenuTree, pathname: string): boolean => {
@@ -86,7 +87,7 @@ export function AppSidebar({ ...props }: ComponentProps) {