feat: user dashboard, gated downloads, Google OAuth sign-in #4763
1 changed file+137−20
Modifiedlanding/src/App.tsx+137−20View fileUnifiedSplit
@@ -45,40 +45,48 @@ const stagger = {
4545export default function App() {
4646 const [legalModal, setLegalModal] = useState<"privacy" | "terms" | null>(null);
4747 const [user, setUser] = useState<GoogleUser | null>(() => getStoredUser());
48 const [page, setPage] = useState<"home" | "dashboard">(() =>
49 window.location.hash === "#dashboard" ? "dashboard" : "home"
50 );
48 const [path, setPath] = useState(() => window.location.pathname);
49
50 const navigate = useCallback((to: string) => {
51 window.history.pushState({}, "", to);
52 setPath(to);
53 }, []);
5154
5255 const handleSignIn = useCallback((u: GoogleUser) => {
5356 storeUser(u);
5457 setUser(u);
55 setPage("dashboard");
56 window.location.hash = "#dashboard";
57 }, []);
58 navigate("/dashboard");
59 }, [navigate]);
5860
5961 const handleSignOut = useCallback(() => {
6062 clearUser();
6163 setUser(null);
62 setPage("home");
63 window.location.hash = "";
64 }, []);
64 navigate("/");
65 }, [navigate]);
6566
66 const goToDashboard = useCallback(() => {
67 setPage("dashboard");
68 window.location.hash = "#dashboard";
69 }, []);
67 const goToDashboard = useCallback(() => navigate("/dashboard"), [navigate]);
7068
7169 useEffect(() => {
72 const onHash = () => {
73 setPage(window.location.hash === "#dashboard" ? "dashboard" : "home");
74 };
75 window.addEventListener("hashchange", onHash);
76 return () => window.removeEventListener("hashchange", onHash);
70 const onPop = () => setPath(window.location.pathname);
71 window.addEventListener("popstate", onPop);
72 return () => window.removeEventListener("popstate", onPop);
7773 }, []);
7874
79 if (page === "dashboard" && user) {
75 if (path === "/privacy") {
76 return <LegalPage type="privacy" />;
77 }
78 if (path === "/terms") {
79 return <LegalPage type="terms" />;
80 }
81 if (path === "/support") {
82 return <SupportPage />;
83 }
84 if (path === "/dashboard" && user) {
8085 return <Dashboard user={user} onSignOut={handleSignOut} />;
8186 }
87 if (path === "/dashboard" && !user) {
88 navigate("/");
89 }
8290
8391 return (
8492 <div className="min-h-screen bg-[#09090b]">
@@ -93,7 +101,7 @@ export default function App() {
93101 <Pricing />
94102 <FAQ />
95103 <CTA user={user} onSignIn={handleSignIn} />
96 <Footer onOpenLegal={setLegalModal} />
104 <Footer onOpenLegal={(type) => navigate(`/${type}`)} />
97105 {legalModal && (
98106 <LegalModal type={legalModal} onClose={() => setLegalModal(null)} />
99107 )}
@@ -1787,3 +1795,112 @@ function TermsContent() {
17871795 </div>
17881796 );
17891797}
1798
1799function LegalPage({ type }: { type: "privacy" | "terms" }) {
1800 return (
1801 <div className="min-h-screen bg-[#09090b] text-white">
1802 <div className="border-b border-white/5 bg-[#0c0c0f]">
1803 <div className="max-w-3xl mx-auto px-6 h-16 flex items-center gap-3">
1804 <a href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
1805 <div className="w-7 h-7 rounded-lg bg-marcoreid-600 flex items-center justify-center">
1806 <Mic className="h-3.5 w-3.5 text-white" />
1807 </div>
1808 <span className="font-bold tracking-tight">Voxlen</span>
1809 </a>
1810 <span className="text-zinc-600">/</span>
1811 <span className="text-zinc-400 text-sm capitalize">{type === "privacy" ? "Privacy Policy" : "Terms of Service"}</span>
1812 </div>
1813 </div>
1814 <div className="max-w-3xl mx-auto px-6 py-12">
1815 {type === "privacy" ? <PrivacyContent /> : <TermsContent />}
1816 <div className="mt-12 pt-6 border-t border-white/10 flex gap-6 text-sm text-zinc-500">
1817 <a href="/privacy" className="hover:text-white transition-colors">Privacy Policy</a>
1818 <a href="/terms" className="hover:text-white transition-colors">Terms of Service</a>
1819 <a href="/support" className="hover:text-white transition-colors">Support</a>
1820 <a href="/" className="hover:text-white transition-colors ml-auto">← Back to Voxlen</a>
1821 </div>
1822 </div>
1823 </div>
1824 );
1825}
1826
1827function SupportPage() {
1828 return (
1829 <div className="min-h-screen bg-[#09090b] text-white">
1830 <div className="border-b border-white/5 bg-[#0c0c0f]">
1831 <div className="max-w-3xl mx-auto px-6 h-16 flex items-center gap-3">
1832 <a href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
1833 <div className="w-7 h-7 rounded-lg bg-marcoreid-600 flex items-center justify-center">
1834 <Mic className="h-3.5 w-3.5 text-white" />
1835 </div>
1836 <span className="font-bold tracking-tight">Voxlen</span>
1837 </a>
1838 <span className="text-zinc-600">/</span>
1839 <span className="text-zinc-400 text-sm">Support</span>
1840 </div>
1841 </div>
1842 <div className="max-w-3xl mx-auto px-6 py-12 space-y-10">
1843 <div>
1844 <h1 className="text-3xl font-black mb-2">Support</h1>
1845 <p className="text-zinc-400">We're here to help. Reach out through any of the channels below.</p>
1846 </div>
1847
1848 <div className="grid gap-4 sm:grid-cols-2">
1849 <a
1850 href="mailto:support@voxlen.ai"
1851 className="block p-6 rounded-2xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] transition-colors group"
1852 >
1853 <div className="text-lg font-bold mb-1 group-hover:text-marcoreid-400 transition-colors">Email Support</div>
1854 <p className="text-zinc-400 text-sm mb-3">General help, billing questions, and account issues.</p>
1855 <span className="text-marcoreid-400 text-sm font-medium">support@voxlen.ai →</span>
1856 </a>
1857 <a
1858 href="mailto:legal@voxlen.ai"
1859 className="block p-6 rounded-2xl border border-white/10 bg-white/[0.02] hover:bg-white/[0.05] transition-colors group"
1860 >
1861 <div className="text-lg font-bold mb-1 group-hover:text-marcoreid-400 transition-colors">Legal & Privacy</div>
1862 <p className="text-zinc-400 text-sm mb-3">Terms, privacy policy, and data handling enquiries.</p>
1863 <span className="text-marcoreid-400 text-sm font-medium">legal@voxlen.ai →</span>
1864 </a>
1865 </div>
1866
1867 <div className="rounded-2xl border border-white/10 bg-white/[0.02] p-6 space-y-5">
1868 <h2 className="text-lg font-bold">Frequently Asked Questions</h2>
1869 {[
1870 {
1871 q: "How do I get my API keys working?",
1872 a: "Open Voxlen → Settings → API Keys. Paste your Deepgram key in the Speech-to-Text field and your Anthropic key in the Grammar AI field. Keys are stored securely on your device and never sent to Voxlen servers.",
1873 },
1874 {
1875 q: "The iOS keyboard isn't appearing.",
1876 a: "Go to iOS Settings → General → Keyboard → Keyboards → Add New Keyboard, then find Voxlen. After adding it, tap the keyboard and enable Full Access to allow microphone use.",
1877 },
1878 {
1879 q: "How do I cancel my subscription?",
1880 a: "Your subscription is managed through Stripe. Email support@voxlen.ai with your account email and we'll process the cancellation same-day. You keep access until the end of your billing period.",
1881 },
1882 {
1883 q: "Is my dictated text private?",
1884 a: "Yes. Voxlen is a pass-through app — your audio goes directly from your device to the STT provider and your text goes directly to the grammar AI. Nothing passes through Voxlen-operated servers. Session history is stored on your device only.",
1885 },
1886 {
1887 q: "Can I use Voxlen for privileged client communications?",
1888 a: "Yes. Enable Offline Mode in Settings for fully on-device transcription (no audio leaves your device). All session history and custom vocabulary is local-only. We never store or access your content.",
1889 },
1890 ].map(({ q, a }) => (
1891 <div key={q} className="border-t border-white/5 pt-4 first:border-0 first:pt-0">
1892 <p className="font-semibold text-white mb-1">{q}</p>
1893 <p className="text-zinc-400 text-sm leading-relaxed">{a}</p>
1894 </div>
1895 ))}
1896 </div>
1897
1898 <div className="mt-8 pt-6 border-t border-white/10 flex gap-6 text-sm text-zinc-500">
1899 <a href="/privacy" className="hover:text-white transition-colors">Privacy Policy</a>
1900 <a href="/terms" className="hover:text-white transition-colors">Terms of Service</a>
1901 <a href="/" className="hover:text-white transition-colors ml-auto">← Back to Voxlen</a>
1902 </div>
1903 </div>
1904 </div>
1905 );
1906}
17901907
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts