sections/testimonials.ex
Testimonials
A testimonials section with quote cards, each a quote in a few lines and its author's initials, name and company.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_testimonials animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Customers" |
|
title |
string | "What operations teams say" |
Up to 44 characters. |
quotes |
list | [%{name: "Marta Nowak", lines: 3, company: "Northwind"}, %{name: "Jan Kowalski", ...}, ...] |
One to three `%{name, company}` with an optional `lines`, the length of the quote from two to four. With three cards: a name of up to 14 characters, a company of 13. |
featured |
integer | 1 |
Index of the quote picked out in lime. A negative index picks none. |
Frame props, shared by every composition
| Prop | Type | Default | What it does |
|---|---|---|---|
animated |
boolean | false |
Turns motion on. Without it the composition renders in its final, still state. |
trigger |
"mount" | "in-view" | "in-view-repeat" | "in-view" |
When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view. |
hover |
boolean | false |
Enables the hover micro-interactions the composition supports. |
glow |
boolean | true |
The warm incandescent halo behind the composition. |
fade_out |
boolean | false |
Fades the bottom edge into the surface underneath. |
isometric |
boolean | false |
Tilts the composition into the isometric plane of the platform stack. |
theme |
"dark" | "light" | "dark" |
`dark` for the site canvas, `light` for the cream reversed surface. |
class |
string | nil |
Extra classes for the frame. |
rest |
global | — |
Any other HTML attribute, set on the frame. |
Source
Two files. Copy them into your app if you would rather own the code than depend on the library.
lib/eh_visuals/sections/testimonials.ex
defmodule EHVisuals.Sections.Testimonials do
@moduledoc """
A testimonials section: quote cards with the quote in a few lines and the
author's avatar, name and company, one of them picked out in lime.
"""
use EHVisuals.Composition,
name: "sections/testimonials",
title: "Testimonials",
description:
"A testimonials section with quote cards, each a quote in a few lines and its author's initials, name and company."
@quotes [
%{name: "Marta Nowak", company: "Northwind", lines: 3},
%{name: "Jan Kowalski", company: "Acme Tools", lines: 4},
%{name: "Ewa Lis", company: "Globex", lines: 3}
]
# How far each line of a quote runs, as a share of the card's inner width.
@reach [1.0, 0.9, 0.96, 0.82]
attr(:eyebrow, :string, default: "Customers")
attr(:title, :string, default: "What operations teams say", doc: "Up to 44 characters.")
attr(:quotes, :list,
default: @quotes,
doc:
"One to three `%{name, company}` with an optional `lines`, the length of the quote from two to four. With three cards: a name of up to 14 characters, a company of 13."
)
attr(:featured, :integer,
default: 1,
doc: "Index of the quote picked out in lime. A negative index picks none."
)
attr(:label, :string, default: "Three customer quotes, each signed with a name and a company")
frame_attrs()
@spec sections_testimonials(map()) :: Phoenix.LiveView.Rendered.t()
def sections_testimonials(assigns) do
quotes = Enum.take(assigns.quotes, 3)
count = max(length(quotes), 1)
width = Float.round((432 - (count - 1) * 10) / count, 1)
counts = Enum.map(quotes, &(&1 |> Map.get(:lines, 3) |> max(2) |> min(4)))
card_h = 48 + Enum.max(counts, fn -> 3 end) * 12 + 10 + 22 + 14
cards =
quotes
|> Enum.zip(counts)
|> Enum.with_index()
|> Enum.map(fn {{item, lines}, index} ->
x = Float.round(24 + index * (width + 10), 1)
%{
x: x,
name: item.name,
company: item.company,
initials: initials(item.name),
lines: skeleton(lines, width - 28),
on: index == assigns.featured,
index: index
}
end)
assigns =
assigns
|> assign(:cards, cards)
|> assign(:width, width)
|> assign(:card_h, card_h)
|> assign(:author_y, 76 + card_h - 25)
|> assign(:height, 76 + card_h + 24)
~H"""
<.frame name="sections/testimonials" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 480 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="479" height={@height - 1} rx="12" />
<text
class="ehv-mono ehv-a-fade ehv-i-1"
x="240"
y="34"
font-size="8.5"
text-anchor="middle"
>
{String.upcase(@eyebrow)}
</text>
<text
class="ehv-t-strong ehv-a-fade ehv-i-2"
x="240"
y="58"
font-size="16"
text-anchor="middle"
>
{@title}
</text>
<g :for={card <- @cards} class={"ehv-a-rise ehv-i-#{card.index + 3}"}>
<g class={[card.on && "ehv-sections-testimonials__featured"]}>
<rect
class={if card.on, do: "ehv-sections-testimonials__card--on", else: "ehv-card-2"}
x={card.x + 0.5}
y="76.5"
width={@width - 1}
height={@card_h - 1}
rx="10"
/>
<path
class={if card.on, do: "ehv-sections-testimonials__mark--on", else: "ehv-fill-4"}
d={quote_mark(card.x + 14, 92)}
/>
<rect
:for={{line_w, row} <- card.lines}
class={"ehv-fill-soft ehv-a-grow-x ehv-i-#{min(card.index + row + 5, 24)}"}
x={card.x + 14}
y={114 + row * 12}
width={line_w}
height="5"
rx="2.5"
/>
<circle class="ehv-card-3" cx={card.x + 25} cy={@author_y} r="11" />
<text
class="ehv-mono ehv-t-2"
x={card.x + 25}
y={@author_y + 2.6}
font-size="7"
text-anchor="middle"
>
{card.initials}
</text>
<text class="ehv-t-strong" x={card.x + 44} y={@author_y - 1.5} font-size="11">
{card.name}
</text>
<text class="ehv-mono" x={card.x + 44} y={@author_y + 10} font-size="7.5">
{String.upcase(card.company)}
</text>
</g>
</g>
</g>
</svg>
</.frame>
"""
end
defp initials(name) do
name |> String.split() |> Enum.take(2) |> Enum.map_join(&String.first/1) |> String.upcase()
end
# The quote as skeleton lines: full ones, then a shorter last line.
defp skeleton(count, width) do
@reach
|> Enum.take(count - 1)
|> Kernel.++([0.58])
|> Enum.map(&Float.round(&1 * width, 1))
|> Enum.with_index()
end
# An opening quotation mark, drawn as two commas with their tails up.
defp quote_mark(x, y) do
Enum.map_join([0, 10], " ", fn dx ->
"M#{x + dx} #{y + 10} v-4.5 a6 6 0 0 1 6 -6 v2.6 a3.4 3.4 0 0 0 -3 3.4 h3 v4.5 z"
end)
end
end
assets/css/sections/testimonials.css
/* sections/testimonials */
.ehv-sections-testimonials { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }
.ehv .ehv-sections-testimonials__card--on {
fill: var(--ehv-surface-2);
stroke: var(--ehv-lime);
stroke-width: 1;
}
/* The featured quote steps forward. */
.ehv--hover .ehv-sections-testimonials__featured { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-sections-testimonials__featured { transform: translateY(-4px); }
/* The line colour of lime, so the mark keeps its contrast on the light surface. */
.ehv .ehv-sections-testimonials__mark--on { fill: var(--ehv-lime); }