Skip to content

Social Proof #777

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 26, 2025
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
160 changes: 160 additions & 0 deletions app/src/components/Testimonials.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Trans } from "@lingui/macro";
import { RiDoubleQuotesL } from "react-icons/ri";

import classNames from "classnames";
import { testimonials, icons } from "../lib/socialProof";

type TestimonialProps = {
quote: string;
username: string;
avatar?: string;
url: string;
site: keyof typeof icons;
index: number;
};

// Color scheme for different sites
const siteColorScheme: Record<string, { text: string; darkText: string }> = {
producthunt: {
text: "text-[#FF6154]",
darkText: "dark:text-[#FF6154]",
},
hackernews: {
text: "text-[#FF6600]",
darkText: "dark:text-[#FF6600]",
},
reddit: {
text: "text-[#FF4500]",
darkText: "dark:text-[#FF4500]",
},
};

// Rotating colors for avatar placeholder backgrounds
const avatarColors = ["bg-blue-500", "bg-purple-500", "bg-orange-500"];

function Testimonial({
quote,
username,
avatar,
url,
site,
index,
}: TestimonialProps) {
const SiteIcon = icons[site];
const colorScheme = siteColorScheme[site];
const avatarColor = avatarColors[index % avatarColors.length];
const firstLetter = username.charAt(0).toUpperCase();

return (
<div className="bg-white dark:bg-neutral-800 rounded-xl shadow-sm border border-neutral-300/80 dark:border-neutral-800/70 flex flex-col justify-between h-full transform transition-all duration-300 hover:-translate-y-1 hover:shadow overflow-hidden max-w-md w-full m-2">
<div className="p-6 relative">
<span className="absolute -top-4 left-6 text-7xl opacity-[0.05] text-purple-700 dark:text-purple-400 dark:opacity-[0.15] -ml-10 mt-4 select-none">
<RiDoubleQuotesL />
</span>

<div className="flex justify-end mb-5">
<div
className={classNames(
"inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium",
colorScheme.text,
colorScheme.darkText
)}
>
<SiteIcon size={14} className="flex-shrink-0" />
<span className="capitalize">{site}</span>
</div>
</div>

<blockquote className="relative z-10">
<p className="text-neutral-700 dark:text-neutral-300 text-sm sm:text-base !leading-normal relative">
{quote}
</p>
</blockquote>
</div>

<div className="mt-auto border-t border-neutral-200/80 dark:border-neutral-800/50 p-4 flex items-center bg-neutral-50/80 dark:bg-neutral-900/60">
{avatar ? (
<img
src={avatar}
alt={username}
className="w-9 h-9 rounded-full mr-3 object-cover ring-2 ring-white/80 dark:ring-neutral-800/80 flex-shrink-0"
/>
) : (
<div
className={`w-9 h-9 ${avatarColor} rounded-full flex items-center justify-center mr-3 text-white font-medium text-base shadow-sm flex-shrink-0`}
>
{firstLetter}
</div>
)}

<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<p className="font-medium text-xs text-neutral-700 dark:text-neutral-300 mr-2 break-words">
{username}
</p>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-neutral-400 hover:text-purple-600 dark:text-neutral-500 dark:hover:text-purple-400 transition-colors ml-auto flex-shrink-0"
aria-label={`View original post on ${site}`}
>
<SiteIcon size={16} />
</a>
</div>
</div>
</div>
</div>
);
}

function Testimonials() {
return (
<div className="py-16 md:py-20 overflow-hidden relative bg-gradient-to-b from-white to-neutral-50 dark:from-transparent dark:to-neutral-900 dark:bg-neutral-900/20">
<div className="absolute inset-0 overflow-hidden opacity-5 dark:opacity-10">
<div
className="absolute -inset-40 bg-center bg-no-repeat bg-[length:100px_100px]"
style={{ backgroundImage: 'url("/images/arrows-purple.svg")' }}
></div>
</div>

<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="text-center mb-14">
<h2 className="text-3xl md:text-4xl font-bold text-neutral-900 dark:text-white mb-4">
<Trans>What our users are saying</Trans>
</h2>
<p className="text-lg text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto">
<Trans>Join thousands of happy users who love Flowchart Fun</Trans>
</p>
</div>

<div className="flex flex-wrap justify-center">
{testimonials.map((testimonial, index) => (
<Testimonial
key={index}
quote={testimonial.quote}
username={testimonial.username}
avatar={testimonial.avatar}
url={testimonial.url}
site={testimonial.site}
index={index}
/>
))}
</div>

<div className="mt-16 text-center">
<a
href="https://www.producthunt.com/products/flowchart-fun"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-6 py-3 rounded-full text-white bg-gradient-to-r from-purple-600 to-purple-700 hover:from-purple-500 hover:to-purple-600 transition-colors font-medium text-sm shadow-sm hover:shadow"
>
<Trans>See more reviews on Product Hunt</Trans>
</a>
</div>
</div>
</div>
);
}

export default Testimonials;
54 changes: 54 additions & 0 deletions app/src/lib/socialProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FaProductHunt, FaHackerNews, FaReddit } from "react-icons/fa6";

export const icons = {
producthunt: FaProductHunt,
hackernews: FaHackerNews,
reddit: FaReddit,
};

export type Site = keyof typeof icons;

export type Testimonial = {
username: string;
quote: string;
avatar?: string;
url: string;
site: Site;
};

export const testimonials: Testimonial[] = [
{
username: `Star Boat`,
quote: `Flowchart.fun is a game changer. I've always thought if something like this exists. There we have it. I would love to try it.`,
avatar: `https://ph-avatars.imgix.net/6337586/6bacd2f7-00a8-40e9-8725-37a0c92c63eb.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=48&h=48&fit=crop&frame=1&dpr=1`,
url: `https://www.producthunt.com/products/flowchart-fun?comment=3820444#flowchart-fun-2`,
site: "producthunt",
},
{
quote: `This tool is amazing, and its coolness comes from the fact it's really simple. [...] The beauty and magic resides in the minimalism.`,
username: `sixti60`,
url: "https://news.ycombinator.com/item?id=26307334",
site: "hackernews",
},
{
quote: `Every so often a utility feels so intuitive that one thinks, "They finally got it right".
Present feeling: They finally got it right. This is how flowcharts should be made, by default.`,
username: "zupreme",
url: "https://news.ycombinator.com/item?id=26308014",
site: "hackernews",
},
{
quote: `It's incredible to see how the app has evolved over the years, especially with the rise of AI. Your ability to find the balance in AI integration is impressive.`,
username: "Emily Grace Thompson",
url: "https://www.producthunt.com/products/flowchart-fun?comment=3820429#flowchart-fun-2",
site: "producthunt",
avatar:
"https://ph-avatars.imgix.net/7359014/acb1006c-01b4-4c8a-9a61-5a8d04ef404e.png?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=48&h=48&fit=crop&frame=1&dpr=1",
},
{
quote: `YES THIS IS SO GOOD`,
username: "Immediate-Country650",
url: "https://www.reddit.com/r/software/comments/ygtaeb/comment/megmlpm/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button",
site: "reddit",
},
];
2 changes: 1 addition & 1 deletion app/src/locales/de/messages.js

Large diffs are not rendered by default.

Loading
Loading