diff --git a/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx b/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx new file mode 100644 index 00000000..0e3fbda4 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { Suspense, useRef, useMemo, useState, useEffect } from 'react'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { Float, MeshTransmissionMaterial, Environment } from '@react-three/drei'; +import * as THREE from 'three'; +import { clsx } from 'clsx'; + +function useIsMobile() { + const [mobile, setMobile] = useState(false); + useEffect(() => { + const check = () => setMobile(window.innerWidth < 768); + check(); + window.addEventListener('resize', check); + return () => window.removeEventListener('resize', check); + }, []); + return mobile; +} + +function HandShape({ position, rotation, color }: { + position: [number, number, number]; + rotation: [number, number, number]; + color: string; +}) { + const group = useRef(null); + + return ( + + {/* Palm */} + + + + + {/* Fingers - four cylinders */} + {[0, 1, 2, 3].map((i) => ( + + + + + ))} + {/* Thumb */} + + + + + + ); +} + +function GlowSphere() { + const ref = useRef(null); + + useFrame(({ clock }) => { + if (!ref.current) return; + const s = 1 + Math.sin(clock.elapsedTime * 2) * 0.15; + ref.current.scale.setScalar(s); + }); + + return ( + + + + + ); +} + +function Particles({ count }: { count: number }) { + const ref = useRef(null); + + const positions = useMemo(() => { + const arr = new Float32Array(count * 3); + for (let i = 0; i < count; i++) { + arr[i * 3] = (Math.random() - 0.5) * 3; + arr[i * 3 + 1] = (Math.random() - 0.5) * 3; + arr[i * 3 + 2] = (Math.random() - 0.5) * 3; + } + return arr; + }, [count]); + + useFrame(({ clock }) => { + if (!ref.current) return; + ref.current.rotation.y = clock.elapsedTime * 0.05; + ref.current.rotation.x = Math.sin(clock.elapsedTime * 0.03) * 0.1; + }); + + return ( + + + + + + + ); +} + +function HandshakeScene({ isMobile }: { isMobile: boolean }) { + const groupRef = useRef(null); + const mouse = useRef({ x: 0, y: 0 }); + + const { viewport } = useThree(); + + useEffect(() => { + const handle = (e: MouseEvent) => { + mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse.current.y = -(e.clientY / window.innerHeight) * 2 + 1; + }; + window.addEventListener('mousemove', handle); + return () => window.removeEventListener('mousemove', handle); + }, []); + + useFrame(({ clock }) => { + if (!groupRef.current) return; + groupRef.current.rotation.y = clock.elapsedTime * 0.15 + mouse.current.x * 0.3; + groupRef.current.rotation.x = Math.sin(clock.elapsedTime * 0.2) * 0.05 + mouse.current.y * 0.15; + }); + + return ( + + {/* Left hand reaching right */} + + {/* Right hand reaching left */} + + {/* Glow at handshake point */} + + {/* Particles */} + + + ); +} + +function LoadingShimmer() { + return ( +
+
+
+
+
+
+
+ ); +} + +interface DealixLogo3DProps { + size?: number; + className?: string; +} + +function DealixLogo3D({ size = 300, className }: DealixLogo3DProps) { + const isMobile = useIsMobile(); + + return ( +
+ }> + + + + + + + + + + + {/* Ambient glow behind the canvas */} +
+
+ ); +} + +export { DealixLogo3D }; +export type { DealixLogo3DProps }; diff --git a/salesflow-saas/frontend/src/components/dealix/lead-score-card.tsx b/salesflow-saas/frontend/src/components/dealix/lead-score-card.tsx new file mode 100644 index 00000000..48ae70ee --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/lead-score-card.tsx @@ -0,0 +1,286 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion, useMotionValue, useTransform, animate } from "framer-motion"; +import { TrendingUp, Sparkles, UserCheck, MousePointerClick, Target } from "lucide-react"; + +/* ───────────── types ───────────── */ +interface BreakdownItem { + key: string; + label: string; + value: number; // 0-25 + icon: typeof TrendingUp; +} + +interface LeadScoreData { + score: number; // 0-100 + breakdown: BreakdownItem[]; + recommendation: string; +} + +/* ───────────── helpers ───────────── */ +function getGrade(score: number): string { + if (score >= 90) return "A+"; + if (score >= 80) return "A"; + if (score >= 70) return "B"; + if (score >= 55) return "C"; + if (score >= 40) return "D"; + return "F"; +} + +function getScoreColor(score: number): string { + if (score >= 75) return "#10b981"; // green + if (score >= 50) return "#eab308"; // yellow + return "#ef4444"; // red +} + +function getGradientId(score: number): string { + return `score-gradient-${score}`; +} + +/* ───────────── sample data ───────────── */ +const sampleData: LeadScoreData = { + score: 78, + breakdown: [ + { key: "engagement", label: "التفاعل", value: 22, icon: MousePointerClick }, + { key: "profile", label: "الملف الشخصي", value: 18, icon: UserCheck }, + { key: "behavior", label: "السلوك", value: 20, icon: TrendingUp }, + { key: "intent", label: "نية الشراء", value: 18, icon: Target }, + ], + recommendation: "عميل واعد — تابع خلال ٢٤ ساعة", +}; + +/* ───────────── circular ring ───────────── */ +function ScoreRing({ + score, + size = 160, + strokeWidth = 10, +}: { + score: number; + size?: number; + strokeWidth?: number; +}) { + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const motionProgress = useMotionValue(0); + const strokeDashoffset = useTransform( + motionProgress, + (v) => circumference - (v / 100) * circumference + ); + const displayScore = useMotionValue(0); + const [displayed, setDisplayed] = useState(0); + + useEffect(() => { + const anim = animate(motionProgress, score, { duration: 1.4, ease: "easeOut" }); + const anim2 = animate(displayScore, score, { + duration: 1.4, + ease: "easeOut", + onUpdate: (v) => setDisplayed(Math.round(v)), + }); + return () => { + anim.stop(); + anim2.stop(); + }; + }, [score, motionProgress, displayScore]); + + const color = getScoreColor(score); + const grade = getGrade(score); + const gradientId = getGradientId(score); + + return ( +
+ + + + + + + + + {/* background ring */} + + {/* progress ring */} + + + + {/* center content */} +
+ + {displayed} + + + {grade} + +
+
+ ); +} + +/* ───────────── breakdown bar ───────────── */ +function BreakdownBar({ + item, + delay, +}: { + item: BreakdownItem; + delay: number; +}) { + const Icon = item.icon; + const pct = (item.value / 25) * 100; + const color = getScoreColor(item.value * 4); // scale 0-25 -> 0-100 + + return ( + +
+ + {item.value}/٢٥ + +
+ {item.label} +
+ +
+
+
+
+ +
+
+ ); +} + +/* ───────────── full variant ───────────── */ +export function LeadScoreCard({ + data = sampleData, + variant = "full", +}: { + data?: LeadScoreData; + variant?: "full" | "compact"; +}) { + if (variant === "compact") { + return ; + } + + return ( + + {/* header */} +
+
+ +
+

تقييم العميل الذكي

+
+ + {/* ring */} +
+ +
+ + {/* breakdown */} +
+ {data.breakdown.map((item, i) => ( + + ))} +
+ + {/* recommendation */} + +
+ + توصية الذكاء الاصطناعي +
+

{data.recommendation}

+
+
+ ); +} + +/* ───────────── compact variant ───────────── */ +function LeadScoreCompact({ data }: { data: LeadScoreData }) { + const color = getScoreColor(data.score); + const grade = getGrade(data.score); + + return ( + + {/* mini ring */} + + + {/* info */} +
+
+ + {data.score} + + + {grade} + +
+

{data.recommendation}

+
+ + {/* mini bars */} +
+ {data.breakdown.map((item) => ( + + ))} +
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/pipeline-kanban.tsx b/salesflow-saas/frontend/src/components/dealix/pipeline-kanban.tsx new file mode 100644 index 00000000..666ba729 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/pipeline-kanban.tsx @@ -0,0 +1,301 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence, Reorder } from "framer-motion"; +import { + GripVertical, + Building2, + User, + Clock, + ChevronDown, + ChevronUp, + TrendingUp, + X, +} from "lucide-react"; + +/* ───────────── types ───────────── */ +interface Deal { + id: string; + company: string; + value: number; + rep: string; + daysInStage: number; + note?: string; + probability: number; +} + +interface Stage { + id: string; + label: string; + color: string; // tailwind ring/border colour + headerBg: string; // gradient header + dotColor: string; + deals: Deal[]; +} + +/* ───────────── sample data ───────────── */ +const initialStages: Stage[] = [ + { + id: "new", + label: "جديد", + color: "border-blue-500", + headerBg: "from-blue-600 to-blue-400", + dotColor: "bg-blue-500", + deals: [ + { id: "d1", company: "شركة الأفق التقنية", value: 45_000, rep: "سالم", daysInStage: 2, probability: 10, note: "تواصل أولي عبر واتساب" }, + { id: "d2", company: "مؤسسة الوفاء", value: 22_000, rep: "نورة", daysInStage: 5, probability: 15 }, + ], + }, + { + id: "negotiation", + label: "تفاوض", + color: "border-yellow-500", + headerBg: "from-yellow-500 to-amber-400", + dotColor: "bg-yellow-500", + deals: [ + { id: "d3", company: "مجموعة الرواد", value: 125_000, rep: "فهد", daysInStage: 8, probability: 45, note: "اجتماع مع المدير التنفيذي يوم الأحد" }, + ], + }, + { + id: "proposal", + label: "عرض سعر", + color: "border-orange-500", + headerBg: "from-orange-500 to-orange-400", + dotColor: "bg-orange-500", + deals: [ + { id: "d4", company: "مصنع الشرق", value: 310_000, rep: "سالم", daysInStage: 3, probability: 60 }, + { id: "d5", company: "حلول البيانات", value: 88_000, rep: "نورة", daysInStage: 12, probability: 55, note: "بانتظار موافقة المشتريات" }, + ], + }, + { + id: "won", + label: "فوز", + color: "border-emerald-500", + headerBg: "from-emerald-500 to-green-400", + dotColor: "bg-emerald-500", + deals: [ + { id: "d6", company: "شركة النخبة", value: 200_000, rep: "فهد", daysInStage: 0, probability: 100 }, + ], + }, + { + id: "lost", + label: "خسارة", + color: "border-red-500", + headerBg: "from-red-500 to-rose-400", + dotColor: "bg-red-500", + deals: [ + { id: "d7", company: "مؤسسة السلام", value: 60_000, rep: "نورة", daysInStage: 0, probability: 0, note: "اختاروا منافس أرخص" }, + ], + }, +]; + +const fmt = (n: number) => + new Intl.NumberFormat("ar-SA", { maximumFractionDigits: 0 }).format(n); + +/* ───────────── progress dots ───────────── */ +const stageOrder = ["new", "negotiation", "proposal", "won", "lost"]; +function ProgressDots({ stageId }: { stageId: string }) { + const idx = stageOrder.indexOf(stageId); + const isLost = stageId === "lost"; + return ( +
+ {stageOrder.slice(0, 4).map((_, i) => ( + + ))} +
+ ); +} + +/* ───────────── deal card ───────────── */ +function DealCard({ deal, stageId }: { deal: Deal; stageId: string }) { + const [expanded, setExpanded] = useState(false); + + return ( + + {/* drag handle */} + + + {/* header */} +
+
+
+ +

{deal.company}

+
+

+ {fmt(deal.value)} ر.س +

+
+ +
+ + {/* meta row */} +
+ + + {deal.rep} + + + + {deal.daysInStage} يوم + + + + {deal.probability}٪ + +
+ + + + {/* expanded details */} + + {expanded && ( + +
+ {deal.note &&

{deal.note}

} +
+ + +
+
+
+ )} +
+
+ ); +} + +/* ───────────── empty state ───────────── */ +function EmptyColumn() { + return ( +
+

لا توجد صفقات

+
+ ); +} + +/* ───────────── column ───────────── */ +function StageColumn({ stage }: { stage: Stage }) { + const total = stage.deals.reduce((s, d) => s + d.value, 0); + const [deals, setDeals] = useState(stage.deals); + + return ( +
+ {/* header */} +
+
+ + {deals.length} + +

{stage.label}

+
+

+ {fmt(total)} ر.س +

+
+ + {/* cards */} +
+ {deals.length === 0 ? ( + + ) : ( + + {deals.map((deal) => ( + + + + ))} + + )} +
+
+ ); +} + +/* ───────────── main component ───────────── */ +export function PipelineKanban() { + const [stages] = useState(initialStages); + + return ( + + {/* title bar */} +
+
+

خط الصفقات

+

+ إجمالي: {fmt(stages.flatMap((s) => s.deals).reduce((a, d) => a + d.value, 0))} ر.س +  |  {stages.flatMap((s) => s.deals).length} صفقة +

+
+ +
+ + {/* kanban board */} +
+ {stages.map((stage, i) => ( + + + + ))} +
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/stats-counter.tsx b/salesflow-saas/frontend/src/components/dealix/stats-counter.tsx new file mode 100644 index 00000000..8ca24736 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/stats-counter.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { useEffect, useRef, useState, useCallback } from 'react'; +import { motion, useSpring, useTransform, useInView } from 'framer-motion'; +import { clsx } from 'clsx'; + +type NumberLocale = 'ar' | 'en'; + +interface StatsCounterProps { + target: number; + label: string; + prefix?: string; + suffix?: string; + currency?: boolean; + locale?: NumberLocale; + duration?: number; + className?: string; +} + +function formatNumber(value: number, locale: NumberLocale, currency: boolean): string { + const opts: Intl.NumberFormatOptions = currency + ? { style: 'currency', currency: 'SAR', maximumFractionDigits: 0 } + : { maximumFractionDigits: 0 }; + + const loc = locale === 'ar' ? 'ar-SA' : 'en-SA'; + return new Intl.NumberFormat(loc, opts).format(value); +} + +function AnimatedNumber({ + target, + locale, + currency, + duration, +}: { + target: number; + locale: NumberLocale; + currency: boolean; + duration: number; +}) { + const ref = useRef(null); + const isInView = useInView(ref, { once: true, margin: '-50px' }); + + const springValue = useSpring(0, { + stiffness: 50, + damping: 20, + duration: duration * 1000, + }); + + const display = useTransform(springValue, (v) => formatNumber(Math.round(v), locale, currency)); + + useEffect(() => { + if (isInView) { + springValue.set(target); + } + }, [isInView, target, springValue]); + + useEffect(() => { + const unsubscribe = display.on('change', (v) => { + if (ref.current) { + ref.current.textContent = v; + } + }); + return unsubscribe; + }, [display]); + + return 0; +} + +function StatsCounter({ + target, + label, + prefix, + suffix, + currency = false, + locale = 'ar', + duration = 2, + className, +}: StatsCounterProps) { + return ( +
+
+ {prefix && {prefix}} + + {suffix && {suffix}} +
+

{label}

+
+ ); +} + +interface StatsGridProps { + stats: StatsCounterProps[]; + className?: string; +} + +function StatsGrid({ stats, className }: StatsGridProps) { + return ( +
+ {stats.map((stat) => ( + + + + ))} +
+ ); +} + +export { StatsCounter, StatsGrid }; +export type { StatsCounterProps, StatsGridProps, NumberLocale }; diff --git a/salesflow-saas/frontend/src/components/dealix/unified-inbox.tsx b/salesflow-saas/frontend/src/components/dealix/unified-inbox.tsx new file mode 100644 index 00000000..bf745476 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/unified-inbox.tsx @@ -0,0 +1,450 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + Search, + Send, + Paperclip, + ArrowRight, + Phone, + MoreVertical, + Sparkles, + Check, + CheckCheck, + MessageSquare, + Mail, + Smartphone, +} from "lucide-react"; + +/* ───────────── types ───────────── */ +type Channel = "whatsapp" | "email" | "sms"; +type FilterTab = "all" | "whatsapp" | "email" | "sms"; + +interface Message { + id: string; + text: string; + sent: boolean; // true = we sent, false = received + time: string; + read?: boolean; +} + +interface Conversation { + id: string; + name: string; + avatar: string; // initials + avatarColor: string; + channel: Channel; + lastMessage: string; + time: string; + unread: number; + messages: Message[]; +} + +/* ───────────── channel config ───────────── */ +const channelConfig: Record = { + whatsapp: { icon: MessageSquare, color: "text-green-400 bg-green-400/20", label: "واتساب" }, + email: { icon: Mail, color: "text-blue-400 bg-blue-400/20", label: "إيميل" }, + sms: { icon: Smartphone, color: "text-purple-400 bg-purple-400/20", label: "رسائل" }, +}; + +const filterTabs: { key: FilterTab; label: string }[] = [ + { key: "all", label: "الكل" }, + { key: "whatsapp", label: "واتساب" }, + { key: "email", label: "إيميل" }, + { key: "sms", label: "رسائل" }, +]; + +/* ───────────── sample data ───────────── */ +const sampleConversations: Conversation[] = [ + { + id: "c1", + name: "أحمد الغامدي", + avatar: "أغ", + avatarColor: "bg-green-600", + channel: "whatsapp", + lastMessage: "تمام، أرسل لي العرض على الإيميل", + time: "١٠:٣٢", + unread: 2, + messages: [ + { id: "m1", text: "السلام عليكم، عندكم حل CRM يدعم العربي؟", sent: false, time: "١٠:١٥" }, + { id: "m2", text: "وعليكم السلام أحمد! أكيد، Dealix مصمم بالكامل للسوق السعودي", sent: true, time: "١٠:١٨", read: true }, + { id: "m3", text: "كم السعر للباقة الاحترافية؟", sent: false, time: "١٠:٢٠" }, + { id: "m4", text: "١٤٩ ر.س شهرياً مع تجربة مجانية ١٤ يوم", sent: true, time: "١٠:٢٥", read: true }, + { id: "m5", text: "تمام، أرسل لي العرض على الإيميل", sent: false, time: "١٠:٣٢" }, + ], + }, + { + id: "c2", + name: "سارة المطيري", + avatar: "سم", + avatarColor: "bg-blue-600", + channel: "email", + lastMessage: "شكراً على العرض التقديمي، سأرجع لكم بعد الاجتماع", + time: "أمس", + unread: 0, + messages: [ + { id: "m6", text: "مرحباً، أرغب بمعرفة المزيد عن خدمات تقييم العملاء بالذكاء الاصطناعي", sent: false, time: "أمس ٠٩:٠٠" }, + { id: "m7", text: "أهلاً سارة! نظام تقييم العملاء يعتمد على ٤ محاور: التفاعل، الملف الشخصي، السلوك، ونية الشراء", sent: true, time: "أمس ٠٩:٤٥", read: true }, + { id: "m8", text: "ممتاز، هل يمكنكم تقديم عرض لفريق من ١٥ شخص؟", sent: false, time: "أمس ١١:٣٠" }, + { id: "m9", text: "بالتأكيد! أرفقت عرض الأسعار للباقة المؤسسية", sent: true, time: "أمس ١٤:٠٠", read: true }, + { id: "m10", text: "شكراً على العرض التقديمي، سأرجع لكم بعد الاجتماع", sent: false, time: "أمس ١٦:٢٠" }, + ], + }, + { + id: "c3", + name: "خالد العتيبي", + avatar: "خع", + avatarColor: "bg-purple-600", + channel: "sms", + lastMessage: "موعدنا يوم الأحد الساعة ١١ صباحاً", + time: "١٢:٠٠", + unread: 1, + messages: [ + { id: "m11", text: "خالد، تذكير بموعد العرض التقديمي", sent: true, time: "١١:٣٠", read: true }, + { id: "m12", text: "موعدنا يوم الأحد الساعة ١١ صباحاً", sent: false, time: "١٢:٠٠" }, + ], + }, + { + id: "c4", + name: "منيرة القحطاني", + avatar: "مق", + avatarColor: "bg-amber-600", + channel: "whatsapp", + lastMessage: "ودي أجرب النظام قبل ما نقرر", + time: "٠٩:١٥", + unread: 3, + messages: [ + { id: "m13", text: "مرحباً، محتاجين نظام CRM لشركة عقارية", sent: false, time: "٠٨:٤٥" }, + { id: "m14", text: "أهلاً منيرة! Dealix يخدم أكثر من ٥٠ شركة عقارية في المملكة", sent: true, time: "٠٩:٠٠", read: true }, + { id: "m15", text: "ودي أجرب النظام قبل ما نقرر", sent: false, time: "٠٩:١٥" }, + ], + }, +]; + +/* ───────────── typing indicator ───────────── */ +function TypingIndicator() { + return ( +
+ {[0, 1, 2].map((i) => ( + + ))} +
+ ); +} + +/* ───────────── conversation list item ───────────── */ +function ConversationItem({ + convo, + isActive, + onClick, +}: { + convo: Conversation; + isActive: boolean; + onClick: () => void; +}) { + const ch = channelConfig[convo.channel]; + const Icon = ch.icon; + + return ( + + {/* avatar */} +
+ {convo.avatar} + + + +
+ + {/* text */} +
+
+ {convo.time} +

{convo.name}

+
+

{convo.lastMessage}

+
+ + {/* unread badge */} + {convo.unread > 0 && ( + + {convo.unread} + + )} +
+ ); +} + +/* ───────────── chat panel ───────────── */ +function ChatPanel({ + convo, + onBack, +}: { + convo: Conversation; + onBack: () => void; +}) { + const [messages, setMessages] = useState(convo.messages); + const [input, setInput] = useState(""); + const [showTyping, setShowTyping] = useState(false); + const scrollRef = useRef(null); + const ch = channelConfig[convo.channel]; + + useEffect(() => { + setMessages(convo.messages); + }, [convo.id, convo.messages]); + + useEffect(() => { + scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); + }, [messages, showTyping]); + + const handleSend = () => { + if (!input.trim()) return; + const newMsg: Message = { + id: `m-${Date.now()}`, + text: input, + sent: true, + time: new Date().toLocaleTimeString("ar-SA", { hour: "2-digit", minute: "2-digit" }), + read: false, + }; + setMessages((prev) => [...prev, newMsg]); + setInput(""); + setShowTyping(true); + setTimeout(() => { + setShowTyping(false); + setMessages((prev) => [ + ...prev, + { + id: `m-${Date.now()}-r`, + text: "شكراً لتواصلك! سأرد عليك في أقرب وقت", + sent: false, + time: new Date().toLocaleTimeString("ar-SA", { hour: "2-digit", minute: "2-digit" }), + }, + ]); + }, 2000); + }; + + return ( +
+ {/* chat header */} +
+
+ + +
+
+
+

{convo.name}

+ + {ch.label} + +
+
+ {convo.avatar} +
+ {/* back button mobile */} + +
+
+ + {/* messages */} +
+ {messages.map((msg) => ( + +
+

{msg.text}

+
+ {msg.time} + {msg.sent && + (msg.read ? ( + + ) : ( + + ))} +
+
+
+ ))} + + + {showTyping && ( + + + + )} + +
+ + {/* AI suggestion chip */} +
+ +
+ + {/* input bar */} +
+ + setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSend()} + placeholder="اكتب رسالتك..." + className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm placeholder:text-white/30 focus:outline-none focus:border-teal-500/50 transition-colors" + /> + +
+
+ ); +} + +/* ───────────── main component ───────────── */ +export function UnifiedInbox() { + const [activeId, setActiveId] = useState(null); + const [filter, setFilter] = useState("all"); + const [search, setSearch] = useState(""); + + const filtered = sampleConversations.filter((c) => { + if (filter !== "all" && c.channel !== filter) return false; + if (search && !c.name.includes(search) && !c.lastMessage.includes(search)) return false; + return true; + }); + + const activeConvo = sampleConversations.find((c) => c.id === activeId) ?? null; + + return ( + + {/* ─── right panel: conversation list ─── */} +
+ {/* search */} +
+
+ + setSearch(e.target.value)} + placeholder="بحث..." + className="w-full bg-white/5 border border-white/10 rounded-xl pr-10 pl-4 py-2 text-sm placeholder:text-white/30 focus:outline-none focus:border-teal-500/40 transition-colors" + /> +
+
+ + {/* filter tabs */} +
+ {filterTabs.map((tab) => ( + + ))} +
+ + {/* list */} +
+ + {filtered.map((convo, i) => ( + + setActiveId(convo.id)} + /> + + ))} + + + {filtered.length === 0 && ( +
+ لا توجد محادثات +
+ )} +
+
+ + {/* ─── left panel: chat thread ─── */} +
+ {activeConvo ? ( + setActiveId(null)} /> + ) : ( +
+
+ +
+

اختر محادثة للبدء

+
+ )} +
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/ui/index.ts b/salesflow-saas/frontend/src/components/ui/index.ts new file mode 100644 index 00000000..852c7987 --- /dev/null +++ b/salesflow-saas/frontend/src/components/ui/index.ts @@ -0,0 +1,17 @@ +export { Button } from './button'; +export type { ButtonProps, ButtonVariant, ButtonSize } from './button'; + +export { Card, CardTitle, CardDescription } from './card'; +export type { CardProps, CardVariant } from './card'; + +export { Input } from './input'; +export type { InputProps, InputType } from './input'; + +export { Modal } from './modal'; +export type { ModalProps, ModalSize } from './modal'; + +export { Badge } from './badge'; +export type { BadgeProps, BadgeVariant } from './badge'; + +export { Sidebar, useSidebar } from './sidebar'; +export type { SidebarProps, NavItem, NavSection } from './sidebar'; diff --git a/salesflow-saas/frontend/src/components/ui/input.tsx b/salesflow-saas/frontend/src/components/ui/input.tsx new file mode 100644 index 00000000..416054fe --- /dev/null +++ b/salesflow-saas/frontend/src/components/ui/input.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { + forwardRef, + useState, + useId, + type InputHTMLAttributes, + type TextareaHTMLAttributes, +} from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { clsx } from 'clsx'; +import { Search, Eye, EyeOff } from 'lucide-react'; + +type InputType = 'text' | 'email' | 'phone' | 'password' | 'search' | 'textarea'; + +interface InputProps + extends Omit, 'type' | 'size'> { + inputType?: InputType; + label?: string; + error?: string; + rows?: number; +} + +const baseStyles = clsx( + 'w-full bg-white/5 backdrop-blur-sm text-white placeholder-transparent', + 'border border-white/10 rounded-lg', + 'transition-all duration-200', + 'focus:outline-none focus:ring-2 focus:ring-teal-400/50 focus:border-teal-400', + 'disabled:opacity-50 disabled:cursor-not-allowed', + 'text-base ps-4 pe-4 pt-5 pb-2', + 'peer', +); + +const labelStyles = clsx( + 'absolute text-sm text-slate-400 duration-200 transform', + 'top-4 start-4 z-10 origin-[right]', + 'peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0', + 'peer-focus:scale-75 peer-focus:-translate-y-2.5', + 'peer-[:not(:placeholder-shown)]:scale-75 peer-[:not(:placeholder-shown)]:-translate-y-2.5', + 'pointer-events-none', +); + +const errorLabelStyles = 'text-red-400'; + +const DealixInput = forwardRef( + ({ inputType = 'text', label, error, className, rows = 4, id, ...props }, ref) => { + const generatedId = useId(); + const inputId = id ?? generatedId; + const [showPassword, setShowPassword] = useState(false); + + const errorId = error ? `${inputId}-error` : undefined; + + const wrapperClass = 'relative w-full'; + + if (inputType === 'textarea') { + return ( +
+