From 3cc50aacbc3abb10d94c7c728b675f8bc43d20fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pol=20Sintes=20Gonz=C3=A1lez?= Date: Sat, 13 Dec 2025 20:15:43 +0100 Subject: [PATCH 1/3] feat: add authentication system with login/register/password reset --- components/Auth.tsx | 325 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 components/Auth.tsx diff --git a/components/Auth.tsx b/components/Auth.tsx new file mode 100644 index 00000000..a1b526b1 --- /dev/null +++ b/components/Auth.tsx @@ -0,0 +1,325 @@ +import React, { useState } from 'react'; +import { Mail, Lock, User, ArrowRight, Loader2, Eye, EyeOff, ArrowLeft } from 'lucide-react'; + +type AuthView = 'login' | 'register' | 'forgot-password' | 'reset-sent'; + +const Auth: React.FC = () => { + const [authView, setAuthView] = useState('login'); + const [isLoading, setIsLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '' + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + if (authView === 'forgot-password') { + setAuthView('reset-sent'); + } + }, 1500); + }; + + const renderLogin = () => ( +
+
+

Welcome Back

+

Sign in to continue to StellarMail

+
+ +
+
+
+ +
+
+ +
+ setFormData({ ...formData, email: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="alex@stellar.io" + required + /> +
+
+ +
+ +
+
+ +
+ setFormData({ ...formData, password: e.target.value })} + className="w-full pl-10 pr-12 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="••••••••" + required + /> + +
+
+ +
+ + +
+ + +
+ +
+ Don't have an account?{' '} + +
+
+
+ ); + + const renderRegister = () => ( +
+
+

Create Account

+

Start your email marketing journey

+
+ +
+
+
+ +
+
+ +
+ setFormData({ ...formData, name: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="Alex Morgan" + required + /> +
+
+ +
+ +
+
+ +
+ setFormData({ ...formData, email: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="alex@stellar.io" + required + /> +
+
+ +
+ +
+
+ +
+ setFormData({ ...formData, password: e.target.value })} + className="w-full pl-10 pr-12 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="••••••••" + required + /> + +
+

Must be at least 8 characters

+
+ +
+ +
+
+ +
+ setFormData({ ...formData, confirmPassword: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="••••••••" + required + /> +
+
+ + +
+ +
+ Already have an account?{' '} + +
+
+
+ ); + + const renderForgotPassword = () => ( +
+ + +
+

Forgot Password?

+

We'll send you a reset link

+
+ +
+
+
+ +
+
+ +
+ setFormData({ ...formData, email: e.target.value })} + className="w-full pl-10 pr-4 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" + placeholder="alex@stellar.io" + required + /> +
+
+ + +
+
+
+ ); + + const renderResetSent = () => ( +
+
+ +
+

Check Your Email

+

+ We've sent a password reset link to {formData.email} +

+ +
+ ); + + return ( +
+ {authView === 'login' && renderLogin()} + {authView === 'register' && renderRegister()} + {authView === 'forgot-password' && renderForgotPassword()} + {authView === 'reset-sent' && renderResetSent()} +
+ ); +}; + +export default Auth; \ No newline at end of file From 7916147b4384334e27065a67dd31a077f246f6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pol=20Sintes=20Gonz=C3=A1lez?= Date: Sat, 13 Dec 2025 20:16:43 +0100 Subject: [PATCH 2/3] feat: add link management with URL shortener and analytics --- components/Links.tsx | 710 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 710 insertions(+) create mode 100644 components/Links.tsx diff --git a/components/Links.tsx b/components/Links.tsx new file mode 100644 index 00000000..4a824bae --- /dev/null +++ b/components/Links.tsx @@ -0,0 +1,710 @@ +"use client"; + +import React, { useState, useMemo } from 'react'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + AreaChart, + Area, + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend, +} from 'recharts'; +import { + Link as LinkIcon, + Plus, + Copy, + ExternalLink, + BarChart3, + Search, + Filter, + Eye, + EyeOff, + Trash2, + Edit, + TrendingUp, + Calendar, + MousePointerClick, +} from 'lucide-react'; +import { toast } from 'sonner'; + +interface LinkData { + id: string; + originalUrl: string; + shortUrl: string; + shortCode: string; + title: string; + createdAt: Date; + expiresAt?: Date; + clicks: number; + status: 'active' | 'inactive' | 'expired'; + tags: string[]; + clickData: { + date: string; + clicks: number; + uniqueVisitors: number; + }[]; +} + +const Links = () => { + const [links, setLinks] = useState([ + { + id: '1', + originalUrl: 'https://example.com/very-long-url-that-needs-shortening', + shortUrl: 'https://stml.ink/abc123', + shortCode: 'abc123', + title: 'Product Launch Page', + createdAt: new Date('2025-12-01'), + clicks: 1247, + status: 'active', + tags: ['marketing', 'product'], + clickData: [ + { date: '2025-12-07', clicks: 45, uniqueVisitors: 38 }, + { date: '2025-12-08', clicks: 67, uniqueVisitors: 52 }, + { date: '2025-12-09', clicks: 89, uniqueVisitors: 71 }, + { date: '2025-12-10', clicks: 123, uniqueVisitors: 98 }, + { date: '2025-12-11', clicks: 156, uniqueVisitors: 124 }, + { date: '2025-12-12', clicks: 178, uniqueVisitors: 142 }, + { date: '2025-12-13', clicks: 92, uniqueVisitors: 74 }, + ], + }, + { + id: '2', + originalUrl: 'https://example.com/blog/how-to-use-stellarmail', + shortUrl: 'https://stml.ink/xyz789', + shortCode: 'xyz789', + title: 'Blog: Getting Started', + createdAt: new Date('2025-12-05'), + clicks: 834, + status: 'active', + tags: ['blog', 'tutorial'], + clickData: [ + { date: '2025-12-07', clicks: 32, uniqueVisitors: 28 }, + { date: '2025-12-08', clicks: 41, uniqueVisitors: 35 }, + { date: '2025-12-09', clicks: 56, uniqueVisitors: 47 }, + { date: '2025-12-10', clicks: 67, uniqueVisitors: 54 }, + { date: '2025-12-11', clicks: 78, uniqueVisitors: 63 }, + { date: '2025-12-12', clicks: 89, uniqueVisitors: 72 }, + { date: '2025-12-13', clicks: 45, uniqueVisitors: 38 }, + ], + }, + { + id: '3', + originalUrl: 'https://example.com/campaign/black-friday-2025', + shortUrl: 'https://stml.ink/bf2025', + shortCode: 'bf2025', + title: 'Black Friday Campaign', + createdAt: new Date('2025-11-15'), + expiresAt: new Date('2025-11-30'), + clicks: 5621, + status: 'expired', + tags: ['campaign', 'sales'], + clickData: [], + }, + ]); + + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [selectedLink, setSelectedLink] = useState(null); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isAnalyticsDialogOpen, setIsAnalyticsDialogOpen] = useState(false); + + // New link form state + const [newLink, setNewLink] = useState({ + originalUrl: '', + title: '', + customCode: '', + tags: '', + }); + + // Filter and search links + const filteredLinks = useMemo(() => { + return links.filter((link) => { + const matchesSearch = + link.title.toLowerCase().includes(searchQuery.toLowerCase()) || + link.shortCode.toLowerCase().includes(searchQuery.toLowerCase()) || + link.originalUrl.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesStatus = + statusFilter === 'all' || link.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + }, [links, searchQuery, statusFilter]); + + // Calculate total stats + const totalStats = useMemo(() => { + const activeLinks = links.filter((l) => l.status === 'active').length; + const totalClicks = links.reduce((sum, l) => sum + l.clicks, 0); + const avgClicks = links.length > 0 ? Math.round(totalClicks / links.length) : 0; + + return { activeLinks, totalClicks, avgClicks }; + }, [links]); + + // Handle create new link + const handleCreateLink = () => { + if (!newLink.originalUrl || !newLink.title) { + toast.error('Please fill in all required fields'); + return; + } + + const shortCode = + newLink.customCode || Math.random().toString(36).substring(2, 8); + + const link: LinkData = { + id: Date.now().toString(), + originalUrl: newLink.originalUrl, + shortUrl: `https://stml.ink/${shortCode}`, + shortCode, + title: newLink.title, + createdAt: new Date(), + clicks: 0, + status: 'active', + tags: newLink.tags.split(',').map((t) => t.trim()).filter(Boolean), + clickData: [], + }; + + setLinks([link, ...links]); + setNewLink({ originalUrl: '', title: '', customCode: '', tags: '' }); + setIsCreateDialogOpen(false); + toast.success('Link created successfully!'); + }; + + // Handle copy link + const handleCopyLink = (shortUrl: string) => { + navigator.clipboard.writeText(shortUrl); + toast.success('Link copied to clipboard!'); + }; + + // Handle toggle link status + const handleToggleStatus = (id: string) => { + setLinks( + links.map((link) => { + if (link.id === id) { + const newStatus = + link.status === 'active' ? 'inactive' : 'active'; + toast.success( + `Link ${newStatus === 'active' ? 'activated' : 'deactivated'}` + ); + return { ...link, status: newStatus }; + } + return link; + }) + ); + }; + + // Handle delete link + const handleDeleteLink = (id: string) => { + setLinks(links.filter((link) => link.id !== id)); + toast.success('Link deleted successfully'); + }; + + // Handle view analytics + const handleViewAnalytics = (link: LinkData) => { + setSelectedLink(link); + setIsAnalyticsDialogOpen(true); + }; + + return ( +
+ {/* Header */} +
+
+

Link Management

+

+ Create and manage shortened URLs with analytics +

+
+ + + + + + + Create Short Link + + Create a shortened URL with custom tracking and analytics + + +
+
+ + + setNewLink({ ...newLink, title: e.target.value }) + } + /> +
+
+ + + setNewLink({ ...newLink, originalUrl: e.target.value }) + } + /> +
+
+ +
+ + stml.ink/ + + + setNewLink({ ...newLink, customCode: e.target.value }) + } + /> +
+
+
+ + + setNewLink({ ...newLink, tags: e.target.value }) + } + /> +
+
+ + + + +
+
+
+ + {/* Stats Cards */} +
+ + + + Active Links + + + + +
{totalStats.activeLinks}
+

+ {links.length} total links +

+
+
+ + + Total Clicks + + + +
+ {totalStats.totalClicks.toLocaleString()} +
+

+ Across all links +

+
+
+ + + + Avg. Clicks/Link + + + + +
{totalStats.avgClicks}
+

+ Performance metric +

+
+
+
+ + {/* Search and Filter */} + + + Your Links + + Manage and track your shortened URLs + + + +
+
+ + setSearchQuery(e.target.value)} + /> +
+ +
+ + {/* Links Table */} +
+ + + + Title + Short URL + Clicks + Status + Created + Actions + + + + {filteredLinks.length === 0 ? ( + + + No links found + + + ) : ( + filteredLinks.map((link) => ( + + +
+
{link.title}
+
+ {link.originalUrl} +
+ {link.tags.length > 0 && ( +
+ {link.tags.map((tag, i) => ( + + {tag} + + ))} +
+ )} +
+
+ +
+ + {link.shortCode} + + +
+
+ +
+ {link.clicks.toLocaleString()} +
+
+ + + {link.status} + + + +
+ {link.createdAt.toLocaleDateString()} +
+
+ +
+ + + +
+
+
+ )) + )} +
+
+
+
+
+ + {/* Analytics Dialog */} + + + + Link Analytics + + {selectedLink?.title} - {selectedLink?.shortUrl} + + + {selectedLink && ( +
+ {/* Quick Stats */} +
+ + + + Total Clicks + + + + +
+ {selectedLink.clicks.toLocaleString()} +
+
+
+ + + + Status + + + + + + {selectedLink.status} + + + + + + + Created + + + + +
+ {selectedLink.createdAt.toLocaleDateString()} +
+
+
+
+ + {/* Charts */} + {selectedLink.clickData.length > 0 && ( + + + Click Trends + Unique Visitors + + + + + Clicks Over Time + + Daily click performance + + + + + + + + + + + + + + + + + + + + Unique Visitors + + Daily unique visitor count + + + + + + + + + + + + + + + + + + )} + + {/* Link Details */} + + + Link Details + + +
+ Short URL: +
+ {selectedLink.shortUrl} + +
+
+
+ + Original URL: + + + View + + +
+ {selectedLink.tags.length > 0 && ( +
+ Tags: +
+ {selectedLink.tags.map((tag, i) => ( + + {tag} + + ))} +
+
+ )} +
+
+
+ )} +
+
+
+ ); +}; + +export default Links; \ No newline at end of file From eb297b6924bed06554bcfe06b2a81ba63b023093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pol=20Sintes=20Gonz=C3=A1lez?= Date: Sat, 13 Dec 2025 20:33:08 +0100 Subject: [PATCH 3/3] feat: add chat/messaging system with conversations and real-time interface --- components/Chat.tsx | 325 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 components/Chat.tsx diff --git a/components/Chat.tsx b/components/Chat.tsx new file mode 100644 index 00000000..fde4f02a --- /dev/null +++ b/components/Chat.tsx @@ -0,0 +1,325 @@ +import React, { useState } from 'react'; +import { Search, Send, Phone, Video, MoreVertical, Paperclip, Smile, Check, CheckCheck } from 'lucide-react'; + +interface Message { + id: string; + text: string; + sender: 'user' | 'contact'; + timestamp: string; + read: boolean; +} + +interface Conversation { + id: string; + name: string; + avatar: string; + lastMessage: string; + timestamp: string; + unreadCount: number; + online: boolean; +} + +const Chat: React.FC = () => { + const [selectedConversation, setSelectedConversation] = useState('1'); + const [messageInput, setMessageInput] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + + const conversations: Conversation[] = [ + { + id: '1', + name: 'Alice Johnson', + avatar: 'AJ', + lastMessage: 'Hey, how are you doing?', + timestamp: '2m ago', + unreadCount: 2, + online: true, + }, + { + id: '2', + name: 'Bob Smith', + avatar: 'BS', + lastMessage: 'Did you see the latest update?', + timestamp: '15m ago', + unreadCount: 0, + online: true, + }, + { + id: '3', + name: 'Carol Williams', + avatar: 'CW', + lastMessage: 'Thanks for your help!', + timestamp: '1h ago', + unreadCount: 5, + online: false, + }, + { + id: '4', + name: 'David Brown', + avatar: 'DB', + lastMessage: 'Let\'s schedule a meeting', + timestamp: '3h ago', + unreadCount: 0, + online: false, + }, + { + id: '5', + name: 'Emma Davis', + avatar: 'ED', + lastMessage: 'Great work on the project!', + timestamp: '1d ago', + unreadCount: 1, + online: true, + }, + ]; + + const messages: Message[] = [ + { + id: '1', + text: 'Hey! How\'s the project going?', + sender: 'contact', + timestamp: '10:30 AM', + read: true, + }, + { + id: '2', + text: 'It\'s going really well! Just finished the main features.', + sender: 'user', + timestamp: '10:32 AM', + read: true, + }, + { + id: '3', + text: 'That\'s awesome! Can you show me a demo?', + sender: 'contact', + timestamp: '10:33 AM', + read: true, + }, + { + id: '4', + text: 'Sure! I\'ll prepare something for tomorrow.', + sender: 'user', + timestamp: '10:35 AM', + read: true, + }, + { + id: '5', + text: 'Perfect! Looking forward to it.', + sender: 'contact', + timestamp: '10:36 AM', + read: true, + }, + { + id: '6', + text: 'Hey, how are you doing?', + sender: 'contact', + timestamp: '2m ago', + read: false, + }, + ]; + + const handleSendMessage = () => { + if (messageInput.trim()) { + // Handle sending message + console.log('Sending message:', messageInput); + setMessageInput(''); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const filteredConversations = conversations.filter((conv) => + conv.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const selectedConv = conversations.find((c) => c.id === selectedConversation); + + return ( +
+ {/* Sidebar - Conversations List */} +
+ {/* Header */} +
+

Messages

+ + {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full bg-gray-800 text-white pl-10 pr-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#2dd4bf]" + /> +
+
+ + {/* Conversations List */} +
+ {filteredConversations.map((conversation) => ( +
setSelectedConversation(conversation.id)} + className={`p-4 cursor-pointer hover:bg-gray-800 transition-colors ${ + selectedConversation === conversation.id ? 'bg-gray-800' : '' + }`} + > +
+ {/* Avatar with Online Indicator */} +
+
+ {conversation.avatar} +
+ {conversation.online && ( +
+ )} +
+ + {/* Conversation Info */} +
+
+

{conversation.name}

+ {conversation.timestamp} +
+
+

{conversation.lastMessage}

+ {conversation.unreadCount > 0 && ( + + {conversation.unreadCount} + + )} +
+
+
+
+ ))} +
+
+ + {/* Main Chat Area */} +
+ {selectedConv ? ( + <> + {/* Chat Header */} +
+
+
+
+ {selectedConv.avatar} +
+ {selectedConv.online && ( +
+ )} +
+
+

{selectedConv.name}

+

+ {selectedConv.online ? 'Online' : 'Offline'} +

+
+
+ + {/* Action Buttons */} +
+ + + +
+
+ + {/* Messages Area */} +
+ {messages.map((message) => ( +
+
+

{message.text}

+
+ + {message.timestamp} + + {message.sender === 'user' && ( + <> + {message.read ? ( + + ) : ( + + )} + + )} +
+
+
+ ))} +
+ + {/* Message Input */} +
+
+ + +
+