neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
Log in to mark this repository.
my brain is clealy full of fuck.2
9c1f4a5 · gmorell · 2023-06-11 04:59
Files changed
added
apps/room_sanctum/lib/room_sanctum/configuration/scribus.ex
+20
−0
| 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
| 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
| 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
| 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
| 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