This commit is contained in:
parent
dac9fab4a8
commit
40d44bcfda
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useApi } from '@/hooks/use-api';
|
||||
|
|
@ -17,6 +18,7 @@ export default function ConferenceDetails({ conferenceId }) {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const { token, isLoggedIn } = useAuth();
|
||||
const plausible = usePlausible();
|
||||
const apiFetch = useApi();
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [currentUserExtension, setCurrentUserExtension] = useState(null);
|
||||
|
|
@ -85,6 +87,7 @@ export default function ConferenceDetails({ conferenceId }) {
|
|||
|
||||
const handleConnectMe = async () => {
|
||||
if (!token) return;
|
||||
plausible('Conference Connect', {props: {room: conferenceId}});
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/conferences/${conferenceId}/connectme`, {
|
||||
|
|
@ -149,7 +152,7 @@ export default function ConferenceDetails({ conferenceId }) {
|
|||
<div className="container py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Button asChild variant="outline" size="sm" onClick={() => plausible('Conference Back to List')}>
|
||||
<Link href="/conferences">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Conferences
|
||||
|
|
@ -165,7 +168,7 @@ export default function ConferenceDetails({ conferenceId }) {
|
|||
Participants currently in the room.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4 mt-6">
|
||||
<Button onClick={fetchDetails} disabled={loading}>
|
||||
<Button onClick={() => { plausible('Conference Refresh'); fetchDetails(); }} disabled={loading}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
{loading ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useApi } from '@/hooks/use-api';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
|
@ -13,6 +14,7 @@ export default function ConferenceList() {
|
|||
const [conferences, setConferences] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const plausible = usePlausible();
|
||||
const apiFetch = useApi();
|
||||
|
||||
const fetchConferences = useCallback(async () => {
|
||||
|
|
@ -58,7 +60,7 @@ export default function ConferenceList() {
|
|||
Join a conversation or see who's currently in a conference.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4 mt-6">
|
||||
<Button onClick={fetchConferences} disabled={loading}>
|
||||
<Button onClick={() => { plausible('Conference Refresh'); fetchConferences(); }} disabled={loading}>
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
{loading ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
|
|
@ -114,7 +116,7 @@ export default function ConferenceList() {
|
|||
<span>{room.locked ? 'Locked' : 'Open'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Button asChild variant="outline" size="sm" onClick={() => plausible('Conference View', {props: {room: room.conferenceId}})}>
|
||||
<Link href={`/conferences?id=${room.conferenceId}`}>
|
||||
View <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useApi } from '@/hooks/use-api';
|
||||
|
|
@ -39,6 +40,7 @@ function CredentialRow({ icon, label, value, onCopy, isCopied }) {
|
|||
|
||||
export function SipCredentialsCard({ details, loading, refetchDetails }) {
|
||||
const { token } = useAuth();
|
||||
const plausible = usePlausible();
|
||||
const apiFetch = useApi();
|
||||
const [newSecret, setNewSecret] = useState(null);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
|
@ -68,6 +70,7 @@ export function SipCredentialsCard({ details, loading, refetchDetails }) {
|
|||
|
||||
const copyToClipboard = (text, id) => {
|
||||
if (!text) return;
|
||||
plausible('Dashboard Credentials Copy', {props: {field: id}});
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedState(prev => ({ ...prev, [id]: true }));
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useApi } from '@/hooks/use-api';
|
||||
|
|
@ -26,6 +27,7 @@ import { Badge } from "@/components/ui/badge"
|
|||
|
||||
export default function ApiKeys() {
|
||||
const { token } = useAuth();
|
||||
const plausible = usePlausible();
|
||||
const apiFetch = useApi();
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -76,6 +78,7 @@ export default function ApiKeys() {
|
|||
}
|
||||
// Use newKeyData.apiKey from the response
|
||||
setNewKey(newKeyData.apiKey);
|
||||
plausible('Dashboard Create API Key');
|
||||
|
||||
// Add the new key to the local state, transforming the object to match the table's expected structure
|
||||
const keyForState = {
|
||||
|
|
@ -108,6 +111,7 @@ export default function ApiKeys() {
|
|||
throw new Error(errorData.error);
|
||||
}
|
||||
setKeys(prevKeys => prevKeys.filter(key => key.key !== keyToDelete));
|
||||
plausible('Dashboard Delete API Key');
|
||||
} catch (error) {
|
||||
if (error.message !== 'Unauthorized') {
|
||||
console.error("Failed to delete key", error);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useApi } from '@/hooks/use-api';
|
||||
|
|
@ -41,6 +42,7 @@ function OverviewCard({ details, deviceStatus, loading }) {
|
|||
|
||||
export default function DashboardClient() {
|
||||
const { isLoggedIn, loading, logout, token, noExtension } = useAuth();
|
||||
const plausible = usePlausible();
|
||||
const [details, setDetails] = useState(null);
|
||||
const [detailsLoading, setDetailsLoading] = useState(true);
|
||||
const [deviceStatus, setDeviceStatus] = useState(null);
|
||||
|
|
@ -110,6 +112,7 @@ export default function DashboardClient() {
|
|||
}, [token, deviceStatusLoading, apiFetch]);
|
||||
|
||||
const handleLogin = () => {
|
||||
plausible('Dashboard Login');
|
||||
window.location.href = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/auth/discord`;
|
||||
};
|
||||
|
||||
|
|
@ -217,7 +220,7 @@ export default function DashboardClient() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" onClick={logout} className="w-full sm:w-auto">
|
||||
<Button variant="outline" onClick={() => { plausible('Dashboard Logout'); logout(); }} className="w-full sm:w-auto">
|
||||
<LogOut className="h-4 w-4 mr-2" />
|
||||
Logout
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useApi } from '@/hooks/use-api';
|
||||
|
|
@ -66,6 +67,7 @@ function DeviceCard({ device }) {
|
|||
|
||||
export function RegisteredDevices() {
|
||||
const { token } = useAuth();
|
||||
const plausible = usePlausible();
|
||||
const [devices, setDevices] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
|
|
@ -81,6 +83,9 @@ export function RegisteredDevices() {
|
|||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setDevices(Array.isArray(data) ? data : []);
|
||||
if (isInitialLoad && Array.isArray(data) && data.length > 0) {
|
||||
plausible('Dashboard View Devices', {props: {count: data.length}});
|
||||
}
|
||||
} else {
|
||||
setDevices([]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { usePlausible } from 'next-plausible';
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useApi } from "@/hooks/use-api";
|
||||
|
|
@ -49,6 +50,7 @@ function formatVoicemailDate(dateStr) {
|
|||
|
||||
export function VoicemailPanel({ extensionId: extProp }) {
|
||||
const { token } = useAuth();
|
||||
const plausible = usePlausible();
|
||||
const apiFetch = useApi();
|
||||
const [extensionId, setExtensionId] = useState(extProp || null);
|
||||
const [loadingExt, setLoadingExt] = useState(!extProp);
|
||||
|
|
@ -127,6 +129,9 @@ export function VoicemailPanel({ extensionId: extProp }) {
|
|||
const toggleExpand = async (vm) => {
|
||||
const isOpening = expandedId !== vm.messageId;
|
||||
setExpandedId(isOpening ? vm.messageId : null);
|
||||
if (isOpening) {
|
||||
plausible('Dashboard Play Voicemail');
|
||||
}
|
||||
if (isOpening && vm.hasAudio && !audioMap[vm.messageId]?.url) {
|
||||
setAudioMap(m => ({ ...m, [vm.messageId]: { url: null, loading: true } }));
|
||||
try {
|
||||
|
|
@ -169,6 +174,7 @@ export function VoicemailPanel({ extensionId: extProp }) {
|
|||
|
||||
const downloadVoicemail = async (vm) => {
|
||||
if (!vm.hasAudio) return;
|
||||
plausible('Dashboard Download Voicemail');
|
||||
try {
|
||||
const res = await apiFetch(`${process.env.NEXT_PUBLIC_API_URL}/extensions/${extensionId}/voicemails/${vm.messageId}/download`);
|
||||
if (!res.ok) return;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { usePlausible } from 'next-plausible'
|
||||
import { Phone, Users, VoicemailIcon, Radio, MoreHorizontal, Network, Bot, LayoutDashboard } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
|
@ -67,8 +69,26 @@ const colorClasses = {
|
|||
}
|
||||
|
||||
export default function Features() {
|
||||
const plausible = usePlausible();
|
||||
const featuresRef = useRef(null);
|
||||
const hasFiredRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = featuresRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting && !hasFiredRef.current) {
|
||||
hasFiredRef.current = true;
|
||||
plausible('Scroll to Features');
|
||||
observer.unobserve(el);
|
||||
}
|
||||
}, { threshold: 0.3 });
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [plausible]);
|
||||
|
||||
return (
|
||||
<section id="features" className="relative py-32 overflow-hidden">
|
||||
<section ref={featuresRef} id="features" className="relative py-32 overflow-hidden">
|
||||
<div className="container relative">
|
||||
<div className="text-center max-w-3xl mx-auto mb-16 px-4">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-white mb-6 bg-gradient-to-r from-white via-blue-100 to-white bg-clip-text text-transparent pb-2">
|
||||
|
|
|
|||
|
|
@ -23,19 +23,19 @@ export default function Header() {
|
|||
<span className="hidden font-bold text-white sm:inline-block">LiteNet</span>
|
||||
</Link>
|
||||
<nav className="flex items-center space-x-6 text-sm font-medium">
|
||||
<Link href="/#features" className="text-gray-300 hover:text-white">
|
||||
<Link href="/#features" className="text-gray-300 hover:text-white" onClick={() => plausible('Nav Click', {props: {section: 'Features'}})}>
|
||||
Features
|
||||
</Link>
|
||||
<Link href="/#updates" className="text-gray-300 hover:text-white">
|
||||
<Link href="/#updates" className="text-gray-300 hover:text-white" onClick={() => plausible('Nav Click', {props: {section: 'Updates'}})}>
|
||||
Updates
|
||||
</Link>
|
||||
<Link href="/#team" className="text-gray-300 hover:text-white">
|
||||
<Link href="/#team" className="text-gray-300 hover:text-white" onClick={() => plausible('Nav Click', {props: {section: 'Team'}})}>
|
||||
Team
|
||||
</Link>
|
||||
<Link href="/hardware-survey" className="text-gray-300 hover:text-white">
|
||||
<Link href="/hardware-survey" className="text-gray-300 hover:text-white" onClick={() => plausible('Nav Click', {props: {section: 'Hardware Survey'}})}>
|
||||
Hardware Survey
|
||||
</Link>
|
||||
<Link href="/conferences" className="text-gray-300 hover:text-white">
|
||||
<Link href="/conferences" className="text-gray-300 hover:text-white" onClick={() => plausible('Nav Click', {props: {section: 'Conferences'}})}>
|
||||
Conferences
|
||||
</Link>
|
||||
</nav>
|
||||
|
|
@ -119,35 +119,35 @@ export default function Header() {
|
|||
<Link
|
||||
href="/#features"
|
||||
className="text-gray-300 hover:text-white transition-colors px-2 py-2 rounded-md hover:bg-gray-800/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => { plausible('Nav Click', {props: {section: 'Features'}}); setMobileMenuOpen(false); }}
|
||||
>
|
||||
Features
|
||||
</Link>
|
||||
<Link
|
||||
href="/#updates"
|
||||
className="text-gray-300 hover:text-white transition-colors px-2 py-2 rounded-md hover:bg-gray-800/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => { plausible('Nav Click', {props: {section: 'Updates'}}); setMobileMenuOpen(false); }}
|
||||
>
|
||||
Updates
|
||||
</Link>
|
||||
<Link
|
||||
href="/#team"
|
||||
className="text-gray-300 hover:text-white transition-colors px-2 py-2 rounded-md hover:bg-gray-800/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => { plausible('Nav Click', {props: {section: 'Team'}}); setMobileMenuOpen(false); }}
|
||||
>
|
||||
Team
|
||||
</Link>
|
||||
<Link
|
||||
href="/hardware-survey"
|
||||
className="text-gray-300 hover:text-white transition-colors px-2 py-2 rounded-md hover:bg-gray-800/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => { plausible('Nav Click', {props: {section: 'Hardware Survey'}}); setMobileMenuOpen(false); }}
|
||||
>
|
||||
Hardware Survey
|
||||
</Link>
|
||||
<Link
|
||||
href="/conferences"
|
||||
className="text-gray-300 hover:text-white transition-colors px-2 py-2 rounded-md hover:bg-gray-800/50"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => { plausible('Nav Click', {props: {section: 'Conferences'}}); setMobileMenuOpen(false); }}
|
||||
>
|
||||
Conferences
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export default function Hero() {
|
|||
<Button
|
||||
size="lg"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white shadow-lg hover:shadow-xl transition-all duration-300 group"
|
||||
onClick={() => plausible('CTA Get Started')}
|
||||
asChild
|
||||
>
|
||||
<Link href="/dashboard">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { usePlausible } from 'next-plausible'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
|
@ -69,8 +71,26 @@ function TeamMember({ name, ext, image, discord, role }) {
|
|||
}
|
||||
|
||||
export default function Team() {
|
||||
const plausible = usePlausible();
|
||||
const teamRef = useRef(null);
|
||||
const hasFiredRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = teamRef.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting && !hasFiredRef.current) {
|
||||
hasFiredRef.current = true;
|
||||
plausible('Scroll to Team');
|
||||
observer.unobserve(el);
|
||||
}
|
||||
}, { threshold: 0.3 });
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [plausible]);
|
||||
|
||||
return (
|
||||
<section id="team" className="relative py-32 overflow-hidden">
|
||||
<section ref={teamRef} id="team" className="relative py-32 overflow-hidden">
|
||||
<div className="container relative">
|
||||
<div className="text-center max-w-3xl mx-auto mb-16 px-4">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-white mb-6 bg-gradient-to-r from-white via-purple-100 to-white bg-clip-text text-transparent pb-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue