Music streaming on ATProto!
1This is a web application written using the Phoenix web framework. 2 3## Project guidelines 4 5- Use `mix precommit` alias when you are done with all changes and fix any pending issues 6- Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps 7 8### Phoenix v1.8 guidelines 9 10- **Always** begin your LiveView templates with `<Layouts.app flash={@flash} ...>` which wraps all inner content 11- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again 12- Anytime you run into errors with no `current_scope` assign: 13 - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `<Layouts.app>` 14 - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed 15- Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module 16- Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar 17- **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors 18- If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your 19custom classes must fully style the input 20 21### JS and CSS guidelines 22 23- **Use Tailwind CSS classes and custom CSS rules** to create polished, responsive, and visually stunning interfaces. 24- Tailwindcss v4 **no longer needs a tailwind.config.js** and uses a new import syntax in `app.css`: 25 26 @import "tailwindcss" source(none); 27 @source "../css"; 28 @source "../js"; 29 @source "../../lib/my_app_web"; 30 31- **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new` 32- **Never** use `@apply` when writing raw css 33- **Always** manually write your own tailwind-based components instead of using daisyUI for a unique, world-class design 34- Out of the box **only the app.js and app.css bundles are supported** 35 - You cannot reference an external vendor'd script `src` or link `href` in the layouts 36 - You must import the vendor deps into app.js and app.css to use them 37 - **Never write inline <script>custom js</script> tags within templates** 38 39### UI/UX & design guidelines 40 41- **Produce world-class UI designs** with a focus on usability, aesthetics, and modern design principles 42- Implement **subtle micro-interactions** (e.g., button hover effects, and smooth transitions) 43- Ensure **clean typography, spacing, and layout balance** for a refined, premium look 44- Focus on **delightful details** like hover effects, loading states, and smooth page transitions 45 46 47<!-- usage-rules-start --> 48 49<!-- phoenix:elixir-start --> 50## Elixir guidelines 51 52- Elixir lists **do not support index based access via the access syntax** 53 54 **Never do this (invalid)**: 55 56 i = 0 57 mylist = ["blue", "green"] 58 mylist[i] 59 60 Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie: 61 62 i = 0 63 mylist = ["blue", "green"] 64 Enum.at(mylist, i) 65 66- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc 67 you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie: 68 69 # INVALID: we are rebinding inside the `if` and the result never gets assigned 70 if connected?(socket) do 71 socket = assign(socket, :val, val) 72 end 73 74 # VALID: we rebind the result of the `if` to a new variable 75 socket = 76 if connected?(socket) do 77 assign(socket, :val, val) 78 end 79 80- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors 81- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets 82- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package) 83- Don't use `String.to_atom/1` on user input (memory leak risk) 84- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards 85- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)` 86- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option 87 88## Mix guidelines 89 90- Read the docs and options before using tasks (by using `mix help task_name`) 91- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed` 92- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason 93 94## Test guidelines 95 96- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests 97- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests 98 - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message: 99 100 ref = Process.monitor(pid) 101 assert_receive {:DOWN, ^ref, :process, ^pid, :normal} 102 103 - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages 104<!-- phoenix:elixir-end --> 105 106<!-- phoenix:phoenix-start --> 107## Phoenix guidelines 108 109- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes. 110 111- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie: 112 113 scope "/admin", AppWeb.Admin do 114 pipe_through :browser 115 116 live "/users", UserLive, :index 117 end 118 119 the UserLive route would point to the `AppWeb.Admin.UserLive` module 120 121- `Phoenix.View` no longer is needed or included with Phoenix, don't use it 122<!-- phoenix:phoenix-end --> 123 124<!-- phoenix:ecto-start --> 125## Ecto Guidelines 126 127- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email` 128- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs` 129- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string` 130- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed 131- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields 132- Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct 133- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied 134<!-- phoenix:ecto-end --> 135 136<!-- phoenix:html-start --> 137## Phoenix HTML guidelines 138 139- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E` 140- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated 141- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]` 142- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`) 143- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name) 144 145- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals. 146 147 **Never do this (invalid)**: 148 149 <%= if condition do %> 150 ... 151 <% else if other_condition %> 152 ... 153 <% end %> 154 155 Instead **always** do this: 156 157 <%= cond do %> 158 <% condition -> %> 159 ... 160 <% condition2 -> %> 161 ... 162 <% true -> %> 163 ... 164 <% end %> 165 166- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `<pre>` or `<code>` block you *must* annotate the parent tag with `phx-no-curly-interpolation`: 167 168 <code phx-no-curly-interpolation> 169 let obj = {key: "val"} 170 </code> 171 172 Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax 173 174- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**: 175 176 <a class={[ 177 "px-2 text-white", 178 @some_flag && "py-5", 179 if(@other_condition, do: "border-red-500", else: "border-blue-100"), 180 ... 181 ]}>Text</a> 182 183 and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`) 184 185 and **never** do this, since it's invalid (note the missing `[` and `]`): 186 187 <a class={ 188 "px-2 text-white", 189 @some_flag && "py-5" 190 }> ... 191 => Raises compile syntax error on invalid HEEx attr syntax 192 193- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>` 194- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`) 195- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`. 196 197 **Always** do this: 198 199 <div id={@id}> 200 {@my_assign} 201 <%= if @some_block_condition do %> 202 {@another_assign} 203 <% end %> 204 </div> 205 206 and **Never** do this – the program will terminate with a syntax error: 207 208 <%!-- THIS IS INVALID NEVER EVER DO THIS --%> 209 <div id="<%= @invalid_interpolation %>"> 210 {if @invalid_block_construct do} 211 {end} 212 </div> 213<!-- phoenix:html-end --> 214 215<!-- phoenix:liveview-start --> 216## Phoenix LiveView guidelines 217 218- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews 219- **Avoid LiveComponent's** unless you have a strong, specific need for them 220- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive` 221 222### LiveView streams 223 224- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations: 225 - basic append of N items - `stream(socket, :messages, [new_msg])` 226 - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items) 227 - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)` 228 - deleting items - `stream_delete(socket, :messages, msg)` 229 230- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be: 231 232 <div id="messages" phx-update="stream"> 233 <div :for={{id, msg} <- @streams.messages} id={id}> 234 {msg.text} 235 </div> 236 </div> 237 238- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**: 239 240 def handle_event("filter", %{"filter" => filter}, socket) do 241 # re-fetch the messages based on the filter 242 messages = list_messages(filter) 243 244 {:noreply, 245 socket 246 |> assign(:messages_empty?, messages == []) 247 # reset the stream with the new messages 248 |> stream(:messages, messages, reset: true)} 249 end 250 251- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes: 252 253 <div id="tasks" phx-update="stream"> 254 <div class="hidden only:block">No tasks yet</div> 255 <div :for={{id, task} <- @stream.tasks} id={id}> 256 {task.name} 257 </div> 258 </div> 259 260 The above only works if the empty state is the only HTML block alongside the stream for-comprehension. 261 262- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items 263 along with the updated assign: 264 265 def handle_event("edit_message", %{"message_id" => message_id}, socket) do 266 message = Chat.get_message!(message_id) 267 edit_form = to_form(Chat.change_message(message, %{content: message.content})) 268 269 # re-insert message so @editing_message_id toggle logic takes effect for that stream item 270 {:noreply, 271 socket 272 |> stream_insert(:messages, message) 273 |> assign(:editing_message_id, String.to_integer(message_id)) 274 |> assign(:edit_form, edit_form)} 275 end 276 277 And in the template: 278 279 <div id="messages" phx-update="stream"> 280 <div :for={{id, message} <- @streams.messages} id={id} class="flex group"> 281 {message.username} 282 <%= if @editing_message_id == message.id do %> 283 <%!-- Edit mode --%> 284 <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit"> 285 ... 286 </.form> 287 <% end %> 288 </div> 289 </div> 290 291- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections 292 293### LiveView JavaScript interop 294 295- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute 296- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised 297 298LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx, 299and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor. 300 301#### Inline colocated js hooks 302 303**Never** write raw embedded `<script>` tags in heex as they are incompatible with LiveView. 304Instead, **always use a colocated js hook script tag (`:type={Phoenix.LiveView.ColocatedHook}`) 305when writing scripts inside the template**: 306 307 <input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" /> 308 <script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber"> 309 export default { 310 mounted() { 311 this.el.addEventListener("input", e => { 312 let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/) 313 if(match) { 314 this.el.value = `${match[1]}-${match[2]}-${match[3]}` 315 } 316 }) 317 } 318 } 319 </script> 320 321- colocated hooks are automatically integrated into the app.js bundle 322- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber` 323 324#### External phx-hook 325 326External JS hooks (`<div id="myhook" phx-hook="MyHook">`) must be placed in `assets/js/` and passed to the 327LiveSocket constructor: 328 329 const MyHook = { 330 mounted() { ... } 331 } 332 let liveSocket = new LiveSocket("/live", Socket, { 333 hooks: { MyHook } 334 }); 335 336#### Pushing events between client and server 337 338Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle. 339**Always** return or rebind the socket on `push_event/3` when pushing events: 340 341 # re-bind socket so we maintain event state to be pushed 342 socket = push_event(socket, "my_event", %{...}) 343 344 # or return the modified socket directly: 345 def handle_event("some_event", _, socket) do 346 {:noreply, push_event(socket, "my_event", %{...})} 347 end 348 349Pushed events can then be picked up in a JS hook with `this.handleEvent`: 350 351 mounted() { 352 this.handleEvent("my_event", data => console.log("from server:", data)); 353 } 354 355Clients can also push an event to the server and receive a reply with `this.pushEvent`: 356 357 mounted() { 358 this.el.addEventListener("click", e => { 359 this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply)); 360 }) 361 } 362 363Where the server handled it via: 364 365 def handle_event("my_event", %{"one" => 1}, socket) do 366 {:reply, %{two: 2}, socket} 367 end 368 369### LiveView tests 370 371- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions 372- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions 373- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests 374- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc 375- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")` 376- Instead of relying on testing text content, which can change, favor testing for the presence of key elements 377- Focus on testing outcomes rather than implementation details 378- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be 379- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie: 380 381 html = render(view) 382 document = LazyHTML.from_fragment(html) 383 matches = LazyHTML.filter(document, "your-complex-selector") 384 IO.inspect(matches, label: "Matches") 385 386### Form handling 387 388#### Creating a form from params 389 390If you want to create a form based on `handle_event` params: 391 392 def handle_event("submitted", params, socket) do 393 {:noreply, assign(socket, form: to_form(params))} 394 end 395 396When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys. 397 398You can also specify a name to nest the params: 399 400 def handle_event("submitted", %{"user" => user_params}, socket) do 401 {:noreply, assign(socket, form: to_form(user_params, as: :user))} 402 end 403 404#### Creating a form from changesets 405 406When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: 407 408 defmodule MyApp.Users.User do 409 use Ecto.Schema 410 ... 411 end 412 413And then you create a changeset that you pass to `to_form`: 414 415 %MyApp.Users.User{} 416 |> Ecto.Changeset.change() 417 |> to_form() 418 419Once the form is submitted, the params will be available under `%{"user" => user_params}`. 420 421In the template, the form form assign can be passed to the `<.form>` function component: 422 423 <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> 424 <.input field={@form[:field]} type="text" /> 425 </.form> 426 427Always give the form an explicit, unique DOM ID, like `id="todo-form"`. 428 429#### Avoiding form errors 430 431**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**: 432 433 <%!-- ALWAYS do this (valid) --%> 434 <.form for={@form} id="my-form"> 435 <.input field={@form[:field]} type="text" /> 436 </.form> 437 438And **never** do this: 439 440 <%!-- NEVER do this (invalid) --%> 441 <.form for={@changeset} id="my-form"> 442 <.input field={@changeset[:field]} type="text" /> 443 </.form> 444 445- You are FORBIDDEN from accessing the changeset in the template as it will cause errors 446- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset 447<!-- phoenix:liveview-end --> 448 449<!-- usage-rules-end -->