Skip to content

Commit 2790c56

Browse files
committed
feat: add datepicker
1 parent d462cd4 commit 2790c56

4 files changed

Lines changed: 186 additions & 1 deletion

File tree

demo/lib/demo_web/live/odyssey_showcase_live.ex

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ defmodule DemoWeb.OdysseyShowcaseLive do
1515
"time_unit" => "Minutes",
1616
"view_mode" => "List",
1717
"discount" => "",
18-
"percentage" => ""
18+
"percentage" => "",
19+
"start_date" => "",
20+
"expiration_date" => ""
1921
}
2022

2123
socket =
@@ -423,6 +425,28 @@ defmodule DemoWeb.OdysseyShowcaseLive do
423425
percentage: <strong>{@form_data["percentage"] || ""}</strong>
424426
</p>
425427
</div>
428+
429+
<div>
430+
<label class="block text-sm font-medium text-gray-700 mb-2">
431+
Date Pickers (start and expiration)
432+
</label>
433+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
434+
<.odyssey_date_picker
435+
field={@form[:start_date]}
436+
label="Start date"
437+
required
438+
/>
439+
<.odyssey_date_picker
440+
field={@form[:expiration_date]}
441+
label="Expiration date"
442+
required
443+
/>
444+
</div>
445+
<p class="text-sm text-gray-500 mt-1">
446+
Start date: <strong>{@form_data["start_date"] || ""}</strong>,
447+
expiration date: <strong>{@form_data["expiration_date"] || ""}</strong>
448+
</p>
449+
</div>
426450
</.form>
427451
</div>
428452

lib/peek_app_sdk/ui/odyssey.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ defmodule PeekAppSDK.UI.Odyssey do
1212
defdelegate odyssey_tabs(assigns), to: PeekAppSDK.UI.Odyssey.Tabs
1313
defdelegate odyssey_toggle_button(assigns), to: PeekAppSDK.UI.Odyssey.ToggleButton
1414
defdelegate odyssey_prefix_input(assigns), to: PeekAppSDK.UI.Odyssey.PrefixInput
15+
defdelegate odyssey_date_picker(assigns), to: PeekAppSDK.UI.Odyssey.DatePicker
1516
end
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
defmodule PeekAppSDK.UI.Odyssey.DatePicker do
2+
@moduledoc """
3+
A date picker input component for selecting dates.
4+
5+
Provides both standalone usage and convenient integration with
6+
`Phoenix.HTML.Form` via the `:field` attribute.
7+
"""
8+
9+
use Phoenix.Component
10+
11+
import PeekAppSDK.UI.Odyssey.Icon
12+
13+
@doc """
14+
Renders a date input with a calendar icon.
15+
16+
## Examples
17+
18+
<.odyssey_date_picker field={@form[:expiration_date]} label="Expiration Date" />
19+
<.odyssey_date_picker field={@form[:start_date]} label="Start Date" required />
20+
"""
21+
attr :id, :any, default: nil
22+
attr :name, :any
23+
attr :label, :string, default: nil
24+
attr :value, :any
25+
attr :errors, :list, default: []
26+
attr :class, :string, default: nil, doc: "additional classes for the input"
27+
28+
attr :field, Phoenix.HTML.FormField,
29+
doc: "a form field struct retrieved from the form, for example: @form[:expiration_date]"
30+
31+
attr :rest, :global, include: ~w(autocomplete disabled form max min readonly required step)
32+
33+
def odyssey_date_picker(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
34+
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
35+
simple_errors = Enum.map(errors, fn {msg, _opts} -> msg end)
36+
37+
assigns
38+
|> assign(field: nil, id: assigns.id || field.id)
39+
|> assign_new(:errors, fn -> simple_errors end)
40+
|> assign_new(:name, fn -> field.name end)
41+
|> assign_new(:value, fn -> field.value end)
42+
|> odyssey_date_picker()
43+
end
44+
45+
def odyssey_date_picker(assigns) do
46+
~H"""
47+
<fieldset class="fieldset mb-2">
48+
<span :if={@label} class="label !mb-0 block">{@label}</span>
49+
<div class="relative inline-block w-[200px]">
50+
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none z-10">
51+
<.odyssey_icon name="hero-calendar-days" class="size-5 text-neutrals-300" />
52+
</div>
53+
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none z-10">
54+
<.odyssey_icon name="hero-chevron-down" class="size-5 text-interaction-300" />
55+
</div>
56+
<input
57+
type="date"
58+
name={@name}
59+
id={@id}
60+
value={Phoenix.HTML.Form.normalize_value("date", @value)}
61+
class={[
62+
"input pl-11 pr-10 w-[200px] cursor-pointer !outline-none",
63+
"before:absolute before:left-11 before:top-1/2 before:-translate-y-1/2",
64+
"before:text-neutrals-300 before:pointer-events-none",
65+
"[&::-webkit-calendar-picker-indicator]:opacity-0",
66+
"[&::-webkit-calendar-picker-indicator]:absolute",
67+
"[&::-webkit-calendar-picker-indicator]:inset-0",
68+
"[&::-webkit-calendar-picker-indicator]:w-full",
69+
"[&::-webkit-calendar-picker-indicator]:h-full",
70+
"[&::-webkit-calendar-picker-indicator]:cursor-pointer",
71+
"[&::-webkit-datetime-edit]:text-neutrals-300",
72+
"[&::-webkit-datetime-edit]:relative",
73+
"[&::-webkit-datetime-edit]:left-[32px]",
74+
"[&:not(:focus):invalid::-webkit-datetime-edit]:opacity-0",
75+
"[&:not(:focus):invalid]:before:block",
76+
"[&:focus]:before:hidden",
77+
"[&:valid]:before:hidden",
78+
@errors != [] && "input-error",
79+
@class
80+
]}
81+
{@rest}
82+
/>
83+
</div>
84+
<.error :for={msg <- @errors}>{msg}</.error>
85+
</fieldset>
86+
"""
87+
end
88+
89+
defp error(assigns) do
90+
~H"""
91+
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
92+
<.odyssey_icon name="hero-exclamation-circle" class="size-5" />
93+
{render_slot(@inner_block)}
94+
</p>
95+
"""
96+
end
97+
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
defmodule PeekAppSDK.UI.Odyssey.DatePickerTest do
2+
use ExUnit.Case, async: true
3+
4+
import Phoenix.Component
5+
import Phoenix.LiveViewTest
6+
7+
alias PeekAppSDK.UI.Odyssey
8+
9+
describe "odyssey_date_picker/1" do
10+
test "renders standalone date picker with label and icons" do
11+
html =
12+
render_component(&Odyssey.odyssey_date_picker/1, %{
13+
id: "start_date",
14+
name: "start_date",
15+
value: ~D[2025-01-01],
16+
label: "Start date"
17+
})
18+
19+
assert html =~ ~s(<fieldset class="fieldset mb-2">)
20+
assert html =~ "Start date"
21+
assert html =~ ~s(type="date")
22+
assert html =~ ~s(name="start_date")
23+
assert html =~ ~s(id="start_date")
24+
assert html =~ "2025-01-01"
25+
assert html =~ "hero-calendar-days"
26+
assert html =~ "hero-chevron-down"
27+
end
28+
29+
test "applies error class and renders error messages" do
30+
html =
31+
render_component(&Odyssey.odyssey_date_picker/1, %{
32+
id: "expiration_date",
33+
name: "expiration_date",
34+
value: "",
35+
label: "Expiration date",
36+
errors: ["is required"]
37+
})
38+
39+
assert html =~ "input-error"
40+
assert html =~ ~s(class="mt-1.5 flex gap-2 items-center text-sm text-error")
41+
assert html =~ "hero-exclamation-circle"
42+
assert html =~ "is required"
43+
end
44+
45+
test "integrates with Phoenix.HTML.Form field" do
46+
form_data = %{"start_date" => ~D[2025-02-02]}
47+
form = to_form(form_data, as: "form")
48+
49+
html =
50+
render_component(&Odyssey.odyssey_date_picker/1, %{
51+
field: form[:start_date],
52+
label: "Start date",
53+
required: true
54+
})
55+
56+
assert html =~ ~s(name="form[start_date]")
57+
assert html =~ ~s(id="form_start_date")
58+
assert html =~ "2025-02-02"
59+
assert html =~ "Start date"
60+
end
61+
end
62+
end
63+

0 commit comments

Comments
 (0)