Innovations

Google Review Cards

Google-styled review cards with colored avatar initials, star ratings, verified badges, and an overall rating summary.

Preview

Source

tsx
"use client";

import { Star } from "lucide-react";
import { googleReviews } from "@/lib/placeholders";

const AVATAR_COLORS = [
  "bg-blue-500",
  "bg-emerald-500",
  "bg-violet-500",
  "bg-amber-500",
  "bg-rose-500",
  "bg-cyan-500",
];

function GoogleGLogo() {
  return (
    <svg viewBox="0 0 24 24" className="w-4 h-4" aria-label="Google">
      <circle cx="12" cy="12" r="12" fill="white" />
      <path
        d="M21.805 12.227c0-.709-.063-1.39-.181-2.045H12v3.868h5.507a4.706 4.706 0 01-2.04 3.086v2.565h3.304c1.932-1.779 3.034-4.401 3.034-7.474z"
        fill="#4285F4"
      />
      <path
        d="M12 22c2.763 0 5.079-.915 6.771-2.479l-3.304-2.565c-.916.614-2.088.977-3.467.977-2.665 0-4.924-1.8-5.732-4.22H2.862v2.646A10.002 10.002 0 0012 22z"
        fill="#34A853"
      />
      <path
        d="M6.268 13.713A6.009 6.009 0 016 12c0-.595.082-1.174.268-1.713V7.641H2.862A10.002 10.002 0 002 12c0 1.614.386 3.141 1.062 4.499l3.206-2.786z"
        fill="#FBBC05"
      />
      <path
        d="M12 5.567c1.501 0 2.848.516 3.908 1.528l2.932-2.932C17.075 2.475 14.759 1.5 12 1.5A10.002 10.002 0 002.862 7.641L6.068 10.287C6.876 7.867 9.135 5.567 12 5.567z"
        fill="#EA4335"
      />
    </svg>
  );
}

function StarRating({ rating, size = "sm" }: { rating: number; size?: "sm" | "md" }) {
  const cls = size === "md" ? "w-5 h-5" : "w-3.5 h-3.5";
  return (
    <div className="flex gap-0.5">
      {Array.from({ length: 5 }).map((_, i) => (
        <Star
          key={i}
          className={`${cls} ${i < rating ? "fill-amber-400 text-amber-400" : "fill-muted text-muted"}`}
        />
      ))}
    </div>
  );
}

export default function GoogleReviewCards() {
  const avgRating = (googleReviews.reduce((s, r) => s + r.rating, 0) / googleReviews.length).toFixed(1);

  return (
    <section className="py-20 px-4 sm:px-6 bg-background">
      <div className="max-w-4xl mx-auto">
        {/* Overall rating summary */}
        <div className="flex flex-col sm:flex-row items-center gap-4 mb-12 bg-card border border-border rounded-2xl p-6 shadow-sm">
          <div className="flex flex-col items-center sm:items-start gap-1 sm:pr-8 sm:border-r sm:border-border">
            <span className="text-5xl font-extrabold text-foreground">{avgRating}</span>
            <StarRating rating={5} size="md" />
            <span className="text-sm text-muted-foreground mt-1">Based on 127 reviews</span>
          </div>
          <div className="flex items-center gap-3 sm:pl-8">
            <div className="flex items-center justify-center w-10 h-10 rounded-full bg-white border border-border shadow-sm">
              <GoogleGLogo />
            </div>
            <div>
              <p className="font-semibold text-foreground">Google Reviews</p>
              <p className="text-sm text-muted-foreground">Verified by Google</p>
            </div>
          </div>
        </div>

        {/* Review cards grid */}
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
          {googleReviews.map((review, idx) => {
            const initial = review.name.charAt(0).toUpperCase();
            const colorClass = AVATAR_COLORS[idx % AVATAR_COLORS.length];
            return (
              <div
                key={review.id}
                className="bg-card border border-border rounded-2xl p-5 shadow-sm hover:shadow-md transition-shadow duration-300 flex flex-col gap-4"
              >
                {/* Reviewer row */}
                <div className="flex items-center gap-3">
                  <div
                    className={`w-10 h-10 rounded-full ${colorClass} flex items-center justify-center text-white font-bold text-sm flex-shrink-0`}
                  >
                    {initial}
                  </div>
                  <div className="flex-1 min-w-0">
                    <p className="font-semibold text-sm text-foreground truncate">{review.name}</p>
                    <p className="text-xs text-muted-foreground">{review.date}</p>
                  </div>
                </div>

                {/* Stars */}
                <StarRating rating={review.rating} />

                {/* Review text */}
                <p className="text-sm text-foreground leading-relaxed flex-1">{review.text}</p>

                {/* Footer */}
                <div className="flex items-center justify-between pt-3 border-t border-border">
                  <div className="flex items-center gap-1.5">
                    <div className="flex items-center justify-center w-6 h-6 rounded-full bg-white border border-border">
                      <GoogleGLogo />
                    </div>
                    <span className="text-xs font-medium text-muted-foreground">Google</span>
                  </div>
                  {review.verified && (
                    <span className="text-xs text-emerald-600 font-medium">Verified review</span>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add google-card

Where to use it

Place near your contact, booking, or pricing section to build trust at conversion points. In Astro: import GoogleReviewCards from '../components/innovations/reviews/google-card'; <GoogleReviewCards client:load /> In Next.js: import GoogleReviewCards from '@/components/innovations/reviews/google-card'; // Add near the contact form, pricing table, or CTA section The overall rating summary shows "4.9 based on 127 reviews" — update the hardcoded count in the component to match your real review total. Edit individual reviews in src/lib/placeholders.ts.