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} + {site.domain}
{isSelected && } @@ -60,11 +57,7 @@ export function SiteSelector() { {site && (
- {site.domain} +
{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 ( + {`Favicon 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.domain} - )} + {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" > - {domain} { - 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 && ( + + )} + {tweet.photos && ( +
+
+ {tweet.photos.map((photo) => ( + {tweet.text} + ))} +
+
+ )} + {!tweet.video && + !tweet.photos && + // @ts-ignore + tweet?.card?.binding_values?.thumbnail_image_large?.image_value.url && ( + {tweet.text} + )} +
+ ); +}; + +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 */}