my brain is clealy full of fuck.2

9c1f4a5 · gmorell · 2023-06-11 04:59

28 files +1786 -0

Files changed

added apps/room_sanctum/lib/room_sanctum/configuration/scribus.ex
+20 −0
@@ -0,0 +1,20 @@
1 +defmodule RoomSanctum.Configuration.Scribus do
2 + use Ecto.Schema
3 + import Ecto.Changeset
4 +
5 + schema "cfg_scribus" do
6 + field :configuration, {:array, :map}
7 + field :name, :string
8 + field :resolution, :string
9 + field :vision, :id
10 +
11 + timestamps()
12 + end
13 +
14 + @doc false
15 + def changeset(scribus, attrs) do
16 + scribus
17 + |> cast(attrs, [:name, :resolution, :configuration])
18 + |> validate_required([:name, :resolution, :configuration])
19 + end
20 +end
added apps/room_sanctum/lib/room_sanctum_web/components/core_components.ex
+741 −0
@@ -0,0 +1,741 @@
1 +defmodule RoomSanctumWeb.CoreComponents do
2 + @moduledoc """
3 + Provides core UI components.
4 +
5 + At the first glance, this module may seem daunting, but its goal is
6 + to provide some core building blocks in your application, such modals,
7 + tables, and forms. The components are mostly markup and well documented
8 + with doc strings and declarative assigns. You may customize and style
9 + them in any way you want, based on your application growth and needs.
10 +
11 + The default components use Tailwind CSS, a utility-first CSS framework.
12 + See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
13 + how to customize them or feel free to swap in another framework altogether.
14 +
15 + Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
16 + """
17 + use Phoenix.Component
18 +
19 + alias Phoenix.LiveView.JS
20 + import RoomSanctumWeb.Gettext
21 +
22 + @doc """
23 + Renders a modal.
24 +
25 + ## Examples
26 +
27 + <.modal id="confirm-modal">
28 + This is a modal.
29 + </.modal>
30 +
31 + JS commands may be passed to the `:on_cancel` to configure
32 + the closing/cancel event, for example:
33 +
34 + <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
35 + This is another modal.
36 + </.modal>
37 +
38 + """
39 + attr :id, :string, required: true
40 + attr :show, :boolean, default: false
41 + attr :on_cancel, JS, default: %JS{}
42 + slot :inner_block, required: true
43 +
44 + def modal(assigns) do
45 + ~H"""
46 + <div
47 + id={@id}
48 + phx-mounted={@show && show_modal(@id)}
49 + phx-remove={hide_modal(@id)}
50 + data-cancel={JS.exec(@on_cancel, "phx-remove")}
51 + class="relative z-50 hidden modal modal-bottom md:modal-middle fade-in"
52 + >
53 + <div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
54 + <div
55 + class="fixed inset-0 overflow-y-auto fade-in-scale bg-base-100 modal-open opacity-80"
56 + aria-labelledby={"#{@id}-title"}
57 + aria-describedby={"#{@id}-description"}
58 + role="dialog"
59 + aria-modal="true"
60 + tabindex="0"
61 + >
62 + <div class="flex min-h-full items-center justify-center">
63 + <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
64 + <.focus_wrap
65 + id={"#{@id}-container"}
66 + phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
67 + phx-key="escape"
68 + phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
69 + class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-primary p-14 shadow-lg ring-1 transition"
70 + >
71 + <div class="absolute top-6 right-5">
72 + <button
73 + phx-click={JS.exec("data-cancel", to: "##{@id}")}
74 + type="button"
75 + class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
76 + aria-label={gettext("close")}
77 + >
78 + <.icon name="hero-x-mark-solid" class="h-5 w-5" />
79 + </button>
80 + </div>
81 + <div id={"#{@id}-content"}>
82 + <%= render_slot(@inner_block) %>
83 + </div>
84 + </.focus_wrap>
85 + </div>
86 + </div>
87 + </div>
88 + </div>
89 + """
90 + end
91 +
92 + @doc """
93 + Renders flash notices.
94 +
95 + ## Examples
96 +
97 + <.flash kind={:info} flash={@flash} />
98 + <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
99 + """
100 + attr :id, :string, default: "flash", doc: "the optional id of flash container"
101 + attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
102 + attr :title, :string, default: nil
103 + attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
104 + attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
105 +
106 + slot :inner_block, doc: "the optional inner block that renders the flash message"
107 +
108 + def flash(assigns) do
109 + ~H"""
110 + <div
111 + :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
112 + id={@id}
113 + phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
114 + role="alert"
115 + class={[
116 + "fixed top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
117 + @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
118 + @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
119 + ]}
120 + {@rest}
121 + >
122 + <p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
123 + <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
124 + <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
125 + <%= @title %>
126 + </p>
127 + <p class="mt-2 text-sm leading-5"><%= msg %></p>
128 + <button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
129 + <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
130 + </button>
131 + </div>
132 + """
133 + end
134 +
135 + @doc """
136 + Shows the flash group with standard titles and content.
137 +
138 + ## Examples
139 +
140 + <.flash_group flash={@flash} />
141 + """
142 + attr :flash, :map, required: true, doc: "the map of flash messages"
143 +
144 + def flash_group(assigns) do
145 + ~H"""
146 + <.flash kind={:info} title="Success!" flash={@flash} />
147 + <.flash kind={:error} title="Error!" flash={@flash} />
148 + <.flash
149 + id="disconnected"
150 + kind={:error}
151 + title="We can't find the internet"
152 + phx-disconnected={show("#disconnected")}
153 + phx-connected={hide("#disconnected")}
154 + hidden
155 + >
156 + Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
157 + </.flash>
158 + """
159 + end
160 +
161 + @doc """
162 + Renders a simple form.
163 +
164 + ## Examples
165 +
166 + <.simple_form for={@form} phx-change="validate" phx-submit="save">
167 + <.input field={@form[:email]} label="Email"/>
168 + <.input field={@form[:username]} label="Username" />
169 + <:actions>
170 + <.button>Save</.button>
171 + </:actions>
172 + </.simple_form>
173 + """
174 + attr :for, :any, required: true, doc: "the datastructure for the form"
175 + attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
176 +
177 + attr :rest, :global,
178 + include: ~w(autocomplete name rel action enctype method novalidate target multipart),
179 + doc: "the arbitrary HTML attributes to apply to the form tag"
180 +
181 + slot :inner_block, required: true
182 + slot :actions, doc: "the slot for form actions, such as a submit button"
183 +
184 + def simple_form(assigns) do
185 + ~H"""
186 + <.form :let={f} for={@for} as={@as} {@rest}>
187 + <div class="mt-10 space-y-8 bg-primary">
188 + <%= render_slot(@inner_block, f) %>
189 + <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
190 + <%= render_slot(action, f) %>
191 + </div>
192 + </div>
193 + </.form>
194 + """
195 + end
196 +
197 + @doc """
198 + Renders a button.
199 +
200 + ## Examples
201 +
202 + <.button>Send!</.button>
203 + <.button phx-click="go" class="ml-2">Send!</.button>
204 + """
205 + attr :type, :string, default: nil
206 + attr :class, :string, default: nil
207 + attr :rest, :global, include: ~w(disabled form name value)
208 +
209 + slot :inner_block, required: true
210 +
211 + def button(assigns) do
212 + ~H"""
213 + <button
214 + type={@type}
215 + class={[
216 + "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
217 + "text-sm font-semibold leading-6 text-white active:text-white/80",
218 + @class
219 + ]}
220 + {@rest}
221 + >
222 + <%= render_slot(@inner_block) %>
223 + </button>
224 + """
225 + end
226 +
227 + @doc """
228 + Renders an input with label and error messages.
229 +
230 + A `Phoenix.HTML.FormField` may be passed as argument,
231 + which is used to retrieve the input name, id, and values.
232 + Otherwise all attributes may be passed explicitly.
233 +
234 + ## Types
235 +
236 + This function accepts all HTML input types, considering that:
237 +
238 + * You may also set `type="select"` to render a `<select>` tag
239 +
240 + * `type="checkbox"` is used exclusively to render boolean values
241 +
242 + * For live file uploads, see `Phoenix.Component.live_file_input/1`
243 +
244 + See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
245 + for more information.
246 +
247 + ## Examples
248 +
249 + <.input field={@form[:email]} type="email" />
250 + <.input name="my-input" errors={["oh no!"]} />
251 + """
252 + attr :id, :any, default: nil
253 + attr :name, :any
254 + attr :label, :string, default: nil
255 + attr :value, :any
256 + attr :lb_a, :string
257 + attr :lb_i, :string
258 + attr :rb_a, :string
259 + attr :rb_i, :string
260 +
261 + attr :type, :string,
262 + default: "text",
263 + values: ~w(checkbox color date datetime-local email file hidden month number password
264 + range radio search select tel text textarea time url week)
265 +
266 + attr :field, Phoenix.HTML.FormField,
267 + doc: "a form field struct retrieved from the form, for example: @form[:email]"
268 +
269 + attr :errors, :list, default: []
270 + attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
271 + attr :prompt, :string, default: "Choose a value", doc: "the prompt for select inputs"
272 + attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
273 + attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
274 +
275 + attr :rest, :global,
276 + include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
277 + multiple pattern placeholder readonly required rows size step)
278 +
279 + slot :inner_block
280 +
281 + def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
282 + assigns
283 + |> assign(field: nil, id: assigns.id || field.id)
284 + |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
285 + |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
286 + |> assign_new(:value, fn -> field.value end)
287 + |> input()
288 + end
289 +
290 + def input(%{type: "checkbox", value: value} = assigns) do
291 + assigns =
292 + assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
293 +
294 + ~H"""
295 + <div phx-feedback-for={@name}>
296 + <label class="flex items-center gap-4 text-sm leading-6 text-primary-content">
297 + <input type="hidden" name={@name} value="false" />
298 + <input
299 + type="checkbox"
300 + id={@id}
301 + name={@name}
302 + value="true"
303 + checked={@checked}
304 + class="rounded border-zinc-300 text-primary-content focus:ring-0"
305 + {@rest}
306 + />
307 + <%= @label %>
308 + </label>
309 + <.error :for={msg <- @errors}><%= msg %></.error>
310 + </div>
311 + """
312 + end
313 +
314 + def input(%{type: "select"} = assigns) do
315 + ~H"""
316 + <div phx-feedback-for={@name}>
317 + <.label for={@id}><%= @label %></.label>
318 + <select
319 + id={@id}
320 + name={@name}
321 + class="mt-2 block w-full rounded-md border border-gray-300 bg-primary text-primary-content shadow-sm focus:border-accent focus:ring-0 sm:text-sm"
322 + multiple={@multiple}
323 + {@rest}
324 + >
325 + <option :if={@prompt} value=""><%= @prompt %></option>
326 + <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
327 + </select>
328 + <.error :for={msg <- @errors}><%= msg %></.error>
329 + </div>
330 + """
331 + end
332 +
333 + def input(%{type: "textarea"} = assigns) do
334 + ~H"""
335 + <div phx-feedback-for={@name}>
336 + <.label for={@id}><%= @label %></.label>
337 + <textarea
338 + id={@id}
339 + name={@name}
340 + class={[
341 + "mt-2 block w-full rounded-lg bg-primary text-primary-content focus:ring-0 sm:text-sm sm:leading-6",
342 + "min-h-[6rem] phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
343 + @errors == [] && "border-zinc-300 focus:border-zinc-400",
344 + @errors != [] && "border-rose-400 focus:border-rose-400"
345 + ]}
346 + {@rest}
347 + ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
348 + <.error :for={msg <- @errors}><%= msg %></.error>
349 + </div>
350 + """
351 + end
352 +
353 + def input(%{lb_a: action, lb_i: icon} = assigns) do
354 + ~H"""
355 + <div phx-feedback-for={@name}>
356 + <.label for={@id}><%= @label %></.label>
357 + <div class="input-group">
358 + <btn class="btn btn-accent" phx-click="{@action}" phx-target={@myself}>
359 + <i class="fa-solid {@icon}"></i>
360 + </btn>
361 + <input
362 + type={@type}
363 + name={@name}
364 + id={@id}
365 + value={Phoenix.HTML.Form.normalize_value(@type, @value)}
366 + class={[
367 + "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
368 + "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
369 + @errors == [] && "border-zinc-300 focus:border-zinc-400",
370 + @errors != [] && "border-rose-400 focus:border-rose-400"
371 + ]}
372 + {@rest}
373 + />
374 + </div>
375 + <.error :for={msg <- @errors}><%= msg %></.error>
376 + </div>
377 + """
378 + end
379 +
380 + def input(%{rb_a: action, rb_i: icon} = assigns) do
381 + ~H"""
382 + <div phx-feedback-for={@name}>
383 + <.label for={@id}><%= @label %></.label>
384 + <div class="input-group">
385 + <input
386 + type={@type}
387 + name={@name}
388 + id={@id}
389 + value={Phoenix.HTML.Form.normalize_value(@type, @value)}
390 + class={[
391 + "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
392 + "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
393 + @errors == [] && "border-zinc-300 focus:border-zinc-400",
394 + @errors != [] && "border-rose-400 focus:border-rose-400"
395 + ]}
396 + {@rest}
397 + />
398 + <btn class="btn btn-accent" phx-click="{@action}" phx-target={@myself}>
399 + <i class="fa-solid {@icon}"></i>
400 + </btn>
401 + </div>
402 + <.error :for={msg <- @errors}><%= msg %></.error>
403 + </div>
404 + """
405 + end
406 +
407 + def input(%{lb_a: actionl, lb_i: iconl, rb_a: actionr, rb_i: iconr} = assigns) do
408 + ~H"""
409 + <div phx-feedback-for={@name}>
410 + <.label for={@id}><%= @label %></.label>
411 + <div class="input-group">
412 + <btn class="btn btn-accent" phx-click="{@actionl}" phx-target={@myself}>
413 + <i class="fa-solid {@iconl}"></i>
414 + </btn>
415 + <input
416 + type={@type}
417 + name={@name}
418 + id={@id}
419 + value={Phoenix.HTML.Form.normalize_value(@type, @value)}
420 + class={[
421 + "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
422 + "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
423 + @errors == [] && "border-zinc-300 focus:border-zinc-400",
424 + @errors != [] && "border-rose-400 focus:border-rose-400"
425 + ]}
426 + {@rest}
427 + />
428 + <btn class="btn btn-accent" phx-click="{@actionr}" phx-target={@myself}>
429 + <i class="fa-solid {@iconr}"></i>
430 + </btn>
431 + </div>
432 + <.error :for={msg <- @errors}><%= msg %></.error>
433 + </div>
434 + """
435 + end
436 +
437 + # All other inputs text, datetime-local, url, password, etc. are handled here...
438 + def input(assigns) do
439 + ~H"""
440 + <div phx-feedback-for={@name}>
441 + <.label for={@id}><%= @label %></.label>
442 + <input
443 + type={@type}
444 + name={@name}
445 + id={@id}
446 + value={Phoenix.HTML.Form.normalize_value(@type, @value)}
447 + class={[
448 + "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
449 + "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
450 + @errors == [] && "border-zinc-300 focus:border-zinc-400",
451 + @errors != [] && "border-rose-400 focus:border-rose-400"
452 + ]}
453 + {@rest}
454 + />
455 + <.error :for={msg <- @errors}><%= msg %></.error>
456 + </div>
457 + """
458 + end
459 +
460 + @doc """
461 + Renders a label.
462 + """
463 + attr :for, :string, default: nil
464 + slot :inner_block, required: true
465 +
466 + def label(assigns) do
467 + ~H"""
468 + <label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
469 + <%= render_slot(@inner_block) %>
470 + </label>
471 + """
472 + end
473 +
474 + @doc """
475 + Generates a generic error message.
476 + """
477 + slot :inner_block, required: true
478 +
479 + def error(assigns) do
480 + ~H"""
481 + <p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600 phx-no-feedback:hidden">
482 + <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
483 + <%= render_slot(@inner_block) %>
484 + </p>
485 + """
486 + end
487 +
488 + @doc """
489 + Renders a header with title.
490 + """
491 + attr :class, :string, default: nil
492 +
493 + slot :inner_block, required: true
494 + slot :subtitle
495 + slot :actions
496 +
497 + def header(assigns) do
498 + ~H"""
499 + <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
500 + <div>
501 + <h1 class="text-lg font-semibold leading-8 text-zinc-800">
502 + <%= render_slot(@inner_block) %>
503 + </h1>
504 + <p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
505 + <%= render_slot(@subtitle) %>
506 + </p>
507 + </div>
508 + <div class="flex-none"><%= render_slot(@actions) %></div>
509 + </header>
510 + """
511 + end
512 +
513 + @doc ~S"""
514 + Renders a table with generic styling.
515 +
516 + ## Examples
517 +
518 + <.table id="users" rows={@users}>
519 + <:col :let={user} label="id"><%= user.id %></:col>
520 + <:col :let={user} label="username"><%= user.username %></:col>
521 + </.table>
522 + """
523 + attr :id, :string, required: true
524 + attr :rows, :list, required: true
525 + attr :row_id, :any, default: nil, doc: "the function for generating the row id"
526 + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
527 +
528 + attr :row_item, :any,
529 + default: &Function.identity/1,
530 + doc: "the function for mapping each row before calling the :col and :action slots"
531 +
532 + slot :col, required: true do
533 + attr :label, :string
534 + end
535 +
536 + slot :action, doc: "the slot for showing user actions in the last table column"
537 +
538 + def table(assigns) do
539 + assigns =
540 + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
541 + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
542 + end
543 +
544 + ~H"""
545 + <div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
546 + <table class="w-[40rem] mt-11 sm:w-full">
547 + <thead class="text-sm text-left leading-6 text-zinc-500">
548 + <tr>
549 + <th :for={col <- @col} class="p-0 pr-6 pb-4 font-normal"><%= col[:label] %></th>
550 + <th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
551 + </tr>
552 + </thead>
553 + <tbody
554 + id={@id}
555 + phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
556 + class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
557 + >
558 + <tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
559 + <td
560 + :for={{col, i} <- Enum.with_index(@col)}
561 + phx-click={@row_click && @row_click.(row)}
562 + class={["relative p-0", @row_click && "hover:cursor-pointer"]}
563 + >
564 + <div class="block py-4 pr-6">
565 + <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
566 + <span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
567 + <%= render_slot(col, @row_item.(row)) %>
568 + </span>
569 + </div>
570 + </td>
571 + <td :if={@action != []} class="relative w-14 p-0">
572 + <div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
573 + <span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
574 + <span
575 + :for={action <- @action}
576 + class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
577 + >
578 + <%= render_slot(action, @row_item.(row)) %>
579 + </span>
580 + </div>
581 + </td>
582 + </tr>
583 + </tbody>
584 + </table>
585 + </div>
586 + """
587 + end
588 +
589 + @doc """
590 + Renders a data list.
591 +
592 + ## Examples
593 +
594 + <.list>
595 + <:item title="Title"><%= @post.title %></:item>
596 + <:item title="Views"><%= @post.views %></:item>
597 + </.list>
598 + """
599 + slot :item, required: true do
600 + attr :title, :string, required: true
601 + end
602 +
603 + def list(assigns) do
604 + ~H"""
605 + <div class="mt-14">
606 + <dl class="-my-4 divide-y divide-zinc-100">
607 + <div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
608 + <dt class="w-1/4 flex-none text-zinc-500"><%= item.title %></dt>
609 + <dd class="text-zinc-700"><%= render_slot(item) %></dd>
610 + </div>
611 + </dl>
612 + </div>
613 + """
614 + end
615 +
616 + @doc """
617 + Renders a back navigation link.
618 +
619 + ## Examples
620 +
621 + <.back navigate={~p"/posts"}>Back to posts</.back>
622 + """
623 + attr :navigate, :any, required: true
624 + slot :inner_block, required: true
625 +
626 + def back(assigns) do
627 + ~H"""
628 + <div class="mt-16">
629 + <.link
630 + navigate={@navigate}
631 + class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
632 + >
633 + <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
634 + <%= render_slot(@inner_block) %>
635 + </.link>
636 + </div>
637 + """
638 + end
639 +
640 + @doc """
641 + Renders a [Heroicon](https://heroicons.com).
642 +
643 + Heroicons come in three styles – outline, solid, and mini.
644 + By default, the outline style is used, but solid an mini may
645 + be applied by using the `-solid` and `-mini` suffix.
646 +
647 + You can customize the size and colors of the icons by setting
648 + width, height, and background color classes.
649 +
650 + Icons are extracted from your `assets/vendor/heroicons` directory and bundled
651 + within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
652 +
653 + ## Examples
654 +
655 + <.icon name="hero-x-mark-solid" />
656 + <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
657 + """
658 + attr :name, :string, required: true
659 + attr :class, :string, default: nil
660 +
661 + def icon(%{name: "hero-" <> _} = assigns) do
662 + ~H"""
663 + <span class={[@name, @class]} />
664 + """
665 + end
666 +
667 + ## JS Commands
668 +
669 + def show(js \\ %JS{}, selector) do
670 + JS.show(js,
671 + to: selector,
672 + transition:
673 + {"transition-all transform ease-out duration-300",
674 + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
675 + "opacity-100 translate-y-0 sm:scale-100"}
676 + )
677 + end
678 +
679 + def hide(js \\ %JS{}, selector) do
680 + JS.hide(js,
681 + to: selector,
682 + time: 200,
683 + transition:
684 + {"transition-all transform ease-in duration-200",
685 + "opacity-100 translate-y-0 sm:scale-100",
686 + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
687 + )
688 + end
689 +
690 + def show_modal(js \\ %JS{}, id) when is_binary(id) do
691 + js
692 + |> JS.show(to: "##{id}")
693 + |> JS.show(
694 + to: "##{id}-bg",
695 + transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
696 + )
697 + |> show("##{id}-container")
698 + |> JS.add_class("overflow-hidden", to: "body")
699 + |> JS.focus_first(to: "##{id}-content")
700 + end
701 +
702 + def hide_modal(js \\ %JS{}, id) do
703 + js
704 + |> JS.hide(
705 + to: "##{id}-bg",
706 + transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
707 + )
708 + |> hide("##{id}-container")
709 + |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
710 + |> JS.remove_class("overflow-hidden", to: "body")
711 + |> JS.pop_focus()
712 + end
713 +
714 + @doc """
715 + Translates an error message using gettext.
716 + """
717 + def translate_error({msg, opts}) do
718 + # When using gettext, we typically pass the strings we want
719 + # to translate as a static argument:
720 + #
721 + # # Translate the number of files with plural rules
722 + # dngettext("errors", "1 file", "%{count} files", count)
723 + #
724 + # However the error messages in our forms and APIs are generated
725 + # dynamically, so we need to translate them by calling Gettext
726 + # with our gettext backend as first argument. Translations are
727 + # available in the errors.po file (as we use the "errors" domain).
728 + if count = opts[:count] do
729 + Gettext.dngettext(RoomSanctumWeb.Gettext, "errors", msg, msg, count, opts)
730 + else
731 + Gettext.dgettext(RoomSanctumWeb.Gettext, "errors", msg, opts)
732 + end
733 + end
734 +
735 + @doc """
736 + Translates the errors for a field from a keyword list of errors.
737 + """
738 + def translate_errors(errors, field) when is_list(errors) do
739 + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
740 + end
741 +end
added apps/room_sanctum/lib/room_sanctum_web/live/cfg_scribus_live/form_component.ex
+98 −0
@@ -0,0 +1,98 @@
1 +defmodule RoomSanctumWeb.ScribusLive.FormComponent do
2 + use RoomSanctumWeb, :live_component
3 +
4 + alias RoomSanctum.Configuration
5 +
6 + @impl true
7 + def render(assigns) do
8 + ~H"""
9 + <div>
10 + <.header>
11 + <%= @title %>
12 + <:subtitle>Use this form to manage scribus records in your database.</:subtitle>
13 + </.header>
14 +
15 + <.simple_form
16 + for={@form}
17 + id="scribus-form"
18 + phx-target={@myself}
19 + phx-change="validate"
20 + phx-submit="save"
21 + >
22 + <.input field={@form[:name]} type="text" label="Name" />
23 + <.input field={@form[:resolution]} type="text" label="Resolution" />
24 + <.input
25 + field={@form[:configuration]}
26 + type="select"
27 + multiple
28 + label="Configuration"
29 + options={[]}
30 + />
31 + <:actions>
32 + <.button phx-disable-with="Saving...">Save Scribus</.button>
33 + </:actions>
34 + </.simple_form>
35 + </div>
36 + """
37 + end
38 +
39 + @impl true
40 + def update(%{scribus: scribus} = assigns, socket) do
41 + changeset = Configuration.change_scribus(scribus)
42 +
43 + {:ok,
44 + socket
45 + |> assign(assigns)
46 + |> assign_form(changeset)}
47 + end
48 +
49 + @impl true
50 + def handle_event("validate", %{"scribus" => scribus_params}, socket) do
51 + changeset =
52 + socket.assigns.scribus
53 + |> Configuration.change_scribus(scribus_params)
54 + |> Map.put(:action, :validate)
55 +
56 + {:noreply, assign_form(socket, changeset)}
57 + end
58 +
59 + def handle_event("save", %{"scribus" => scribus_params}, socket) do
60 + save_scribus(socket, socket.assigns.action, scribus_params)
61 + end
62 +
63 + defp save_scribus(socket, :edit, scribus_params) do
64 + case Configuration.update_scribus(socket.assigns.scribus, scribus_params) do
65 + {:ok, scribus} ->
66 + notify_parent({:saved, scribus})
67 +
68 + {:noreply,
69 + socket
70 + |> put_flash(:info, "Scribus updated successfully")
71 + |> push_patch(to: socket.assigns.patch)}
72 +
73 + {:error, %Ecto.Changeset{} = changeset} ->
74 + {:noreply, assign_form(socket, changeset)}
75 + end
76 + end
77 +
78 + defp save_scribus(socket, :new, scribus_params) do
79 + case Configuration.create_scribus(scribus_params) do
80 + {:ok, scribus} ->
81 + notify_parent({:saved, scribus})
82 +
83 + {:noreply,
84 + socket
85 + |> put_flash(:info, "Scribus created successfully")
86 + |> push_patch(to: socket.assigns.patch)}
87 +
88 + {:error, %Ecto.Changeset{} = changeset} ->
89 + {:noreply, assign_form(socket, changeset)}
90 + end
91 + end
92 +
93 + defp assign_form(socket, %Ecto.Changeset{} = changeset) do
94 + assign(socket, :form, to_form(changeset))
95 + end
96 +
97 + defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
98 +end
added apps/room_sanctum/lib/room_sanctum_web/live/cfg_scribus_live/index.ex
+47 −0
@@ -0,0 +1,47 @@
1 +defmodule RoomSanctumWeb.ScribusLive.Index do
2 + use RoomSanctumWeb, :live_view
3 +
4 + alias RoomSanctum.Configuration
5 + alias RoomSanctum.Configuration.Scribus
6 +
7 + @impl true
8 + def mount(_params, _session, socket) do
9 + {:ok, stream(socket, :cfg_scribus, Configuration.list_cfg_scribus())}
10 + end
11 +
12 + @impl true
13 + def handle_params(params, _url, socket) do
14 + {:noreply, apply_action(socket, socket.assigns.live_action, params)}
15 + end
16 +
17 + defp apply_action(socket, :edit, %{"id" => id}) do
18 + socket
19 + |> assign(:page_title, "Edit Scribus")
20 + |> assign(:scribus, Configuration.get_scribus!(id))
21 + end
22 +
23 + defp apply_action(socket, :new, _params) do
24 + socket
25 + |> assign(:page_title, "New Scribus")
26 + |> assign(:scribus, %Scribus{})
27 + end
28 +
29 + defp apply_action(socket, :index, _params) do
30 + socket
31 + |> assign(:page_title, "Listing Cfg scribus")
32 + |> assign(:scribus, nil)
33 + end
34 +
35 + @impl true
36 + def handle_info({RoomSanctumWeb.ScribusLive.FormComponent, {:saved, scribus}}, socket) do
37 + {:noreply, stream_insert(socket, :cfg_scribus, scribus)}
38 + end
39 +
40 + @impl true
41 + def handle_event("delete", %{"id" => id}, socket) do
42 + scribus = Configuration.get_scribus!(id)
43 + {:ok, _} = Configuration.delete_scribus(scribus)
44 +
45 + {:noreply, stream_delete(socket, :cfg_scribus, scribus)}
46 + end
47 +end
added apps/room_sanctum/lib/room_sanctum_web/live/cfg_scribus_live/index.html.heex
+43 −0
@@ -0,0 +1,43 @@
1 +<.header>
2 + Listing Cfg scribus
3 + <:actions>
4 + <.link patch={~p"/cfg_scribus/new"}>
5 + <.button>New Scribus</.button>
6 + </.link>
7 + </:actions>
8 +</.header>
9 +
10 +<.table
11 + id="cfg_scribus"
12 + rows={@streams.cfg_scribus}
13 + row_click={fn {_id, scribus} -> JS.navigate(~p"/cfg_scribus/#{scribus}") end}
14 +>
15 + <:col :let={{_id, scribus}} label="Name"><%= scribus.name %></:col>
16 + <:col :let={{_id, scribus}} label="Resolution"><%= scribus.resolution %></:col>
17 + <:col :let={{_id, scribus}} label="Configuration"><%= scribus.configuration %></:col>
18 + <:action :let={{_id, scribus}}>
19 + <div class="sr-only">
20 + <.link navigate={~p"/cfg_scribus/#{scribus}"}>Show</.link>
21 + </div>
22 + <.link patch={~p"/cfg_scribus/#{scribus}/edit"}>Edit</.link>
23 + </:action>
24 + <:action :let={{id, scribus}}>
25 + <.link
26 + phx-click={JS.push("delete", value: %{id: scribus.id}) |> hide("##{id}")}
27 + data-confirm="Are you sure?"
28 + >
29 + Delete
30 + </.link>
31 + </:action>
32 +</.table>
33 +
34 +<.modal :if={@live_action in [:new, :edit]} id="scribus-modal" show on_cancel={JS.patch(~p"/cfg_scribus")}>
35 + <.live_component
36 + module={RoomSanctumWeb.ScribusLive.FormComponent}
37 + id={@scribus.id || :new}
38 + title={@page_title}
39 + action={@live_action}
40 + scribus={@scribus}
41 + patch={~p"/cfg_scribus"}
42 + />
43 +</.modal>
added apps/room_sanctum/lib/room_sanctum_web/live/cfg_scribus_live/show.ex
+21 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/cfg_scribus_live/show.html.heex
+28 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_confirmation_instructions_live.ex
+51 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_confirmation_live.ex
+58 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_forgot_password_live.ex
+50 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_login_live.ex
+43 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_registration_live.ex
+87 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_reset_password_live.ex
+89 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/live/user_settings_live.ex
+167 −0

Click to load diff…

added apps/room_sanctum/lib/room_sanctum_web/user_auth.ex
+227 −0

Click to load diff…

added apps/room_sanctum/priv/repo/migrations/20230609152457_create_cfg_scribus.exs
+16 −0

Click to load diff…

Parents: 17b1c36