mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Added notification for admin user (#106)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react' // Import useEffect, useRef
|
||||
import { useSearchParams } from 'next/navigation' // Import useSearchParams
|
||||
import { t2d, d2s, getNow, isSameDate } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
@@ -37,8 +38,32 @@ export default function CoinsManager() {
|
||||
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
const [note, setNote] = useState('')
|
||||
const searchParams = useSearchParams()
|
||||
const highlightId = searchParams.get('highlight')
|
||||
const userIdFromQuery = searchParams.get('user') // Get user ID from query
|
||||
const transactionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
// Effect to set selected user from query param if admin
|
||||
useEffect(() => {
|
||||
if (currentUser?.isAdmin && userIdFromQuery && userIdFromQuery !== selectedUser) {
|
||||
// Check if the user ID from query exists in usersData
|
||||
if (usersData.users.some(u => u.id === userIdFromQuery)) {
|
||||
setSelectedUser(userIdFromQuery);
|
||||
}
|
||||
}
|
||||
// Only run when userIdFromQuery or currentUser changes, avoid re-running on selectedUser change within this effect
|
||||
}, [userIdFromQuery, currentUser, usersData.users]);
|
||||
|
||||
// Effect to scroll to highlighted transaction
|
||||
useEffect(() => {
|
||||
if (highlightId && transactionRefs.current[highlightId]) {
|
||||
transactionRefs.current[highlightId]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
}, [highlightId, transactions]); // Re-run if highlightId or transactions change
|
||||
|
||||
const handleSaveNote = async (transactionId: string, note: string) => {
|
||||
await updateNote(transactionId, note)
|
||||
@@ -249,13 +274,17 @@ export default function CoinsManager() {
|
||||
}
|
||||
}
|
||||
|
||||
const isHighlighted = transaction.id === highlightId;
|
||||
return (
|
||||
<div
|
||||
key={transaction.id}
|
||||
className="flex justify-between items-center p-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
ref={(el) => { transactionRefs.current[transaction.id] = el; }} // Assign ref correctly
|
||||
className={`flex justify-between items-center p-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
||||
isHighlighted ? 'ring-2 ring-blue-500 bg-blue-50 dark:bg-blue-900/30' : '' // Apply highlight styles
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="space-y-1 flex-grow mr-4"> {/* Added flex-grow and margin */}
|
||||
<div className="flex items-center gap-2 flex-wrap"> {/* Added flex-wrap */}
|
||||
{transaction.relatedItemId ? (
|
||||
<Link
|
||||
href={`${transaction.type === 'WISH_REDEMPTION' ? '/wishlist' : '/habits'}?highlight=${transaction.relatedItemId}`}
|
||||
@@ -274,12 +303,13 @@ export default function CoinsManager() {
|
||||
</span>
|
||||
{transaction.userId && currentUser?.isAdmin && (
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage
|
||||
src={usersData.users.find(u => u.id === transaction.userId)?.avatarPath &&
|
||||
`/api/avatars/${usersData.users.find(u => u.id === transaction.userId)?.avatarPath?.split('/').pop()}` || ""}
|
||||
<AvatarImage
|
||||
src={usersData.users.find(u => u.id === transaction.userId)?.avatarPath ?
|
||||
`/api/avatars/${usersData.users.find(u => u.id === transaction.userId)?.avatarPath?.split('/').pop()}` : undefined}
|
||||
alt={usersData.users.find(u => u.id === transaction.userId)?.username}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{usersData.users.find(u => u.id === transaction.userId)?.username[0]}
|
||||
{usersData.users.find(u => u.id === transaction.userId)?.username?.[0] || '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
@@ -294,14 +324,16 @@ export default function CoinsManager() {
|
||||
onDelete={handleDeleteNote}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={`font-mono ${transaction.amount >= 0
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{transaction.amount >= 0 ? '+' : ''}{transaction.amount}
|
||||
</span>
|
||||
<div className="flex-shrink-0 text-right"> {/* Ensure amount stays on the right */}
|
||||
<span
|
||||
className={`font-mono ${transaction.amount >= 0
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{transaction.amount >= 0 ? '+' : ''}{transaction.amount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { coinsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { Bell, Menu, Settings, User, Info, Coins } from 'lucide-react'
|
||||
import { Menu, Settings, User, Info, Coins } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Logo } from '@/components/Logo'
|
||||
import NotificationBell from './NotificationBell'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -19,6 +20,7 @@ import AboutModal from './AboutModal'
|
||||
import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Profile } from './Profile'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
|
||||
interface HeaderProps {
|
||||
className?: string
|
||||
@@ -52,9 +54,7 @@ export default function Header({ className }: HeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Button variant="ghost" size="icon" aria-label="Notifications">
|
||||
<Bell className="h-5 w-5" />
|
||||
</Button>
|
||||
<NotificationBell />
|
||||
<Profile />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
133
components/NotificationBell.tsx
Normal file
133
components/NotificationBell.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { coinsAtom, habitsAtom, wishlistAtom, usersAtom } from '@/lib/atoms'
|
||||
import { Bell } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import NotificationDropdown from './NotificationDropdown';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { updateLastNotificationReadTimestamp } from '@/app/actions/data';
|
||||
import { d2t, getNow, t2d } from '@/lib/utils';
|
||||
import { useHelpers } from '@/lib/client-helpers';
|
||||
import { User, CoinTransaction } from '@/lib/types';
|
||||
|
||||
export default function NotificationBell() {
|
||||
const { currentUser } = useHelpers();
|
||||
const [coinsData] = useAtom(coinsAtom)
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
const [wishlistData] = useAtom(wishlistAtom)
|
||||
const [usersData] = useAtom(usersAtom);
|
||||
|
||||
// --- Calculate Unread and Read Notifications ---
|
||||
const { unreadNotifications, displayedReadNotifications } = useMemo(() => {
|
||||
const unread: CoinTransaction[] = [];
|
||||
const read: CoinTransaction[] = [];
|
||||
const MAX_READ_NOTIFICATIONS = 10; // Limit the number of past notifications shown
|
||||
|
||||
if (!currentUser || !currentUser.id) {
|
||||
return { unreadNotifications: [], displayedReadNotifications: [] };
|
||||
}
|
||||
|
||||
const lastReadTimestamp = currentUser.lastNotificationReadTimestamp
|
||||
? t2d({ timestamp: currentUser.lastNotificationReadTimestamp, timezone: 'UTC' })
|
||||
: null;
|
||||
|
||||
// Iterate through transactions (assuming they are sorted newest first)
|
||||
for (const tx of coinsData.transactions) {
|
||||
// Stop processing if we have enough read notifications
|
||||
if (read.length >= MAX_READ_NOTIFICATIONS && (!lastReadTimestamp || t2d({ timestamp: tx.timestamp, timezone: 'UTC' }) <= lastReadTimestamp)) {
|
||||
break; // Optimization: stop early if we have enough read and are past the unread ones
|
||||
}
|
||||
|
||||
// Basic checks: must have a related item and be triggered by someone else
|
||||
if (!tx.relatedItemId || tx.userId === currentUser.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the transaction type indicates a notification-worthy event
|
||||
const isRelevantType = tx.type === 'HABIT_COMPLETION' || tx.type === 'TASK_COMPLETION' || tx.type === 'WISH_REDEMPTION';
|
||||
if (!isRelevantType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the related item is shared with the current user
|
||||
let isShared = false;
|
||||
const isHabitCompletion = tx.type === 'HABIT_COMPLETION' || tx.type === 'TASK_COMPLETION';
|
||||
const isWishRedemption = tx.type === 'WISH_REDEMPTION';
|
||||
|
||||
if (isHabitCompletion) {
|
||||
const habit = habitsData.habits.find(h => h.id === tx.relatedItemId);
|
||||
if (habit?.userIds?.includes(currentUser.id) && tx.userId && habit.userIds.includes(tx.userId)) {
|
||||
isShared = true;
|
||||
}
|
||||
} else if (isWishRedemption) {
|
||||
const wish = wishlistData.items.find(w => w.id === tx.relatedItemId);
|
||||
if (wish?.userIds?.includes(currentUser.id) && tx.userId && wish.userIds.includes(tx.userId)) {
|
||||
isShared = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isShared) {
|
||||
continue; // Skip if not shared
|
||||
}
|
||||
|
||||
// Transaction is relevant, determine if read or unread
|
||||
const txTimestamp = t2d({ timestamp: tx.timestamp, timezone: 'UTC' });
|
||||
if (!lastReadTimestamp || txTimestamp > lastReadTimestamp) {
|
||||
unread.push(tx);
|
||||
} else if (read.length < MAX_READ_NOTIFICATIONS) {
|
||||
// Only add to read if we haven't hit the limit
|
||||
read.push(tx);
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions are assumed to be sorted newest first from the source
|
||||
return { unreadNotifications: unread, displayedReadNotifications: read };
|
||||
}, [coinsData.transactions, habitsData.habits, wishlistData.items, currentUser]);
|
||||
// --- End Calculate Notifications ---
|
||||
|
||||
const unreadCount = unreadNotifications.length;
|
||||
|
||||
const handleNotificationClick = async () => {
|
||||
if (!currentUser || !currentUser.id || unreadCount === 0) return; // Only update if there are unread notifications
|
||||
try {
|
||||
const nowTimestamp = d2t({ dateTime: getNow({}) });
|
||||
await updateLastNotificationReadTimestamp(currentUser.id, nowTimestamp);
|
||||
} catch (error) {
|
||||
console.error("Failed to update notification read timestamp:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu onOpenChange={(open) => {
|
||||
// Update timestamp only when opening the dropdown and there are unread notifications
|
||||
if (open && unreadCount > 0) {
|
||||
handleNotificationClick();
|
||||
}
|
||||
}}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label="Notifications" className="relative">
|
||||
<Bell className="h-5 w-5" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1 right-1 block h-2 w-2 rounded-full bg-red-500 ring-1 ring-white dark:ring-gray-800" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="p-0 w-80 md:w-96">
|
||||
<NotificationDropdown
|
||||
currentUser={currentUser as User | null} // Cast needed as useHelpers can return undefined initially
|
||||
unreadNotifications={unreadNotifications}
|
||||
displayedReadNotifications={displayedReadNotifications}
|
||||
habitsData={habitsData} // Pass necessary data down
|
||||
wishlistData={wishlistData}
|
||||
usersData={usersData}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
135
components/NotificationDropdown.tsx
Normal file
135
components/NotificationDropdown.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { CoinsData, HabitsData, WishlistData, UserData, User, CoinTransaction } from '@/lib/types';
|
||||
import { t2d } from '@/lib/utils';
|
||||
import Link from 'next/link';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { Info } from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
interface NotificationDropdownProps {
|
||||
currentUser: User | null;
|
||||
unreadNotifications: CoinTransaction[];
|
||||
displayedReadNotifications: CoinTransaction[];
|
||||
habitsData: HabitsData; // Keep needed props
|
||||
wishlistData: WishlistData;
|
||||
usersData: UserData;
|
||||
}
|
||||
|
||||
// Helper function to generate notification message
|
||||
const getNotificationMessage = (tx: CoinTransaction, triggeringUser?: User, relatedItemName?: string): string => {
|
||||
const username = triggeringUser?.username || 'Someone';
|
||||
const itemName = relatedItemName || 'a shared item';
|
||||
switch (tx.type) {
|
||||
case 'HABIT_COMPLETION':
|
||||
case 'TASK_COMPLETION':
|
||||
return `${username} completed ${itemName}.`;
|
||||
case 'WISH_REDEMPTION':
|
||||
return `${username} redeemed ${itemName}.`;
|
||||
// Add other relevant transaction types if needed
|
||||
default:
|
||||
return `Activity related to ${itemName} by ${username}.`; // Fallback message
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get the name of the related item
|
||||
const getRelatedItemName = (tx: CoinTransaction, habitsData: HabitsData, wishlistData: WishlistData): string | undefined => {
|
||||
if (!tx.relatedItemId) return undefined;
|
||||
if (tx.type === 'HABIT_COMPLETION' || tx.type === 'TASK_COMPLETION') {
|
||||
return habitsData.habits.find(h => h.id === tx.relatedItemId)?.name;
|
||||
}
|
||||
if (tx.type === 'WISH_REDEMPTION') {
|
||||
return wishlistData.items.find(w => w.id === tx.relatedItemId)?.name;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
export default function NotificationDropdown({
|
||||
currentUser,
|
||||
unreadNotifications, // Use props directly
|
||||
displayedReadNotifications, // Use props directly
|
||||
habitsData,
|
||||
wishlistData,
|
||||
usersData,
|
||||
}: NotificationDropdownProps) {
|
||||
if (!currentUser) {
|
||||
return <div className="p-4 text-sm text-gray-500">Not logged in.</div>;
|
||||
}
|
||||
|
||||
// Removed the useMemo block for calculating notifications
|
||||
|
||||
const renderNotification = (tx: CoinTransaction, isUnread: boolean) => {
|
||||
const triggeringUser = usersData.users.find(u => u.id === tx.userId);
|
||||
const relatedItemName = getRelatedItemName(tx, habitsData, wishlistData);
|
||||
const message = getNotificationMessage(tx, triggeringUser, relatedItemName);
|
||||
const txTimestamp = t2d({ timestamp: tx.timestamp, timezone: 'UTC' });
|
||||
const timeAgo = txTimestamp.toRelative(); // e.g., "2 hours ago"
|
||||
// Add the triggering user's ID to the query params if it exists
|
||||
const linkHref = `/coins?highlight=${tx.id}${tx.userId ? `&user=${tx.userId}` : ''}`;
|
||||
|
||||
return (
|
||||
// Wrap the Link with DropdownMenuItem and use asChild to pass props
|
||||
<DropdownMenuItem key={tx.id} asChild className={`p-0 focus:bg-inherit dark:focus:bg-inherit cursor-pointer`}>
|
||||
<Link href={linkHref} className={`block hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors ${isUnread ? 'bg-blue-50 dark:bg-blue-900/20' : ''}`} scroll={true}>
|
||||
<div className="p-3 flex items-start gap-3">
|
||||
<Avatar className="h-8 w-8 mt-1">
|
||||
<AvatarImage src={triggeringUser?.avatarPath ? `/api/avatars/${triggeringUser.avatarPath.split('/').pop()}` : undefined} alt={triggeringUser?.username} />
|
||||
<AvatarFallback>{triggeringUser?.username?.charAt(0).toUpperCase() || '?'}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<p className={`text-sm ${isUnread ? 'font-semibold' : ''}`}>{message}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{timeAgo}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
{/* Removed the outer div as width is now set on DropdownMenuContent in NotificationBell */}
|
||||
<>
|
||||
<div className="p-3 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium">Notifications</h4>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Shows completions or redemptions by other users for habits or wishlist that you shared with them (you must be admin)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ScrollArea className="h-[400px]">
|
||||
{unreadNotifications.length === 0 && displayedReadNotifications.length === 0 && (
|
||||
<div className="p-4 text-center text-sm text-gray-500">No notifications yet.</div>
|
||||
)}
|
||||
|
||||
{unreadNotifications.length > 0 && (
|
||||
<>
|
||||
{unreadNotifications.map(tx => renderNotification(tx, true))}
|
||||
{displayedReadNotifications.length > 0 && <Separator className="my-2" />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{displayedReadNotifications.length > 0 && (
|
||||
<>
|
||||
{displayedReadNotifications.map(tx => renderNotification(tx, false))}
|
||||
</>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</> {/* Close the fragment */}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
48
components/ui/scroll-area.tsx
Normal file
48
components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
31
components/ui/separator.tsx
Normal file
31
components/ui/separator.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
Reference in New Issue
Block a user