defmodule DalaPreview.Server.DesignLive do use Phoenix.LiveView, layout: {DalaPreview.Server.Layouts, :app} alias DalaPreview.Server.Design @component_types [:column, :row, :text, :button] def mount(_params, _session, socket) do socket = socket |> assign(:design, Design.new()) |> assign(:component_types, @component_types) |> assign(:selected_node, nil) |> assign(:active_tab, "components") |> assign(:export_format, "sigil") |> assign(:generated_code, "") |> assign(:zoom, 100) |> assign(:grid_visible, true) |> assign(:snap_to_grid, true) |> assign(:grid_size, 20) {:ok, socket} end def render(assigns) do ~H"""
<%= if @active_tab == "components" do %>
<%= for type <- @component_types do %> <% end %>
<% end %>
<%= if Map.get(@design, :nodes, []) != [] do %> <%= for node <- @design.nodes do %>
<%= node_icon(node.type) %> <%= node.type %>
<% end %> <% else %> Canvas (empty) <% end %>

Preview

<%= if Map.get(@design, :nodes, []) != [] do %> <%= for node <- @design.nodes do %>
<%= preview_node(node) %>
<% end %> <% else %> Preview (empty) <% end %>
""" end def handle_event("add_component", %{"type" => type}, socket) do design = socket.assigns.design case find_component(String.to_atom(type)) do nil -> {:noreply, socket} component_type -> props = default_props(component_type) node = %{type: component_type, props: props, children: []} updated_design = Design.add_node(design, node) generated_code = generate_code(updated_design, socket.assigns.export_format) socket = socket |> assign(:design, updated_design) |> assign(:generated_code, generated_code) {:noreply, socket} end end def handle_event("select_node", %{"id" => id}, socket) do {:noreply, assign(socket, :selected_node, id)} end def handle_event("update_property", %{"id" => id, "key" => key, "value" => value}, socket) do design = socket.assigns.design updated_design = Design.update_node_property(design, id, key, value) generated_code = generate_code(updated_design, socket.assigns.export_format) socket = socket |> assign(:design, updated_design) |> assign(:generated_code, generated_code) {:noreply, socket} end def handle_event("delete_node", %{"id" => id}, socket) do design = socket.assigns.design updated_design = Design.delete_node(design, id) generated_code = generate_code(updated_design, socket.assigns.export_format) socket = socket |> assign(:design, updated_design) |> assign(:generated_code, generated_code) |> assign(:selected_node, nil) {:noreply, socket} end def handle_event("clear_canvas", _params, socket) do new_design = Design.new() generated_code = generate_code(new_design, socket.assigns.export_format) socket = socket |> assign(:design, new_design) |> assign(:generated_code, generated_code) |> assign(:selected_node, nil) {:noreply, socket} end def handle_event("set_export_format", %{"format" => format}, socket) do generated_code = generate_code(socket.assigns.design, format) socket = socket |> assign(:export_format, format) |> assign(:generated_code, generated_code) {:noreply, socket} end def handle_event("select_tab", %{"tab" => tab}, socket) do {:noreply, assign(socket, :active_tab, tab)} end # Private functions defp find_component(type) do if type in @component_types do type else nil end end defp default_props(:column), do: %{"gap" => "space_md"} defp default_props(:row), do: %{"gap" => "space_md"} defp default_props(:text), do: %{"text" => "Sample"} defp default_props(:button), do: %{"text" => "Button"} defp default_props(_), do: %{} defp generate_code(design, format) do nodes = Map.get(design, :nodes, []) case format do "sigil" -> nodes_to_sigil(nodes) "dsl" -> nodes_to_dsl(nodes) "map" -> nodes_to_map(nodes) _ -> nodes_to_sigil(nodes) end end defp nodes_to_sigil(nodes) when is_list(nodes) do nodes |> Enum.map(&node_to_sigil/1) |> Enum.join("\n") end defp nodes_to_dsl(nodes) when is_list(nodes) do nodes |> Enum.map(&node_to_dsl/1) |> Enum.join("\n") end defp nodes_to_map(nodes) when is_list(nodes) do nodes |> Enum.map(&node_to_map/1) |> inspect() end defp node_to_sigil(%{type: type, props: props, children: children}) when is_list(children) and children != [] do props_str = props_to_sigil(props) children_str = nodes_to_sigil(children) "<#{type}#{props_str}>\n#{children_str}\n" end defp node_to_sigil(%{type: type, props: props}) do props_str = props_to_sigil(props) "<#{type}#{props_str} />" end defp node_to_sigil(_), do: "" defp node_to_dsl(_node), do: "# DSL output" defp node_to_map(_node), do: %{} defp props_to_sigil(props) when is_map(props) do props |> Enum.map(fn {k, v} -> " #{k}=\"#{v}\"" end) |> Enum.join("") end defp props_to_sigil(_), do: "" defp node_icon(:column), do: "📊" defp node_icon(:row), do: "➡️" defp node_icon(:text), do: "📝" defp node_icon(:button), do: "🔘" defp node_icon(_), do: "❓" defp preview_node(%{type: :text, props: props}) do text = Map.get(props, "text", "Sample") "#{text}" end defp preview_node(%{type: :button, props: props}) do text = Map.get(props, "text", "Button") "" end defp preview_node(_), do: "
Unknown
" end