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