Skip to content

Commit c973485

Browse files
authored
Landing page generator (#20)
2 parents 6e4f551 + 3b56597 commit c973485

File tree

21 files changed

+502
-48
lines changed

21 files changed

+502
-48
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
defmodule BloomSiteWeb.Components.GradientBlob do
2+
use Phoenix.Component
3+
4+
@doc """
5+
Renders a gradient blob component with a randomly generated polygon shape.
6+
7+
## Assigns
8+
9+
- `position`: The position of the blob. Can be either "top" or "bottom". Default is "top".
10+
- `offset`: The offset of the blob from the top or bottom edge in rem units. Default is 0.
11+
- `width`: The width of the blob in rem units. Default is 36.
12+
- `rotate`: The rotation of the blob in degrees. Default is 0.
13+
- `from_color`: The starting color of the gradient. Default is "#ff80b5".
14+
- `to_color`: The ending color of the gradient. Default is "#9089fc".
15+
- `seed`: A seed value used to generate the random polygon shape. Default is 0.
16+
"""
17+
attr :position, :string, default: "top"
18+
attr :offset, :integer, default: 0
19+
attr :width, :integer, default: 36
20+
attr :rotate, :integer, default: 0
21+
attr :from_color, :string, default: "#ff80b5"
22+
attr :to_color, :string, default: "#9089fc"
23+
attr :seed, :integer, default: 0
24+
25+
def gradient_blob(assigns) do
26+
position_style = get_position_style(assigns.position, assigns.offset)
27+
width_style = "width: #{assigns.width}rem;"
28+
rotate_style = "rotate: #{assigns.rotate}deg;"
29+
polygon = generate_polygon(assigns.seed)
30+
31+
~H"""
32+
<div class="pointer-events-none absolute left-0 right-0 -z-50 transform translate-z-0 overflow-hidden blur-2xl max-w-full" style={"#{position_style} #{width_style}"} aria-hidden="true">
33+
<div class="relative left-1/2 aspect-[1155/678] -translate-x-1/2 opacity-30" style={"background-image: linear-gradient(to top right, #{@from_color}, #{@to_color}); clip-path: polygon(#{polygon}); #{rotate_style}"}>
34+
</div>
35+
</div>
36+
"""
37+
end
38+
39+
defp get_position_style("top", offset), do: "top: -#{offset}rem;"
40+
defp get_position_style("bottom", offset), do: "bottom: -#{offset}rem;"
41+
42+
defp generate_polygon(seed) do
43+
:rand.seed(:exsss, {seed, seed, seed})
44+
num_points = Enum.random(5..10)
45+
points =
46+
for _ <- 1..num_points do
47+
x = :rand.uniform() * 100
48+
y = :rand.uniform() * 100
49+
"#{x}% #{y}%"
50+
end
51+
Enum.join(points, ", ")
52+
end
53+
end

bloom_site/lib/bloom_site_web/components/hero.ex

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@ defmodule BloomSiteWeb.Components.Hero do
88
attr(:class, :string, default: "", doc: "CSS class for the hero component")
99
slot(:inner_block, required: true, doc: "Main content block of the hero component")
1010
slot(:subtitle, required: false, doc: "Subtitle content block")
11+
slot(:announcement_badge, required: false, doc: "Annoucement badge content block")
1112
slot(:actions, required: false, doc: "Action buttons or links")
1213

1314
def hero(assigns) do
1415
~H"""
15-
<div class={["container mx-auto px-4 py-8", @class]}>
16-
<div class="text-8xl text-center font-bold text-pretty">
16+
<div class={["container mx-auto max-w-4xl py-12", @class]}>
17+
<div class="hidden sm:mb-8 sm:flex sm:justify-center">
18+
<div :if={@announcement_badge != []} class="relative rounded-full px-3 py-1 text-sm leading-6 text-gray-600 ring-1 ring-gray-900/10 hover:ring-gray-900/20">
19+
<%= render_slot(@announcement_badge) %>
20+
</div>
21+
</div>
22+
<div class="text-8xl text-center font-bold text-pretty tracking-tight text-gray-900">
1723
<h1><%= render_slot(@inner_block) %></h1>
1824
</div>
19-
<div class="mt-6 text-center text-2xl flex justify-center font-medium text-pretty">
25+
<div class="mt-6 text-center text-2xl flex leading-8 text-gray-600 justify-center font-medium text-pretty">
2026
<%= render_slot(@subtitle) %>
2127
</div>
22-
<div class="mt-14 flex justify-center flex-row gap-4 items-center">
28+
<div class="mt-16 flex justify-center flex-row gap-x-6 items-center">
2329
<%= render_slot(@actions) %>
2430
</div>
2531
</div>

bloom_site/lib/bloom_site_web/router.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ defmodule BloomSiteWeb.Router do
2525
live "/", LandingLive, :home
2626
live "/showcase", ShowcaseLive
2727
live_storybook("/storybook", backend_module: Elixir.BloomSiteWeb.Storybook)
28+
29+
live_session :landing do
30+
live "/landing_page", LandingPageLive
31+
end
2832
end
2933

3034
# Other scopes may use custom stacks.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule BloomSite.Storybook.BloomComponents.GradientBlob do
2+
use PhoenixStorybook.Story, :component
3+
4+
def function, do: &BloomSiteWeb.Components.GradientBlob.gradient_blob/1
5+
6+
def variations do
7+
[
8+
%Variation{
9+
id: :default,
10+
slots: []
11+
},
12+
%Variation{
13+
id: :with_seed,
14+
slots: [],
15+
attributes: %{
16+
seed: 10
17+
}
18+
}
19+
]
20+
end
21+
end

bloom_site/storybook/bloom_components/hero.story.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ defmodule BloomSite.Storybook.BloomComponents.Hero do
2424
"Hero",
2525
~s|<:actions><button class="border px-4 py-1 bg-black text-white rounded text-lg" phx-click="go">Go</button></:actions>|
2626
]
27+
},
28+
%Variation{
29+
id: :with_announcement_label,
30+
slots: [
31+
"Hero",
32+
~s|<:announcement_badge>We just launched! <a class="text-blue-500" href="#">Read more here</a></:announcement_badge>|
33+
]
2734
}
2835
]
2936
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
defmodule Storybook.Examples.LandingPageGenerator do
2+
use PhoenixStorybook.Story, :example
3+
import BloomSiteWeb.CoreComponents
4+
import BloomSiteWeb.Components.{Hero, GradientBlob}
5+
6+
alias Phoenix.LiveView.JS
7+
8+
def doc do
9+
"You can create a landing page for your site complete with an Ecto backed waitlist for your users by running `mix bloom.landing_page`."
10+
end
11+
12+
@impl true
13+
def mount(_params, _session, socket) do
14+
{:ok,
15+
assign(socket,
16+
form: nil
17+
)}
18+
end
19+
20+
@impl true
21+
def render(assigns) do
22+
~H"""
23+
<div class="bg-transparent">
24+
<div class="relative isolate px-6 pt-14 lg:px-8">
25+
<.gradient_blob position="top" seed={6} offset="0" width={80} rotate={10} />
26+
<.hero>
27+
Some sort of really interesting hero title
28+
<:subtitle>
29+
Here's where you put something else to really make people realise the value of your product
30+
</:subtitle>
31+
<:announcement_badge>
32+
Announcing our next round of funding or something.
33+
<a href="#" class="font-semibold text-violet-500">
34+
<span class="absolute inset-0" aria-hidden="true"></span>Read more
35+
<span aria-hidden="true">&rarr;</span>
36+
</a>
37+
</:announcement_badge>
38+
<:actions>
39+
<div class="mt-10 flex items-center justify-center gap-x-6">
40+
<.form
41+
for={@form}
42+
phx-submit="register_interest"
43+
class="flex items-center justify-center space-x-4 flex-row"
44+
>
45+
<input
46+
type="email"
47+
field={:email}
48+
value=""
49+
name="email"
50+
placeholder="Enter your email"
51+
required
52+
class="w-72 rounded-md border-gray-300 shadow-sm focus:border-violet-600 focus:ring-violet-500 sm:text-sm !mt-0"
53+
/>
54+
<button
55+
type="submit"
56+
class="rounded-md bg-violet-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-violet-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-500"
57+
>
58+
Join Waitlist
59+
</button>
60+
</.form>
61+
</div>
62+
</:actions>
63+
</.hero>
64+
<.gradient_blob position="bottom" seed={831} offset="33" width={80} rotate={-20} />
65+
</div>
66+
</div>
67+
"""
68+
end
69+
70+
end

lib/bloom/components/gradient_blob.ex

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
defmodule Bloom.Components.GradientBlob do
2+
use Phoenix.Component
3+
4+
@doc """
5+
Renders a gradient blob component with a randomly generated polygon shape.
6+
7+
## Assigns
8+
9+
- `position`: The position of the blob. Can be either "top" or "bottom". Default is "top".
10+
- `offset`: The offset of the blob from the top or bottom edge in rem units. Default is 0.
11+
- `width`: The width of the blob in rem units. Default is 36.
12+
- `rotate`: The rotation of the blob in degrees. Default is 0.
13+
- `from_color`: The starting color of the gradient. Default is "#ff80b5".
14+
- `to_color`: The ending color of the gradient. Default is "#9089fc".
15+
- `seed`: A seed value used to generate the random polygon shape. Default is 0.
16+
"""
17+
attr(:position, :string, default: "top")
18+
attr(:offset, :integer, default: 0)
19+
attr(:width, :integer, default: 36)
20+
attr(:rotate, :integer, default: 0)
21+
attr(:from_color, :string, default: "#ff80b5")
22+
attr(:to_color, :string, default: "#9089fc")
23+
attr(:seed, :integer, default: 0)
24+
25+
def gradient_blob(assigns) do
26+
position_style = get_position_style(assigns.position, assigns.offset)
27+
width_style = "width: #{assigns.width}rem;"
28+
rotate_style = "rotate: #{assigns.rotate}deg;"
29+
polygon = generate_polygon(assigns.seed)
30+
31+
~H"""
32+
<div
33+
class="translate-z-0 pointer-events-none absolute right-0 left-0 -z-50 max-w-full transform overflow-hidden blur-2xl"
34+
style={"#{position_style} #{width_style}"}
35+
aria-hidden="true"
36+
>
37+
<div
38+
class="aspect-[1155/678] relative left-1/2 -translate-x-1/2 opacity-30"
39+
style={"background-image: linear-gradient(to top right, #{@from_color}, #{@to_color}); clip-path: polygon(#{polygon}); #{rotate_style}"}
40+
>
41+
</div>
42+
</div>
43+
"""
44+
end
45+
46+
defp get_position_style("top", offset), do: "top: -#{offset}rem;"
47+
defp get_position_style("bottom", offset), do: "bottom: -#{offset}rem;"
48+
49+
defp generate_polygon(seed) do
50+
:rand.seed(:exsss, {seed, seed, seed})
51+
num_points = Enum.random(5..10)
52+
53+
points =
54+
for _ <- 1..num_points do
55+
x = :rand.uniform() * 100
56+
y = :rand.uniform() * 100
57+
"#{x}% #{y}%"
58+
end
59+
60+
Enum.join(points, ", ")
61+
end
62+
end

lib/bloom/components/hero.ex

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
defmodule Bloom.Components.Hero do
22
use Phoenix.Component
33

4-
@moduledoc """
4+
@doc """
55
Hero component
66
"""
77

88
attr(:class, :string, default: "", doc: "CSS class for the hero component")
99
slot(:inner_block, required: true, doc: "Main content block of the hero component")
1010
slot(:subtitle, required: false, doc: "Subtitle content block")
11+
slot(:announcement_badge, required: false, doc: "Annoucement badge content block")
1112
slot(:actions, required: false, doc: "Action buttons or links")
1213

1314
def hero(assigns) do
1415
~H"""
15-
<div class={["container mx-auto px-4 py-8", @class]}>
16-
<div class="text-pretty text-center text-8xl font-bold">
16+
<div class={["container mx-auto max-w-4xl py-32 sm:py-48 lg:py-56", @class]}>
17+
<div class="hidden sm:mb-8 sm:flex sm:justify-center">
18+
<div
19+
:if={@announcement_badge != []}
20+
class="ring-gray-900/10 relative rounded-full px-3 py-1 text-sm leading-6 text-gray-600 ring-1 hover:ring-gray-900/20"
21+
>
22+
<%= render_slot(@announcement_badge) %>
23+
</div>
24+
</div>
25+
<div class="text-pretty text-center text-6xl font-bold tracking-tight text-gray-900">
1726
<h1><%= render_slot(@inner_block) %></h1>
1827
</div>
19-
<div class="text-pretty mt-6 flex justify-center text-center text-2xl font-medium">
28+
<div class="text-pretty mt-6 flex justify-center text-center text-lg font-medium leading-8 text-gray-600">
2029
<%= render_slot(@subtitle) %>
2130
</div>
22-
<div class="mt-14 flex flex-row items-center justify-center gap-4">
31+
<div class="mt-6 flex flex-row items-center justify-center gap-x-6">
2332
<%= render_slot(@actions) %>
2433
</div>
2534
</div>

lib/tasks/landing_page.ex

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
defmodule Mix.Tasks.Bloom.LandingPage do
2+
@moduledoc "Mix task to generate Landing Page with Ecto Waitlist for your application"
3+
use Mix.Task
4+
5+
@doc """
6+
Run the mix task to generate the landing page.
7+
8+
## Examples
9+
10+
iex> Mix.Tasks.Bloom.LandingPage.run([])
11+
"""
12+
@impl true
13+
def run(args) do
14+
case args do
15+
["help"] ->
16+
print_usage_and_components()
17+
18+
_ ->
19+
install_landing_page()
20+
end
21+
end
22+
23+
defp install_landing_page do
24+
env = Mix.env() |> Atom.to_string()
25+
project_name = Mix.Project.config()[:app] |> Atom.to_string() |> String.downcase()
26+
files_path = "_build/#{env}/lib/bloom/priv/templates/landing_page"
27+
module_name = project_name |> Macro.camelize()
28+
29+
component_dir = component_dir(project_name)
30+
File.mkdir_p(component_dir)
31+
32+
File.ls!(files_path)
33+
|> Enum.each(fn file ->
34+
source_file = "#{files_path}/#{file}"
35+
36+
source_code =
37+
EEx.eval_file(source_file, module_name: module_name, assigns: %{module_name: module_name})
38+
39+
target_path = "#{component_dir}/#{file}"
40+
File.write!(target_path, source_code)
41+
42+
Mix.shell().info("Successfully generated #{target_path} ✅")
43+
end)
44+
45+
Mix.shell().info(
46+
"Landing page generated - don't forget to run the migration specified in the waitlist.ex module"
47+
)
48+
end
49+
50+
defp print_usage_and_components do
51+
Mix.shell().info("Usage: mix bloom.landing_page - requires hero and gradient_blob components")
52+
end
53+
54+
defp component_dir(app_name), do: "lib/#{app_name}_web/live" |> String.downcase()
55+
end

priv/templates/avatar.ex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ defmodule <%= @module_name %>Web.Components.Avatar do
4444
attr(:rest, :global)
4545

4646
def avatar(assigns) do
47+
image =
48+
assigns[:img_src] ||
49+
"https://api.dicebear.com/8.x/#{assigns[:style]}/svg?seed=#{assigns[:name]}"
50+
4751
~H"""
48-
<div class={["flex items-center w-12 h-12", @class]} {@rest}>
49-
<img
50-
src={@img_src || "https://api.dicebear.com/7.x/#{@style}/svg?seed=#{@name}"}
51-
alt={"#{@name} avatar"}
52-
class="rounded-lg"
53-
/>
52+
<div class={["flex h-12 w-12 items-center", @class]} {@rest}>
53+
<img src={image} alt={"#{@name} avatar"} class="rounded-lg" />
5454
</div>
5555
"""
5656
end

0 commit comments

Comments
 (0)