chore: clean up repo for public release
- Remove BMAD framework, IDE configs, dev screenshots, test files, internal docs, and backup files - Rename keep-notes/ to memento-note/ - Update all references from keep-notes to memento-note - Add Apache 2.0 license with Commons Clause (non-commercial restriction) - Add clean .gitignore and .env.docker.example
This commit is contained in:
203
memento-note/components/notification-panel.tsx
Normal file
203
memento-note/components/notification-panel.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Bell, Check, X, Clock } from 'lucide-react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { getPendingShareRequests, respondToShareRequest } from '@/app/actions/notes'
|
||||
import { toast } from 'sonner'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
interface ShareRequest {
|
||||
id: string
|
||||
status: string
|
||||
permission: string
|
||||
createdAt: Date
|
||||
note: {
|
||||
id: string
|
||||
title: string | null
|
||||
content: string
|
||||
color: string
|
||||
createdAt: Date
|
||||
}
|
||||
sharer: {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string
|
||||
image: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export function NotificationPanel() {
|
||||
const { triggerRefresh } = useNoteRefresh()
|
||||
const { t } = useLanguage()
|
||||
const [requests, setRequests] = useState<ShareRequest[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const loadRequests = useCallback(async () => {
|
||||
try {
|
||||
const data = await getPendingShareRequests()
|
||||
setRequests(data as any)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load share requests:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadRequests()
|
||||
const interval = setInterval(loadRequests, 10000)
|
||||
const onFocus = () => loadRequests()
|
||||
window.addEventListener('focus', onFocus)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
window.removeEventListener('focus', onFocus)
|
||||
}
|
||||
}, [loadRequests])
|
||||
|
||||
const pendingCount = requests.length
|
||||
|
||||
const handleAccept = async (shareId: string) => {
|
||||
try {
|
||||
await respondToShareRequest(shareId, 'accept')
|
||||
setRequests(prev => prev.filter(r => r.id !== shareId))
|
||||
toast.success(t('notification.accepted'), {
|
||||
description: t('collaboration.nowHasAccess', { name: 'Note' }),
|
||||
duration: 3000,
|
||||
})
|
||||
triggerRefresh()
|
||||
setOpen(false)
|
||||
} catch (error: any) {
|
||||
console.error('[NOTIFICATION] Error:', error)
|
||||
toast.error(error.message || t('general.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDecline = async (shareId: string) => {
|
||||
try {
|
||||
await respondToShareRequest(shareId, 'decline')
|
||||
setRequests(prev => prev.filter(r => r.id !== shareId))
|
||||
toast.info(t('notification.declined'))
|
||||
if (requests.length <= 1) setOpen(false)
|
||||
} catch (error: any) {
|
||||
console.error('[NOTIFICATION] Error:', error)
|
||||
toast.error(error.message || t('general.error'))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="relative h-9 w-9 p-0 hover:bg-accent/50 transition-all duration-200"
|
||||
>
|
||||
<Bell className="h-4 w-4 transition-transform duration-200 hover:scale-110" />
|
||||
{pendingCount > 0 && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs animate-pulse shadow-lg"
|
||||
>
|
||||
{pendingCount > 9 ? '9+' : pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-80 p-0">
|
||||
<div className="px-4 py-3 border-b bg-gradient-to-r from-primary/5 to-primary/10 dark:from-primary/10 dark:to-primary/15">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="h-4 w-4 text-primary dark:text-primary-foreground" />
|
||||
<span className="font-semibold text-sm">{t('notification.notifications')}</span>
|
||||
</div>
|
||||
{pendingCount > 0 && (
|
||||
<Badge className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-md">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full mx-auto mb-2" />
|
||||
</div>
|
||||
) : requests.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||
<Bell className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||
<p className="font-medium">{t('notification.noNotifications') || 'No new notifications'}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{requests.map((request) => (
|
||||
<div
|
||||
key={request.id}
|
||||
className="p-4 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-xs shadow-md shrink-0">
|
||||
{(request.sharer.name || request.sharer.email)[0].toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold truncate">
|
||||
{request.sharer.name || request.sharer.email}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
{t('notification.shared', { title: request.note.title || t('notification.untitled') })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={() => handleDecline(request.id)}
|
||||
className={cn(
|
||||
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
|
||||
"border border-border bg-background",
|
||||
"text-muted-foreground",
|
||||
"hover:bg-muted hover:text-foreground",
|
||||
"transition-all duration-200",
|
||||
"flex items-center justify-center gap-1.5",
|
||||
"active:scale-95"
|
||||
)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
{t('notification.decline') || t('general.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAccept(request.id)}
|
||||
className={cn(
|
||||
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
|
||||
"bg-primary text-primary-foreground",
|
||||
"hover:bg-primary/90",
|
||||
"shadow-sm hover:shadow",
|
||||
"transition-all duration-200",
|
||||
"flex items-center justify-center gap-1.5",
|
||||
"active:scale-95"
|
||||
)}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
{t('notification.accept') || t('general.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 mt-3 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{new Date(request.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user