Reorganize repository into monorepo structure

Restructured the project to support multiple frontend applications:
- Move web app to web/ directory
- Create pezkuwi-sdk-ui/ for Polkadot SDK clone (planned)
- Create mobile/ directory for mobile app development
- Add shared/ directory with common utilities, types, and blockchain code
- Update README.md with comprehensive documentation
- Remove obsolete DKSweb/ directory

This monorepo structure enables better code sharing and organized
development across web, mobile, and SDK UI projects.
This commit is contained in:
Claude
2025-11-14 00:46:35 +00:00
parent bb3d9aeb29
commit c48ded7ff2
206 changed files with 502 additions and 4 deletions
@@ -0,0 +1,206 @@
import { useState, useEffect } from 'react';
import { Bell, Check, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useAuth } from '@/contexts/AuthContext';
import { supabase } from '@/lib/supabase';
import { formatDistanceToNow } from 'date-fns';
interface Notification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error' | 'system';
read: boolean;
action_url?: string;
created_at: string;
}
export default function NotificationBell() {
const { user } = useAuth();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [open, setOpen] = useState(false);
useEffect(() => {
if (user) {
loadNotifications();
subscribeToNotifications();
}
}, [user]);
const loadNotifications = async () => {
if (!user) return;
const { data } = await supabase
.from('notifications')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(10);
if (data) {
setNotifications(data);
setUnreadCount(data.filter(n => !n.read).length);
}
};
const subscribeToNotifications = () => {
const channel = supabase
.channel('notifications')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${user?.id}`,
},
(payload) => {
setNotifications(prev => [payload.new as Notification, ...prev]);
setUnreadCount(prev => prev + 1);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
};
const markAsRead = async (notificationId: string) => {
await supabase.functions.invoke('notifications-manager', {
body: {
action: 'markRead',
userId: user?.id,
notificationId
}
});
setNotifications(prev =>
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
};
const markAllAsRead = async () => {
await supabase.functions.invoke('notifications-manager', {
body: {
action: 'markAllRead',
userId: user?.id
}
});
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
};
const deleteNotification = async (notificationId: string) => {
await supabase.functions.invoke('notifications-manager', {
body: {
action: 'delete',
userId: user?.id,
notificationId
}
});
setNotifications(prev => prev.filter(n => n.id !== notificationId));
};
const getTypeColor = (type: string) => {
switch (type) {
case 'success': return 'text-green-600';
case 'warning': return 'text-yellow-600';
case 'error': return 'text-red-600';
case 'system': return 'text-blue-600';
default: return 'text-gray-600';
}
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center">
{unreadCount}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="flex items-center justify-between p-4 border-b">
<h3 className="font-semibold">Notifications</h3>
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={markAllAsRead}
>
Mark all read
</Button>
)}
</div>
<ScrollArea className="h-96">
{notifications.length === 0 ? (
<div className="p-4 text-center text-muted-foreground">
No notifications
</div>
) : (
<div className="divide-y">
{notifications.map((notification) => (
<div
key={notification.id}
className={`p-4 hover:bg-muted/50 transition-colors ${
!notification.read ? 'bg-muted/20' : ''
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className={`font-medium ${getTypeColor(notification.type)}`}>
{notification.title}
</p>
<p className="text-sm text-muted-foreground mt-1">
{notification.message}
</p>
<p className="text-xs text-muted-foreground mt-2">
{formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })}
</p>
</div>
<div className="flex items-center gap-1 ml-2">
{!notification.read && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => markAsRead(notification.id)}
>
<Check className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => deleteNotification(notification.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)}
</ScrollArea>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,265 @@
import React, { useState, useEffect } from 'react';
import { Bell, MessageCircle, AtSign, Heart, Award, TrendingUp, X, Check, Settings } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { useWebSocket } from '@/contexts/WebSocketContext';
import { useToast } from '@/hooks/use-toast';
import { useTranslation } from 'react-i18next';
interface Notification {
id: string;
type: 'mention' | 'reply' | 'vote' | 'badge' | 'proposal';
title: string;
message: string;
timestamp: Date;
read: boolean;
actionUrl?: string;
sender?: {
name: string;
avatar: string;
};
}
export const NotificationCenter: React.FC = () => {
const { t } = useTranslation();
const { subscribe, unsubscribe } = useWebSocket();
const { toast } = useToast();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const [settings, setSettings] = useState({
mentions: true,
replies: true,
votes: true,
badges: true,
proposals: true,
pushEnabled: false,
});
useEffect(() => {
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Subscribe to WebSocket events
const handleMention = (data: any) => {
const notification: Notification = {
id: Date.now().toString(),
type: 'mention',
title: 'You were mentioned',
message: `${data.sender} mentioned you in a discussion`,
timestamp: new Date(),
read: false,
actionUrl: data.url,
sender: data.senderInfo,
};
addNotification(notification);
};
const handleReply = (data: any) => {
const notification: Notification = {
id: Date.now().toString(),
type: 'reply',
title: 'New reply',
message: `${data.sender} replied to your comment`,
timestamp: new Date(),
read: false,
actionUrl: data.url,
sender: data.senderInfo,
};
addNotification(notification);
};
subscribe('mention', handleMention);
subscribe('reply', handleReply);
return () => {
unsubscribe('mention', handleMention);
unsubscribe('reply', handleReply);
};
}, [subscribe, unsubscribe]);
const addNotification = (notification: Notification) => {
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Show toast
toast({
title: notification.title,
description: notification.message,
});
// Show push notification if enabled
if (settings.pushEnabled && 'Notification' in window && Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message,
icon: '/logo.png',
});
}
};
const markAsRead = (id: string) => {
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, read: true } : n)
);
setUnreadCount(prev => Math.max(0, prev - 1));
};
const markAllAsRead = () => {
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
setUnreadCount(0);
};
const getIcon = (type: string) => {
switch (type) {
case 'mention': return <AtSign className="h-4 w-4" />;
case 'reply': return <MessageCircle className="h-4 w-4" />;
case 'vote': return <Heart className="h-4 w-4" />;
case 'badge': return <Award className="h-4 w-4" />;
case 'proposal': return <TrendingUp className="h-4 w-4" />;
default: return <Bell className="h-4 w-4" />;
}
};
return (
<div className="relative">
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(!isOpen)}
className="relative"
>
<Bell className="h-5 w-5" />
{unreadCount > 0 && (
<Badge className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center">
{unreadCount}
</Badge>
)}
</Button>
{isOpen && (
<Card className="absolute right-0 top-12 w-96 z-50">
<Tabs defaultValue="all" className="w-full">
<div className="flex items-center justify-between p-4 border-b">
<TabsList>
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="unread">Unread</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<Button variant="ghost" size="icon" onClick={() => setIsOpen(false)}>
<X className="h-4 w-4" />
</Button>
</div>
<TabsContent value="all" className="p-0">
<div className="flex items-center justify-between px-4 py-2 border-b">
<span className="text-sm text-muted-foreground">
{notifications.length} notifications
</span>
<Button variant="ghost" size="sm" onClick={markAllAsRead}>
<Check className="h-3 w-3 mr-1" />
Mark all read
</Button>
</div>
<ScrollArea className="h-96">
{notifications.map(notification => (
<div
key={notification.id}
className={`p-4 border-b hover:bg-accent cursor-pointer ${
!notification.read ? 'bg-accent/50' : ''
}`}
onClick={() => markAsRead(notification.id)}
>
<div className="flex items-start gap-3">
<div className="p-2 rounded-full bg-primary/10">
{getIcon(notification.type)}
</div>
<div className="flex-1">
<p className="font-medium text-sm">{notification.title}</p>
<p className="text-sm text-muted-foreground mt-1">
{notification.message}
</p>
<p className="text-xs text-muted-foreground mt-2">
{new Date(notification.timestamp).toLocaleTimeString()}
</p>
</div>
</div>
</div>
))}
</ScrollArea>
</TabsContent>
<TabsContent value="unread" className="p-0">
<ScrollArea className="h-96">
{notifications.filter(n => !n.read).map(notification => (
<div
key={notification.id}
className="p-4 border-b hover:bg-accent cursor-pointer bg-accent/50"
onClick={() => markAsRead(notification.id)}
>
<div className="flex items-start gap-3">
<div className="p-2 rounded-full bg-primary/10">
{getIcon(notification.type)}
</div>
<div className="flex-1">
<p className="font-medium text-sm">{notification.title}</p>
<p className="text-sm text-muted-foreground mt-1">
{notification.message}
</p>
</div>
</div>
</div>
))}
</ScrollArea>
</TabsContent>
<TabsContent value="settings" className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="push">Push Notifications</Label>
<Switch
id="push"
checked={settings.pushEnabled}
onCheckedChange={(checked) => {
if (checked && 'Notification' in window) {
Notification.requestPermission().then(permission => {
setSettings(prev => ({ ...prev, pushEnabled: permission === 'granted' }));
});
} else {
setSettings(prev => ({ ...prev, pushEnabled: checked }));
}
}}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="mentions">Mentions</Label>
<Switch
id="mentions"
checked={settings.mentions}
onCheckedChange={(checked) =>
setSettings(prev => ({ ...prev, mentions: checked }))}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="replies">Replies</Label>
<Switch
id="replies"
checked={settings.replies}
onCheckedChange={(checked) =>
setSettings(prev => ({ ...prev, replies: checked }))}
/>
</div>
</div>
</TabsContent>
</Tabs>
</Card>
)}
</div>
);
};