diff --git a/README.md b/README.md index c4adf5e..1902f08 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ SoundDocs is provided free of charge and is open source under the AGPL v3 licens ![SoundDocs Hero Preview](https://i.postimg.cc/bJdc5Hmz/Screenshot-2025-06-05-at-19-21-01.png) +## 🌍 Trusted Worldwide + +SoundDocs is trusted by production companies, freelancers, and event professionals around the globe. From national tours and corporate events to theatrical productions and broadcast operations, professionals rely on SoundDocs to streamline their documentation workflow and deliver exceptional results. + ## ✨ Core Features SoundDocs empowers you to manage event audio documentation efficiently: diff --git a/apps/web/src/components/TrustedBy.tsx b/apps/web/src/components/TrustedBy.tsx new file mode 100644 index 0000000..ec09a30 --- /dev/null +++ b/apps/web/src/components/TrustedBy.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useState } from "react"; +import { Users, Building2, Briefcase, Radio, Globe } from "lucide-react"; +import { fetchUserCount } from "../lib/supabase"; + +const TrustedBy: React.FC = () => { + const [userCount, setUserCount] = useState(null); + const [displayCount, setDisplayCount] = useState(0); + + useEffect(() => { + const loadUserCount = async () => { + const count = await fetchUserCount(); + if (count !== null) { + setUserCount(count); + } + }; + + loadUserCount(); + }, []); + + // Animated counter effect using requestAnimationFrame for smoother animation + useEffect(() => { + if (userCount === null) return; + + const duration = 2000; // 2 seconds + let animationFrameId: number; + let startTime: number | undefined; + + const animate = (timestamp: number) => { + if (startTime === undefined) { + startTime = timestamp; + } + + const elapsed = timestamp - startTime; + const progress = Math.min(elapsed / duration, 1); + const currentDisplayCount = Math.floor(progress * userCount); + + setDisplayCount(currentDisplayCount); + + if (progress < 1) { + animationFrameId = requestAnimationFrame(animate); + } + }; + + animationFrameId = requestAnimationFrame(animate); + + return () => cancelAnimationFrame(animationFrameId); + }, [userCount]); + + const formatNumber = (num: number) => { + return num.toLocaleString(); + }; + + return ( +
+
+
+

+ Trusted by Production Companies & Event Professionals{" "} + Worldwide +

+

+ Join thousands of professionals who rely on SoundDocs for their event documentation + needs +

+
+ + {/* User Count Stat */} +
+
+
+ +
+
+ {userCount !== null ? `${formatNumber(displayCount)}+` : "Loading..."} +
+

+ Event Professionals and Production Companies Worldwide +

+
+
+ + {/* Use Cases Grid */} +
+
+
+
+ +
+
+

Production Companies

+

+ National tours, regional events, and multi-venue productions +

+
+ +
+
+
+ +
+
+

Corporate Events

+

+ Conferences, trade shows, and corporate presentations +

+
+ +
+
+
+ +
+
+

Broadcast & Theater

+

+ Live broadcasts, theatrical productions, and studio sessions +

+
+ +
+
+
+ +
+
+

Freelancers

+

+ Independent engineers, designers, and production professionals +

+
+
+
+
+ ); +}; + +export default TrustedBy; diff --git a/apps/web/src/lib/supabase.ts b/apps/web/src/lib/supabase.ts index 9bf8dca..4a1c2d0 100644 --- a/apps/web/src/lib/supabase.ts +++ b/apps/web/src/lib/supabase.ts @@ -137,3 +137,24 @@ export const savePixelMap = async (payload: PixelMapPayload) => { throw error; } }; + +/** + * Fetches the total count of registered users by counting unique users who created documents. + * @returns The total number of unique users or null if query fails. + */ +export const fetchUserCount = async (): Promise => { + try { + // Use RPC function to count unique users across all document types + const { data, error } = await supabase.rpc("get_total_user_count"); + + if (error) { + console.error("Error fetching user count:", error); + return null; + } + + return data; + } catch (err) { + console.error("Error fetching user count:", err); + return null; + } +}; diff --git a/apps/web/src/pages/Landing.tsx b/apps/web/src/pages/Landing.tsx index 30b349d..681ae5e 100644 --- a/apps/web/src/pages/Landing.tsx +++ b/apps/web/src/pages/Landing.tsx @@ -3,6 +3,7 @@ import { Helmet } from "react-helmet"; import Header from "../components/Header"; import Hero from "../components/Hero"; import Features from "../components/Features"; +import TrustedBy from "../components/TrustedBy"; import GetStarted from "../components/GetStarted"; import Footer from "../components/Footer"; @@ -29,6 +30,7 @@ const Landing: React.FC = () => {
+