From 70b6414c1a8959f1d96e7eed0eb8809327fcf91f Mon Sep 17 00:00:00 2001
From: Bill Yang <45103519+goldflag@users.noreply.github.com>
Date: Thu, 8 May 2025 20:41:15 -0700
Subject: [PATCH] improve favicon and docs
---
.../components/Sidebar/SiteSelector.tsx | 13 +-
.../main/components/sections/Referrers.tsx | 6 +-
client/src/components/Favicon.tsx | 33 ++
client/src/components/MobileSidebar.tsx | 9 +-
client/src/components/SiteCard.tsx | 13 +-
docs/package-lock.json | 40 ++-
docs/package.json | 1 +
docs/src/app/components/Tweet.tsx | 287 ++++++++++++++++++
docs/src/app/page.jsx | 51 ++--
9 files changed, 391 insertions(+), 62 deletions(-)
create mode 100644 client/src/components/Favicon.tsx
create mode 100644 docs/src/app/components/Tweet.tsx
diff --git a/client/src/app/[site]/components/Sidebar/SiteSelector.tsx b/client/src/app/[site]/components/Sidebar/SiteSelector.tsx
index 378b09c..3d36a73 100644
--- a/client/src/app/[site]/components/Sidebar/SiteSelector.tsx
+++ b/client/src/app/[site]/components/Sidebar/SiteSelector.tsx
@@ -9,6 +9,7 @@ import {
} from "../../../../components/ui/dropdown-menu";
import { userStore } from "../../../../lib/userStore";
import { resetStore, useStore } from "../../../../lib/store";
+import { Favicon } from "../../../../components/Favicon";
function SiteSelectorContent() {
const { data: sites } = useGetSites();
@@ -33,11 +34,7 @@ function SiteSelectorContent() {
}`}
>
-
+
{site.domain}
{isSelected && }
@@ -60,11 +57,7 @@ export function SiteSelector() {
{site && (
-
+
{site.domain}
)}
diff --git a/client/src/app/[site]/main/components/sections/Referrers.tsx b/client/src/app/[site]/main/components/sections/Referrers.tsx
index 620a608..e050622 100644
--- a/client/src/app/[site]/main/components/sections/Referrers.tsx
+++ b/client/src/app/[site]/main/components/sections/Referrers.tsx
@@ -10,6 +10,7 @@ import { Card, CardContent } from "../../../../../components/ui/card";
import { StandardSection } from "../../../components/shared/StandardSection/StandardSection";
import { Button } from "../../../../../components/ui/button";
import { Expand } from "lucide-react";
+import { Favicon } from "../../../../../components/Favicon";
type Tab =
| "referrers"
@@ -62,10 +63,7 @@ export function Referrers() {
getLink={(e) => `https://${e.value}`}
getLabel={(e) => (
-
+
{e.value ? e.value : "Direct"}
)}
diff --git a/client/src/components/Favicon.tsx b/client/src/components/Favicon.tsx
new file mode 100644
index 0000000..6cfa901
--- /dev/null
+++ b/client/src/components/Favicon.tsx
@@ -0,0 +1,33 @@
+import { useState } from "react";
+
+export function Favicon({
+ domain,
+ className,
+}: {
+ domain: string;
+ className?: string;
+}) {
+ const [imageError, setImageError] = useState(false);
+ const firstLetter = domain.charAt(0).toUpperCase();
+
+ if (imageError) {
+ return (
+
+ {firstLetter}
+
+ );
+ }
+
+ return (
+ setImageError(true)}
+ />
+ );
+}
diff --git a/client/src/components/MobileSidebar.tsx b/client/src/components/MobileSidebar.tsx
index c6842a3..c5a469c 100644
--- a/client/src/components/MobileSidebar.tsx
+++ b/client/src/components/MobileSidebar.tsx
@@ -14,6 +14,7 @@ import {
import { Menu } from "lucide-react";
import { VisuallyHidden } from "radix-ui";
+import { Favicon } from "./Favicon";
export function MobileSidebar() {
const pathname = usePathname();
@@ -36,13 +37,7 @@ export function MobileSidebar() {
- {site && (
-
- )}
+ {site && }
);
}
diff --git a/client/src/components/SiteCard.tsx b/client/src/components/SiteCard.tsx
index af0fa04..faee7dc 100644
--- a/client/src/components/SiteCard.tsx
+++ b/client/src/components/SiteCard.tsx
@@ -6,6 +6,7 @@ import { SiteSessionChart } from "./SiteSessionChart";
import { SiteSettings } from "./SiteSettings/SiteSettings";
import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton";
+import { Favicon } from "./Favicon";
interface SiteCardProps {
siteId: number;
@@ -71,17 +72,7 @@ export function SiteCard({ siteId, domain }: SiteCardProps) {
href={`/${siteId}`}
className="group flex gap-3 items-center duration-200"
>
- {
- const target = e.target as HTMLImageElement;
- target.src =
- "https://placehold.co/48/374151/FFFFFF?text=" +
- domain.charAt(0).toUpperCase();
- }}
- />
+
{domain}
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 86cf02a..2b6ba76 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -23,6 +23,7 @@
"postcss": "^8.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "react-tweet": "^3.2.2",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4"
},
@@ -2148,7 +2149,7 @@
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
- "dev": true,
+ "devOptional": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2522,7 +2523,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true
+ "devOptional": true
},
"node_modules/cytoscape": {
"version": "3.31.2",
@@ -5903,6 +5904,20 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/react-tweet": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/react-tweet/-/react-tweet-3.2.2.tgz",
+ "integrity": "sha512-hIkxAVPpN2RqWoDEbo3TTnN/pDcp9/Jb6pTgiA4EbXa9S+m2vHIvvZKHR+eS0PDIsYqe+zTmANRa5k6+/iwGog==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.3",
+ "clsx": "^2.0.0",
+ "swr": "^2.2.4"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/reading-time": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz",
@@ -6638,6 +6653,18 @@
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="
},
+ "node_modules/swr": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz",
+ "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==",
+ "dependencies": {
+ "dequal": "^2.0.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/system-architecture": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz",
@@ -6757,7 +6784,6 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
- "dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6935,6 +6961,14 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
diff --git a/docs/package.json b/docs/package.json
index 75cee69..010b3ba 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -25,6 +25,7 @@
"postcss": "^8.5.3",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "react-tweet": "^3.2.2",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4"
},
diff --git a/docs/src/app/components/Tweet.tsx b/docs/src/app/components/Tweet.tsx
new file mode 100644
index 0000000..5caa85e
--- /dev/null
+++ b/docs/src/app/components/Tweet.tsx
@@ -0,0 +1,287 @@
+/* eslint-disable @next/next/no-img-element */
+import { Suspense } from "react";
+import {
+ enrichTweet,
+ type EnrichedTweet,
+ type TweetProps,
+ type TwitterComponents,
+} from "react-tweet";
+import { getTweet, type Tweet } from "react-tweet/api";
+
+import { cn } from "@/lib/utils";
+
+interface TwitterIconProps {
+ className?: string;
+ [key: string]: unknown;
+}
+const Twitter = ({ className, ...props }: TwitterIconProps) => (
+
+
+
+
+
+
+);
+
+const Verified = ({ className, ...props }: TwitterIconProps) => (
+
+
+
+
+
+);
+
+export const truncate = (str: string | null, length: number) => {
+ if (!str || str.length <= length) return str;
+ return `${str.slice(0, length - 3)}...`;
+};
+
+const Skeleton = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+
+export const TweetSkeleton = ({
+ className,
+ ...props
+}: {
+ className?: string;
+ [key: string]: unknown;
+}) => (
+
+);
+
+export const TweetNotFound = ({
+ className,
+ ...props
+}: {
+ className?: string;
+ [key: string]: unknown;
+}) => (
+
+
Tweet not found
+
+);
+
+export const TweetHeader = ({ tweet }: { tweet: EnrichedTweet }) => (
+
+);
+
+export const TweetBody = ({ tweet }: { tweet: EnrichedTweet }) => (
+
+ {tweet.entities.map((entity, idx) => {
+ switch (entity.type) {
+ case "url":
+ case "symbol":
+ case "hashtag":
+ case "mention":
+ return (
+
+ {entity.text}
+
+ );
+ case "text":
+ return (
+
+ );
+ }
+ })}
+
+);
+
+export const TweetMedia = ({ tweet }: { tweet: EnrichedTweet }) => {
+ if (!tweet.video && !tweet.photos) return null;
+ return (
+
+ {tweet.video && (
+
+
+ Your browser does not support the video tag.
+
+ )}
+ {tweet.photos && (
+
+
+ {tweet.photos.map((photo) => (
+
+ ))}
+
+
+ )}
+ {!tweet.video &&
+ !tweet.photos &&
+ // @ts-ignore
+ tweet?.card?.binding_values?.thumbnail_image_large?.image_value.url && (
+
+ )}
+
+ );
+};
+
+export const MagicTweet = ({
+ tweet,
+ components,
+ className,
+ ...props
+}: {
+ tweet: Tweet;
+ components?: TwitterComponents;
+ className?: string;
+}) => {
+ const enrichedTweet = enrichTweet(tweet);
+ return (
+
+
+
+ {/* */}
+
+ );
+};
+
+/**
+ * TweetCard (Server Side Only)
+ */
+export const TweetCard = async ({
+ id,
+ components,
+ fallback = ,
+ onError,
+ ...props
+}: TweetProps & {
+ className?: string;
+}) => {
+ const tweet = id
+ ? await getTweet(id).catch((err) => {
+ if (onError) {
+ onError(err);
+ } else {
+ console.error(err);
+ }
+ })
+ : undefined;
+
+ if (!tweet) {
+ const NotFound = components?.TweetNotFound || TweetNotFound;
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/docs/src/app/page.jsx b/docs/src/app/page.jsx
index ec3c5ba..d476c31 100644
--- a/docs/src/app/page.jsx
+++ b/docs/src/app/page.jsx
@@ -18,6 +18,7 @@ import { UserSessions } from "./components/Cards/UserSessions";
import { Integrations } from "./components/integrations";
import { Logo } from "./components/Logo";
import { PricingSection } from "./components/PricingSection";
+import { TweetCard } from "./components/Tweet";
const tilt_wrap = Tilt_Warp({
subsets: ["latin"],
@@ -120,41 +121,37 @@ export default function IndexPage() {
+
+
{/* Testimonial Section */}
-
-
- {/* Background glow effects - toned down */}
-
-
-
- {/* Quote mark - smaller */}
-
- "
+
+
+
+ User Testimonials
+
+ What People Are Saying
+
+
+ See what others think about Rybbit Analytics
+
+
- {/* Testimonial content */}
-
-
- Rybbit has completely transformed how we understand our users.
- The real-time data is incredible, and I've finally ditched
- Google Analytics for something that respects privacy.
-
-
-
-
-
-
Chris Weaver
-
CEO at Onyx AI
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
{/* Pricing Section */}