Merge caitlin/CRM_ENVR: AI chat ref+health, tips panel, cyberpunk theme, app-shell/bg improvements

This commit is contained in:
JCBSComputer
2026-07-01 09:36:04 +02:00
56 changed files with 2693 additions and 504 deletions
+120 -72
View File
@@ -1,13 +1,23 @@
"use client"
import { useState, useCallback, useEffect } from "react"
import { useState, useCallback, useRef, useEffect } from "react"
import { AIChat } from "@/components/ai/ai-chat"
import { JobSelector } from "@/components/ai/job-selector"
import { Bot, Lightbulb, Target, MessageSquare, Wifi, WifiOff } from "lucide-react"
import { Bot, ChevronRight, Wifi, WifiOff } from "lucide-react"
const tips = [
"Ask for cold email templates for a specific job",
"Request objection handling tips",
"Ask for outreach strategies per industry",
"Generate a follow up sequence",
"Get LinkedIn connection message templates",
]
export default function AIAssistantPage() {
const [selectedJob, setSelectedJob] = useState<{ job_title: string; keywords: string[]; industry: string; description: string } | null>(null)
const [recentPrompts, setRecentPrompts] = useState<string[]>([])
const [aiOnline, setAiOnline] = useState<boolean | null>(null)
const aiChatRef = useRef<{ fillInput: (text: string) => void } | null>(null)
const handleJobSelect = useCallback((job: typeof selectedJob) => {
setSelectedJob(job)
@@ -28,83 +38,121 @@ export default function AIAssistantPage() {
return () => { cancelled = true; clearInterval(id) }
}, [])
const handleTipClick = useCallback((tip: string) => {
aiChatRef.current?.fillInput(tip)
}, [])
const handleRecentPromptClick = useCallback((prompt: string) => {
aiChatRef.current?.fillInput(prompt)
}, [])
const handleMessageSent = useCallback((msg: string) => {
setRecentPrompts((prev) => {
const next = [msg, ...prev.filter((p) => p !== msg)].slice(0, 3)
return next
})
}, [])
return (
<div className="flex h-[calc(100vh-3.5rem)]">
<div className="flex-1 flex flex-col min-w-0">
<div className="border-b border-[#2a2a35] px-6 py-4">
<div className="flex items-center gap-3">
<div className="h-9 w-9 rounded-lg bg-[#1BB0CE]/15 flex items-center justify-center">
<Bot className="h-5 w-5 text-[#1BB0CE]" />
<div className="flex h-[calc(100vh-3.5rem)] bg-background">
<div className="flex-1 flex flex-col min-w-0 border border-foreground transition-colors overflow-hidden">
<div className="bg-card/80 backdrop-blur-md border-b border-border px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-primary flex items-center justify-center" style={{boxShadow: "0 0 40px hsl(var(--primary) / 0.3)"}}>
<Bot className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-foreground font-bold text-lg">AI Sales Assistant</h1>
<p className="text-muted-foreground text-xs mt-0.5">Powered by local AI</p>
</div>
</div>
<div className="flex-1">
<h1 className="text-lg font-semibold text-[#e8e8ef]">AI Sales Assistant</h1>
<p className="text-xs text-[#6a6a75]">Uncensored sales tips and strategies powered by local AI</p>
</div>
<div className="flex items-center gap-2 text-xs">
<span className={aiOnline === null ? "text-[#6a6a75]" : aiOnline ? "text-green-400" : "text-red-400"}>
{aiOnline === null ? "Checking..." : aiOnline ? "Connected" : "Disconnected"}
</span>
{aiOnline === null ? (
<span className="h-2 w-2 rounded-full bg-[#6a6a75]" />
) : aiOnline ? (
<Wifi className="h-3.5 w-3.5 text-green-400" />
) : (
<WifiOff className="h-3.5 w-3.5 text-red-400" />
)}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-xs">
<span className={aiOnline === null ? "text-[#6a6a75]" : aiOnline ? "text-green-400" : "text-red-400"}>
{aiOnline === null ? "Checking..." : aiOnline ? "Connected" : "Disconnected"}
</span>
{aiOnline === null ? (
<span className="h-2 w-2 rounded-full bg-[#6a6a75]" />
) : aiOnline ? (
<Wifi className="h-3.5 w-3.5 text-green-400" />
) : (
<WifiOff className="h-3.5 w-3.5 text-red-400" />
)}
</div>
<div className="w-px h-5 bg-border" />
<div className="bg-muted/50 rounded-lg px-3 py-1.5">
<span className="text-muted-foreground text-xs">Local AI Model</span>
</div>
</div>
</div>
</div>
<div className="flex-1 flex">
<div className="flex-1 flex flex-col min-w-0 border-r border-[#2a2a35]">
<AIChat />
<AIChat ref={aiChatRef} onMessageSent={handleMessageSent} />
</div>
<div className="w-[300px] flex-none bg-sidebar border border-foreground transition-colors p-5 overflow-y-auto h-full flex flex-col">
<div>
<div className="flex items-center gap-2 mb-3">
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
<span className="text-primary text-[10px] font-bold uppercase tracking-[0.15em]">Target Job</span>
</div>
<div className="w-72 flex-none p-4 space-y-4 overflow-y-auto">
<div>
<h3 className="text-xs font-semibold text-[#6a6a75] uppercase tracking-wider mb-2 flex items-center gap-1.5">
<Target className="h-3.5 w-3.5" />
Target Job
</h3>
<JobSelector onSelect={handleJobSelect} />
</div>
{selectedJob && (
<div className="bg-[#1a1a24] border border-[#2a2a35] rounded-lg p-3 space-y-2">
<h4 className="text-sm font-medium text-[#e8e8ef]">{selectedJob.job_title}</h4>
<div className="flex items-center gap-1.5">
<span className="text-xs px-1.5 py-0.5 rounded bg-[#1BB0CE]/10 text-[#1BB0CE]">{selectedJob.industry}</span>
</div>
<p className="text-xs text-[#8a8a95]">{selectedJob.description}</p>
<div className="flex flex-wrap gap-1">
{selectedJob.keywords.map((kw, i) => (
<span key={i} className="text-xs px-1.5 py-0.5 rounded bg-[#2a2a35] text-[#6a6a75]">
{kw}
</span>
))}
</div>
<JobSelector onSelect={handleJobSelect} />
{selectedJob && (
<div className="bg-card/50 border border-border rounded-xl p-3.5 mt-3 space-y-2">
<h4 className="text-sm font-semibold text-foreground">{selectedJob.job_title}</h4>
<span className="text-xs px-2 py-0.5 rounded-md bg-primary/10 text-primary font-medium inline-block">{selectedJob.industry}</span>
<p className="text-xs text-muted-foreground leading-relaxed">{selectedJob.description}</p>
<div className="flex flex-wrap gap-1.5">
{selectedJob.keywords.map((kw, i) => (
<span key={i} className="text-xs px-2 py-0.5 rounded-md bg-muted/50 text-muted-foreground">{kw}</span>
))}
</div>
)}
<div className="bg-[#1a1a24] border border-[#2a2a35] rounded-lg p-3 space-y-2">
<h4 className="text-xs font-semibold text-[#6a6a75] uppercase tracking-wider flex items-center gap-1.5">
<Lightbulb className="h-3.5 w-3.5" />
Tips
</h4>
<ul className="space-y-1.5 text-xs text-[#8a8a95]">
<li className="flex gap-2">
<MessageSquare className="h-3 w-3 mt-0.5 flex-none text-[#1BB0CE]" />
Ask for cold email templates for a specific job
</li>
<li className="flex gap-2">
<MessageSquare className="h-3 w-3 mt-0.5 flex-none text-[#1BB0CE]" />
Request objection handling tips
</li>
<li className="flex gap-2">
<MessageSquare className="h-3 w-3 mt-0.5 flex-none text-[#1BB0CE]" />
Ask for outreach strategies per industry
</li>
</ul>
</div>
)}
</div>
<div className="mt-6">
<div className="flex items-center gap-2 mb-3">
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
<span className="text-primary text-[10px] font-bold uppercase tracking-[0.15em]">Quick Tips</span>
</div>
{tips.map((tip, i) => (
<div
key={i}
onClick={() => handleTipClick(tip)}
className="flex items-start gap-2.5 bg-card/50 hover:bg-card border border-border hover:border-primary/20 rounded-xl p-3.5 mb-2 cursor-pointer transition-all duration-200 group"
>
<ChevronRight className="h-3.5 w-3.5 mt-0.5 text-muted-foreground group-hover:text-primary transition-colors duration-200 flex-shrink-0" />
<span className="text-muted-foreground text-xs leading-5 group-hover:text-foreground transition-colors duration-200">{tip}</span>
</div>
))}
</div>
<div className="mt-6">
<div className="flex items-center gap-2 mb-3">
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
<span className="text-primary text-[10px] font-bold uppercase tracking-[0.15em]">Recent Prompts</span>
</div>
{recentPrompts.length > 0 ? (
recentPrompts.map((prompt, i) => (
<div
key={i}
onClick={() => handleRecentPromptClick(prompt)}
className="bg-card/50 rounded-xl p-3 mb-2 border border-border text-muted-foreground text-xs truncate hover:text-foreground cursor-pointer transition-colors duration-200"
>
{prompt}
</div>
))
) : (
<div className="text-muted-foreground text-xs text-center py-4">Your recent prompts will appear here</div>
)}
</div>
<div className="mt-auto border-t border-border pt-4">
<div className="flex gap-4">
<div className="flex-1">
<div className="text-muted-foreground text-[10px] uppercase tracking-wide">Messages today</div>
<div className="text-foreground text-sm font-semibold mt-0.5">24</div>
</div>
<div className="flex-1">
<div className="text-muted-foreground text-[10px] uppercase tracking-wide">Tokens used</div>
<div className="text-foreground text-sm font-semibold mt-0.5">12.4k</div>
</div>
</div>
</div>
+2 -2
View File
@@ -86,7 +86,7 @@ export default function DashboardPage() {
</PageHeader>
</div>
<p className="text-xs font-semibold tracking-widest uppercase text-[#888888] dark:text-[#666666]">Pipeline Overview</p>
<p className="text-xs font-semibold tracking-widest uppercase text-[#8A9078] dark:text-[#666666]">Pipeline Overview</p>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
{stats
@@ -97,7 +97,7 @@ export default function DashboardPage() {
}
</div>
<p className="text-xs font-semibold tracking-widest uppercase text-[#888888] dark:text-[#666666]">Analytics</p>
<p className="text-xs font-semibold tracking-widest uppercase text-[#8A9078] dark:text-[#666666]">Analytics</p>
<div className="grid gap-6 lg:grid-cols-2">
<LeadStatusChart data={stats?.statusDistribution ?? []} />
+57
View File
@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from "next/server"
import { getSessionUser } from "@/lib/auth"
const GIPHY_API_KEY = process.env.GIPHY_API_KEY
const GIPHY_BASE = "https://api.giphy.com/v1/gifs"
export async function GET(request: NextRequest) {
try {
const user = await getSessionUser()
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
if (!GIPHY_API_KEY) {
return NextResponse.json({ error: "GIPHY API key not configured" }, { status: 500 })
}
const { searchParams } = new URL(request.url)
const type = searchParams.get("type") || "trending"
const query = searchParams.get("q") || ""
const offset = parseInt(searchParams.get("offset") || "0", 10)
const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 50)
let url: string
if (type === "search" && query) {
url = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}&rating=g`
} else {
url = `${GIPHY_BASE}/trending?api_key=${GIPHY_API_KEY}&limit=${limit}&offset=${offset}&rating=g`
}
const res = await fetch(url)
if (!res.ok) {
const errBody = await res.text()
console.error("GIPHY API error:", res.status, errBody)
return NextResponse.json({ error: `GIPHY API error (${res.status}): ${errBody}` }, { status: res.status })
}
const data = await res.json()
const gifs = (data.data || []).map((gif: any) => ({
id: gif.id,
title: gif.title,
url: gif.images?.original?.url || "",
previewUrl: gif.images?.fixed_width?.url || "",
previewHeight: gif.images?.fixed_width?.height || 150,
width: gif.images?.original?.width || 0,
height: gif.images?.original?.height || 0,
}))
return NextResponse.json({
gifs,
pagination: data.pagination || { total_count: 0, count: gifs.length, offset },
})
} catch (error: any) {
console.error("GIPHY proxy error:", error)
return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 })
}
}
+7
View File
@@ -0,0 +1,7 @@
import { NextResponse } from "next/server"
import { checkAiServiceStatus } from "@/lib/ai"
export async function GET() {
const ok = await checkAiServiceStatus()
return NextResponse.json({ status: ok ? "ok" : "unavailable" }, { status: ok ? 200 : 503 })
}
+62 -25
View File
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server"
import { getSessionUser } from "@/lib/auth"
const TENOR_API_KEY = process.env.TENOR_API_KEY || ""
const TENOR_BASE = "https://tenor.googleapis.com/v2"
const GIPHY_API_KEY = process.env.GIPHY_API_KEY
const GIPHY_BASE = "https://api.giphy.com/v1/gifs"
export async function GET(request: NextRequest) {
try {
@@ -12,41 +12,78 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const q = searchParams.get("q") || ""
const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 50)
const pos = searchParams.get("pos") || ""
const pos = parseInt(searchParams.get("pos") || "0", 10) || 0
if (!TENOR_API_KEY) {
if (!GIPHY_API_KEY) {
console.error("Missing GIPHY_API_KEY environment variable")
return NextResponse.json({ results: [], noKey: true })
}
const endpoint = q
? `${TENOR_BASE}/search?q=${encodeURIComponent(q)}&key=${TENOR_API_KEY}&limit=${limit}&media_filter=minimal`
: `${TENOR_BASE}/featured?key=${TENOR_API_KEY}&limit=${limit}&media_filter=minimal`
let data: any
const url = pos ? `${endpoint}&pos=${pos}` : endpoint
if (q) {
const url = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(q)}&limit=${limit}&offset=${pos}&rating=g`
const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
if (!res.ok) {
const errBody = await res.text()
console.error("GIPHY API search error:", res.status, errBody)
return NextResponse.json({ results: [], error: `GIPHY error: ${errBody}` }, { status: 502 })
}
data = await res.json()
} else {
const trendingUrl = `${GIPHY_BASE}/trending?api_key=${GIPHY_API_KEY}&limit=${limit}&offset=${pos}&rating=g`
const trendingRes = await fetch(trendingUrl, { signal: AbortSignal.timeout(8000) })
const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
if (!res.ok) {
return NextResponse.json({ results: [], error: "Tenor API error" }, { status: 502 })
if (trendingRes.ok) {
data = await trendingRes.json()
if (!data.data?.length && pos === 0) {
console.log("Trending returned no results, falling back to default search")
const fallbackUrl = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=funny&limit=${limit}&offset=${pos}&rating=g`
const fallbackRes = await fetch(fallbackUrl, { signal: AbortSignal.timeout(8000) })
if (!fallbackRes.ok) {
const errBody = await fallbackRes.text()
console.error("GIPHY fallback search error:", fallbackRes.status, errBody)
return NextResponse.json({ results: [], error: `GIPHY error: ${errBody}` }, { status: 502 })
}
data = await fallbackRes.json()
}
} else {
const errBody = await trendingRes.text()
console.error("GIPHY trending error:", trendingRes.status, errBody)
console.log("Trending failed, falling back to default search")
const fallbackUrl = `${GIPHY_BASE}/search?api_key=${GIPHY_API_KEY}&q=funny&limit=${limit}&offset=${pos}&rating=g`
const fallbackRes = await fetch(fallbackUrl, { signal: AbortSignal.timeout(8000) })
if (!fallbackRes.ok) {
const fallbackErr = await fallbackRes.text()
console.error("GIPHY fallback search error:", fallbackRes.status, fallbackErr)
return NextResponse.json({ results: [], error: `GIPHY error: ${fallbackErr}` }, { status: 502 })
}
data = await fallbackRes.json()
}
}
const data = await res.json()
const results = (data.results || []).map((item: any) => {
const media = item.media_formats?.gif || item.media_formats?.tinygif || {}
const preview = item.media_formats?.tinygif || item.media_formats?.gif || {}
const results = (data.data || []).map((item: any) => {
const dims = item.images?.original
return {
id: item.id,
title: item.title || "",
url: media.url || "",
previewUrl: preview.url || "",
width: media.dims?.[0] || 200,
height: media.dims?.[1] || 200,
url: dims?.url || "",
previewUrl: item.images?.fixed_width?.url || "",
width: dims?.width ? parseInt(dims.width, 10) : 200,
height: dims?.height ? parseInt(dims.height, 10) : 200,
}
})
return NextResponse.json({ results, next: data.next || "" })
} catch (error) {
console.error("GIF API error:", error)
return NextResponse.json({ results: [], error: "Failed to fetch GIFs" }, { status: 500 })
const nextOffset = pos + results.length
const total = data.pagination?.total_count || 0
const hasMore = total ? nextOffset < total : results.length === limit
const nextPos = hasMore ? String(nextOffset) : ""
return NextResponse.json({ results, next: nextPos })
} catch (error: any) {
console.error("GIF proxy error:", error)
const message = error?.message === "Failed to fetch"
? "Could not reach the GIF service."
: "Failed to fetch GIFs."
return NextResponse.json({ results: [], error: message }, { status: 500 })
}
}
+40 -40
View File
@@ -53,24 +53,24 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
if (!joined) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#CC0000]/10 to-[#990000]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#E0E0E0] dark:border-[#CC0000]/20 shadow-lg text-center">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#C84B4B]/10 to-[#C84B4B]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#D4D8CC] dark:border-[#CC0000]/20 shadow-lg text-center">
{connectionTimeout ? (
<>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-3">This call is no longer available.</h1>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-3">This call is no longer available.</h1>
</>
) : !isReady ? (
<>
<div className="flex flex-col items-center gap-4 py-6">
<Loader2 className="h-8 w-8 animate-spin text-[#CC0000]" />
<p className="text-[#888888] text-sm">Connecting to call...</p>
<Loader2 className="h-8 w-8 animate-spin text-[#C84B4B]" />
<p className="text-[#8A9078] text-sm">Connecting to call...</p>
</div>
</>
) : (
<>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-3">Join Call</h1>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-3">Join Call</h1>
{micError && (
<p className="text-[#CC0000] text-xs mb-4">{micError}</p>
<p className="text-[#C84B4B] text-xs mb-4">{micError}</p>
)}
<input
type="text"
@@ -78,12 +78,12 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
onChange={(e) => setDisplayName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") handleJoin() }}
placeholder="Enter your name"
className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-4 outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]"
className="bg-[#FAFAF6] dark:bg-[#1A1A1A] border border-[#D4D8CC] dark:border-[#333333] text-[#2D3020] dark:text-white placeholder-[#8A9078] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-4 outline-none transition-colors focus:border-[#C84B4B] dark:focus:border-[#FF4444]"
/>
<button
onClick={handleJoin}
disabled={!displayName.trim()}
className="w-full bg-[#CC0000] hover:bg-[#990000] disabled:bg-[#CCCCCC] disabled:cursor-not-allowed dark:bg-[#FF1111] dark:hover:bg-[#CC0000] dark:disabled:bg-[#333333] text-white font-semibold text-sm rounded-xl py-2.5 flex items-center justify-center gap-2 transition-all duration-200"
className="w-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 disabled:bg-[#8A9078] disabled:cursor-not-allowed dark:bg-[#FF1111] dark:hover:bg-[#CC0000] dark:disabled:bg-[#333333] text-white font-semibold text-sm rounded-xl py-2.5 flex items-center justify-center gap-2 transition-all duration-200"
>
<Phone className="h-4 w-4" />
Join Call
@@ -96,14 +96,14 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#CC0000]/10 to-[#990000]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#E0E0E0] dark:border-[#CC0000]/20 shadow-lg text-center">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#C84B4B]/10 to-[#C84B4B]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#D4D8CC] dark:border-[#CC0000]/20 shadow-lg text-center">
{callState === "calling" && (
<div>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-4">Calling</h1>
<p className="text-[#888888] text-sm animate-pulse">Connecting to call room...</p>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-4">Calling</h1>
<p className="text-[#8A9078] text-sm animate-pulse">Connecting to call room...</p>
<div className="flex justify-center mt-6">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<PhoneOff className="h-5 w-5" />
</button>
</div>
@@ -112,21 +112,21 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
{callState === "waiting" && (
<div>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-4">Waiting for participant</h1>
<p className="text-[#888888] text-sm animate-pulse">Share the call link to invite someone...</p>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-4">Waiting for participant</h1>
<p className="text-[#8A9078] text-sm animate-pulse">Share the call link to invite someone...</p>
{participants.length > 0 && (
<div className="mt-4 bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3">
<div className="flex items-center gap-2 text-xs text-[#888888] mb-2">
<div className="mt-4 bg-[#FAFAF6] dark:bg-[#1A1A1A] rounded-xl p-3">
<div className="flex items-center gap-2 text-xs text-[#8A9078] mb-2">
<Users className="h-3 w-3" />
Participants ({participants.length})
</div>
{participants.map((p) => (
<div key={p.id} className="text-sm text-[#111111] dark:text-white text-left">{p.label}</div>
<div key={p.id} className="text-sm text-[#2D3020] dark:text-white text-left">{p.label}</div>
))}
</div>
)}
<div className="flex justify-center mt-6">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<PhoneOff className="h-5 w-5" />
</button>
</div>
@@ -135,21 +135,21 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
{callState === "participant_joined" && (
<div>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-4">Participant joined</h1>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-4">Participant joined</h1>
{participants.length > 0 && (
<div className="mb-4 bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3">
<div className="flex items-center gap-2 text-xs text-[#888888] mb-2">
<div className="mb-4 bg-[#FAFAF6] dark:bg-[#1A1A1A] rounded-xl p-3">
<div className="flex items-center gap-2 text-xs text-[#8A9078] mb-2">
<Users className="h-3 w-3" />
Participants ({participants.length})
</div>
{participants.map((p) => (
<div key={p.id} className="text-sm text-[#111111] dark:text-white text-left">{p.label}</div>
<div key={p.id} className="text-sm text-[#2D3020] dark:text-white text-left">{p.label}</div>
))}
</div>
)}
<p className="text-[#888888] text-sm animate-pulse">Connecting...</p>
<p className="text-[#8A9078] text-sm animate-pulse">Connecting...</p>
<div className="flex justify-center mt-6">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<PhoneOff className="h-5 w-5" />
</button>
</div>
@@ -158,10 +158,10 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
{callState === "connecting" && (
<div>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-4">Connecting</h1>
<p className="text-[#888888] text-sm animate-pulse">Establishing secure connection...</p>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-4">Connecting</h1>
<p className="text-[#8A9078] text-sm animate-pulse">Establishing secure connection...</p>
<div className="flex justify-center mt-6">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<button onClick={handleEnd} className="w-14 h-14 rounded-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200">
<PhoneOff className="h-5 w-5" />
</button>
</div>
@@ -170,37 +170,37 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
{callState === "connected" && (
<div>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-1">Connected</h1>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-1">Connected</h1>
{participants.length > 0 && (
<div className="mb-4 bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-3">
<div className="flex items-center gap-2 text-xs text-[#888888] mb-2">
<div className="mb-4 bg-[#FAFAF6] dark:bg-[#1A1A1A] rounded-xl p-3">
<div className="flex items-center gap-2 text-xs text-[#8A9078] mb-2">
<Users className="h-3 w-3" />
Participants ({participants.length})
</div>
{participants.map((p) => (
<div key={p.id} className="text-sm text-[#111111] dark:text-white text-left">{p.label}</div>
<div key={p.id} className="text-sm text-[#2D3020] dark:text-white text-left">{p.label}</div>
))}
</div>
)}
<div className="text-[#CC0000] font-mono text-lg mb-6">
<div className="text-[#C84B4B] font-mono text-lg mb-6">
{formatDuration(callDuration)}
</div>
<div className="flex justify-center gap-4">
<button
onClick={toggleSpeaker}
className={`w-14 h-14 rounded-full flex items-center justify-center text-white font-bold transition-all duration-200 ${isSpeakerOn ? 'bg-[#0033CC] dark:bg-[#1144FF]' : 'bg-[#444444] dark:bg-[#333333]'}`}
className={`w-14 h-14 rounded-full flex items-center justify-center text-white font-bold transition-all duration-200 ${isSpeakerOn ? 'bg-[#5A8FC4] dark:bg-[#1144FF]' : 'bg-[#8A9078] dark:bg-[#333333]'}`}
>
{isSpeakerOn ? <Volume2 className="h-5 w-5" /> : <VolumeX className="h-5 w-5" />}
</button>
<button
onClick={toggleMute}
className={`w-14 h-14 rounded-full flex items-center justify-center text-white font-bold transition-all duration-200 ${isMuted ? 'bg-[#0033CC] dark:bg-[#1144FF]' : 'bg-[#444444] dark:bg-[#333333]'}`}
className={`w-14 h-14 rounded-full flex items-center justify-center text-white font-bold transition-all duration-200 ${isMuted ? 'bg-[#5A8FC4] dark:bg-[#1144FF]' : 'bg-[#8A9078] dark:bg-[#333333]'}`}
>
{isMuted ? <MicOff className="h-5 w-5" /> : <Mic className="h-5 w-5" />}
</button>
<button
onClick={handleEnd}
className="w-14 h-14 rounded-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
className="w-14 h-14 rounded-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 dark:bg-[#FF1111] flex items-center justify-center text-white font-bold transition-all duration-200"
>
<PhoneOff className="h-5 w-5" />
</button>
@@ -210,15 +210,15 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str
{(callState === "ended" || callState === "idle") && (
<div>
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-4">Call ended</h1>
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-4">Call ended</h1>
{callDuration > 0 && (
<p className="text-[#888888] text-sm mb-6">Duration: {formatDuration(callDuration)}</p>
<p className="text-[#8A9078] text-sm mb-6">Duration: {formatDuration(callDuration)}</p>
)}
</div>
)}
{error && (
<p className="text-[#CC0000] text-xs mt-4">{error}</p>
<p className="text-[#C84B4B] text-xs mt-4">{error}</p>
)}
</div>
</div>
+34 -25
View File
@@ -2,6 +2,8 @@
@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@800&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Audiowide&display=swap');
@import "../styles/ai-assistant.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -76,35 +78,41 @@
}
:root {
--background: 220 14% 84%;
--foreground: 222.2 84% 4.9%;
--card: 220 10% 91%;
--card-foreground: 222.2 84% 4.9%;
--popover: 220 14% 96%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 120 100% 50%;
--background: 60 20% 95%;
--foreground: 71 20% 16%;
--card: 60 29% 97%;
--card-foreground: 71 20% 16%;
--popover: 60 29% 97%;
--popover-foreground: 71 20% 16%;
--primary: 0 53% 54%;
--primary-foreground: 0 0% 100%;
--secondary: 220 14% 88%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 220 14% 88%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 220 14% 88%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--sidebar: 216 12% 92%;
--sidebar-foreground: 0 0% 20%;
--sidebar-primary: 0 91.2% 59.8%;
--secondary: 210 47% 56%;
--secondary-foreground: 0 0% 100%;
--muted: 75 10% 93%;
--muted-foreground: 75 10% 52%;
--accent: 159 32% 43%;
--accent-foreground: 0 0% 100%;
--destructive: 0 53% 54%;
--destructive-foreground: 0 0% 100%;
--border: 80 13% 82%;
--input: 80 13% 82%;
--ring: 0 53% 54%;
--sidebar: 80 17% 92%;
--sidebar-foreground: 71 20% 16%;
--sidebar-primary: 0 53% 54%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 218 16% 87%;
--sidebar-accent-foreground: 0 0% 20%;
--sidebar-border: 220 11% 84%;
--sidebar-ring: 0 76.3% 48%;
--sidebar-accent: 80 17% 92%;
--sidebar-accent-foreground: 71 20% 16%;
--sidebar-border: 80 13% 82%;
--sidebar-ring: 0 53% 54%;
--radius: 0.5rem;
--theme-primary: hsl(var(--primary));
--color-blue: 210 47% 56%;
--color-teal: 159 32% 43%;
--color-red-tint: 0 62% 95%;
--color-blue-tint: 208 54% 93%;
--color-teal-tint: 157 35% 91%;
--color-avatar: 102 21% 52%;
--event-call: 160 84% 39%;
--event-follow_up: 38 92% 48%;
--event-website_creation: 263 70% 50%;
@@ -728,3 +736,4 @@ main {
.checkbox-text { color: rgba(232,232,239,0.38); }
.footer-text { color: rgba(232,232,239,0.2); }
+12 -12
View File
@@ -7,9 +7,9 @@ interface Props {
function ErrorPage({ message }: { message: string }) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#CC0000]/10 to-[#990000]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#E0E0E0] dark:border-[#CC0000]/20 shadow-lg text-center">
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-3">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#C84B4B]/10 to-[#C84B4B]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#D4D8CC] dark:border-[#CC0000]/20 shadow-lg text-center">
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-3">
{message}
</h1>
</div>
@@ -42,24 +42,24 @@ export default async function JoinPage({ params }: Props) {
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#CC0000]/10 to-[#990000]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#E0E0E0] dark:border-[#CC0000]/20 shadow-lg text-center">
<h1 className="text-2xl font-bold text-[#111111] dark:text-white mb-3">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#C84B4B]/10 to-[#C84B4B]/10 p-4">
<div className="bg-white dark:bg-[#141414] rounded-2xl p-8 w-full max-w-md border border-[#D4D8CC] dark:border-[#CC0000]/20 shadow-lg text-center">
<h1 className="text-2xl font-bold text-[#2D3020] dark:text-white mb-3">
You&apos;re Invited!
</h1>
<p className="text-[#888888] text-sm mb-6">
<p className="text-[#8A9078] text-sm mb-6">
Someone wants to connect with you on our platform.
</p>
<div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-xl p-4 mb-6 text-left">
<p className="text-xs text-[#888888] mb-1">Contact Number</p>
<p className="text-sm font-medium text-[#111111] dark:text-white">{invite.phone}</p>
<div className="bg-[#FAFAF6] dark:bg-[#1A1A1A] rounded-xl p-4 mb-6 text-left">
<p className="text-xs text-[#8A9078] mb-1">Contact Number</p>
<p className="text-sm font-medium text-[#2D3020] dark:text-white">{invite.phone}</p>
</div>
<p className="text-xs text-[#888888] mb-6">
<p className="text-xs text-[#8A9078] mb-6">
Create an account to start making free voice calls to this person and others on the platform.
</p>
<a
href="/register"
className="block w-full bg-[#CC0000] hover:bg-[#990000] dark:bg-[#FF1111] dark:hover:bg-[#CC0000] text-white font-semibold text-sm rounded-xl py-3 transition-all duration-200"
className="block w-full bg-[#C84B4B] hover:bg-[#C84B4B]/80 dark:bg-[#FF1111] dark:hover:bg-[#CC0000] text-white font-semibold text-sm rounded-xl py-3 transition-all duration-200"
>
Create Account
</a>
+2 -1
View File
@@ -4,6 +4,7 @@ import { ThemeProvider } from "@/providers/theme-provider"
import { WebsiteThemeProvider } from "@/providers/website-theme-provider"
import { Toaster } from "@/components/ui/sonner"
import "./globals.css"
import "../../Web_Backgrounds/default-theme.css"
import "../../Web_Backgrounds/spidey-theme.css"
const inter = Inter({
@@ -32,7 +33,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} min-h-screen antialiased`}>
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
<ThemeProvider attribute="class" defaultTheme="light" disableTransitionOnChange>
<WebsiteThemeProvider>
{children}
<Toaster />
+5
View File
@@ -4,6 +4,8 @@ import { useState, useEffect, useRef, Suspense } from "react"
import { useSearchParams, useRouter } from "next/navigation"
import { Eye, EyeOff, Loader2 } from "lucide-react"
const WEBSITE_THEME_KEY = "crm-website-theme"
const waves = [
{ a: 16, f: 0.011, s: 0.018, y: 0.38, fill: "rgba(27,176,206,0.18)" },
{ a: 20, f: 0.008, s: 0.013, y: 0.52, fill: "rgba(27,176,206,0.25)" },
@@ -237,6 +239,9 @@ function LoginForm() {
})
if (res.ok) {
localStorage.removeItem(WEBSITE_THEME_KEY)
const root = document.documentElement
root.className = root.className.split(" ").filter((c) => !c.startsWith("theme-")).join(" ").trim()
const redirectTo = searchParams.get("redirect") || "/dashboard"
router.push(redirectTo)
} else {