diff --git a/Web_Backgrounds/spidey-theme.css b/Web_Backgrounds/spidey-theme.css index 1fbd3d7..d59ec03 100644 --- a/Web_Backgrounds/spidey-theme.css +++ b/Web_Backgrounds/spidey-theme.css @@ -4,14 +4,37 @@ ============================================================================ */ .theme-spidey { + --background: 240 18% 97%; + --foreground: 240 8% 14%; + --card: 0 0% 100%; + --card-foreground: 240 8% 14%; + --popover: 0 0% 100%; + --popover-foreground: 240 8% 14%; + --secondary: 220 75% 48%; + --secondary-foreground: 0 0% 100%; + --muted: 240 8% 92%; + --muted-foreground: 240 5% 50%; + --accent: 220 15% 90%; + --accent-foreground: 220 60% 28%; + --destructive: 0 70% 46%; + --destructive-foreground: 0 0% 100%; + --border: 240 8% 84%; + --input: 240 8% 84%; + --radius: 0.5rem; + --sidebar: 0 0% 99%; + --sidebar-foreground: 240 8% 14%; + --sidebar-accent: 220 20% 93%; + --sidebar-accent-foreground: 220 70% 28%; + --sidebar-border: 240 8% 84%; +} + +.dark.theme-spidey { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - --primary: 0 100% 53%; - --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; @@ -22,16 +45,12 @@ --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; - --ring: 0 100% 53%; --radius: 0.5rem; --sidebar: 222.2 84% 4.9%; --sidebar-foreground: 210 40% 98%; - --sidebar-primary: 0 100% 53%; - --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 217.2 32.6% 17.5%; --sidebar-accent-foreground: 210 40% 98%; --sidebar-border: 217.2 32.6% 17.5%; - --sidebar-ring: 0 100% 53%; } .theme-spidey body::before { diff --git a/src/app/(dashboard)/ai-assistant/page.tsx b/src/app/(dashboard)/ai-assistant/page.tsx index 5e43d62..2a30a5b 100644 --- a/src/app/(dashboard)/ai-assistant/page.tsx +++ b/src/app/(dashboard)/ai-assistant/page.tsx @@ -76,17 +76,17 @@ export default function AIAssistantPage() { }, []) return ( -
-
-
+
+
+
-
+
-

AI Sales Assistant

-

Powered by local AI

+

AI Sales Assistant

+

Powered by local AI

@@ -94,30 +94,30 @@ export default function AIAssistantPage() { Online
-
-
- Local AI Model +
+
+ Local AI Model
-
+
- - Target Job + + Target Job
{selectedJob && ( -
-

{selectedJob.job_title}

- {selectedJob.industry} -

{selectedJob.description}

+
+

{selectedJob.job_title}

+ {selectedJob.industry} +

{selectedJob.description}

{selectedJob.keywords.map((kw, i) => ( - {kw} + {kw} ))}
@@ -125,48 +125,48 @@ export default function AIAssistantPage() {
- - Quick Tips + + Quick Tips
{tips.map((tip, i) => (
handleTipClick(tip)} - className="flex items-start gap-2.5 bg-[#1a1d2e]/50 hover:bg-[#1a1d2e] border border-[#ffffff08] hover:border-[#f97316]/20 rounded-xl p-3.5 mb-2 cursor-pointer transition-all duration-200 group" + 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" > - - {tip} + + {tip}
))}
- - Recent Prompts + + Recent Prompts
{recentPrompts.length > 0 ? ( recentPrompts.map((prompt, i) => (
handleRecentPromptClick(prompt)} - className="bg-[#1a1d2e]/50 rounded-xl p-3 mb-2 border border-[#ffffff08] text-[#6b7280] text-xs truncate hover:text-[#9ca3af] cursor-pointer transition-colors duration-200" + 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}
)) ) : ( -
Your recent prompts will appear here
+
Your recent prompts will appear here
)}
-
+
-
Messages today
-
24
+
Messages today
+
24
-
Tokens used
-
12.4k
+
Tokens used
+
12.4k
diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index d061bf2..cb288a3 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -24,8 +24,10 @@ import { SelectValue, } from "@/components/ui/select" import { DashboardStats } from "@/types" +import { useWebsiteTheme } from "@/providers/website-theme-provider" export default function DashboardPage() { + const { websiteTheme } = useWebsiteTheme() const [period, setPeriod] = useState("6months") const [stats, setStats] = useState(null) const pollingRef = useRef(null) @@ -84,7 +86,7 @@ export default function DashboardPage() {
-

Pipeline Overview

+

Pipeline Overview

{stats @@ -95,7 +97,7 @@ export default function DashboardPage() { }
-

Analytics

+

Analytics

@@ -104,12 +106,13 @@ export default function DashboardPage() {
- {/* Daily Bugle watermark */} -
- - DAILY BUGLE - -
+ {websiteTheme === "spidey" && ( +
+ + DAILY BUGLE + +
+ )}
) } diff --git a/src/app/api/ai/giphy/route.ts b/src/app/api/ai/giphy/route.ts new file mode 100644 index 0000000..be69b06 --- /dev/null +++ b/src/app/api/ai/giphy/route.ts @@ -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 }) + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index ea1cd8a..5e1fbcc 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -12,6 +12,7 @@ import { setSessionContext, SESSION_COOKIE, } from "@/lib/auth" +import { query } from "@/lib/db" function jsonResponse(data: unknown, status: number) { return new Response(JSON.stringify(data), { @@ -114,6 +115,11 @@ export async function POST(request: NextRequest) { true ) + await query( + `UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`, + [dbUser.id, JSON.stringify({ website_theme: "default" })], + ) + const token = await createSession(dbUser.id, dbUser.role_name) await setSessionContext(dbUser.id, ipAddress) diff --git a/src/app/api/dashboard/route.ts b/src/app/api/dashboard/route.ts index f1a2112..940b71b 100644 --- a/src/app/api/dashboard/route.ts +++ b/src/app/api/dashboard/route.ts @@ -80,8 +80,8 @@ function countStatuses(leads: any[]) { return counts } -function buildMonthlyBreakdown(leads: any[], period: string) { - const { start, end } = getPeriodDateRange(period) +function buildMonthlyBreakdown(leads: any[], period: string, rangeOverride?: { start: Date; end: Date }) { + const { start, end } = rangeOverride || getPeriodDateRange(period) const result: { label: string; total: number; open: number; contacted: number; pending: number; closed: number; ignored: number }[] = [] const current = new Date(start) const isMonthly = period === "6months" || period === "12months" @@ -170,7 +170,7 @@ export async function GET(request: NextRequest) { updatedAt: r.updated_at, })) - const monthlyBreakdown = buildMonthlyBreakdown(currentLeads, period) + const monthlyBreakdown = buildMonthlyBreakdown(currentLeads, period, yearParam ? { start, end } : undefined) const trends = { totalLeads: computeTrend(currentCounts.open + currentCounts.contacted + currentCounts.pending + currentCounts.closed + currentCounts.ignored, diff --git a/src/app/api/gifs/route.ts b/src/app/api/gifs/route.ts index 8ecf18e..1b7c5b6 100644 --- a/src/app/api/gifs/route.ts +++ b/src/app/api/gifs/route.ts @@ -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 }) } } diff --git a/src/app/api/system/monitor/route.ts b/src/app/api/system/monitor/route.ts index 0051f42..baecb2e 100644 --- a/src/app/api/system/monitor/route.ts +++ b/src/app/api/system/monitor/route.ts @@ -8,9 +8,6 @@ let prevTime = Date.now() export async function GET() { const sessionUser = await getSessionUser() if (!sessionUser) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) - if (sessionUser.role !== "admin" && sessionUser.role !== "super_admin") { - return NextResponse.json({ error: "Forbidden" }, { status: 403 }) - } const now = Date.now() const elapsed = now - prevTime diff --git a/src/app/call/[roomId]/page.tsx b/src/app/call/[roomId]/page.tsx index 780d963..f438469 100644 --- a/src/app/call/[roomId]/page.tsx +++ b/src/app/call/[roomId]/page.tsx @@ -53,24 +53,24 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str if (!joined) { return ( -
-
+
+
{connectionTimeout ? ( <> -

This call is no longer available.

+

This call is no longer available.

) : !isReady ? ( <>
- -

Connecting to call...

+ +

Connecting to call...

) : ( <> -

Join Call

+

Join Call

{micError && ( -

{micError}

+

{micError}

)} 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]" />
@@ -112,21 +112,21 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str {callState === "waiting" && (
-

Waiting for participant

-

Share the call link to invite someone...

+

Waiting for participant

+

Share the call link to invite someone...

{participants.length > 0 && ( -
-
+
+
Participants ({participants.length})
{participants.map((p) => ( -
{p.label}
+
{p.label}
))}
)}
-
@@ -135,21 +135,21 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str {callState === "participant_joined" && (
-

Participant joined

+

Participant joined

{participants.length > 0 && ( -
-
+
+
Participants ({participants.length})
{participants.map((p) => ( -
{p.label}
+
{p.label}
))}
)} -

Connecting...

+

Connecting...

-
@@ -158,10 +158,10 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str {callState === "connecting" && (
-

Connecting

-

Establishing secure connection...

+

Connecting

+

Establishing secure connection...

-
@@ -170,37 +170,37 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str {callState === "connected" && (
-

Connected

+

Connected

{participants.length > 0 && ( -
-
+
+
Participants ({participants.length})
{participants.map((p) => ( -
{p.label}
+
{p.label}
))}
)} -
+
{formatDuration(callDuration)}
@@ -210,15 +210,15 @@ export default function CallRoomPage({ params }: { params: Promise<{ roomId: str {(callState === "ended" || callState === "idle") && (
-

Call ended

+

Call ended

{callDuration > 0 && ( -

Duration: {formatDuration(callDuration)}

+

Duration: {formatDuration(callDuration)}

)}
)} {error && ( -

{error}

+

{error}

)}
diff --git a/src/app/globals.css b/src/app/globals.css index 4e4f58f..7ccdf14 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -78,69 +78,88 @@ } :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%; } .dark { - --background: 0 0% 10%; - --foreground: 210 40% 98%; - --card: 0 0% 9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 11%; - --popover-foreground: 210 40% 98%; - --primary: 0 100% 53%; + /* ── Slate palette tokens ── */ + --bg-page: 225 18% 9%; + --bg-sidebar: 227 25% 7%; + --bg-card: 224 24% 11%; + --bg-card-inset: 224 24% 13%; + --bg-nav-active: 221 43% 20%; + --border: 228 21% 21%; + --border-strong: 220 69% 59%; + --text-primary: 226 30% 95%; + --text-secondary: 228 13% 59%; + --text-active: 221 100% 87%; + --accent-hover: 220 70% 66%; + + /* ── Existing CSS variable overrides ── */ + --background: 225 18% 9%; + --foreground: 226 30% 95%; + --card: 224 24% 11%; + --card-foreground: 226 30% 95%; + --popover: 224 24% 13%; + --popover-foreground: 226 30% 95%; + --primary: 220 69% 59%; --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --secondary: 224 24% 13%; + --secondary-foreground: 226 30% 95%; + --muted: 224 24% 13%; + --muted-foreground: 228 13% 59%; + --accent: 224 24% 13%; + --accent-foreground: 226 30% 95%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 0 100% 53%; - --sidebar: 0 0% 11%; - --sidebar-foreground: 210 40% 98%; - --sidebar-primary: 145 65% 50%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 217.2 32.6% 17.5%; - --sidebar-accent-foreground: 210 40% 98%; - --sidebar-border: 217.2 32.6% 17.5%; - --sidebar-ring: 0 100% 53%; - + --border: 228 21% 21%; + --input: 228 21% 21%; + --ring: 220 69% 59%; + --sidebar: 227 25% 7%; + --sidebar-foreground: 226 30% 95%; + --sidebar-primary: 221 43% 20%; + --sidebar-primary-foreground: 221 100% 87%; + --sidebar-accent: 224 24% 13%; + --sidebar-accent-foreground: 226 30% 95%; + --sidebar-border: 228 21% 21%; + --sidebar-ring: 220 69% 59%; } .ocean { diff --git a/src/app/join/[token]/page.tsx b/src/app/join/[token]/page.tsx index fb1fb49..690040e 100644 --- a/src/app/join/[token]/page.tsx +++ b/src/app/join/[token]/page.tsx @@ -7,9 +7,9 @@ interface Props { function ErrorPage({ message }: { message: string }) { return ( -
-
-

+
+
+

{message}

@@ -42,24 +42,24 @@ export default async function JoinPage({ params }: Props) { } return ( -
-
-

+
+
+

You're Invited!

-

+

Someone wants to connect with you on our platform.

-
-

Contact Number

-

{invite.phone}

+
+

Contact Number

+

{invite.phone}

-

+

Create an account to start making free voice calls to this person and others on the platform.

Create Account diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ad977a6..ceac5ab 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -33,7 +33,7 @@ export default function RootLayout({ return ( - + {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 587932b..081bf85 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -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)" }, @@ -227,6 +229,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 { diff --git a/src/components/ai/ai-chat.tsx b/src/components/ai/ai-chat.tsx index 7b5ba2d..b289b77 100644 --- a/src/components/ai/ai-chat.tsx +++ b/src/components/ai/ai-chat.tsx @@ -2,13 +2,14 @@ import { useState, useRef, useEffect, Fragment, forwardRef, useImperativeHandle, useCallback } from "react" import { Bot, Terminal } from "lucide-react" +import { GifPicker } from "./gif-picker" function linkifyText(text: string) { const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/ const parts = text.split(urlRegex) return parts.map((part, i) => { if (part.startsWith("http://") || part.startsWith("https://")) { - return {part} + return {part} } return {part} }) @@ -22,7 +23,7 @@ function formatContent(text: string) { const content = trimmed.replace(/^[•\-]\s*/, "") return (
- + {linkifyText(content)}
) @@ -35,27 +36,17 @@ function formatContent(text: string) { interface ChatMessage { role: "user" | "assistant" content: string + gif?: { + url: string + previewUrl: string + title: string + } } interface AIChatProps { onMessageSent?: (msg: string) => void } -const quickActions = [ - { icon: "✉️", iconBg: "bg-[#f97316]/10", iconColor: "text-[#f97316]", title: "Cold Email Template", desc: "Generate targeted outreach emails", prompt: "Write a cold email template for a Software Developer" }, - { icon: "🛡️", iconBg: "bg-[#3b82f6]/10", iconColor: "text-[#3b82f6]", title: "Handle Objections", desc: "Get scripts for common objections", prompt: "Give me objection handling scripts for sales" }, - { icon: "🎯", iconBg: "bg-[#8b5cf6]/10", iconColor: "text-[#8b5cf6]", title: "Target Industry", desc: "Find leads in specific industries", prompt: "How do I target leads in the tech industry" }, - { icon: "📋", iconBg: "bg-[#22c55e]/10", iconColor: "text-[#22c55e]", title: "Build Lead List", desc: "Strategies to grow your pipeline", prompt: "Help me build a lead list strategy" }, - { icon: "📞", iconBg: "bg-[#f97316]/10", iconColor: "text-[#f97316]", title: "Call Scripts", desc: "Proven phone sales scripts", prompt: "Give me a cold call script for outreach" }, - { icon: "📊", iconBg: "bg-[#ec4899]/10", iconColor: "text-[#ec4899]", title: "Sales Strategy", desc: "Industry specific sales tactics", prompt: "What sales strategies work best per industry" }, -] - -const commandPills = [ - { icon: "📋", label: "Lists", prompt: "lists" }, - { icon: "👥", label: "Leads", prompt: "leads" }, - { icon: "💡", label: "Tips", prompt: "Give me some sales tips" }, - { icon: "✉️", label: "Templates", prompt: "Show me email templates" }, -] export interface AIChatHandle { fillInput: (text: string) => void @@ -68,9 +59,12 @@ export const AIChat = forwardRef(({ onMessageSent }, const [loading, setLoading] = useState(false) const [error, setError] = useState("") const [bootState, setBootState] = useState<"booting" | "ready" | "error">("booting") + const [showGifPicker, setShowGifPicker] = useState(false) const messagesEndRef = useRef(null) const textareaRef = useRef(null) + const containerRef = useRef(null) const hasUserMessage = messages.some(m => m.role === "user") + const loadedFromStorage = useRef(false) useImperativeHandle(ref, () => ({ fillInput(text: string) { @@ -90,6 +84,44 @@ export const AIChat = forwardRef(({ onMessageSent }, }, }), []) + const handleGifSelect = useCallback((gif: { url: string; previewUrl: string; title: string }) => { + setMessages((prev) => [...prev, { role: "user", content: gif.title || "Sent a GIF", gif }]) + setShowGifPicker(false) + }, []) + + useEffect(() => { + if (showGifPicker) { + const handler = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setShowGifPicker(false) + } + } + document.addEventListener("mousedown", handler) + return () => document.removeEventListener("mousedown", handler) + } + }, [showGifPicker]) + + useEffect(() => { + const saved = localStorage.getItem("ai-chat-messages") + if (saved) { + try { + const parsed = JSON.parse(saved) + if (Array.isArray(parsed) && parsed.length > 0) { + setMessages(parsed) + loadedFromStorage.current = true + } + } catch {} + } else { + loadedFromStorage.current = false + } + }, []) + + useEffect(() => { + if (messages.length > 0) { + localStorage.setItem("ai-chat-messages", JSON.stringify(messages)) + } + }, [messages]) + const checkServer = useCallback(async () => { try { const res = await fetch("/api/ai/chat", { @@ -108,6 +140,7 @@ export const AIChat = forwardRef(({ onMessageSent }, }, []) useEffect(() => { + if (loadedFromStorage.current) return fetch("/api/ai/jobs") .then((r) => r.json()) .then((data) => { @@ -192,34 +225,24 @@ export const AIChat = forwardRef(({ onMessageSent }, t.style.height = t.scrollHeight + "px" }, []) - const handleQuickAction = useCallback((prompt: string) => { - setInput(prompt) - setTimeout(() => sendMessage(prompt), 50) - }, [sendMessage]) - - const handleCommandPill = useCallback((prompt: string) => { - setInput(prompt) - setTimeout(() => sendMessage(prompt), 50) - }, [sendMessage]) - if (bootState === "booting") { return (
-

Servers booting...

-
+

Servers booting...

+
- - - - - - - - - - + + + + + + + + + +
@@ -230,48 +253,23 @@ export const AIChat = forwardRef(({ onMessageSent }, if (bootState === "ready" && !hasUserMessage) { return ( -
+
-
+
-
+
-

What can I help you with?

-

Your AI sales assistant is ready. Choose a quick action or type your question below.

-
-
- {quickActions.map((action, i) => ( -
handleQuickAction(action.prompt)} - className="float-in bg-[#1a1d2e] hover:bg-[#1f2437] rounded-xl p-4 border border-[#ffffff0a] hover:border-[#f97316]/30 cursor-pointer transition-all duration-200 hover:shadow-[0_4px_20px_rgba(249,115,22,0.1)] hover:-translate-y-0.5" - style={{ animationDelay: `${0.1 + i * 0.05}s` }} - > -
{action.icon}
-

{action.title}

-

{action.desc}

-
- ))} -
-
- {commandPills.map((pill, i) => ( -
handleCommandPill(pill.prompt)} - className="bg-[#1a1d2e] border border-[#ffffff0f] hover:border-[#f97316]/40 hover:bg-[#1f2437] rounded-full px-4 py-2 text-xs text-[#9ca3af] hover:text-[#f97316] transition-all duration-200 cursor-pointer flex items-center gap-1.5" - > - {pill.icon} - {pill.label} -
- ))} +

What can I help you with?

+

Your AI sales assistant is ready. Choose a quick action or type your question below.

-
+
-
- +
+ + {showGifPicker && setShowGifPicker(false)} />}