Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
137 changes: 137 additions & 0 deletions apps/web/src/components/TrustedBy.tsx
Original file line number Diff line number Diff line change
@@ -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<number | null>(null);
const [displayCount, setDisplayCount] = useState<number>(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 (
<section className="py-20 bg-gray-850 px-4">
<div className="container mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-white mb-4">
Trusted by Production Companies & Event Professionals{" "}
<span className="text-indigo-400">Worldwide</span>
</h2>
<p className="text-gray-300 max-w-2xl mx-auto text-lg">
Join thousands of professionals who rely on SoundDocs for their event documentation
needs
</p>
</div>

{/* User Count Stat */}
<div className="max-w-4xl mx-auto mb-16">
<div className="bg-gradient-to-r from-indigo-600/20 to-purple-600/20 backdrop-blur-sm p-8 rounded-xl border border-indigo-500/30 text-center">
<div className="flex items-center justify-center mb-4">
<Globe className="h-12 w-12 text-indigo-400" />
</div>
<div className="text-5xl md:text-6xl font-bold text-white mb-2">
{userCount !== null ? `${formatNumber(displayCount)}+` : "Loading..."}
</div>
<p className="text-xl text-gray-300">
Event Professionals and Production Companies Worldwide
</p>
</div>
</div>

{/* Use Cases Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-gray-800 p-6 rounded-lg border border-gray-700 text-center hover:border-indigo-500/30 transition-all duration-300">
<div className="flex justify-center mb-4">
<div className="p-3 bg-indigo-600/20 rounded-lg">
<Building2 className="h-8 w-8 text-indigo-400" />
</div>
</div>
<h3 className="text-white font-semibold mb-2">Production Companies</h3>
<p className="text-gray-400 text-sm">
National tours, regional events, and multi-venue productions
</p>
</div>

<div className="bg-gray-800 p-6 rounded-lg border border-gray-700 text-center hover:border-indigo-500/30 transition-all duration-300">
<div className="flex justify-center mb-4">
<div className="p-3 bg-indigo-600/20 rounded-lg">
<Briefcase className="h-8 w-8 text-indigo-400" />
</div>
</div>
<h3 className="text-white font-semibold mb-2">Corporate Events</h3>
<p className="text-gray-400 text-sm">
Conferences, trade shows, and corporate presentations
</p>
</div>

<div className="bg-gray-800 p-6 rounded-lg border border-gray-700 text-center hover:border-indigo-500/30 transition-all duration-300">
<div className="flex justify-center mb-4">
<div className="p-3 bg-indigo-600/20 rounded-lg">
<Radio className="h-8 w-8 text-indigo-400" />
</div>
</div>
<h3 className="text-white font-semibold mb-2">Broadcast & Theater</h3>
<p className="text-gray-400 text-sm">
Live broadcasts, theatrical productions, and studio sessions
</p>
</div>

<div className="bg-gray-800 p-6 rounded-lg border border-gray-700 text-center hover:border-indigo-500/30 transition-all duration-300">
<div className="flex justify-center mb-4">
<div className="p-3 bg-indigo-600/20 rounded-lg">
<Users className="h-8 w-8 text-indigo-400" />
</div>
</div>
<h3 className="text-white font-semibold mb-2">Freelancers</h3>
<p className="text-gray-400 text-sm">
Independent engineers, designers, and production professionals
</p>
</div>
</div>
</div>
</section>
);
};

export default TrustedBy;
21 changes: 21 additions & 0 deletions apps/web/src/lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number | null> => {
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;
}
};
2 changes: 2 additions & 0 deletions apps/web/src/pages/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -29,6 +30,7 @@ const Landing: React.FC = () => {
<main>
<Hero />
<Features />
<TrustedBy />
<GetStarted />
</main>
<Footer />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
# Create RPC function to get total user count

This function counts all registered users from the auth.users table.
Uses SECURITY DEFINER to allow querying the auth schema.
*/

CREATE OR REPLACE FUNCTION get_total_user_count()
RETURNS bigint
LANGUAGE sql
SECURITY DEFINER
SET search_path = ''
AS $$
SELECT COUNT(*)
FROM auth.users;
$$;

-- Security: Revoke public access and grant only to necessary roles
REVOKE ALL ON FUNCTION get_total_user_count() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION get_total_user_count() TO anon, authenticated;