refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client

This commit is contained in:
Sepehr Ramezani
2026-04-19 19:21:27 +02:00
parent 5296c4da2c
commit 25529a24b8
2476 changed files with 127934 additions and 101962 deletions

View File

@@ -1,15 +1,15 @@
'use client'
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Bell, Check, X, Clock, User } from 'lucide-react'
import { Bell, Check, X, Clock } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { getPendingShareRequests, respondToShareRequest, removeSharedNoteFromView } from '@/app/actions/notes'
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'
@@ -40,37 +40,40 @@ export function NotificationPanel() {
const { t } = useLanguage()
const [requests, setRequests] = useState<ShareRequest[]>([])
const [isLoading, setIsLoading] = useState(false)
const [pendingCount, setPendingCount] = useState(0)
const [open, setOpen] = useState(false)
const loadRequests = async () => {
setIsLoading(true)
const loadRequests = useCallback(async () => {
try {
const data = await getPendingShareRequests()
setRequests(data)
setPendingCount(data.length)
setRequests(data as any)
} catch (error: any) {
console.error('Failed to load share requests:', error)
} finally {
setIsLoading(false)
}
}
}, [])
useEffect(() => {
loadRequests()
const interval = setInterval(loadRequests, 30000)
return () => clearInterval(interval)
}, [])
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))
setPendingCount(prev => prev - 1)
triggerRefresh()
toast.success(t('notes.noteCreated'), {
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'))
@@ -81,27 +84,17 @@ export function NotificationPanel() {
try {
await respondToShareRequest(shareId, 'decline')
setRequests(prev => prev.filter(r => r.id !== shareId))
setPendingCount(prev => prev - 1)
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'))
}
}
const handleRemove = async (shareId: string) => {
try {
await removeSharedNoteFromView(shareId)
setRequests(prev => prev.filter(r => r.id !== shareId))
toast.info(t('notification.removed'))
} catch (error: any) {
toast.error(error.message || t('general.error'))
}
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
@@ -117,8 +110,8 @@ export function NotificationPanel() {
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
</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">
@@ -136,12 +129,11 @@ export function NotificationPanel() {
{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" />
{t('general.loading')}
</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('search.noResults')}</p>
<p className="font-medium">{t('notification.noNotifications') || 'No new notifications'}</p>
</div>
) : (
<div className="max-h-96 overflow-y-auto">
@@ -151,7 +143,7 @@ export function NotificationPanel() {
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">
<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">
@@ -162,63 +154,50 @@ export function NotificationPanel() {
{t('notification.shared', { title: request.note.title || t('notification.untitled') })}
</p>
</div>
<Badge
variant="secondary"
className="text-xs capitalize bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-foreground border-0"
>
{request.permission}
</Badge>
</div>
<div className="flex gap-2 mt-3">
<button
onClick={() => handleAccept(request.id)}
className={cn(
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
"bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700",
"text-white shadow-md hover:shadow-lg",
"transition-all duration-200",
"flex items-center justify-center gap-1.5",
"active:scale-95"
)}
>
<Check className="h-3.5 w-3.5" />
{t('general.confirm')}
</button>
<button
onClick={() => handleDecline(request.id)}
className={cn(
"flex-1 h-9 px-4 text-xs font-semibold rounded-lg",
"bg-white dark:bg-gray-800",
"border-2 border-gray-200 dark:border-gray-700",
"text-gray-700 dark:text-gray-300",
"hover:bg-gray-50 dark:hover:bg-gray-700",
"hover:border-gray-300 dark:hover:border-gray-600",
"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('general.cancel')}
{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>
<button
onClick={() => handleRemove(request.id)}
className="ml-auto text-muted-foreground hover:text-foreground transition-colors duration-150"
>
{t('general.close')}
</button>
</div>
</div>
))}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
</PopoverContent>
</Popover>
)
}