|
| 1 | +import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" |
| 2 | +import { clsx } from "clsx" |
| 3 | +import Image from "next-image-export-optimizer" |
| 4 | + |
| 5 | +import maskBlur from "./mask.webp" |
| 6 | + |
| 7 | +export interface TestimonialsProps extends React.HTMLAttributes<HTMLElement> {} |
| 8 | + |
| 9 | +interface Testimonial { |
| 10 | + quote: string |
| 11 | + author: { |
| 12 | + name: string |
| 13 | + role: string |
| 14 | + avatar: string |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +const testimonials: Testimonial[] = [ |
| 19 | + { |
| 20 | + quote: |
| 21 | + "GraphQL is evolving to new use cases every day and it's really a competitive advantage to experience them first hand with everyone that matters. I look forward the next edition!", |
| 22 | + author: { |
| 23 | + name: "Vincent Desmares", |
| 24 | + role: "Teamstarter, CTO", |
| 25 | + avatar: |
| 26 | + "https://avatars.sched.co/d/cc/21066875/avatar.jpg.320x320px.jpg?f80", |
| 27 | + }, |
| 28 | + }, |
| 29 | + { |
| 30 | + quote: |
| 31 | + "As a beginner in GraphQL, it was very helpful to see real use cases and honest accounts of the challenges along the way. I learned a lot about performance and security and had a great opportunity to network with other participants and potential vendors.", |
| 32 | + author: { |
| 33 | + name: "Nicolai Draslov", |
| 34 | + role: "Danish Agency for Digital Government", |
| 35 | + avatar: |
| 36 | + "https://media.licdn.com/dms/image/v2/C4E03AQGlrdt3GpJI9w/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1528203207471?e=1753920000&v=beta&t=H6CMhDZFoXJxGUu4XYwC_rEX9Jjwh7OdPIDm8JaeXAU", |
| 37 | + }, |
| 38 | + }, |
| 39 | + { |
| 40 | + quote: |
| 41 | + "GraphQLConf 24 was well organized event which empowers new and existing organizations to adopt GraphQL and help navigate how to rollout within their organizations by building understanding of ecosystem.", |
| 42 | + author: { |
| 43 | + name: "Satish Chitnis", |
| 44 | + role: "Paramount, Principal Architect", |
| 45 | + avatar: |
| 46 | + "https://avatars.sched.co/1/c3/21496512/avatar.jpg.320x320px.jpg?0c2", |
| 47 | + }, |
| 48 | + }, |
| 49 | +] |
| 50 | + |
| 51 | +export function Testimonials({ className, ...rest }: TestimonialsProps) { |
| 52 | + return ( |
| 53 | + <section |
| 54 | + className={clsx( |
| 55 | + "gql-conf-container py-8 max-md:px-4 md:pb-16 md:pt-24 md:[mask-image:linear-gradient(to_right,transparent,black_5%,black_95%,transparent)]", |
| 56 | + className, |
| 57 | + )} |
| 58 | + {...rest} |
| 59 | + > |
| 60 | + <h2 className="text-center text-neu-800 typography-h2"> |
| 61 | + How was the previous edition? |
| 62 | + </h2> |
| 63 | + <div className="flex w-full snap-x snap-mandatory flex-row gap-10 overflow-x-auto px-4 py-6 lg:mt-16 lg:py-16"> |
| 64 | + {testimonials.map((testimonial, i) => ( |
| 65 | + <div |
| 66 | + key={i} |
| 67 | + className="flex shrink-0 snap-start flex-row-reverse items-center gap-6 max-md:flex-col md:px-10" |
| 68 | + > |
| 69 | + <div> |
| 70 | + <p className="max-w-[calc(100vw-32px)] !leading-[1.1] typography-body-lg max-md:text-center md:max-w-[544px]"> |
| 71 | + {testimonial.quote} |
| 72 | + </p> |
| 73 | + <AuthorNameAndRole |
| 74 | + author={testimonial.author} |
| 75 | + className="mt-4 max-md:hidden" |
| 76 | + /> |
| 77 | + </div> |
| 78 | + <TestimonialAuthor author={testimonial.author} /> |
| 79 | + </div> |
| 80 | + ))} |
| 81 | + </div> |
| 82 | + </section> |
| 83 | + ) |
| 84 | +} |
| 85 | + |
| 86 | +function TestimonialAuthor({ author }: { author: Testimonial["author"] }) { |
| 87 | + return ( |
| 88 | + <div className="relative flex shrink-0 flex-col items-center justify-center whitespace-pre md:px-6 lg:h-full lg:px-8"> |
| 89 | + <div className="relative bg-neu-500 dark:bg-neu-200 dark:opacity-90"> |
| 90 | + <Image |
| 91 | + src={author.avatar} |
| 92 | + alt={author.name} |
| 93 | + width={128} |
| 94 | + height={128} |
| 95 | + className="size-16 saturate-0 xl:size-32" |
| 96 | + /> |
| 97 | + <div className="absolute inset-0 z-[1] bg-pri-darker mix-blend-plus-lighter" /> |
| 98 | + <Stripes /> |
| 99 | + </div> |
| 100 | + <AuthorNameAndRole author={author} className="contents md:hidden" /> |
| 101 | + <div |
| 102 | + // the separator |
| 103 | + className="absolute inset-y-0 right-0 w-px bg-gradient-to-b from-transparent via-pri-lighter to-transparent max-md:hidden" |
| 104 | + /> |
| 105 | + </div> |
| 106 | + ) |
| 107 | +} |
| 108 | + |
| 109 | +function AuthorNameAndRole({ |
| 110 | + author, |
| 111 | + className, |
| 112 | +}: { |
| 113 | + author: Testimonial["author"] |
| 114 | + className?: string |
| 115 | +}) { |
| 116 | + return ( |
| 117 | + <div className={className}> |
| 118 | + <div className="mt-3 typography-body-sm">{author.name}</div> |
| 119 | + <div className="text-neu-700 typography-body-xs">{author.role}</div> |
| 120 | + </div> |
| 121 | + ) |
| 122 | +} |
| 123 | + |
| 124 | +function Stripes() { |
| 125 | + const mask = `url(${maskBlur.src})` |
| 126 | + return ( |
| 127 | + <div |
| 128 | + role="presentation" |
| 129 | + className="pointer-events-none absolute inset-0" |
| 130 | + style={{ |
| 131 | + maskImage: mask, |
| 132 | + WebkitMaskImage: mask, |
| 133 | + maskSize: "cover", |
| 134 | + WebkitMaskSize: "cover", |
| 135 | + maskPosition: "left", |
| 136 | + WebkitMaskPosition: "left", |
| 137 | + }} |
| 138 | + > |
| 139 | + <StripesDecoration |
| 140 | + evenClassName="bg-gradient-to-b from-pri-light/0 to-pri-lighter/25" |
| 141 | + stripeWidth="8px" |
| 142 | + /> |
| 143 | + </div> |
| 144 | + ) |
| 145 | +} |
0 commit comments