Who We Are

We are Optimum BH - A cutting edge software development agency specializing in Full-stack development with a focus on web and mobile applications built on top of PETAL stack.

What We Do

At Optimum BH, we are dedicated to pushing the boundaries of software development, delivering solutions that empower businesses to thrive in the digital landscape.

Web app development

We create dynamic and user-friendly web applications tailored to meet your specific needs and objectives.

Mobile app development

We design and develop mobile applications that captivate users, delivering an unparalleled experience across iOS and Android platforms.

Maintenance and support

Our commitment doesn't end with deployment. We provide ongoing maintenance and support to ensure your applications remain up-to-date, secure, and optimized for peak performance.

Blog Articles

Post Image

Usability Heuristics for UI/UX design

What are usability heuristics? Developed by Jakob Nielsen, a prominent usability expert, usability heuristics are rules of thumb that can be used to evaluate user interfaces. These heuristics are based on principles of human-computer interaction and cognitive psychology. They serve as a checklist for designers, enabling them to create interfaces that enhance user satisfaction and reduce frustration, ultimately leading to a better user experience. The 10 Usability Heuristics by Jakob Nielsen 1. Visibility of System Status Keep users informed about the system's status through appropriate feedback in a reasonable time. This includes visual indicators, loading animations, or status messages so users know their actions have been recognized. 2. Match Between System and the Real World Use familiar language, concepts, and visual elements that align with users’ real-world experiences. Avoid technical jargon, and instead use terms users understand. For example, a magnifying glass icon indicates a search function. 3. User Control and Freedom Provide undo and redo options to empower users, allowing them control over their actions, especially when they select system functions by mistake. 4. Consistency and Standards Design should be consistent across screens and platforms. Users shouldn’t wonder whether different words, situations, or actions mean the same thing. 5. Error Prevention Prevent errors by using constraints, clear instructions, and confirmations for critical actions. For example, display password criteria to avoid mistakes in login forms. 6. Help Users Recognize, Diagnose, and Recover from Errors Provide clear, constructive error messages that use plain language and offer solutions, making it easy for users to understand and fix issues. 7. Recognition Rather Than Recall Reduce users' memory load by making objects, actions, and options visible, rather than forcing them to recall information from one part of the interface to another. 8. Aesthetic and Minimalist Design Avoid information overload. Strive for a clean, minimalist design that highlights key elements and avoids unnecessary clutter. 9. Help and Documentation If necessary, provide accessible help and documentation that is focused on the user's task and context. 10. Flexibility and Efficiency of Use Allow users to customize frequent actions. Provide shortcuts, accelerators, and customizations to enhance efficiency for experienced users while keeping the interface accessible for beginners. Applying These Rules While Designing Usability heuristics are invaluable tools for creating user-friendly interfaces. Conduct a heuristic evaluation by having a small group check your UI design against these principles. Follow up with user testing to observe real interactions, and make improvements based on feedback. Stay updated with industry trends to continuously improve your design skills.
Oghogho Igbinomwanhia
Post Image

Getting Started with Ash Framework in Elixir

Are you looking for a powerful and flexible way to build Elixir applications? Look no further than the Ash framework! In this blog post, we'll introduce you to Ash, explain why it's great for building applications, and show you how to get started. What is Ash Framework? Ash is a declarative, resource-based framework for building Elixir applications. It provides a set of powerful tools and abstractions that make it easier to build complex, data-driven applications while maintaining clean and maintainable code. Why Use Ash Framework? Declarative Design: Ash allows you to define your application's structure and behavior declaratively, making your code more readable and maintainable. Resource-Based Architecture: With Ash, you model your application around resources, which encapsulate data and behavior in a cohesive way. Built-in Features: Ash provides many built-in features like pagination, filtering, and authorization, reducing the amount of boilerplate code you need to write. Extensibility: The framework is highly extensible, allowing you to customize and extend its functionality to fit your specific needs. Integration with Phoenix: Ash integrates seamlessly with Phoenix, making it easy to build web applications with a powerful backend. Installing Ash Framework To get started with Ash in a new or existing Elixir project, you'll need to add the necessary dependencies to your mix.exs file: defp deps do [ {:ash_phoenix, "~> 2.0"}, {:ash, "~> 3.0"}, # ... other dependencies ] end Then, run mix deps.get to install the dependencies. For more detailed installation instructions and configuration options, check out the Ash Installation Guide. Key Features of Ash Framework Let's explore a few key features of Ash that make it powerful for building applications: 1. Ash Resources In Ash, you define your application's data model using resources. Here's an example of a simple Post resource: defmodule AshBlog.Posts.Post do use Ash.Resource, data_layer: AshPostgres.DataLayer, domain: AshBlog.Posts postgres do table "posts" repo AshBlog.Repo end actions do defaults [:read, :destroy, create: :*, update: :*] end attributes do uuid_primary_key :id attribute :title, :string, allow_nil?: false, public?: true attribute :body, :string, allow_nil?: false, public?: true create_timestamp :inserted_at update_timestamp :updated_at end end This resource definition includes attributes, actions, and database configuration. Ash takes care of generating the necessary database schema and provides a high-level API for interacting with your data. 2. Built-in Pagination One of the powerful features of Ash is its built-in support for pagination. Ash comes with both offset and keyset pagination out of the box. With just a few lines of code, you can implement pagination in your application. To setup keyset pagination in your resource, just add this, under actions: # A `:read` action that returns a paginated list of posts, # with a default of 10 posts per page read :list do pagination keyset?: true, default_limit: 10 prepare build(sort: :inserted_at) end And here’s how you can use it in your LiveView: defp list_posts(%{assigns: %{load_more_token: nil}} = socket) do case Posts.read(Post, action: :list, page: [limit: 10]) do {:ok, %{results: posts}} -> load_more_token = List.last(posts) && List.last(posts).__metadata__.keyset socket |> assign(:load_more_token, load_more_token) |> stream(:posts, posts, reset: socket.assigns.load_more_token == nil) {:error, error} -> put_flash(socket, :error, "Error loading posts: #{inspect(error)}") end end defp list_posts(%{assigns: %{load_more_token: load_more_token}} = socket) do case Posts.read(Post, action: :list, page: [after: load_more_token, limit: 10]) do {:ok, %{results: posts}} -> load_more_token = List.last(posts) && List.last(posts).__metadata__.keyset socket |> assign(:load_more_token, load_more_token) |> stream(:posts, posts, at: -1, reset: socket.assigns.load_more_token == nil) {:error, error} -> put_flash(socket, :error, "Error loading posts: #{inspect(error)}") end end This implementation allows for efficient loading of posts as the user scrolls, creating an infinite scrolling behaviour. 3. Ash.Notifier Another powerful feature of Ash is the ability to broadcast changes in resources using Ash.Notifier. This is particularly useful when you want to update the UI in real-time when data changes. Here's an example of how to set up a notifier: defmodule AshBlog.Notifiers do use Ash.Notifier def notify(%{action: %{type: :create}, data: post}) do Phoenix.PubSub.broadcast(AshBlog.PubSub, "post_creation", {:post_created, post}) end end This notifier broadcasts a message whenever a new post is created. Then, add the notifier to your resource: defmodule AshBlog.Posts.Post do use Ash.Resource, data_layer: AshPostgres.DataLayer, domain: AshBlog.Posts, notifiers: [AshBlog.Notifiers] # <-- add this # ...rest of your code end You can then subscribe to these notifications in your Phoenix LiveView to update the UI in real-time. 4. Integration with Phoenix LiveView Ash integrates seamlessly with Phoenix LiveView, allowing you to build reactive user interfaces. Here's an example of how to use Ash with LiveView: defmodule AshBlogWeb.PostLive.Index do use AshBlogWeb, :live_view alias AshBlog.Posts alias AshBlog.Posts.Post @impl Phoenix.LiveView def mount(_params, _session, socket) do if connected?(socket), do: Phoenix.PubSub.subscribe(AshBlog.PubSub, "post_creation") form = Post |> AshPhoenix.Form.for_create(:create) |> to_form() {:ok, socket |> assign(:page_title, "AshBlog Posts") |> assign(:load_more_token, nil) |> assign(:form, form) |> stream(:posts, [])} end # ... other LiveView callbacks and event handlers end This LiveView lists posts, handles pagination, and updates in real-time when new posts are created. To see a complete example of an Ash-powered blog application, you can check out this sample AshBlog project on GitHub Conclusion Ash framework provides a powerful and flexible way to build Elixir applications. Its declarative approach, resource-based architecture, built-in features like pagination, and integration with Phoenix make it an excellent choice for building complex, data-driven applications.To learn more about Ash and dive deeper into its features, check out the following resources: Ash Documentation AshPhoenix Documentation Phoenix LiveView Documentation Phoenix Framework Guides Happy coding with Ash framework!
Amos Kibet
Post Image

Exciting updates to phx.tools

In the ever-evolving landscape of software development, it's essential to keep our tools lean, efficient, and up-to-date. We're excited to share the latest updates to phx.tools, the complete development environment for Elixir and Phoenix. If you’ve been following our journey since the initial release (as documented in our previous blog post), you’ll appreciate the enhancements we've made to streamline and modernize the toolset. Removal of Unnecessary Software One of the primary goals of this update was to eliminate any bloatware that didn’t contribute directly to the development workflow. We took a closer look at the included software packages and some of the removed packages are: Chrome and Chromedriver: While these tools are useful in some contexts, they aren't always needed for the Elixir and Phoenix development tasks. By removing them, we've reduced the overall footprint of phx.tools, making it more efficient and less resource-intensive. Docker: Docker is a powerful tool, but it’s not a necessity for all developers. Recognizing that not every project requires containerization, we’ve removed Docker to simplify the environment. Developers who need it can still easily install it separately. Node.js: Node.js is a powerful tool, but it’s not a necessity for all Phoenix projects. Recognizing that not every project requires Node.js, we’ve removed it to simplify the environment. Developers who need it can still easily install it separately. These removals not only slim down the installation but also reduce potential security vulnerabilities and maintenance overhead. Updated Software Versions In addition, all the remaining software has been updated to their latest versions. This ensures you have access to the most recent features and improvements, providing a more robust and up-to-date development setup. mise: A Superior Replacement for asdf In this update, we've also replaced asdf with mise as our tool for managing language and package versions. Mise, available at mise.jdx.dev, offers a more streamlined and efficient experience compared to asdf. Performance: Mise is optimized for speed, significantly reducing the time it takes to switch between versions of Elixir, Erlang, or other tools. It also installs multiple tools in parallel. This performance boost helps you maintain your flow without the delays often encountered with asdf. Simplicity: Mise has a more intuitive setup and fewer dependencies, making it easier to configure and use. Unlike asdf, which often requires additional tools like direnv to manage environment variables, mise natively reads your .env file, eliminating the need for external software and simplifying your workflow. Erlang Build Support: When building Erlang, mise automatically takes into account your ~/.kerlrc configuration file, ensuring that your custom settings are applied seamlessly. By adopting mise, we've made phx.tools faster and more user-friendly, ensuring that you have the best possible tools at your disposal. Shell Flexibility Perhaps the most user-friendly update is the change in how we handle shell environments. Previously, we "forced" users to adopt the Zsh shell. While Zsh offers many powerful features, we recognized that forcing a specific shell setup could disrupt developers who were accustomed to their existing environments. phx.tools now automatically detects your current shell configuration and uses it, whether you’re working with Bash or Zsh. This change ensures a smoother, more personalized experience, allowing you to work in the environment you’re most comfortable with. Conclusion The latest update to phx.tools represents our commitment to creating a streamlined, up-to-date, and user-friendly development environment. By removing unnecessary software, updating the remaining tools, and introducing shell flexibility, we’ve made phx.tools more efficient and adaptable to your needs. We’re excited to see how these changes enhance your development experience. As always, we welcome your feedback and look forward to continuing to evolve phx.tools to meet the needs of the Elixir community. Stay tuned for more updates, and happy coding!
Amos Kibet
Post Image

Optimum infrastructure generator

In the Elixir DevOps blog post series we wrote about our development workflows and the infrastructure facilitating them. Those are the tools we reach for on most of the projects. Fly.io is our platform of choice, but even when we’re not the ones making that decision, we at least set up the continuous integration the way we described in the Optimum Elixir CI with GitHub Actions. There are many moving pieces involved in the infrastructure setup, which can incur a great cost in terms of developer hours, even if following along our blog post series. As a small business owner, development team lead, or anyone involved in decision-making, you’ll have a tough time justifying money spent on developers reinventing the wheel which is a CI/CD pipeline and other aspects of infrastructure setup versus taking an off-the-shelf solution. We didn’t want to do this manually on all our projects, so we created a generator that simplifies the process greatly. And now we offer that to everyone else, too. We target startups and small businesses that don’t yet require a huge infrastructure (AWS, Google Cloud, Terraform, Kubernetes, custom setup on bare metal, you name it). Even if you’re already on Kubernetes, maybe you should reconsider whether it’s appropriate for your needs and your scale.   Anyway, the generator serves as a glue between different tools we covered in the blog post series: Optimum CI with a revolutionary yet simple caching strategy automatic deployment to the staging server on Fly.io on merge automatic creation of preview apps on Fly.io on PR creation and updates config for the production server on Fly.io   Plus: AppSignal configuration health check mise setup   Whether you’re working on an existing app, or a completely new one, we got you. Both plain Elixir and Phoenix apps are supported with different feature sets.   If you’re working on an Elixir app without Phoenix you get: mise config (.tool-versions file, reading env variables from .env file) local code checks (compilation warnings, Credo, Dialyzer, dependencies audit, Sobelow, Prettier formatting, Elixir formatting, tests, and coverage) CI on GitHub Actions docs release setup   If you’re working on a Phoenix app, on top of that, you get: health checks AppSignal configuration Dockerfile for environments on Fly.io preview, staging, and production environments config for Fly.io CI and CD on GitHub Actions setup for preview apps and staging deployment   We offer all of this at a predictable, streamlined pricing. Buy it once for $499 and run it as many times as you want on any project. We support both plain Elixir and Phoenix apps. Running the generator in Elixir apps sets up mise and CI (locally and on GitHub), while for Phoenix apps it additionally set up CD. Visit hex.codecodeship.com/package/optimum_gen_infra to get started.
Almir Sarajčić
Post Image

Client vs Server side interactions in Phoenix LiveView

The effectiveness of server-side frameworks like Phoenix LiveView for creating fully interactive web applications has sparked considerable debate, as seen in discussions such as these: https://x.com/t3dotgg/status/1796850200528732192 https://x.com/josevalim/status/1798008439895195960 While Phoenix LiveView can achieve significant functionality independently, the strategic decision of when to initiate server round trips becomes crucial for crafting truly interactive web experiences. This post explores the dynamics of client-side versus server-side interactions within Phoenix LiveView. Client vs Server To ensure a smooth user experience, it's crucial to determine which interactions require minimal latency. For instance, actions like dragging and dropping elements across a screen or dynamically creating UI components should be executed without delay; otherwise, your application may feel sluggish and unresponsive. Thus, the decision between client-side and server-side processing hinges on understanding when each approach is most appropriate. Showing and Hiding Content: For interactions such as displaying modals or toggling visibility based on user actions (e.g. clicking a button), handling these tasks on the client side is generally preferable. This approach is suitable unless: The content must be dynamically loaded to optimize network or application load. The interaction requires state changes that must be synchronized with the backend. Example: On a settings page, showing a modal when a user clicks "Change Email Address" can be managed client-side. Showing form in a modal from client-side However, triggering a confirmation message after sending an email with a verification link typically involves a backend state change, making it appropriate for server-side handling. Showing a success message from server-side Zero Latency Demands: Interactions that demand instant responsiveness, such as drag-and-drop interfaces, should primarily be managed client-side to ensure a seamless user experience. Server-Side Necessity: Any interaction that inherently involves the server should be handled server-side. Examples include: Uploading files. Saving data to a database. Broadcasting messages to other clients with Phoenix PubSub. By discerning when to delegate tasks to the client versus the server, applications can optimize performance and responsiveness, delivering an intuitive user experience across various functionalities. How to build a rich client experience in Liveview? LiveView provides developers with convenient ways to incorporate JavaScript code when building interactive applications. Here are some of the available options: LiveView.JS The Phoenix.LiveView.JS module enables developers to seamlessly integrate JavaScript functionality into Elixir code, offering commands for executing essential client-side operations. These commands support common tasks such as toggling CSS classes, dispatching DOM events, etc. While these operations can be accomplished via client-side hooks, JS commands are DOM-patch aware, so operations applied by the JS APIs will stick to elements across patches from the server. https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html In addition to purely client-side utilities, the JS commands include a rich push API, for extending the default phx-binding pushes with options to customize targets, loading states, and additional payload values. Below is an example demonstrating how to utilize these commands to dynamically apply styles while showing and hiding a modal. <a phx-click={show_settings_modal("change-email-modal")} > Change email address </a> def show_settings_modal(modal) do %JS{} |> JS.add_class("blur-md pointer-events-none", to: ".settings-container") |> JS.show(to: "##{modal}") end JS Hooks A Javascript object provided by phx-hook implementing methods like: mounted(), updated(), beforeUpdate(), destroyed(), disconnected(), reconnected(). For example, one can implement a reorderable drag-and-drop list using Hooks. import Sortable let Hooks = {} Hooks.Sortable = { mounted(){ let group = this.el.dataset.group let isDragging = false this.el.addEventListener("focusout", e => isDragging && e.stopImmediatePropagation()) let sorter = new Sortable(this.el, { group: group ? {name: group, pull: true, put: true} : undefined, animation: 150, dragClass: "drag-item", ghostClass: "drag-ghost", onStart: e => isDragging = true, // prevent phx-blur from firing while dragging onEnd: e => { isDragging = false let params = {old: e.oldIndex, new: e.newIndex, to: e.to.dataset, ...e.item.dataset} this.pushEventTo(this.el, this.el.dataset["drop"] || "reposition", params) } }) } } let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) def render(assigns) do ~H""" <div id="drag-and-drop" phx-hook="Sortable"> ... </div> """ end Learn more about hooks: Client hooks via phx-hook Why is LiveView not a zero-JS framework but a zero-boring-JS framework? Building a simple countdown timer app with LiveView Alpine.js Alpine.js is well-suited for developing LiveView-like applications. While many features of Alpine.js can now be achieved using LiveView.JS or Hooks, it remains prevalent in numerous codebases, especially those originally built on the popular PETAL stack during the earlier days of Phoenix LiveView. Alpine.js is still useful for handling events not covered with LiveView.JS. Here's an example demonstrating Alpine.js usage to toggle and transition components: <div x-data="{ isOpen: false }"> <button @click="isOpen = !isOpen" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> Toggle Component </button> <div x-show.transition="isOpen" class="bg-gray-200 p-4 mt-2"> <!-- Your content here --> This content will toggle with a nice transition effect. </div> </div> One can combine JavaScript commands with Alpine.js effectively. For instance, you can dispatch DOM events when a button is clicked and handle them in Alpine.js: <button phx-click={JS.dispatch("set-slide", detail: %{slide: "chat"})}> Open Chat </button> # Elsewhere in the codebase <div x-data="{ slide: null }" @set-slide-over.window="slide = (slide == $event.detail.slide) ? null : $event.detail.slide" > ... </div> In this example, clicking the button dispatches a custom event (set-slide) with specific data. Alpine.js then listens for and handles this event, demonstrating seamless integration with JavaScript commands in a mixed codebase environment. Check out Alpine.js here. Built-in defaults to enrich client-side interactions LiveView includes client-side features that allow developers to provide users with instant feedback while waiting for actions that may have latency. Some of these features include: phx-disable-with: This feature allows buttons to switch text while a form is being submitted. <button type="submit" phx-disable-with="Updating...">Update</button> The button's innerText will change from "Update" to "Updating..." and restore it to "Update" on acknowledgment. LiveView's CSS classes LiveView includes built-in CSS classes that facilitate providing feedback. For instance, you can dynamically swap form content while a form is being submitted using LiveView's CSS loading state classes. .while-submitting { display: none; } .inputs { display: block; } .phx-submit-loading .while-submitting { display: block; } .phx-submit-loading .inputs { display: none; } <form phx-change="update"> <div class="while-submitting">Please wait while we save our content...</div> <div class="inputs"> <input type="text" name="text" value={@text}> </div> </form> You can learn more about this here. Global events dispatched for page navigation: LiveView emits several events to the browsers and allows developers to submit their events too. For example, phx:page-loading-start and phx:page-loading-stop are dispatched, providing developers with the ability to give users feedback during main page transitions. These events can be utilized to display or conceal an animation bar that spans the page as shown below. // Show progress bar on live navigation and form submits topbar.config({...}) window.addEventListener("phx:page-loading-start", info => topbar.show()) window.addEventListener("phx:page-loading-stop", info => topbar.hide()) Other resources Optimizing user experience in LiveView phx- HTML attributes cheatsheet JavaScript interoperability
Nyakio Muriuki
Post Image

Zero downtime deployments with Fly.io

If you were wondering why you saw the topbar loading for ~5 seconds every time you deployed to Fly.io, you’re at the right place. We need to talk about deployment strategies. Typically, there are several, but Fly.io supports these: immediate rolling bluegreen canary   The complexity and cost go from low to high as we go down the list. The default option is rolling. That means, your machines will be replaced by new ones one by one. In case you only have one machine, it will be destroyed before there’s a new one that can handle requests. That’s why you’re waiting to be reconnected whenever you deploy. You can read more about these deployment strategies at https://fly.io/docs/apps/deploy/#deployment-strategy.   We’re using the blue-green deployment strategy as it strikes a balance between the benefits, cost, and ease of setup.   If you’re using volumes, I have to disappoint you as the blue-green strategy doesn’t work with them yet, but Fly.io plans to support that in the future.   Setup You need to configure at least one health check to use the bluegreen strategy. I won’t go into details. You can find more at https://fly.io/docs/reference/configuration/#http_service-checks.   Here’s a configuration we use: [[http_service.checks]] grace_period = "10s" interval = "30s" method = "GET" path = "/health" timeout = "5s"   Then, add strategy = “bluegreen” under [deploy] in your fly.toml file: [deploy] strategy = "bluegreen" and run fly deploy.   That’s it! You probably expected the setup to be more complex than this. So did I!   Conclusion While Fly.io is moving you from a blue to a green machine, your websocket connection will be dropped, but it will quickly reestablish. You shouldn’t even notice it unless you have your browser console open or you’re navigating through pages during the deployment.   One thing you should keep in mind, though, is that your client-side state (form data) might be lost if you don’t address that explicitly.   Another thing to think about is the way you run Ecto migrations. In case you’re dropping tables or columns, you might want to do that in multiple stages. For example, you might introduce changes in the code so you stop depending on specific columns or tables and deploy that change. After that, you can have subsequent deployment for the structural changes of the database. That way, both blue and green machines will have the same expectations regarding the database structure.   The future will bring us more options for deployment. Recently, Chris McCord teased us with hot deploys.   https://x.com/chris_mccord/status/1785678249424461897   Can’t wait for this!   This was a post from our Elixir DevOps series.
Almir Sarajčić

Portfolio

  • Phx.tools

    Powerful shell script designed for Linux and macOS that simplifies the process of setting up a development environment for Phoenix applications using the Elixir programming language. It configures the environment in just a few easy steps, allowing users to start the database server, create a new Phoenix application, and launch the server seamlessly. The script is particularly useful for new developers who may find the setup process challenging. With Phoenix Tools, the Elixir ecosystem becomes more approachable and accessible, allowing developers to unlock the full potential of the Phoenix and Elixir stack.

    Phx.tools
  • Prati.ba

    Bosnian news aggregator website that collects and curates news articles from various sources, including local news outlets and international media. The website provides news on a variety of topics, including politics, sports, business, culture, and entertainment, among others.

    Prati.ba
  • StoryDeck

    StoryDeck is a cloud-based video production tool that offers a range of features for content creators. It allows users to store and archive all their content in one location, track tasks and collaborate easily with team members, and use a multi-use text editor to manage multiple contributors. The platform also offers a timecode video review feature, allowing users to provide precise feedback on video files and a publishing tool with SEO optimization capabilities for traffic-driving content.

    StoryDeck