diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..ef8840c --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,6 @@ +[ + import_deps: [:ecto, :ecto_sql, :phoenix], + subdirectories: ["priv/*/migrations"], + plugins: [Phoenix.LiveView.HTMLFormatter], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] +] diff --git a/.gitignore b/.gitignore index a9f66e5..9d95dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,40 @@ vite.config.ts.timestamp-* # Nix result -# Dumps -erl_crash.dump \ No newline at end of file +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore package tarball (built via "mix hex.build"). +comet-*.tar + +# Ignore assets that are produced by build tools. +/priv/static/assets/ + +# Ignore digested assets cache. +/priv/static/cache_manifest.json + +# In case you use Node.js/npm, you want to ignore these. +npm-debug.log +/assets/node_modules/ + diff --git a/.vscode/settings.json b/.vscode/settings.json index ee4d1da..436d006 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,8 @@ { "json.schemas": [ { - "fileMatch": ["/packages/lexicons/*/**/*.json"], + "fileMatch": ["/lexicons/*/**/*.json"], "url": "https://gist.githubusercontent.com/mary-ext/6e428031c18799d1587048b456d118cb/raw/4322c492384ac5da33986dee9588938a88d922f1/schema.json" } - ], - "elixirLS.projectDir": "./apps/backend", - "search.exclude": { - "**/node_modules": true, - "**/bower_components": true, - "**/*.code-search": true, - "packages/lexicons/src/**/*": true - } + ], } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ee89293 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,449 @@ +This is a web application written using the Phoenix web framework. + +## Project guidelines + +- Use `mix precommit` alias when you are done with all changes and fix any pending issues +- 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 + +### Phoenix v1.8 guidelines + +- **Always** begin your LiveView templates with `` which wraps all inner content +- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again +- Anytime you run into errors with no `current_scope` assign: + - You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `` + - **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed +- 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 +- 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 +- **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 +- 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 +custom classes must fully style the input + +### JS and CSS guidelines + +- **Use Tailwind CSS classes and custom CSS rules** to create polished, responsive, and visually stunning interfaces. +- Tailwindcss v4 **no longer needs a tailwind.config.js** and uses a new import syntax in `app.css`: + + @import "tailwindcss" source(none); + @source "../css"; + @source "../js"; + @source "../../lib/my_app_web"; + +- **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new` +- **Never** use `@apply` when writing raw css +- **Always** manually write your own tailwind-based components instead of using daisyUI for a unique, world-class design +- Out of the box **only the app.js and app.css bundles are supported** + - You cannot reference an external vendor'd script `src` or link `href` in the layouts + - You must import the vendor deps into app.js and app.css to use them + - **Never write inline tags within templates** + +### UI/UX & design guidelines + +- **Produce world-class UI designs** with a focus on usability, aesthetics, and modern design principles +- Implement **subtle micro-interactions** (e.g., button hover effects, and smooth transitions) +- Ensure **clean typography, spacing, and layout balance** for a refined, premium look +- Focus on **delightful details** like hover effects, loading states, and smooth page transitions + + + + + +## Elixir guidelines + +- Elixir lists **do not support index based access via the access syntax** + + **Never do this (invalid)**: + + i = 0 + mylist = ["blue", "green"] + mylist[i] + + Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie: + + i = 0 + mylist = ["blue", "green"] + Enum.at(mylist, i) + +- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc + 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: + + # INVALID: we are rebinding inside the `if` and the result never gets assigned + if connected?(socket) do + socket = assign(socket, :val, val) + end + + # VALID: we rebind the result of the `if` to a new variable + socket = + if connected?(socket) do + assign(socket, :val, val) + end + +- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors +- **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 +- 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) +- Don't use `String.to_atom/1` on user input (memory leak risk) +- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards +- 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)` +- 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 + +## Mix guidelines + +- Read the docs and options before using tasks (by using `mix help task_name`) +- 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` +- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason + +## Test guidelines + +- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests +- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests + - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message: + + ref = Process.monitor(pid) + assert_receive {:DOWN, ^ref, :process, ^pid, :normal} + + - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages + + + +## Phoenix guidelines + +- 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. + +- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie: + + scope "/admin", AppWeb.Admin do + pipe_through :browser + + live "/users", UserLive, :index + end + + the UserLive route would point to the `AppWeb.Admin.UserLive` module + +- `Phoenix.View` no longer is needed or included with Phoenix, don't use it + + + +## Ecto Guidelines + +- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email` +- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs` +- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string` +- `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 +- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields +- 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 +- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied + + + +## Phoenix HTML guidelines + +- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E` +- **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 +- 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]` +- **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">`) +- 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) + +- 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. + + **Never do this (invalid)**: + + <%= if condition do %> + ... + <% else if other_condition %> + ... + <% end %> + + Instead **always** do this: + + <%= cond do %> + <% condition -> %> + ... + <% condition2 -> %> + ... + <% true -> %> + ... + <% end %> + +- 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 `
` or `` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
+
+      
+        let obj = {key: "val"}
+      
+
+  Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
+
+- 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**:
+
+      Text
+
+  and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
+
+  and **never** do this, since it's invalid (note the missing `[` and `]`):
+
+       ...
+      => Raises compile syntax error on invalid HEEx attr syntax
+
+- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
+- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
+- 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 `<%= ... %>`.
+
+  **Always** do this:
+
+      
+ {@my_assign} + <%= if @some_block_condition do %> + {@another_assign} + <% end %> +
+ + and **Never** do this – the program will terminate with a syntax error: + + <%!-- THIS IS INVALID NEVER EVER DO THIS --%> +
+ {if @invalid_block_construct do} + {end} +
+ + + +## Phoenix LiveView guidelines + +- **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 +- **Avoid LiveComponent's** unless you have a strong, specific need for them +- 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` + +### LiveView streams + +- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations: + - basic append of N items - `stream(socket, :messages, [new_msg])` + - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items) + - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)` + - deleting items - `stream_delete(socket, :messages, msg)` + +- 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: + +
+
+ {msg.text} +
+
+ +- 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**: + + def handle_event("filter", %{"filter" => filter}, socket) do + # re-fetch the messages based on the filter + messages = list_messages(filter) + + {:noreply, + socket + |> assign(:messages_empty?, messages == []) + # reset the stream with the new messages + |> stream(:messages, messages, reset: true)} + end + +- 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: + +
+ +
+ {task.name} +
+
+ + The above only works if the empty state is the only HTML block alongside the stream for-comprehension. + +- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items + along with the updated assign: + + def handle_event("edit_message", %{"message_id" => message_id}, socket) do + message = Chat.get_message!(message_id) + edit_form = to_form(Chat.change_message(message, %{content: message.content})) + + # re-insert message so @editing_message_id toggle logic takes effect for that stream item + {:noreply, + socket + |> stream_insert(:messages, message) + |> assign(:editing_message_id, String.to_integer(message_id)) + |> assign(:edit_form, edit_form)} + end + + And in the template: + +
+
+ {message.username} + <%= if @editing_message_id == message.id do %> + <%!-- Edit mode --%> + <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit"> + ... + + <% end %> +
+
+ +- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections + +### LiveView JavaScript interop + +- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute +- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised + +LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx, +and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor. + +#### Inline colocated js hooks + +**Never** write raw embedded ` + +- colocated hooks are automatically integrated into the app.js bundle +- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber` + +#### External phx-hook + +External JS hooks (`
`) must be placed in `assets/js/` and passed to the +LiveSocket constructor: + + const MyHook = { + mounted() { ... } + } + let liveSocket = new LiveSocket("/live", Socket, { + hooks: { MyHook } + }); + +#### Pushing events between client and server + +Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle. +**Always** return or rebind the socket on `push_event/3` when pushing events: + + # re-bind socket so we maintain event state to be pushed + socket = push_event(socket, "my_event", %{...}) + + # or return the modified socket directly: + def handle_event("some_event", _, socket) do + {:noreply, push_event(socket, "my_event", %{...})} + end + +Pushed events can then be picked up in a JS hook with `this.handleEvent`: + + mounted() { + this.handleEvent("my_event", data => console.log("from server:", data)); + } + +Clients can also push an event to the server and receive a reply with `this.pushEvent`: + + mounted() { + this.el.addEventListener("click", e => { + this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply)); + }) + } + +Where the server handled it via: + + def handle_event("my_event", %{"one" => 1}, socket) do + {:reply, %{two: 2}, socket} + end + +### LiveView tests + +- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions +- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions +- 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 +- **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 +- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")` +- Instead of relying on testing text content, which can change, favor testing for the presence of key elements +- Focus on testing outcomes rather than implementation details +- 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 +- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie: + + html = render(view) + document = LazyHTML.from_fragment(html) + matches = LazyHTML.filter(document, "your-complex-selector") + IO.inspect(matches, label: "Matches") + +### Form handling + +#### Creating a form from params + +If you want to create a form based on `handle_event` params: + + def handle_event("submitted", params, socket) do + {:noreply, assign(socket, form: to_form(params))} + end + +When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys. + +You can also specify a name to nest the params: + + def handle_event("submitted", %{"user" => user_params}, socket) do + {:noreply, assign(socket, form: to_form(user_params, as: :user))} + end + +#### Creating a form from changesets + +When 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: + + defmodule MyApp.Users.User do + use Ecto.Schema + ... + end + +And then you create a changeset that you pass to `to_form`: + + %MyApp.Users.User{} + |> Ecto.Changeset.change() + |> to_form() + +Once the form is submitted, the params will be available under `%{"user" => user_params}`. + +In the template, the form form assign can be passed to the `<.form>` function component: + + <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> + <.input field={@form[:field]} type="text" /> + + +Always give the form an explicit, unique DOM ID, like `id="todo-form"`. + +#### Avoiding form errors + +**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**: + + <%!-- ALWAYS do this (valid) --%> + <.form for={@form} id="my-form"> + <.input field={@form[:field]} type="text" /> + + +And **never** do this: + + <%!-- NEVER do this (invalid) --%> + <.form for={@changeset} id="my-form"> + <.input field={@changeset[:field]} type="text" /> + + +- You are FORBIDDEN from accessing the changeset in the template as it will cause errors +- **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 + + + \ No newline at end of file diff --git a/apps/backend/.formatter.exs b/apps/backend/.formatter.exs deleted file mode 100644 index 9ab9bf9..0000000 --- a/apps/backend/.formatter.exs +++ /dev/null @@ -1,5 +0,0 @@ -[ - import_deps: [:ecto, :ecto_sql, :phoenix], - subdirectories: ["priv/*/migrations"], - inputs: ["*.{ex,exs}", "{config,lib,test,priv}/**/*.{ex,exs}", "priv/*/seeds.exs"] -] diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore deleted file mode 100644 index 6cc3958..0000000 --- a/apps/backend/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# The directory Mix will write compiled artifacts to. -/_build/ - -# If you run "mix test --cover", coverage assets end up here. -/cover/ - -# The directory Mix downloads your dependencies sources to. -/deps/ - -# Where 3rd-party dependencies like ExDoc output generated docs. -/doc/ - -# Ignore .fetch files in case you like to edit your project deps locally. -/.fetch - -# If the VM crashes, it generates a dump, let's ignore it too. -erl_crash.dump - -# Also ignore archive artifacts (built via "mix archive.build"). -*.ez - -# Temporary files, for example, from tests. -/tmp/ - -# Ignore package tarball (built via "mix hex.build"). -comet-*.tar - diff --git a/apps/backend/README.md b/apps/backend/README.md deleted file mode 100644 index 5b202e7..0000000 --- a/apps/backend/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Comet AppView - -[Phoenix](https://www.phoenixframework.org)-powered AppView for Comet. - ---- - -To start your Phoenix server: - -- Run `mix setup` to install and setup dependencies -- Start Phoenix endpoint with `mix phx.server` or inside IEx with - `iex -S mix phx.server` - -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. - -Ready to run in production? Please -[check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). - -## Learn more - -- Official website: https://www.phoenixframework.org/ -- Guides: https://hexdocs.pm/phoenix/overview.html -- Docs: https://hexdocs.pm/phoenix -- Forum: https://elixirforum.com/c/phoenix-forum -- Source: https://github.com/phoenixframework/phoenix diff --git a/apps/backend/config/config.exs b/apps/backend/config/config.exs deleted file mode 100644 index 05bf5ae..0000000 --- a/apps/backend/config/config.exs +++ /dev/null @@ -1,37 +0,0 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Config module. -# -# This configuration file is loaded before any dependency and -# is restricted to this project. - -# General application configuration -import Config - -config :comet, - ecto_repos: [Comet.Repo], - generators: [timestamp_type: :utc_datetime, binary_id: true] - -config :comet, Comet.Repo, migration_primary_key: [name: :id, type: :binary_id] - -# Configures the endpoint -config :comet, CometWeb.Endpoint, - url: [host: "localhost"], - adapter: Bandit.PhoenixAdapter, - render_errors: [ - formats: [json: CometWeb.ErrorJSON], - layout: false - ], - pubsub_server: Comet.PubSub, - live_view: [signing_salt: "oq2xYeBj"] - -# Configures Elixir's Logger -config :logger, :console, - format: "$time $metadata[$level] $message\n", - metadata: [:request_id] - -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason - -# Import environment specific config. This must remain at the bottom -# of this file so it overrides the configuration defined above. -import_config "#{config_env()}.exs" diff --git a/apps/backend/config/prod.exs b/apps/backend/config/prod.exs deleted file mode 100644 index 1fe2d9e..0000000 --- a/apps/backend/config/prod.exs +++ /dev/null @@ -1,7 +0,0 @@ -import Config - -# Do not print debug messages in production -config :logger, level: :info - -# Runtime production configuration, including reading -# of environment variables, is done on config/runtime.exs. diff --git a/apps/backend/lib/atproto/atproto.ex b/apps/backend/lib/atproto/atproto.ex deleted file mode 100644 index d159980..0000000 --- a/apps/backend/lib/atproto/atproto.ex +++ /dev/null @@ -1,173 +0,0 @@ -# AUTOGENERATED: This file was generated using the mix task `lexgen`. -defmodule Atproto do - @default_pds_hostname Application.compile_env( - :atproto, - :default_pds_hostname, - "https://bsky.social" - ) - - @typedoc """ - A type representing the names of the options that can be passed to `query/3` and `procedure/3`. - """ - @type xrpc_opt :: :pds_hostname | :authorization - - @typedoc """ - A keyword list of options that can be passed to `query/3` and `procedure/3`. - """ - @type xrpc_opts :: [{xrpc_opt(), any()}] - - @doc """ - Converts a JSON string, or decoded JSON map, into a struct based on the given module. - - This function uses `String.to_existing_atom/1` to convert the keys of the map to atoms, meaning this will throw an error if the input JSON contains keys which are not already defined as atoms in the existing structs or codebase. - """ - @spec decode_to_struct(module(), binary() | map()) :: map() - def decode_to_struct(module, json) when is_binary(json) do - decode_to_struct(module, Jason.decode!(json, keys: :atoms!)) - end - - def decode_to_struct(module, map) when is_map(map) do - Map.merge(module.new(), map) - end - - @doc """ - Raises an error if any required parameters are missing from the given map. - """ - @spec ensure_required(map(), [String.t()]) :: map() - def ensure_required(params, required) do - if Enum.all?(required, fn key -> Map.has_key?(params, key) end) do - params - else - raise ArgumentError, "Missing one or more required parameters: #{Enum.join(required, ", ")}" - end - end - - @doc """ - Executes a "GET" HTTP request and returns the response body as a map. - - If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used. - """ - @spec query(map(), String.t(), xrpc_opts()) :: Req.Request.t() - def query(params, target, opts \\ []) do - target - |> endpoint(opts) - |> URI.new!() - |> URI.append_query(URI.encode_query(params)) - |> Req.get(build_req_auth(opts)) - |> handle_response(opts) - end - - @doc """ - Executes a "POST" HTTP request and returns the response body as a map. - - If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used. - """ - @spec procedure(map(), String.t(), xrpc_opts()) :: {:ok | :refresh | :error, map()} - def procedure(params, target, opts \\ []) do - req_opts = - opts - |> build_req_auth() - |> build_req_headers(opts, target) - |> build_req_body(params, target) - - target - |> endpoint(opts) - |> URI.new!() - |> Req.post(req_opts) - |> handle_response(opts) - end - - defp build_req_auth(opts) do - case Keyword.get(opts, :access_token) do - nil -> - case Keyword.get(opts, :admin_token) do - nil -> - [] - - token -> - [auth: {:basic, "admin:#{token}"}] - end - - token -> - [auth: {:bearer, token}] - end - end - - defp build_req_headers(req_opts, opts, "com.atproto.repo.uploadBlob") do - [ - {:headers, - [ - {"Content-Type", Keyword.fetch!(opts, :content_type)}, - {"Content-Length", Keyword.fetch!(opts, :content_length)} - ]} - | req_opts - ] - end - - defp build_req_headers(req_opts, _opts, _target), do: req_opts - - defp build_req_body(opts, blob, "com.atproto.repo.uploadBlob") do - [{:body, blob} | opts] - end - - defp build_req_body(opts, %{} = params, _target) when map_size(params) > 0 do - [{:json, params} | opts] - end - - defp build_req_body(opts, _params, _target), do: opts - - defp endpoint(target, opts) do - (Keyword.get(opts, :pds_hostname) || @default_pds_hostname) <> "/xrpc/" <> target - end - - defp handle_response({:ok, %Req.Response{} = response}, opts) do - case response.status do - x when x in 200..299 -> - {:ok, response.body} - - _ -> - if response.body["error"] == "ExpiredToken" do - {:ok, user} = - Com.Atproto.Server.RefreshSession.main(%{}, - access_token: Keyword.get(opts, :refresh_token) - ) - - {:refresh, user} - else - {:error, response.body} - end - end - end - - defp handle_response(error, _opts), do: error - - @doc """ - Converts a "map-like" entity into a standard map. This will also omit any entries that have a `nil` value. - - This is useful for converting structs or schemas into regular maps before sending them over XRPC requests. - - You may optionally pass in an keyword list of options: - - - `:stringify` - `boolean` - If `true`, converts the keys to strings. Otherwise, converts keys to atoms. Default is `false`. - - *Note*: When `false`, this feature uses the `to_existing_atom/1` function to avoid reckless conversion of string keys. - """ - @spec to_map(map() | struct()) :: map() - def to_map(%{__struct__: _} = m, opts \\ []) do - string_keys = Keyword.get(opts, :stringify, false) - - m - |> Map.drop([:__struct__, :__meta__]) - |> Enum.map(fn - {_, nil} -> - nil - - {k, v} when is_atom(k) -> - if string_keys, do: {to_string(k), v}, else: {k, v} - - {k, v} when is_binary(k) -> - if string_keys, do: {k, v}, else: {String.to_existing_atom(k), v} - end) - |> Enum.reject(&is_nil/1) - |> Enum.into(%{}) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/actor/getProfile/xrpc.ex b/apps/backend/lib/atproto/sh/comet/v0/actor/getProfile/xrpc.ex deleted file mode 100644 index ab523ee..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/actor/getProfile/xrpc.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Sh.Comet.V0.Actor.GetProfile do - - @doc """ - Get the profile view of an actor. - """ - @spec main(%{ - actor: String.t() - }, Atproto.xrpc_opts()) :: {:ok, Sh.Comet.V0.Actor.Profile.View.t()} | {:error, any} - def main(params \\ %{}, opts \\ []) do - params - |> Map.take([:actor]) - |> Atproto.ensure_required([:actor]) - |> Atproto.query("sh.comet.v0.actor.getProfile", opts) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/actor/getProfiles/xrpc.ex b/apps/backend/lib/atproto/sh/comet/v0/actor/getProfiles/xrpc.ex deleted file mode 100644 index 0a9ecf4..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/actor/getProfiles/xrpc.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Sh.Comet.V0.Actor.GetProfiles do - - @doc """ - Get the profile views of multiple actors. - """ - @spec main(%{ - actors: list(String.t()) - }, Atproto.xrpc_opts()) :: {:ok, %{profiles: list(Sh.Comet.V0.Actor.Profile.View.t())}} | {:error, any} - def main(params \\ %{}, opts \\ []) do - params - |> Map.take([:actors]) - |> Atproto.ensure_required([:actors]) - |> Atproto.query("sh.comet.v0.actor.getProfiles", opts) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/actor/profile/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/actor/profile/schema.ex deleted file mode 100644 index fccf6cf..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/actor/profile/schema.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Sh.Comet.V0.Actor.Profile do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - A user's Comet profile. - """ - @primary_key {:id, :binary_id, autogenerate: false} - schema "sh.comet.v0.actor.profile" do - field :avatar, :map - field :banner, :map - field :createdAt, :utc_datetime - field :description, :string - field :descriptionFacets, :map - field :displayName, :string - field :featuredItems, {:array, :string} - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.actor.profile" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:avatar, :banner, :createdAt, :description, :descriptionFacets, :displayName, :featuredItems]) - |> validate_length(:featuredItems, max: 5) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/actor/profile/structs.ex b/apps/backend/lib/atproto/sh/comet/v0/actor/profile/structs.ex deleted file mode 100644 index b465d51..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/actor/profile/structs.ex +++ /dev/null @@ -1,104 +0,0 @@ - -defmodule Sh.Comet.V0.Actor.Profile.ViewerState do - @moduledoc """ - Metadata about the requesting account's relationship with the user. TODO: determine if we create our own graph or inherit bsky's. - """ - - @derive Jason.Encoder - defstruct [ - - ] - - @type t() :: %__MODULE__{ - - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Actor.Profile.ViewFull do - @moduledoc """ - - """ - - @derive Jason.Encoder - defstruct [ - avatar: nil, - banner: nil, - createdAt: nil, - description: nil, - descriptionFacets: nil, - did: nil, - displayName: nil, - featuredItems: [], - followersCount: 0, - followsCount: 0, - handle: nil, - indexedAt: nil, - playlistsCount: 0, - tracksCount: 0, - viewer: nil - ] - - @type t() :: %__MODULE__{ - avatar: String.t(), - banner: String.t(), - createdAt: DateTime.t(), - description: String.t(), - descriptionFacets: Sh.Comet.V0.Richtext.Facet.Main.t(), - did: String.t(), - displayName: String.t(), - featuredItems: list(String.t()), - followersCount: integer, - followsCount: integer, - handle: String.t(), - indexedAt: DateTime.t(), - playlistsCount: integer, - tracksCount: integer, - viewer: Sh.Comet.V0.Actor.Profile.ViewerState.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Actor.Profile.View do - @moduledoc """ - - """ - - @derive Jason.Encoder - defstruct [ - avatar: nil, - createdAt: nil, - did: nil, - displayName: nil, - handle: nil, - indexedAt: nil, - viewer: nil - ] - - @type t() :: %__MODULE__{ - avatar: String.t(), - createdAt: DateTime.t(), - did: String.t(), - displayName: String.t(), - handle: String.t(), - indexedAt: DateTime.t(), - viewer: Sh.Comet.V0.Actor.Profile.ViewerState.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/comment/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/comment/schema.ex deleted file mode 100644 index fbd2bf8..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/comment/schema.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Sh.Comet.V0.Feed.Comment do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - A comment on a piece of Comet media. - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.comment" do - field :createdAt, :utc_datetime - field :facets, {:array, :map} - field :langs, {:array, :string} - field :reply, :string - field :subject, :string - field :text, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.comment" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:createdAt, :facets, :langs, :reply, :subject, :text]) - |> validate_required([:createdAt, :subject, :text]) - |> validate_length(:langs, max: 3) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/defs/structs.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/defs/structs.ex deleted file mode 100644 index 529af9f..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/defs/structs.ex +++ /dev/null @@ -1,49 +0,0 @@ - -defmodule Sh.Comet.V0.Feed.Defs.ViewerState do - @moduledoc """ - Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests. - """ - - @derive Jason.Encoder - defstruct [ - featured: false, - like: nil, - repost: nil - ] - - @type t() :: %__MODULE__{ - featured: boolean, - like: String.t(), - repost: String.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Feed.Defs.Link do - @moduledoc """ - Link for the track. Usually to acquire it in some way, e.g. via free download or purchase. | TODO: multiple links? - """ - - @derive Jason.Encoder - defstruct [ - type: nil, - value: nil - ] - - @type t() :: %__MODULE__{ - type: String.t(), - value: String.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/getActorPlaylists/xrpc.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/getActorPlaylists/xrpc.ex deleted file mode 100644 index 2f90536..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/getActorPlaylists/xrpc.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Sh.Comet.V0.Feed.GetActorPlaylists do - - @doc """ - Get a list of an actor's playlists. - """ - @spec main(%{ - actor: String.t(), - cursor: String.t(), - limit: integer - }, Atproto.xrpc_opts()) :: {:ok, %{cursor: String.t(), playlists: list(Sh.Comet.V0.Feed.Playlist.View.t())}} | {:error, any} - def main(params \\ %{}, opts \\ []) do - params - |> Map.take([:actor, :cursor, :limit]) - |> Atproto.ensure_required([:actor]) - |> Atproto.query("sh.comet.v0.feed.getActorPlaylists", opts) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/getActorTracks/xrpc.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/getActorTracks/xrpc.ex deleted file mode 100644 index 3250d14..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/getActorTracks/xrpc.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Sh.Comet.V0.Feed.GetActorTracks do - - @doc """ - Get a list of an actor's tracks. - """ - @spec main(%{ - actor: String.t(), - cursor: String.t(), - limit: integer - }, Atproto.xrpc_opts()) :: {:ok, %{cursor: String.t(), tracks: list(Sh.Comet.V0.Feed.Track.View.t())}} | {:error, any} - def main(params \\ %{}, opts \\ []) do - params - |> Map.take([:actor, :cursor, :limit]) - |> Atproto.ensure_required([:actor]) - |> Atproto.query("sh.comet.v0.feed.getActorTracks", opts) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/like/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/like/schema.ex deleted file mode 100644 index 5fabb1f..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/like/schema.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Sh.Comet.V0.Feed.Like do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - Record representing a 'like' of some media. Weakly linked with just an at-uri. - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.like" do - field :createdAt, :utc_datetime - field :subject, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.like" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:createdAt, :subject]) - |> validate_required([:createdAt, :subject]) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/play/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/play/schema.ex deleted file mode 100644 index 525f987..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/play/schema.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Sh.Comet.V0.Feed.Play do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - Record representing a 'play' of some media. - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.play" do - field :createdAt, :utc_datetime - field :subject, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.play" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:createdAt, :subject]) - |> validate_required([:createdAt, :subject]) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/playlist/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/playlist/schema.ex deleted file mode 100644 index 09d1c4f..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/playlist/schema.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Sh.Comet.V0.Feed.Playlist do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - A Comet playlist, containing many audio tracks. - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.playlist" do - field :createdAt, :utc_datetime - field :description, :string - field :descriptionFacets, :map - field :image, :map - field :link, :map - field :tags, {:array, :string} - field :title, :string - field :type, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.playlist" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:createdAt, :description, :descriptionFacets, :image, :link, :tags, :title, :type]) - |> validate_required([:createdAt, :title, :type]) - |> validate_length(:tags, max: 8) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/playlist/structs.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/playlist/structs.ex deleted file mode 100644 index 8a57359..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/playlist/structs.ex +++ /dev/null @@ -1,44 +0,0 @@ - -defmodule Sh.Comet.V0.Feed.Playlist.View do - @moduledoc """ - - """ - - @derive Jason.Encoder - defstruct [ - author: nil, - cid: nil, - commentCount: 0, - image: nil, - indexedAt: nil, - likeCount: 0, - record: nil, - repostCount: 0, - trackCount: 0, - tracks: [], - uri: nil, - viewer: nil - ] - - @type t() :: %__MODULE__{ - author: Sh.Comet.V0.Actor.Profile.ViewFull.t(), - cid: String.t(), - commentCount: integer, - image: String.t(), - indexedAt: DateTime.t(), - likeCount: integer, - record: Sh.Comet.V0.Feed.Playlist.Main.t(), - repostCount: integer, - trackCount: integer, - tracks: list(Sh.Comet.V0.Feed.Track.View.t()), - uri: String.t(), - viewer: Sh.Comet.V0.Feed.Defs.ViewerState.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/playlistTrack/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/playlistTrack/schema.ex deleted file mode 100644 index a8ddc15..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/playlistTrack/schema.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Sh.Comet.V0.Feed.PlaylistTrack do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - A link between a Comet track and a playlist. - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.playlistTrack" do - field :playlist, :string - field :position, :integer - field :track, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.playlistTrack" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:playlist, :position, :track]) - |> validate_required([:playlist, :position, :track]) - |> validate_length(:position, min: 0) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/repost/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/repost/schema.ex deleted file mode 100644 index f8a169d..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/repost/schema.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Sh.Comet.V0.Feed.Repost do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - Record representing a 'repost' of some media. Weakly linked with just an at-uri. - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.repost" do - field :createdAt, :utc_datetime - field :subject, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.repost" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:createdAt, :subject]) - |> validate_required([:createdAt, :subject]) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/track/schema.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/track/schema.ex deleted file mode 100644 index 2495824..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/track/schema.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Sh.Comet.V0.Feed.Track do - use Ecto.Schema - import Ecto.Changeset - - @doc """ - A Comet audio track. TODO: should probably have some sort of pre-calculated waveform, or have a query to get one from a blob? - """ - @primary_key {:id, :id, autogenerate: false} - schema "sh.comet.v0.feed.track" do - field :audio, :map - field :createdAt, :utc_datetime - field :description, :string - field :descriptionFacets, :map - field :image, :map - field :link, :map - field :tags, {:array, :string} - field :title, :string - - # DO NOT CHANGE! This field is required for all records and must be set to the NSID of the lexicon. - # Ensure that you do not change this field via manual manipulation or changeset operations. - field :"$type", :string, default: "sh.comet.v0.feed.track" - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:audio, :createdAt, :description, :descriptionFacets, :image, :link, :tags, :title]) - |> validate_required([:audio, :createdAt, :title]) - |> validate_length(:tags, max: 8) - end -end diff --git a/apps/backend/lib/atproto/sh/comet/v0/feed/track/structs.ex b/apps/backend/lib/atproto/sh/comet/v0/feed/track/structs.ex deleted file mode 100644 index 3b611dd..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/feed/track/structs.ex +++ /dev/null @@ -1,44 +0,0 @@ - -defmodule Sh.Comet.V0.Feed.Track.View do - @moduledoc """ - - """ - - @derive Jason.Encoder - defstruct [ - audio: nil, - author: nil, - cid: nil, - commentCount: 0, - image: nil, - indexedAt: nil, - likeCount: 0, - playCount: 0, - record: nil, - repostCount: 0, - uri: nil, - viewer: nil - ] - - @type t() :: %__MODULE__{ - audio: String.t(), - author: Sh.Comet.V0.Actor.Profile.ViewFull.t(), - cid: String.t(), - commentCount: integer, - image: String.t(), - indexedAt: DateTime.t(), - likeCount: integer, - playCount: integer, - record: Sh.Comet.V0.Feed.Track.Main.t(), - repostCount: integer, - uri: String.t(), - viewer: Sh.Comet.V0.Feed.Defs.ViewerState.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - diff --git a/apps/backend/lib/atproto/sh/comet/v0/richtext/facet/structs.ex b/apps/backend/lib/atproto/sh/comet/v0/richtext/facet/structs.ex deleted file mode 100644 index 7ab3908..0000000 --- a/apps/backend/lib/atproto/sh/comet/v0/richtext/facet/structs.ex +++ /dev/null @@ -1,131 +0,0 @@ - -defmodule Sh.Comet.V0.Richtext.Facet.Timestamp do - @moduledoc """ - Facet feature for a timestamp in a track. The text usually is in the format of 'hh:mm:ss' with the hour section being omitted if unnecessary. - """ - - @derive Jason.Encoder - defstruct [ - timestamp: 0 - ] - - @type t() :: %__MODULE__{ - timestamp: integer - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Richtext.Facet.Tag do - @moduledoc """ - Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). - """ - - @derive Jason.Encoder - defstruct [ - tag: nil - ] - - @type t() :: %__MODULE__{ - tag: String.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Richtext.Facet.Mention do - @moduledoc """ - Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. - """ - - @derive Jason.Encoder - defstruct [ - did: nil - ] - - @type t() :: %__MODULE__{ - did: String.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Richtext.Facet.Main do - @moduledoc """ - Annotation of a sub-string within rich text. - """ - - @derive Jason.Encoder - defstruct [ - features: [], - index: nil - ] - - @type t() :: %__MODULE__{ - features: list(any), - index: Sh.Comet.V0.Richtext.Facet.ByteSlice.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Richtext.Facet.Link do - @moduledoc """ - Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. - """ - - @derive Jason.Encoder - defstruct [ - uri: nil - ] - - @type t() :: %__MODULE__{ - uri: String.t() - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - -defmodule Sh.Comet.V0.Richtext.Facet.ByteSlice do - @moduledoc """ - Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. - """ - - @derive Jason.Encoder - defstruct [ - byteEnd: 0, - byteStart: 0 - ] - - @type t() :: %__MODULE__{ - byteEnd: integer, - byteStart: integer - } - - @spec new() :: t() - def new(), do: %__MODULE__{} - - @spec from(binary() | map()) :: t() - def from(json), do: Atproto.decode_to_struct(__MODULE__, json) -end - diff --git a/apps/backend/lib/atproto/tid.ex b/apps/backend/lib/atproto/tid.ex deleted file mode 100644 index 17c222d..0000000 --- a/apps/backend/lib/atproto/tid.ex +++ /dev/null @@ -1,105 +0,0 @@ -defmodule Atproto.TID do - @moduledoc """ - A module for encoding and decoding TIDs. - - [TID](https://atproto.com/specs/tid) stands for "Timestamp Identifier". It is a 13-character string calculated from 53 bits representing a unix timestamp, in microsecond precision, plus 10 bits for an arbitrary "clock identifier", to help with uniqueness in distributed systems. - - The string is encoded as "base32-sortable", meaning that the characters for the base 32 encoding are set up in such a way that string comparisons yield the same result as integer comparisons, i.e. if the integer representation of the timestamp that creates TID "A" is greater than the integer representation of the timestamp that creates TID "B", then "A" > "B" is also true, and vice versa. - """ - - import Bitwise - - @tid_char_set ~c(234567abcdefghijklmnopqrstuvwxyz) - @tid_char_set_length 32 - - defstruct [ - :timestamp, - :clock_id, - :string - ] - - @typedoc """ - TIDs are composed of two parts: a timestamp and a clock identifier. They also have a human-readable string representation as a "base32-sortable" encoded string. - """ - @type t() :: %__MODULE__{ - timestamp: integer(), - clock_id: integer(), - string: binary() - } - - @doc """ - Generates a random 10-bit clock identifier. - """ - @spec random_clock_id() :: integer() - def random_clock_id(), do: :rand.uniform(1024) - 1 - - @doc """ - Generates a new TID for the current time. - - This is equivalent to calling `encode(nil)`. - """ - @spec new() :: t() - def new(), do: encode(nil) - - @doc """ - Encodes an integer or DateTime struct into a 13-character string that is "base32-sortable" encoded. - - If `timestamp` is nil, or not provided, the current time will be used as represented by `DateTime.utc_now()`. - - If `clock_id` is nil, or not provided, a random 10-bit integer will be used. - - If `timestamp` is an integer value, it *MUST* be a unix timestamp measured in microseconds. This function does not validate integer values. - """ - @spec encode(nil | integer() | DateTime.t(), nil | integer()) :: t() - def encode(timestamp \\ nil, clock_id \\ nil) - - def encode(nil, clock_id), do: encode(DateTime.utc_now(), clock_id) - - def encode(timestamp, nil), do: encode(timestamp, random_clock_id()) - - def encode(%DateTime{} = datetime, clock_id) do - datetime - |> DateTime.to_unix(:microsecond) - |> encode(clock_id) - end - - def encode(timestamp, clock_id) when is_integer(timestamp) and is_integer(clock_id) do - # Ensure we only use the lower 10 bit of clock_id - clock_id = clock_id &&& 1023 - str = - timestamp - |> bsr(10) - |> bsl(10) - |> bxor(clock_id) - |> do_encode("") - %__MODULE__{timestamp: timestamp, clock_id: clock_id, string: str} - end - - defp do_encode(0, acc), do: acc - - defp do_encode(number, acc) do - c = rem(number, @tid_char_set_length) - number = div(number, @tid_char_set_length) - do_encode(number, <> <> acc) - end - - @doc """ - Decodes a binary string into a TID struct. - """ - @spec decode(binary()) :: t() - def decode(tid) do - num = do_decode(tid, 0) - %__MODULE__{timestamp: bsr(num, 10), clock_id: num &&& 1023, string: tid} - end - - defp do_decode(<<>>, acc), do: acc - - defp do_decode(<>, acc) do - idx = Enum.find_index(@tid_char_set, fn x -> x == char end) - do_decode(rest, (acc * @tid_char_set_length) + idx) - end -end - -defimpl String.Chars, for: Atproto.TID do - def to_string(tid), do: tid.string -end diff --git a/apps/backend/lib/comet/aturi.ex b/apps/backend/lib/comet/aturi.ex deleted file mode 100644 index 0ab93dc..0000000 --- a/apps/backend/lib/comet/aturi.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule Comet.AtURI do - use TypedStruct - - @did "did:(?:plc|web):[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]" - @handle "(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" - @nsid "[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)" - - @authority "(?(?:#{@did})|(?:#{@handle}))" - @collection "(?#{@nsid})" - @rkey "(?[a-zA-Z0-9.-_:~]{1,512})" - - @re ~r"^at://#{@authority}(?:/#{@collection}(?:/#{@rkey})?)?$" - - typedstruct do - field :authority, String.t(), enforce: true - field :collection, String.t() | nil - field :rkey, String.t() | nil - end - - @spec new(String.t()) :: {:ok, t()} | :error - def new(string) when is_binary(string) do - case Regex.named_captures(@re, string) do - %{} = captures -> {:ok, from_named_captures(captures)} - nil -> :error - end - end - - @spec new!(String.t()) :: t() - def new!(string) when is_binary(string) do - case new(string) do - {:ok, uri} -> uri - :error -> raise ArgumentError, message: "Malformed at:// URI" - end - end - - @spec match?(String.t()) :: boolean() - def match?(string), do: Regex.match?(@re, string) - - @spec to_string(t()) :: String.t() - def to_string(%__MODULE__{} = uri) do - "at://#{uri.authority}/#{uri.collection}/#{uri.rkey}" - |> String.trim_trailing("/") - end - - defp from_named_captures(%{"authority" => authority, "collection" => "", "rkey" => ""}), - do: %__MODULE__{authority: authority} - - defp from_named_captures(%{"authority" => authority, "collection" => collection, "rkey" => ""}), - do: %__MODULE__{authority: authority, collection: collection} - - defp from_named_captures(%{ - "authority" => authority, - "collection" => collection, - "rkey" => rkey - }), - do: %__MODULE__{authority: authority, collection: collection, rkey: rkey} -end - -defimpl String.Chars, for: Comet.AtURI do - def to_string(%Comet.AtURI{} = uri), do: Comet.AtURI.to_string(uri) -end diff --git a/apps/backend/lib/comet/repo/comment.ex b/apps/backend/lib/comet/repo/comment.ex deleted file mode 100644 index 446409c..0000000 --- a/apps/backend/lib/comet/repo/comment.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Comet.Repo.Comment do - @moduledoc """ - Schema containing information about a Comet comment. - """ - use Comet.Schema - import Ecto.Changeset - - schema "comments" do - field :rkey, :string - field :text, :string - embeds_one :facets, Repo.Embed.Facet, on_replace: :update - field :subject_id, :binary_id - field :subject_type, Ecto.Enum, values: [:track, :playlist] - field :langs, {:array, :string} - field :created_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - belongs_to :parent, __MODULE__, foreign_key: :reply_id - has_many :replies, __MODULE__, foreign_key: :reply_id - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:rkey, :did, :text, :facets, :subject_id, :subject_type, :langs, :created_at]) - |> validate_required([:rkey, :text]) - end -end diff --git a/apps/backend/lib/comet/repo/embed/facet.ex b/apps/backend/lib/comet/repo/embed/facet.ex deleted file mode 100644 index 19bfe5b..0000000 --- a/apps/backend/lib/comet/repo/embed/facet.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Comet.Repo.Embed.Facet do - use Comet.Schema - import Ecto.Changeset - - @primary_key false - embedded_schema do - embeds_one :index, ByteSlice do - field :byte_start, :integer - field :byte_end, :integer - end - - # Sadly Ecto doesn't support union types/embeds so this has to be generic, without doing weirdness in the database at least - field :features, {:array, :map} - end - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:features]) - |> cast_embed(:index, required: true) - |> validate_required([:features]) - end -end diff --git a/apps/backend/lib/comet/repo/embed/link.ex b/apps/backend/lib/comet/repo/embed/link.ex deleted file mode 100644 index 757e77d..0000000 --- a/apps/backend/lib/comet/repo/embed/link.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Comet.Repo.Embed.Link do - use Comet.Schema - import Ecto.Changeset - - @primary_key false - embedded_schema do - field :type, :string - field :value, :string - end - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:type, :value]) - |> validate_required([:type, :value]) - end -end diff --git a/apps/backend/lib/comet/repo/identity.ex b/apps/backend/lib/comet/repo/identity.ex deleted file mode 100644 index 4ee1309..0000000 --- a/apps/backend/lib/comet/repo/identity.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Comet.Repo.Identity do - @moduledoc """ - Schema containing information about an ATProtocol identity. - """ - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:did, :string, autogenerate: false} - @foreign_key_type :string - - schema "identity" do - field :handle, :string - field :active, :boolean - field :status, :string - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:did, :handle, :active, :status]) - |> validate_required([:did, :active]) - end -end diff --git a/apps/backend/lib/comet/repo/like.ex b/apps/backend/lib/comet/repo/like.ex deleted file mode 100644 index 05ac9bd..0000000 --- a/apps/backend/lib/comet/repo/like.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Comet.Repo.Like do - @moduledoc """ - Schema containing information about a Comet like. - """ - use Comet.Schema - import Ecto.Changeset - - schema "likes" do - field :rkey, :string - field :subject_id, :binary_id - field :subject_type, Ecto.Enum, values: [:track, :playlist] - field :created_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:rkey, :did, :subject_id, :subject_type, :created_at]) - |> validate_required([:rkey, :did, :subject_id, :subject_type, :created_at]) - end -end diff --git a/apps/backend/lib/comet/repo/playlist.ex b/apps/backend/lib/comet/repo/playlist.ex deleted file mode 100644 index f153888..0000000 --- a/apps/backend/lib/comet/repo/playlist.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule Comet.Repo.Playlist do - @moduledoc """ - Sch ema containing information about a Comet playlist. - """ - use Comet.Schema - import Ecto.Changeset - - schema "playlists" do - field :rkey, :string - field :title, :string - field :image, :string - field :description, :string - # TODO: see how this looks with/without primary id - embeds_one :description_facets, Repo.Embed.Facet, on_replace: :update - field :type, :string - field :tags, {:array, :string} - embeds_one :link, Repo.Embed.Link, on_replace: :update - field :created_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - has_many :tracks, Repo.Playlist - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:rkey, :did, :title, :image, :description, :type, :tags, :created_at]) - |> cast_embed(:description_facets) - |> cast_embed(:link) - |> validate_required([:rkey, :did, :title, :type, :created_at]) - end -end diff --git a/apps/backend/lib/comet/repo/playlist_track.ex b/apps/backend/lib/comet/repo/playlist_track.ex deleted file mode 100644 index cac831d..0000000 --- a/apps/backend/lib/comet/repo/playlist_track.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Comet.Repo.PlaylistTrack do - @moduledoc """ - Schema containing information about a track in a Comet playlist. - """ - use Comet.Schema - import Ecto.Changeset - - schema "playlist_tracks" do - field :rkey, :string - field :position, :integer - field :created_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - belongs_to :track, Repo.Track - belongs_to :playlist, Repo.Playlist - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:rkey, :did, :position, :created_at, :track_id, :playlist_id]) - |> validate_required([:rkey, :did, :position, :created_at, :track_id, :playlist_id]) - end -end diff --git a/apps/backend/lib/comet/repo/profile.ex b/apps/backend/lib/comet/repo/profile.ex deleted file mode 100644 index cc9e2b1..0000000 --- a/apps/backend/lib/comet/repo/profile.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Comet.Repo.Profile do - @moduledoc """ - Schema containing information about a Comet profile. - """ - use Comet.Schema - import Ecto.Changeset - - # TODO: should probably keep track of CID so as to not do unnecessary writes - schema "profiles" do - field :rkey, :string, default: "self" - field :display_name, :string - field :description, :string - embeds_one :description_facets, Repo.Embed.Facet, on_replace: :update - field :avatar, :string - field :banner, :string - field :featured_items, {:array, :string} - field :created_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [ - :rkey, - :did, - :display_name, - :description, - :avatar, - :banner, - :featured_items, - :created_at - ]) - |> cast_embed(:description_facets) - |> validate_required([:rkey, :did]) - end -end diff --git a/apps/backend/lib/comet/repo/repost.ex b/apps/backend/lib/comet/repo/repost.ex deleted file mode 100644 index 6a8bc3c..0000000 --- a/apps/backend/lib/comet/repo/repost.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Comet.Repo.Repost do - @moduledoc """ - Schema containing information about a Comet repost. - """ - use Comet.Schema - import Ecto.Changeset - - schema "reposts" do - field :rkey, :string - field :subject_id, :binary_id - field :subject_type, Ecto.Enum, values: [:track, :playlist] - field :created_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [:rkey, :did, :subject_id, :subject_type, :created_at]) - |> validate_required([:rkey, :did, :subject_id, :subject_type, :created_at]) - end -end diff --git a/apps/backend/lib/comet/repo/track.ex b/apps/backend/lib/comet/repo/track.ex deleted file mode 100644 index dc3961e..0000000 --- a/apps/backend/lib/comet/repo/track.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Comet.Repo.Track do - @moduledoc """ - Schema containing information about a Comet track. - """ - use Comet.Schema - import Ecto.Changeset - - schema "tracks" do - field :rkey, :string - field :title, :string - field :audio, :string - field :image, :string - field :description, :string - embeds_one :description_facets, Repo.Embed.Facet, on_replace: :update - field :explicit, :boolean - field :tags, {:array, :string} - embeds_one :link, Repo.Embed.Link, on_replace: :update - field :created_at, :utc_datetime - field :released_at, :utc_datetime - - belongs_to :identity, Repo.Identity, foreign_key: :did, references: :did - - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - def new(params \\ %{}), do: changeset(%__MODULE__{}, params) - - def changeset(struct, params \\ %{}) do - struct - |> cast(params, [ - :rkey, - :did, - :title, - :audio, - :image, - :description, - :explicit, - :tags, - :created_at, - :released_at - ]) - |> cast_embed(:description_facets) - |> cast_embed(:link) - |> validate_required([:rkey, :did, :audio, :title, :created_at]) - end -end diff --git a/apps/backend/lib/comet/schema.ex b/apps/backend/lib/comet/schema.ex deleted file mode 100644 index 5229994..0000000 --- a/apps/backend/lib/comet/schema.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Comet.Schema do - defmacro __using__(_) do - quote do - use Ecto.Schema - alias Comet.Repo - - @primary_key {:id, :binary_id, autogenerate: true} - @foreign_key_type :binary_id - end - end -end diff --git a/apps/backend/mix.lock b/apps/backend/mix.lock deleted file mode 100644 index a76029b..0000000 --- a/apps/backend/mix.lock +++ /dev/null @@ -1,45 +0,0 @@ -%{ - "bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"}, - "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "car": {:hex, :car, "0.1.1", "a5bc4c5c1be96eab437634b3c0ccad1fe17b5e3d68c22a4031241ae1345aebd4", [:mix], [{:cbor, "~> 1.0.0", [hex: :cbor, repo: "hexpm", optional: false]}, {:typedstruct, "~> 0.5", [hex: :typedstruct, repo: "hexpm", optional: false]}, {:varint, "~> 1.4", [hex: :varint, repo: "hexpm", optional: false]}], "hexpm", "f895dda8123d04dd336db5a2bf0d0b47f4559cd5383f83fcca0700c1b45bfb6a"}, - "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, - "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, - "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, - "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, - "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, - "drinkup": {:hex, :drinkup, "0.1.0", "a21d563163a0a19db448820f0c4cf9278d52e94b7276b099c60cb8544b16b14e", [:mix], [{:car, "~> 0.1.0", [hex: :car, repo: "hexpm", optional: false]}, {:cbor, "~> 1.0.0", [hex: :cbor, repo: "hexpm", optional: false]}, {:certifi, "~> 2.15", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.2", [hex: :gun, repo: "hexpm", optional: false]}, {:typedstruct, "~> 0.5", [hex: :typedstruct, repo: "hexpm", optional: false]}], "hexpm", "e2a22b12386936f4a62c6307050c3484c41ecd07c50dc1b1af653195d539baa2"}, - "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, - "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, - "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, - "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, - "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "lexgen": {:hex, :lexgen, "1.0.0", "1ca22ba00b86f9fa97718651b77b87a5965b8a9f71109ac2c11cb573f17499aa", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ff64e0e192645208e7ce1b6468037a6d4ebfb98a506ab15d30fb46ca492ec275"}, - "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, - "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, - "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.14", "621f075577e286ff1e67d6de085ddf6f364f934d229c1c5564be1ef4c77908b9", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6dcb3f236044cd9d1c0d0996331bef72716b1991bbd8e0725a617c0d95a9483"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, - "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, - "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, - "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, - "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"}, - "typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"}, - "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, - "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, -} diff --git a/apps/backend/priv/repo/migrations/20250602100037_init.exs b/apps/backend/priv/repo/migrations/20250602100037_init.exs deleted file mode 100644 index 57cd7ab..0000000 --- a/apps/backend/priv/repo/migrations/20250602100037_init.exs +++ /dev/null @@ -1,129 +0,0 @@ -defmodule Comet.Repo.Migrations.Init do - use Ecto.Migration - - defmacrop did_rkey do - quote do - add :did, references(:identity, column: :did, type: :string), null: false - add :rkey, :string, null: false - end - end - - def change do - create table(:identity, primary_key: false) do - add :did, :string, primary_key: true, null: false - add :handle, :string - add :active, :boolean, null: false - add :status, :string - # TODO: cache of did record? - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - create table(:profiles) do - did_rkey() - add :display_name, :string - add :description, :string - # TODO: as a table? - add :description_facets, :map - add :avatar, :string - add :banner, :string - # TODO: as a table? - add :featured_items, {:array, :string} - add :created_at, :utc_datetime - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - create unique_index(:profiles, [:did, :rkey]) - - create table(:tracks) do - did_rkey() - add :title, :string, null: false - add :audio, :string, null: false - add :image, :string - add :description, :string - add :description_facets, :map - add :explicit, :boolean - # TODO: table for easier linking? - add :tags, {:array, :string} - add :link, :map - add :created_at, :utc_datetime, null: false - add :released_at, :utc_datetime - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - create index(:tracks, [:did, :rkey]) - - create table(:playlists) do - did_rkey() - add :title, :string, null: false - add :image, :string - add :description, :string - add :description_facets, :map - add :type, :string, null: false - add :tags, {:array, :string} - add :link, :map - add :created_at, :utc_datetime, null: false - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - # TODO: probably still can do unique index on this - create index(:playlists, [:did, :rkey]) - - create table(:playlist_tracks) do - did_rkey() - - add :track_id, references(:tracks), null: false - add :playlist_id, references(:playlists), null: false - - add :position, :integer, null: true - add :created_at, :utc_datetime, null: false - timestamps(inserted_at: :indexed_at, updated_at: false) - end - - create index(:playlist_tracks, [:did, :rkey]) - # create unique_index() - - create table(:likes) do - did_rkey() - # add :subject_did, :string, null: false - add :subject_id, :binary_id, null: false - add :subject_type, :string, null: false - add :created_at, :utc_datetime, null: false - end - - create index(:likes, [:did, :rkey]) - create unique_index(:likes, [:did, :subject_id]) - - create table(:reposts) do - did_rkey() - # add :subject_did, :string, null: false - add :subject_id, :binary_id, null: false - add :subject_type, :string, null: false - add :created_at, :utc_datetime, null: false - end - - create index(:reposts, [:did, :rkey]) - create unique_index(:reposts, [:did, :subject_id]) - - create table(:plays) do - did_rkey() - add :subject, references(:tracks), null: false - add :created_at, :utc_datetime, null: false - end - - create index(:plays, [:did, :rkey]) - - create table(:comments) do - did_rkey() - add :text, :string, null: false - add :subject_id, :binary_id, null: false - add :subject_type, :string, null: false - add :reply_id, references(:comments) - # add :reply, :string - add :langs, {:array, :string} - add :facets, :map - add :created_at, :utc_datetime, null: false - end - - create index(:comments, [:did, :rkey]) - end -end diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js deleted file mode 100644 index 1869f9b..0000000 --- a/apps/frontend/eslint.config.js +++ /dev/null @@ -1,37 +0,0 @@ -import prettier from "eslint-config-prettier"; -import js from "@eslint/js"; -import { includeIgnoreFile } from "@eslint/compat"; -import svelte from "eslint-plugin-svelte"; -import globals from "globals"; -import { fileURLToPath } from "node:url"; -import ts from "typescript-eslint"; -import svelteConfig from "./svelte.config.js"; - -const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url)); - -export default ts.config( - includeIgnoreFile(gitignorePath), - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs.recommended, - prettier, - ...svelte.configs.prettier, - { - languageOptions: { - globals: { ...globals.browser, ...globals.node }, - }, - rules: { "no-undef": "off" }, - }, - { - files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], - ignores: ["eslint.config.js", "svelte.config.js"], - languageOptions: { - parserOptions: { - projectService: true, - extraFileExtensions: [".svelte"], - parser: ts.parser, - svelteConfig, - }, - }, - }, -); diff --git a/apps/frontend/package.json b/apps/frontend/package.json deleted file mode 100644 index bc7a509..0000000 --- a/apps/frontend/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "comet", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" - }, - "devDependencies": { - "@eslint/compat": "^1.2.5", - "@eslint/js": "^9.18.0", - "@fontsource-variable/inter": "^5.2.5", - "@lucide/svelte": "^0.487.0", - "@sveltejs/adapter-auto": "^4.0.0", - "@sveltejs/kit": "^2.16.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tailwindcss/vite": "^4.0.0", - "bits-ui": "^1.3.17", - "clsx": "^2.1.1", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-svelte": "^3.0.0", - "globals": "^16.0.0", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "tailwindcss": "^4.0.0", - "typescript": "^5.0.0", - "typescript-eslint": "^8.20.0", - "vite": "^6.2.5" - } -} diff --git a/apps/frontend/src/app.css b/apps/frontend/src/app.css deleted file mode 100644 index aad9422..0000000 --- a/apps/frontend/src/app.css +++ /dev/null @@ -1,13 +0,0 @@ -@import "tailwindcss"; -@import "@fontsource-variable/inter"; - -@theme { - --font-sans: - "Inter Variable", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --font-sans--font-feature-settings: "tnum", "ss01", "ss02"; -} - -body { - @apply bg-slate-100; -} diff --git a/apps/frontend/src/app.d.ts b/apps/frontend/src/app.d.ts deleted file mode 100644 index 520c421..0000000 --- a/apps/frontend/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/apps/frontend/src/app.html b/apps/frontend/src/app.html deleted file mode 100644 index 84ffad1..0000000 --- a/apps/frontend/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/frontend/src/lib/components/Navbar.svelte b/apps/frontend/src/lib/components/Navbar.svelte deleted file mode 100644 index 3f9767b..0000000 --- a/apps/frontend/src/lib/components/Navbar.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/apps/frontend/src/lib/components/Player1.svelte b/apps/frontend/src/lib/components/Player1.svelte deleted file mode 100644 index a360181..0000000 --- a/apps/frontend/src/lib/components/Player1.svelte +++ /dev/null @@ -1,107 +0,0 @@ - - -{#snippet plainButton(Icon: typeof LucideIcon, label: string)} - -{/snippet} - -{#snippet clickable(content: string)} - {content} -{/snippet} - - - diff --git a/apps/frontend/src/lib/components/Player2.svelte b/apps/frontend/src/lib/components/Player2.svelte deleted file mode 100644 index 571d72e..0000000 --- a/apps/frontend/src/lib/components/Player2.svelte +++ /dev/null @@ -1,201 +0,0 @@ - - -{#snippet plainButton(Icon: typeof LucideIcon, label: string)} - -{/snippet} - -{#snippet clickable(content: string)} - {content} -{/snippet} - - - - diff --git a/apps/frontend/src/lib/index.ts b/apps/frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/apps/frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/frontend/src/routes/+layout.svelte b/apps/frontend/src/routes/+layout.svelte deleted file mode 100644 index ae80df9..0000000 --- a/apps/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - -
- {@render children()} -
- - diff --git a/apps/frontend/src/routes/+page.svelte b/apps/frontend/src/routes/+page.svelte deleted file mode 100644 index 6824d5d..0000000 --- a/apps/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ -
-
-

Comet

-

- Your music, on ATProto. -

-
- -
-

Why?

-
    -
  1. - free yourself from Big Tech™️; don't let them tell you what music you - can and can't make -
  2. -
  3. - Listen to music in full* quality, without being subject to horrendous - sounding data compression. -
  4. -
  5. - if I die, all your data is yours, and can be used by other projects! -
  6. -
  7. - Integrate your playback with - teal.fm - - and let everyone else know what you're listening to! -
  8. -
  9. borpa
  10. -
-
-
diff --git a/apps/frontend/static/favicon.png b/apps/frontend/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4i*LmhD`I74h#$ojKx9jP7LeL$-HD>V7=?< z;uunK>uv1q+}9QYp`3aLRhP6(c_0+yMGvW%`8w9Z4+AO$r=LiV^}WjuyMS z-`u@3yY$$SKDCAN>H9tj&u}g}XS4E2pY^*PkNZ3lroTLubH?oMf4#J9vzMpzCckct zowYnCeNLl>-tC#oR|mW7^83!__-WF*w@N!}ZnWO)*sV9e?K!tS_p_50%zk=doa_6a z9Ze7`Oq(1d_U2FyZ`xGx-xFfEE={d84w$f9rS697HscGE-dGl;{V=G?`XscL>&++e z>-U#%oq6{6`Q0U4b%OVNS0!jYj1*aM;g7Z{*A0W>9o{wqst=zQyeQ;ij+$byPs!T7 zP$;gE@43N(Z#KN^`%g~qnDC*fQ@9u5T z`010b%(4WBW*Igy)jqkB43?QUzqIP?pR?n;uR)>M{Z!UJzVqjLRQx?D9Xlud=JZMH zM4Dt}4@n0eu2Ixgk=cKk=N7x!uIU#Qr)QK{iRNADxA(7D>^{r+(1Q28%d+>r`1fM! z3!#PnEzgzrWhp(JYGANY`@x@o);^8RydN}I>CasHT;#z0|7n-*i5nc8wD_Qhgrn&1 z_=)HIc7)82SrZn{n5O;yq=&Wj!=(QfoT>{MZZVX#Y(9Ll{di*M8HGwlu2VN3EWtQvP&QN2PVR8N7!YC#E{m*$kY8i3^zQiv$JVzqYQu{#r;icEl z=UhCnkiGRom%yd1b*>!Dbz(=$xA=OCK9sWW?kjHnIi=(8N8<^v!`m)TWlU#3bX%EA zWW|L?Hd)uCpL{-aJW{t>_loq&RRZ@V7u56iZFu;9u7AgewTA2m_dYzmg!LfHN%jio z6)Tz8tu4PtytF=2_HyzK35Her8CehRz9{eceYP>8gyZOsf1e+R9eAyLo&U`AQ!>jL z>i##s-geN-*K&%b<6Sll}2Yvce^~*xG<6XX?1V3gp#~N zkiz|sN2i}Ey4w@5>yt(|hpKnhq#5tcY68zPo5(r8-F4X1_Ok2&?FGW>zrU(?x;Q#c?bx4-5dPgKtZr*@mZ=`3%X zy6%Z`^e5Hr^Y+>AzJm9>p2Z^BF4@_(4b1il6W?8xzhU;SdYz;2?R&rX$y{t=F!Y`E zR$}w<@9ihF9(X_5v`6rxTqRqD=!eToZeQ5@{QQC0#~w>6ukx<+wTnGF|CY_6xJ%zz zHr<`$x>nEcQ1ZHi?IAA|7rHVEt3Nz@U3=2vrr*f|MGoxkLSkxhhdzJg^6Z@Zq2`!6 zN9X(x&t6IHdUbyJoZ{}h2h3KD+a6rvIIkc7=B91g%$DcR#pk?L*qd_b^D~z6tu4>l zcb}QJe%gmWZyk5-zxe)Gtrh1xH}U_oBV*EcOaCc*(s}2g+Fj}Irl)7WovQcub7w{E upWAk8&OVF%_w#jD-*4ISJB6Y@xbv5oeJi^*U66r+fx*+&&t;ucLK6TBI`Z29 diff --git a/apps/frontend/svelte.config.js b/apps/frontend/svelte.config.js deleted file mode 100644 index a8bb58a..0000000 --- a/apps/frontend/svelte.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import adapter from "@sveltejs/adapter-auto"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter(), - }, -}; - -export default config; diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json deleted file mode 100644 index f4d0a0e..0000000 --- a/apps/frontend/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts deleted file mode 100644 index 8668317..0000000 --- a/apps/frontend/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import tailwindcss from "@tailwindcss/vite"; -import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [tailwindcss(), sveltekit()], -}); diff --git a/assets/css/app.css b/assets/css/app.css new file mode 100644 index 0000000..e658e06 --- /dev/null +++ b/assets/css/app.css @@ -0,0 +1,24 @@ +/* See the Tailwind configuration guide for advanced usage + https://tailwindcss.com/docs/configuration */ + +@import "tailwindcss" source(none); +@source "../css"; +@source "../js"; +@source "../../lib/comet_web"; + +/* A Tailwind plugin that makes "hero-#{ICON}" classes available. + The heroicons installation itself is managed by your mix.exs */ +@plugin "../vendor/heroicons"; + +/* Add variants based on LiveView classes */ +@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &); +@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &); +@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &); + +/* Use the data attribute for dark mode */ +@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); + +/* Make LiveView wrapper divs transparent for layout */ +[data-phx-session], [data-phx-teleported-src] { display: contents } + +/* This file is for your main application CSS */ diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..38680da --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,83 @@ +// If you want to use Phoenix channels, run `mix help phx.gen.channel` +// to get started and then uncomment the line below. +// import "./user_socket.js" + +// You can include dependencies in two ways. +// +// The simplest option is to put them in assets/vendor and +// import them using relative paths: +// +// import "../vendor/some-package.js" +// +// Alternatively, you can `npm install some-package --prefix assets` and import +// them using a path starting with the package name: +// +// import "some-package" +// +// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file. +// To load it, simply add a second `` to your `root.html.heex` file. + +// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. +import "phoenix_html" +// Establish Phoenix Socket and LiveView configuration. +import {Socket} from "phoenix" +import {LiveSocket} from "phoenix_live_view" +import {hooks as colocatedHooks} from "phoenix-colocated/comet" +import topbar from "../vendor/topbar" + +const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +const liveSocket = new LiveSocket("/live", Socket, { + longPollFallbackMs: 2500, + params: {_csrf_token: csrfToken}, + hooks: {...colocatedHooks}, +}) + +// Show progress bar on live navigation and form submits +topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) +window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) +window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) + +// connect if there are any LiveViews on the page +liveSocket.connect() + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket + +// The lines below enable quality of life phoenix_live_reload +// development features: +// +// 1. stream server logs to the browser console +// 2. click on elements to jump to their definitions in your code editor +// +if (process.env.NODE_ENV === "development") { + window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => { + // Enable server log streaming to client. + // Disable with reloader.disableServerLogs() + reloader.enableServerLogs() + + // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component + // + // * click with "c" key pressed to open at caller location + // * click with "d" key pressed to open at function component definition location + let keyDown + window.addEventListener("keydown", e => keyDown = e.key) + window.addEventListener("keyup", _e => keyDown = null) + window.addEventListener("click", e => { + if(keyDown === "c"){ + e.preventDefault() + e.stopImmediatePropagation() + reloader.openEditorAtCaller(e.target) + } else if(keyDown === "d"){ + e.preventDefault() + e.stopImmediatePropagation() + reloader.openEditorAtDef(e.target) + } + }, true) + + window.liveReloader = reloader + }) +} + diff --git a/assets/tsconfig.json b/assets/tsconfig.json new file mode 100644 index 0000000..a9401b6 --- /dev/null +++ b/assets/tsconfig.json @@ -0,0 +1,32 @@ +// This file is needed on most editors to enable the intelligent autocompletion +// of LiveView's JavaScript API methods. You can safely delete it if you don't need it. +// +// Note: This file assumes a basic esbuild setup without node_modules. +// We include a generic paths alias to deps to mimic how esbuild resolves +// the Phoenix and LiveView JavaScript assets. +// If you have a package.json in your project, you should remove the +// paths configuration and instead add the phoenix dependencies to the +// dependencies section of your package.json: +// +// { +// ... +// "dependencies": { +// ..., +// "phoenix": "../deps/phoenix", +// "phoenix_html": "../deps/phoenix_html", +// "phoenix_live_view": "../deps/phoenix_live_view" +// } +// } +// +// Feel free to adjust this configuration however you need. +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["../deps/*"] + }, + "allowJs": true, + "noEmit": true + }, + "include": ["js/**/*"] +} diff --git a/assets/vendor/heroicons.js b/assets/vendor/heroicons.js new file mode 100644 index 0000000..296f80e --- /dev/null +++ b/assets/vendor/heroicons.js @@ -0,0 +1,43 @@ +const plugin = require("tailwindcss/plugin") +const fs = require("fs") +const path = require("path") + +module.exports = plugin(function({matchComponents, theme}) { + let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized") + let values = {} + let icons = [ + ["", "/24/outline"], + ["-solid", "/24/solid"], + ["-mini", "/20/solid"], + ["-micro", "/16/solid"] + ] + icons.forEach(([suffix, dir]) => { + fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { + let name = path.basename(file, ".svg") + suffix + values[name] = {name, fullPath: path.join(iconsDir, dir, file)} + }) + }) + matchComponents({ + "hero": ({name, fullPath}) => { + let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") + content = encodeURIComponent(content) + let size = theme("spacing.6") + if (name.endsWith("-mini")) { + size = theme("spacing.5") + } else if (name.endsWith("-micro")) { + size = theme("spacing.4") + } + return { + [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, + "-webkit-mask": `var(--hero-${name})`, + "mask": `var(--hero-${name})`, + "mask-repeat": "no-repeat", + "background-color": "currentColor", + "vertical-align": "middle", + "display": "inline-block", + "width": size, + "height": size + } + } + }, {values}) +}) diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js new file mode 100644 index 0000000..0552337 --- /dev/null +++ b/assets/vendor/topbar.js @@ -0,0 +1,138 @@ +/** + * @license MIT + * topbar 3.0.0 + * http://buunguyen.github.io/topbar + * Copyright (c) 2024 Buu Nguyen + */ +(function (window, document) { + "use strict"; + + var canvas, + currentProgress, + showing, + progressTimerId = null, + fadeTimerId = null, + delayTimerId = null, + addEvent = function (elem, type, handler) { + if (elem.addEventListener) elem.addEventListener(type, handler, false); + else if (elem.attachEvent) elem.attachEvent("on" + type, handler); + else elem["on" + type] = handler; + }, + options = { + autoRun: true, + barThickness: 3, + barColors: { + 0: "rgba(26, 188, 156, .9)", + ".25": "rgba(52, 152, 219, .9)", + ".50": "rgba(241, 196, 15, .9)", + ".75": "rgba(230, 126, 34, .9)", + "1.0": "rgba(211, 84, 0, .9)", + }, + shadowBlur: 10, + shadowColor: "rgba(0, 0, 0, .6)", + className: null, + }, + repaint = function () { + canvas.width = window.innerWidth; + canvas.height = options.barThickness * 5; // need space for shadow + + var ctx = canvas.getContext("2d"); + ctx.shadowBlur = options.shadowBlur; + ctx.shadowColor = options.shadowColor; + + var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + for (var stop in options.barColors) + lineGradient.addColorStop(stop, options.barColors[stop]); + ctx.lineWidth = options.barThickness; + ctx.beginPath(); + ctx.moveTo(0, options.barThickness / 2); + ctx.lineTo( + Math.ceil(currentProgress * canvas.width), + options.barThickness / 2 + ); + ctx.strokeStyle = lineGradient; + ctx.stroke(); + }, + createCanvas = function () { + canvas = document.createElement("canvas"); + var style = canvas.style; + style.position = "fixed"; + style.top = style.left = style.right = style.margin = style.padding = 0; + style.zIndex = 100001; + style.display = "none"; + if (options.className) canvas.classList.add(options.className); + addEvent(window, "resize", repaint); + }, + topbar = { + config: function (opts) { + for (var key in opts) + if (options.hasOwnProperty(key)) options[key] = opts[key]; + }, + show: function (delay) { + if (showing) return; + if (delay) { + if (delayTimerId) return; + delayTimerId = setTimeout(() => topbar.show(), delay); + } else { + showing = true; + if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); + if (!canvas) createCanvas(); + if (!canvas.parentElement) document.body.appendChild(canvas); + canvas.style.opacity = 1; + canvas.style.display = "block"; + topbar.progress(0); + if (options.autoRun) { + (function loop() { + progressTimerId = window.requestAnimationFrame(loop); + topbar.progress( + "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) + ); + })(); + } + } + }, + progress: function (to) { + if (typeof to === "undefined") return currentProgress; + if (typeof to === "string") { + to = + (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 + ? currentProgress + : 0) + parseFloat(to); + } + currentProgress = to > 1 ? 1 : to; + repaint(); + return currentProgress; + }, + hide: function () { + clearTimeout(delayTimerId); + delayTimerId = null; + if (!showing) return; + showing = false; + if (progressTimerId != null) { + window.cancelAnimationFrame(progressTimerId); + progressTimerId = null; + } + (function loop() { + if (topbar.progress("+.1") >= 1) { + canvas.style.opacity -= 0.05; + if (canvas.style.opacity <= 0.05) { + canvas.style.display = "none"; + fadeTimerId = null; + return; + } + } + fadeTimerId = window.requestAnimationFrame(loop); + })(); + }, + }; + + if (typeof module === "object" && typeof module.exports === "object") { + module.exports = topbar; + } else if (typeof define === "function" && define.amd) { + define(function () { + return topbar; + }); + } else { + this.topbar = topbar; + } +}.call(this, window, document)); diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 6f02016..0000000 --- a/bun.lock +++ /dev/null @@ -1,31 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "@comet/workspace", - "devDependencies": { - "@types/bun": "latest", - "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.11", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], - - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], - - "prettier": ["prettier@3.7.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ=="], - - "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - } -} diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..e95d805 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,67 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Config module. +# +# This configuration file is loaded before any dependency and +# is restricted to this project. + +# General application configuration +import Config + +config :comet, + ecto_repos: [Comet.Repo], + generators: [timestamp_type: :utc_datetime, binary_id: true] + +# Configure the endpoint +config :comet, CometWeb.Endpoint, + url: [host: "localhost"], + adapter: Bandit.PhoenixAdapter, + render_errors: [ + formats: [html: CometWeb.ErrorHTML, json: CometWeb.ErrorJSON], + layout: false + ], + pubsub_server: Comet.PubSub, + live_view: [signing_salt: "ObastmTN"] + +# Configure the mailer +# +# By default it uses the "Local" adapter which stores the emails +# locally. You can see the emails in your browser, at "/dev/mailbox". +# +# For production it's recommended to configure a different adapter +# at the `config/runtime.exs`. +config :comet, Comet.Mailer, adapter: Swoosh.Adapters.Local + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.25.4", + comet: [ + args: + ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), + cd: Path.expand("../assets", __DIR__), + env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]} + ] + +# Configure tailwind (the version is required) +config :tailwind, + version: "4.1.12", + comet: [ + args: ~w( + --input=assets/css/app.css + --output=priv/static/assets/css/app.css + ), + cd: Path.expand("..", __DIR__) + ], + version_check: false, + path: System.get_env("TAILWINDCSS_PATH", Path.expand("../assets/node_modules/.bin/tailwindcss", __DIR__)) + +# Configure Elixir's Logger +config :logger, :default_formatter, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" diff --git a/apps/backend/config/dev.exs b/config/dev.exs similarity index 55% rename from apps/backend/config/dev.exs rename to config/dev.exs index adbbde4..271c003 100644 --- a/apps/backend/config/dev.exs +++ b/config/dev.exs @@ -2,8 +2,8 @@ import Config # Configure your database config :comet, Comet.Repo, - username: "comet", - password: "comet", + username: "postgres", + password: "postgres", hostname: "localhost", database: "comet_dev", stacktrace: true, @@ -19,12 +19,15 @@ config :comet, Comet.Repo, config :comet, CometWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {127, 0, 0, 1}], check_origin: false, code_reloader: true, debug_errors: true, - secret_key_base: "Vw9UaVO8YBKiooaOlZ2Rhx7xJHydL9s2YIviOwiiQz8Cy24+mLBB3Fj+9jvOIdQE", - watchers: [] + secret_key_base: "qEVbyaHJ1+52Mfh7Kfoc94yEwNc/e5vkRDUcAR4b3DofYJg7LgUjm4kd+3u+RelM", + watchers: [ + esbuild: {Esbuild, :install_and_run, [:comet, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:comet, ~w(--watch)]} + ] # ## SSL Support # @@ -49,11 +52,26 @@ config :comet, CometWeb.Endpoint, # configured to run both http and https servers on # different ports. +# Reload browser tabs when matching files change. +config :comet, CometWeb.Endpoint, + live_reload: [ + web_console_logger: true, + patterns: [ + # Static assets, except user uploads + ~r"priv/static/(?!uploads/).*\.(js|css|png|jpeg|jpg|gif|svg)$", + # Gettext translations + ~r"priv/gettext/.*\.po$", + # Router, Controllers, LiveViews and LiveComponents + ~r"lib/comet_web/router\.ex$", + ~r"lib/comet_web/(controllers|live|components)/.*\.(ex|heex)$" + ] + ] + # Enable dev routes for dashboard and mailbox config :comet, dev_routes: true # Do not include metadata nor timestamps in development logs -config :logger, :console, format: "[$level] $message\n" +config :logger, :default_formatter, format: "[$level] $message\n" # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. @@ -61,3 +79,14 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime + +config :phoenix_live_view, + # Include debug annotations and locations in rendered markup. + # Changing this configuration will require mix clean and a full recompile. + debug_heex_annotations: true, + debug_attributes: true, + # Enable helpful, but potentially expensive runtime checks + enable_expensive_runtime_checks: true + +# Disable swoosh api client as it is only required for production adapters. +config :swoosh, :api_client, false diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..c376546 --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,24 @@ +import Config + +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix assets.deploy` task, +# which you should run after static files are built and +# before starting your production server. +config :comet, CometWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +# Force using SSL in production. This also sets the "strict-security-transport" header, +# also known as HSTS. `:force_ssl` is required to be set at compile-time. +config :comet, CometWeb.Endpoint, force_ssl: [rewrite_on: [:x_forwarded_proto]] + +# Configure Swoosh API Client +config :swoosh, api_client: Swoosh.ApiClient.Req + +# Disable Swoosh Local Memory Storage +config :swoosh, local: false + +# Do not print debug messages in production +config :logger, level: :info + +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/apps/backend/config/runtime.exs b/config/runtime.exs similarity index 79% rename from apps/backend/config/runtime.exs rename to config/runtime.exs index 6e2128a..d233a63 100644 --- a/apps/backend/config/runtime.exs +++ b/config/runtime.exs @@ -20,6 +20,8 @@ if System.get_env("PHX_SERVER") do config :comet, CometWeb.Endpoint, server: true end +config :comet, CometWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))] + if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || @@ -34,6 +36,8 @@ if config_env() == :prod do # ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + # For machines with several cores, consider starting multiple pools of `pool_size` + # pool_count: 4, socket_options: maybe_ipv6 # The secret key base is used to sign/encrypt cookies and other secrets. @@ -49,7 +53,6 @@ if config_env() == :prod do """ host = System.get_env("PHX_HOST") || "example.com" - port = String.to_integer(System.get_env("PORT") || "4000") config :comet, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") @@ -60,8 +63,7 @@ if config_env() == :prod do # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0}, - port: port + ip: {0, 0, 0, 0, 0, 0, 0, 0} ], secret_key_base: secret_key_base @@ -96,4 +98,22 @@ if config_env() == :prod do # force_ssl: [hsts: true] # # Check `Plug.SSL` for all available options in `force_ssl`. + + # ## Configuring the mailer + # + # In production you need to configure the mailer to use a different adapter. + # Here is an example configuration for Mailgun: + # + # config :comet, Comet.Mailer, + # adapter: Swoosh.Adapters.Mailgun, + # api_key: System.get_env("MAILGUN_API_KEY"), + # domain: System.get_env("MAILGUN_DOMAIN") + # + # Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney, + # and Finch out-of-the-box. This configuration is typically done at + # compile-time in your config/prod.exs: + # + # config :swoosh, :api_client, Swoosh.ApiClient.Req + # + # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end diff --git a/apps/backend/config/test.exs b/config/test.exs similarity index 66% rename from apps/backend/config/test.exs rename to config/test.exs index 51db87a..57d0c63 100644 --- a/apps/backend/config/test.exs +++ b/config/test.exs @@ -17,11 +17,21 @@ config :comet, Comet.Repo, # you can enable the server option below. config :comet, CometWeb.Endpoint, http: [ip: {127, 0, 0, 1}, port: 4002], - secret_key_base: "eaG5CrPmVserxnUlu8DyG8I6i3m3TBDOi8fsKn2niwYUMhjps0YkWWMGRnoSXvGf", + secret_key_base: "p9+pymZBaKsKSlOfPLjw9HWpolaQwJSmVaepPAdfGpv3YUp/BO5SHkaS+Faavmec", server: false +# In test we don't send emails +config :comet, Comet.Mailer, adapter: Swoosh.Adapters.Test + +# Disable swoosh api client as it is only required for production adapters +config :swoosh, :api_client, false + # Print only warnings and errors during test config :logger, level: :warning # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime + +# Enable helpful, but potentially expensive runtime checks +config :phoenix_live_view, + enable_expensive_runtime_checks: true diff --git a/flake.nix b/flake.nix index f73a9cf..5b92765 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,11 @@ in { devShells = defaultForSystems (pkgs: pkgs.mkShell { - nativeBuildInputs = with pkgs; [nodejs_22 bun elixir erlang]; + nativeBuildInputs = with pkgs; [elixir erlang nodejs pnpm tailwindcss_4 watchman]; + + shellHook = '' + export TAILWINDCSS_PATH="${pkgs.lib.getExe pkgs.tailwindcss_4}" + ''; }); }; } diff --git a/apps/backend/lib/comet.ex b/lib/comet.ex similarity index 100% rename from apps/backend/lib/comet.ex rename to lib/comet.ex diff --git a/apps/backend/lib/comet/application.ex b/lib/comet/application.ex similarity index 100% rename from apps/backend/lib/comet/application.ex rename to lib/comet/application.ex diff --git a/lib/comet/mailer.ex b/lib/comet/mailer.ex new file mode 100644 index 0000000..30fe048 --- /dev/null +++ b/lib/comet/mailer.ex @@ -0,0 +1,3 @@ +defmodule Comet.Mailer do + use Swoosh.Mailer, otp_app: :comet +end diff --git a/apps/backend/lib/comet/repo.ex b/lib/comet/repo.ex similarity index 100% rename from apps/backend/lib/comet/repo.ex rename to lib/comet/repo.ex diff --git a/apps/backend/lib/comet_web.ex b/lib/comet_web.ex similarity index 55% rename from apps/backend/lib/comet_web.ex rename to lib/comet_web.ex index 669d73f..1270585 100644 --- a/apps/backend/lib/comet_web.ex +++ b/lib/comet_web.ex @@ -26,6 +26,7 @@ defmodule CometWeb do # Import common connection and controller functions to use in pipelines import Plug.Conn import Phoenix.Controller + import Phoenix.LiveView.Router end end @@ -37,9 +38,9 @@ defmodule CometWeb do def controller do quote do - use Phoenix.Controller, - formats: [:html, :json], - layouts: [html: CometWeb.Layouts] + use Phoenix.Controller, formats: [:html, :json] + + use Gettext, backend: CometWeb.Gettext import Plug.Conn @@ -47,6 +48,54 @@ defmodule CometWeb do end end + def live_view do + quote do + use Phoenix.LiveView + + unquote(html_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(html_helpers()) + end + end + + def html do + quote do + use Phoenix.Component + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) + end + end + + defp html_helpers do + quote do + # Translation + use Gettext, backend: CometWeb.Gettext + + # HTML escaping functionality + import Phoenix.HTML + # Core UI components + import CometWeb.CoreComponents + + # Common modules used in templates + alias Phoenix.LiveView.JS + alias CometWeb.Layouts + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + def verified_routes do quote do use Phoenix.VerifiedRoutes, diff --git a/lib/comet_web/components/core_components.ex b/lib/comet_web/components/core_components.ex new file mode 100644 index 0000000..e45f6f8 --- /dev/null +++ b/lib/comet_web/components/core_components.ex @@ -0,0 +1,493 @@ +defmodule CometWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + At first glance, this module may seem daunting, but its goal is to provide + core building blocks for your application, such as tables, forms, and + inputs. The components consist mostly of markup and are well-documented + with doc strings and declarative assigns. You may customize and style + them in any way you want, based on your application growth and needs. + + The foundation for styling is Tailwind CSS, a utility-first CSS framework. Here are useful references: + + * [Tailwind CSS](https://tailwindcss.com) - the foundational framework + we build on. You will use it for layout, sizing, flexbox, grid, and + spacing. + + * [Heroicons](https://heroicons.com) - see `icon/1` for usage. + + * [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) - + the component system used by Phoenix. Some components, such as `<.link>` + and `<.form>`, are defined there. + + """ + use Phoenix.Component + use Gettext, backend: CometWeb.Gettext + + alias Phoenix.LiveView.JS + + @doc """ + Renders flash notices. + + ## Examples + + <.flash kind={:info} flash={@flash} /> + <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back! + """ + attr :id, :string, doc: "the optional id of flash container" + attr :flash, :map, default: %{}, doc: "the map of flash messages to display" + attr :title, :string, default: nil + attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" + attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container" + + slot :inner_block, doc: "the optional inner block that renders the flash message" + + def flash(assigns) do + assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end) + + ~H""" +
hide("##{@id}")} + role="alert" + class="toast toast-top toast-end z-50" + {@rest} + > +
+ <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" /> + <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" /> +
+

{@title}

+

{msg}

+
+
+ +
+
+ """ + end + + @doc """ + Renders a button with navigation support. + + ## Examples + + <.button>Send! + <.button phx-click="go" variant="primary">Send! + <.button navigate={~p"/"}>Home + """ + attr :rest, :global, include: ~w(href navigate patch method download name value disabled) + attr :class, :any + attr :variant, :string, values: ~w(primary) + slot :inner_block, required: true + + def button(%{rest: rest} = assigns) do + variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"} + + assigns = + assign_new(assigns, :class, fn -> + ["btn", Map.fetch!(variants, assigns[:variant])] + end) + + if rest[:href] || rest[:navigate] || rest[:patch] do + ~H""" + <.link class={@class} {@rest}> + {render_slot(@inner_block)} + + """ + else + ~H""" + + """ + end + end + + @doc """ + Renders an input with label and error messages. + + A `Phoenix.HTML.FormField` may be passed as argument, + which is used to retrieve the input name, id, and values. + Otherwise all attributes may be passed explicitly. + + ## Types + + This function accepts all HTML input types, considering that: + + * You may also set `type="select"` to render a ` + """ + end + + def input(%{type: "checkbox"} = assigns) do + assigns = + assign_new(assigns, :checked, fn -> + Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) + end) + + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" +
+ + <.error :for={msg <- @errors}>{msg} +
+ """ + end + + # Helper used by inputs to generate form errors + defp error(assigns) do + ~H""" +

+ <.icon name="hero-exclamation-circle" class="size-5" /> + {render_slot(@inner_block)} +

+ """ + end + + @doc """ + Renders a header with title. + """ + slot :inner_block, required: true + slot :subtitle + slot :actions + + def header(assigns) do + ~H""" +
+
+

+ {render_slot(@inner_block)} +

+

+ {render_slot(@subtitle)} +

+
+
{render_slot(@actions)}
+
+ """ + end + + @doc """ + Renders a table with generic styling. + + ## Examples + + <.table id="users" rows={@users}> + <:col :let={user} label="id">{user.id} + <:col :let={user} label="username">{user.username} + + """ + attr :id, :string, required: true + attr :rows, :list, required: true + attr :row_id, :any, default: nil, doc: "the function for generating the row id" + attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" + + attr :row_item, :any, + default: &Function.identity/1, + doc: "the function for mapping each row before calling the :col and :action slots" + + slot :col, required: true do + attr :label, :string + end + + slot :action, doc: "the slot for showing user actions in the last table column" + + def table(assigns) do + assigns = + with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do + assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) + end + + ~H""" + + + + + + + + + + + + + +
{col[:label]} + {gettext("Actions")} +
+ {render_slot(col, @row_item.(row))} + +
+ <%= for action <- @action do %> + {render_slot(action, @row_item.(row))} + <% end %> +
+
+ """ + end + + @doc """ + Renders a data list. + + ## Examples + + <.list> + <:item title="Title">{@post.title} + <:item title="Views">{@post.views} + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" +
    +
  • +
    +
    {item.title}
    +
    {render_slot(item)}
    +
    +
  • +
+ """ + end + + @doc """ + Renders a [Heroicon](https://heroicons.com). + + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + + You can customize the size and colors of the icons by setting + width, height, and background color classes. + + Icons are extracted from the `deps/heroicons` directory and bundled within + your compiled app.css by the plugin in `assets/vendor/heroicons.js`. + + ## Examples + + <.icon name="hero-x-mark" /> + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + """ + attr :name, :string, required: true + attr :class, :any, default: "size-4" + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + time: 300, + transition: + {"transition-all ease-out duration-300", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", + "opacity-100 translate-y-0 sm:scale-100"} + ) + end + + def hide(js \\ %JS{}, selector) do + JS.hide(js, + to: selector, + time: 200, + transition: + {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100", + "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} + ) + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # However the error messages in our forms and APIs are generated + # dynamically, so we need to translate them by calling Gettext + # with our gettext backend as first argument. Translations are + # available in the errors.po file (as we use the "errors" domain). + if count = opts[:count] do + Gettext.dngettext(CometWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(CometWeb.Gettext, "errors", msg, opts) + end + end + + @doc """ + Translates the errors for a field from a keyword list of errors. + """ + def translate_errors(errors, field) when is_list(errors) do + for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) + end +end diff --git a/lib/comet_web/components/layouts.ex b/lib/comet_web/components/layouts.ex new file mode 100644 index 0000000..e596a23 --- /dev/null +++ b/lib/comet_web/components/layouts.ex @@ -0,0 +1,154 @@ +defmodule CometWeb.Layouts do + @moduledoc """ + This module holds layouts and related functionality + used by your application. + """ + use CometWeb, :html + + # Embed all files in layouts/* within this module. + # The default root.html.heex file contains the HTML + # skeleton of your application, namely HTML headers + # and other static content. + embed_templates "layouts/*" + + @doc """ + Renders your app layout. + + This function is typically invoked from every template, + and it often contains your application menu, sidebar, + or similar. + + ## Examples + + +

Content

+
+ + """ + attr :flash, :map, required: true, doc: "the map of flash messages" + + attr :current_scope, :map, + default: nil, + doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)" + + slot :inner_block, required: true + + def app(assigns) do + ~H""" +
+ +
+
+ {render_slot(@inner_block)} +
+
+ + <.flash_group flash={@flash} /> + """ + end + + @doc """ + Shows the flash group with standard titles and content. + + ## Examples + + <.flash_group flash={@flash} /> + """ + attr :flash, :map, required: true, doc: "the map of flash messages" + attr :id, :string, default: "flash-group", doc: "the optional id of flash container" + + def flash_group(assigns) do + ~H""" +
+ <.flash kind={:info} flash={@flash} /> + <.flash kind={:error} flash={@flash} /> + + <.flash + id="client-error" + kind={:error} + title={gettext("We can't find the internet")} + phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")} + phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})} + hidden + > + {gettext("Attempting to reconnect")} + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + + + <.flash + id="server-error" + kind={:error} + title={gettext("Something went wrong!")} + phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")} + phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})} + hidden + > + {gettext("Attempting to reconnect")} + <.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" /> + +
+ """ + end + + @doc """ + Provides dark vs light theme toggle based on themes defined in app.css. + + See in root.html.heex which applies the theme before page load. + """ + def theme_toggle(assigns) do + ~H""" +
+
+ + + + + + +
+ """ + end +end diff --git a/lib/comet_web/components/layouts/root.html.heex b/lib/comet_web/components/layouts/root.html.heex new file mode 100644 index 0000000..0ac5260 --- /dev/null +++ b/lib/comet_web/components/layouts/root.html.heex @@ -0,0 +1,36 @@ + + + + + + + <.live_title default="Comet" suffix=" · Phoenix Framework"> + {assigns[:page_title]} + + + + + + + {@inner_content} + + diff --git a/lib/comet_web/controllers/error_html.ex b/lib/comet_web/controllers/error_html.ex new file mode 100644 index 0000000..3cceb29 --- /dev/null +++ b/lib/comet_web/controllers/error_html.ex @@ -0,0 +1,24 @@ +defmodule CometWeb.ErrorHTML do + @moduledoc """ + This module is invoked by your endpoint in case of errors on HTML requests. + + See config/config.exs. + """ + use CometWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/comet_web/controllers/error_html/404.html.heex + # * lib/comet_web/controllers/error_html/500.html.heex + # + # embed_templates "error_html/*" + + # The default is to render a plain text page based on + # the template name. For example, "404.html" becomes + # "Not Found". + def render(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/apps/backend/lib/comet_web/controllers/error_json.ex b/lib/comet_web/controllers/error_json.ex similarity index 100% rename from apps/backend/lib/comet_web/controllers/error_json.ex rename to lib/comet_web/controllers/error_json.ex diff --git a/lib/comet_web/controllers/page_controller.ex b/lib/comet_web/controllers/page_controller.ex new file mode 100644 index 0000000..de43558 --- /dev/null +++ b/lib/comet_web/controllers/page_controller.ex @@ -0,0 +1,7 @@ +defmodule CometWeb.PageController do + use CometWeb, :controller + + def home(conn, _params) do + render(conn, :home) + end +end diff --git a/lib/comet_web/controllers/page_html.ex b/lib/comet_web/controllers/page_html.ex new file mode 100644 index 0000000..33ff6d8 --- /dev/null +++ b/lib/comet_web/controllers/page_html.ex @@ -0,0 +1,10 @@ +defmodule CometWeb.PageHTML do + @moduledoc """ + This module contains pages rendered by PageController. + + See the `page_html` directory for all templates available. + """ + use CometWeb, :html + + embed_templates "page_html/*" +end diff --git a/lib/comet_web/controllers/page_html/home.html.heex b/lib/comet_web/controllers/page_html/home.html.heex new file mode 100644 index 0000000..b107fd0 --- /dev/null +++ b/lib/comet_web/controllers/page_html/home.html.heex @@ -0,0 +1,202 @@ + + +
+
+ +
+

+ Phoenix Framework + + v{Application.spec(:phoenix, :vsn)} + +

+ +
+ +

+ Peace of mind from prototype to production. +

+

+ Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. +

+ +
+
diff --git a/apps/backend/lib/comet_web/endpoint.ex b/lib/comet_web/endpoint.ex similarity index 76% rename from apps/backend/lib/comet_web/endpoint.ex rename to lib/comet_web/endpoint.ex index c51b865..fd63bcd 100644 --- a/apps/backend/lib/comet_web/endpoint.ex +++ b/lib/comet_web/endpoint.ex @@ -7,7 +7,7 @@ defmodule CometWeb.Endpoint do @session_options [ store: :cookie, key: "_comet_key", - signing_salt: "zgKytneJ", + signing_salt: "a/yCy9X7", same_site: "Lax" ] @@ -17,17 +17,21 @@ defmodule CometWeb.Endpoint do # Serve at "/" the static files from "priv/static" directory. # - # You should set gzip to true if you are running phx.digest - # when deploying your static files in production. + # When code reloading is disabled (e.g., in production), + # the `gzip` option is enabled to serve compressed + # static files generated by running `phx.digest`. plug Plug.Static, at: "/", from: :comet, - gzip: false, - only: CometWeb.static_paths() + gzip: not code_reloading?, + only: CometWeb.static_paths(), + raise_on_missing_only: code_reloading? # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader plug Phoenix.CodeReloader plug Phoenix.Ecto.CheckRepoStatus, otp_app: :comet end diff --git a/lib/comet_web/gettext.ex b/lib/comet_web/gettext.ex new file mode 100644 index 0000000..f090290 --- /dev/null +++ b/lib/comet_web/gettext.ex @@ -0,0 +1,25 @@ +defmodule CometWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations + that you can use in your application. To use this Gettext backend module, + call `use Gettext` and pass it as an option: + + use Gettext, backend: CometWeb.Gettext + + # Simple translation + gettext("Here is the string to translate") + + # Plural translation + ngettext("Here is the string to translate", + "Here are the strings to translate", + 3) + + # Domain-based translation + dgettext("errors", "Here is the error message to translate") + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext.Backend, otp_app: :comet +end diff --git a/apps/backend/lib/comet_web/router.ex b/lib/comet_web/router.ex similarity index 53% rename from apps/backend/lib/comet_web/router.ex rename to lib/comet_web/router.ex index 28c7904..7dc33ab 100644 --- a/apps/backend/lib/comet_web/router.ex +++ b/lib/comet_web/router.ex @@ -1,15 +1,31 @@ defmodule CometWeb.Router do use CometWeb, :router + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, html: {CometWeb.Layouts, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + pipeline :api do plug :accepts, ["json"] end - scope "/api", CometWeb do - pipe_through :api + scope "/", CometWeb do + pipe_through :browser + + get "/", PageController, :home end - # Enable LiveDashboard in development + # Other scopes may use custom stacks. + # scope "/api", CometWeb do + # pipe_through :api + # end + + # Enable LiveDashboard and Swoosh mailbox preview in development if Application.compile_env(:comet, :dev_routes) do # If you want to use the LiveDashboard in production, you should put # it behind authentication and allow only admins to access it. @@ -19,9 +35,10 @@ defmodule CometWeb.Router do import Phoenix.LiveDashboard.Router scope "/dev" do - pipe_through [:fetch_session, :protect_from_forgery] + pipe_through :browser live_dashboard "/dashboard", metrics: CometWeb.Telemetry + forward "/mailbox", Plug.Swoosh.MailboxPreview end end end diff --git a/apps/backend/lib/comet_web/telemetry.ex b/lib/comet_web/telemetry.ex similarity index 100% rename from apps/backend/lib/comet_web/telemetry.ex rename to lib/comet_web/telemetry.ex diff --git a/apps/backend/mix.exs b/mix.exs similarity index 51% rename from apps/backend/mix.exs rename to mix.exs index 4ee6fb9..f8e370d 100644 --- a/apps/backend/mix.exs +++ b/mix.exs @@ -5,11 +5,13 @@ defmodule Comet.MixProject do [ app: :comet, version: "0.1.0", - elixir: "~> 1.14", + elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps() + deps: deps(), + compilers: [:phoenix_live_view] ++ Mix.compilers(), + listeners: [Phoenix.CodeReloader] ] end @@ -23,6 +25,12 @@ defmodule Comet.MixProject do ] end + def cli do + [ + preferred_envs: [precommit: :test] + ] + end + # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] @@ -32,21 +40,32 @@ defmodule Comet.MixProject do # Type `mix help deps` for examples and options. defp deps do [ - {:phoenix, "~> 1.7.21"}, + {:phoenix, "~> 1.8.2"}, {:phoenix_ecto, "~> 4.5"}, - {:ecto_sql, "~> 3.10"}, + {:ecto_sql, "~> 3.13"}, {:postgrex, ">= 0.0.0"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 1.1.0"}, + {:lazy_html, ">= 0.1.0", only: :test}, {:phoenix_live_dashboard, "~> 0.8.3"}, + {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, + {:tailwind, "~> 0.3", runtime: Mix.env() == :dev}, + {:heroicons, + github: "tailwindlabs/heroicons", + tag: "v2.2.0", + sparse: "optimized", + app: false, + compile: false, + depth: 1}, + {:swoosh, "~> 1.16"}, + {:req, "~> 0.5"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, + {:gettext, "~> 1.0"}, {:jason, "~> 1.2"}, - {:dns_cluster, "~> 0.1.1"}, - {:bandit, "~> 1.5"}, - {:lexgen, "~> 1.0.0", only: [:dev]}, - {:req, "~> 0.5.0"}, - {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:drinkup, "~> 0.1"}, - {:typedstruct, "~> 0.5"} + {:dns_cluster, "~> 0.2.0"}, + {:bandit, "~> 1.5"} ] end @@ -57,14 +76,19 @@ defmodule Comet.MixProject do # # See the documentation for `Mix` for more info on aliases. defp aliases do - lexicon_paths = Path.wildcard("../../packages/lexicons/defs/**/*.json") - [ - setup: ["deps.get", "ecto.setup"], + setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], - "gen.lexicons": ["lexgen" | lexicon_paths] |> Enum.join(" ") + "assets.setup": ["cmd pnpm install", "tailwind.install --if-missing", "esbuild.install --if-missing"], + "assets.build": ["compile", "tailwind comet", "esbuild comet"], + "assets.deploy": [ + "tailwind comet --minify", + "esbuild comet --minify", + "phx.digest" + ], + precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"] ] end end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..347a210 --- /dev/null +++ b/mix.lock @@ -0,0 +1,46 @@ +%{ + "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, + "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, + "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, + "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, + "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, + "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, + "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "phoenix": {:hex, :phoenix, "1.8.2", "75aba5b90081d88a54f2fc6a26453d4e76762ab095ff89be5a3e7cb28bff9300", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "19ea65b4064f17b1ab0515595e4d0ea65742ab068259608d5d7b139a73f47611"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.18", "b5410017b3d4edf261d9c98ebc334e0637d7189457c730720cfc13e206443d43", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f189b759595feff0420e9a1d544396397f9cf9e2d5a8cb98ba5b6cab01927da0"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, + "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, + "swoosh": {:hex, :swoosh, "1.19.8", "0576f2ea96d1bb3a6e02cc9f79cbd7d497babc49a353eef8dce1a1f9f82d7915", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d7503c2daf0f9899afd8eba9923eeddef4b62e70816e1d3b6766e4d6c60e94ad"}, + "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, +} diff --git a/package.json b/package.json index 798d2b4..d5bbf2e 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,16 @@ "name": "@comet/workspace", "version": "1.0.0", "devDependencies": { - "@types/bun": "latest", + "@tailwindcss/cli": "^4.1.17", "prettier": "^3.5.3", - "prettier-plugin-tailwindcss": "^0.6.11" - }, - "peerDependencies": { + "prettier-plugin-tailwindcss": "^0.6.11", + "tailwindcss": "^4.1.17", "typescript": "^5" }, "private": true, "scripts": { "format": "prettier --write ." }, - "type": "module", + "type": "commonjs", "workspaces": [] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..964a5a6 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,696 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@tailwindcss/cli': + specifier: ^4.1.17 + version: 4.1.17 + prettier: + specifier: ^3.5.3 + version: 3.7.1 + prettier-plugin-tailwindcss: + specifier: ^0.6.11 + version: 0.6.14(prettier@3.7.1) + tailwindcss: + specifier: ^4.1.17 + version: 4.1.17 + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.17': + resolution: {integrity: sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg==} + hasBin: true + + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} + engines: {node: '>= 10'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + prettier-plugin-tailwindcss@0.6.14: + resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.7.1: + resolution: {integrity: sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==} + engines: {node: '>=14'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.17': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + enhanced-resolve: 5.18.3 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.17 + + '@tailwindcss/node@4.1.17': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.17 + + '@tailwindcss/oxide-android-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide@4.1.17': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + graceful-fs@4.2.11: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + node-addon-api@7.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + prettier-plugin-tailwindcss@0.6.14(prettier@3.7.1): + dependencies: + prettier: 3.7.1 + + prettier@3.7.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.17: {} + + tapable@2.3.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + typescript@5.9.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..986f5a3 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - '@parcel/watcher' diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po new file mode 100644 index 0000000..844c4f5 --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -0,0 +1,112 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" + +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} byte(s)" +msgid_plural "should be %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} byte(s)" +msgid_plural "should be at least %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} byte(s)" +msgid_plural "should be at most %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot new file mode 100644 index 0000000..eef2de2 --- /dev/null +++ b/priv/gettext/errors.pot @@ -0,0 +1,109 @@ +## This is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here has no +## effect: edit them in PO (`.po`) files instead. +## From Ecto.Changeset.cast/4 +msgid "can't be blank" +msgstr "" + +## From Ecto.Changeset.unique_constraint/3 +msgid "has already been taken" +msgstr "" + +## From Ecto.Changeset.put_change/3 +msgid "is invalid" +msgstr "" + +## From Ecto.Changeset.validate_acceptance/3 +msgid "must be accepted" +msgstr "" + +## From Ecto.Changeset.validate_format/3 +msgid "has invalid format" +msgstr "" + +## From Ecto.Changeset.validate_subset/3 +msgid "has an invalid entry" +msgstr "" + +## From Ecto.Changeset.validate_exclusion/3 +msgid "is reserved" +msgstr "" + +## From Ecto.Changeset.validate_confirmation/3 +msgid "does not match confirmation" +msgstr "" + +## From Ecto.Changeset.no_assoc_constraint/3 +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +## From Ecto.Changeset.validate_length/3 +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be %{count} byte(s)" +msgid_plural "should be %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} byte(s)" +msgid_plural "should be at least %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} byte(s)" +msgid_plural "should be at most %{count} byte(s)" +msgstr[0] "" +msgstr[1] "" + +## From Ecto.Changeset.validate_number/3 +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/apps/backend/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs similarity index 100% rename from apps/backend/priv/repo/migrations/.formatter.exs rename to priv/repo/migrations/.formatter.exs diff --git a/apps/backend/priv/repo/seeds.exs b/priv/repo/seeds.exs similarity index 100% rename from apps/backend/priv/repo/seeds.exs rename to priv/repo/seeds.exs diff --git a/apps/backend/priv/static/favicon.ico b/priv/static/favicon.ico similarity index 100% rename from apps/backend/priv/static/favicon.ico rename to priv/static/favicon.ico diff --git a/priv/static/images/logo.svg b/priv/static/images/logo.svg new file mode 100644 index 0000000..9f26bab --- /dev/null +++ b/priv/static/images/logo.svg @@ -0,0 +1,6 @@ + diff --git a/apps/backend/priv/static/robots.txt b/priv/static/robots.txt similarity index 100% rename from apps/backend/priv/static/robots.txt rename to priv/static/robots.txt diff --git a/test/comet_web/controllers/error_html_test.exs b/test/comet_web/controllers/error_html_test.exs new file mode 100644 index 0000000..b8cb8d3 --- /dev/null +++ b/test/comet_web/controllers/error_html_test.exs @@ -0,0 +1,14 @@ +defmodule CometWeb.ErrorHTMLTest do + use CometWeb.ConnCase, async: true + + # Bring render_to_string/4 for testing custom views + import Phoenix.Template, only: [render_to_string: 4] + + test "renders 404.html" do + assert render_to_string(CometWeb.ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(CometWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/apps/backend/test/comet_web/controllers/error_json_test.exs b/test/comet_web/controllers/error_json_test.exs similarity index 100% rename from apps/backend/test/comet_web/controllers/error_json_test.exs rename to test/comet_web/controllers/error_json_test.exs diff --git a/test/comet_web/controllers/page_controller_test.exs b/test/comet_web/controllers/page_controller_test.exs new file mode 100644 index 0000000..f0633e5 --- /dev/null +++ b/test/comet_web/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule CometWeb.PageControllerTest do + use CometWeb.ConnCase + + test "GET /", %{conn: conn} do + conn = get(conn, ~p"/") + assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + end +end diff --git a/apps/backend/test/support/conn_case.ex b/test/support/conn_case.ex similarity index 100% rename from apps/backend/test/support/conn_case.ex rename to test/support/conn_case.ex diff --git a/apps/backend/test/support/data_case.ex b/test/support/data_case.ex similarity index 100% rename from apps/backend/test/support/data_case.ex rename to test/support/data_case.ex diff --git a/apps/backend/test/test_helper.exs b/test/test_helper.exs similarity index 100% rename from apps/backend/test/test_helper.exs rename to test/test_helper.exs