Skip to content

Commit

Permalink
Merge pull request naymspace#468 from thomasfortes/feature/theme-sele…
Browse files Browse the repository at this point in the history
…ctor

daisyUI theme selector
  • Loading branch information
Flo0807 authored Aug 19, 2024
2 parents aa5ebfe + e11d625 commit d66dccc
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 38 deletions.
53 changes: 52 additions & 1 deletion demo/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,57 @@ window.addEventListener('phx:page-loading-stop', function (info) {
topbar.hide()
})

/**
* theme selector
*/

const Hooks = {}

// We want this to run as soon as possible to minimize
// flashes with the old theme in some situations
const storedTheme = window.localStorage.getItem('backpexTheme')
if (storedTheme != null) {
document.documentElement.setAttribute('data-theme', storedTheme)
}
Hooks.BackpexThemeSelector = {
mounted () {
const form = document.querySelector('#backpex-theme-selector-form')
const storedTheme = window.localStorage.getItem('backpexTheme')

// Marking current theme as active
if (storedTheme != null) {
const activeThemeRadio = form.querySelector(
`input[name='theme-selector'][value='${storedTheme}']`
)
activeThemeRadio.checked = true
}

// Event listener that handles the theme changes and store
// the selected theme in the session and also in localStorage
window.addEventListener('backpex:theme-change', async (event) => {
const cookiePath = form.dataset.cookiePath
const selectedTheme = form.querySelector(
'input[name="theme-selector"]:checked'
)
if (selectedTheme) {
window.localStorage.setItem('backpexTheme', selectedTheme.value)
document.documentElement.setAttribute(
'data-theme',
selectedTheme.value
)
await fetch(cookiePath, {
body: `select_theme=${selectedTheme.value}`,
method: 'POST',
headers: {
'Content-type': 'application/x-www-form-urlencoded',
'x-csrf-token': csrfToken
}
})
}
})
}
}

/**
* phoenix_live_view
*/
Expand Down Expand Up @@ -59,7 +110,7 @@ const liveSocket = new LiveSocket('/live', Socket, {
params: {
_csrf_token: csrfToken
},
hooks: {}
hooks: Hooks
})

liveSocket.connect()
Expand Down
39 changes: 32 additions & 7 deletions demo/assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,39 @@ module.exports = {
'primary-content': 'white',
secondary: '#f39325',
'secondary-content': 'white'
},
dark: {
...require('daisyui/src/theming/themes').dark
},
cyberpunk: {
...require('daisyui/src/theming/themes').cyberpunk
}
}
},
'dark',
'cupcake',
'bumblebee',
'emerald',
'corporate',
'synthwave',
'retro',
'cyberpunk',
'valentine',
'halloween',
'garden',
'forest',
'aqua',
'lofi',
'pastel',
'fantasy',
'wireframe',
'black',
'luxury',
'dracula',
'cmyk',
'autumn',
'business',
'acid',
'lemonade',
'night',
'coffee',
'winter',
'dim',
'nord',
'sunset'
]
},
content: [
Expand Down
37 changes: 37 additions & 0 deletions demo/lib/demo_web/components/layouts/admin.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,43 @@
<:topbar>
<Backpex.HTML.Layout.topbar_branding />

<Backpex.HTML.Layout.theme_selector
socket={@socket}
themes={[
{"Light", "light"},
{"Dark", "dark"},
{"Cupcake", "cupcake"},
{"Bumblebee", "bumblebee"},
{"Emerald", "emerald"},
{"Corporate", "corporate"},
{"Synthwave", "synthwave"},
{"Retro", "retro"},
{"Cyberpunk", "cyberpunk"},
{"Valentine", "valentine"},
{"Halloween", "halloween"},
{"Garden", "garden"},
{"Forest", "forest"},
{"Aqua", "aqua"},
{"Lofi", "lofi"},
{"Pastel", "pastel"},
{"Fantasy", "fantasy"},
{"Wireframe", "wireframe"},
{"Black", "black"},
{"Luxury", "luxury"},
{"Dracula", "dracula"},
{"CMYK", "cmyk"},
{"Autumn", "autumn"},
{"Business", "business"},
{"Acid", "acid"},
{"Lemonade", "lemonade"},
{"Night", "night"},
{"Coffee", "coffee"},
{"Winter", "winter"},
{"Dim", "dim"},
{"Nord", "nord"},
{"Sunset", "sunset"}
]}
/>
<Backpex.HTML.Layout.topbar_dropdown>
<:label>
<label tabindex="0" class="btn btn-square btn-ghost">
Expand Down
2 changes: 1 addition & 1 deletion demo/lib/demo_web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="h-full [scrollbar-gutter:stable]" data-theme="light">
<html lang="en" class="h-full [scrollbar-gutter:stable]" data-theme={assigns[:theme] || "light"}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand Down
1 change: 1 addition & 0 deletions demo/lib/demo_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule DemoWeb.Router do
plug :put_root_layout, {DemoWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Backpex.ThemeSelectorPlug
end

scope "/" do
Expand Down
151 changes: 141 additions & 10 deletions guides/get_started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,11 @@ If you need this color on your body tag to style your application, consider usin

## Set daisyUI theme

Backpex supports daisyUI themes, to use them you need to do two things:
Backpex supports daisyUI themes. The following steps will guide you through setting up daisyUI themes in your application and optionally adding a theme selector to your layout.

1. Add the themes to your application.
**1. Add the themes to your application.**

First, you need to add the themes to your `tailwind.config.js` file. You can add the themes to the `daisyui` key in the configuration file. The following example shows how to add the `light`, `dark`, and `cyberpunk` themes to your application.

```js
// tailwind.config.js
Expand All @@ -298,22 +300,29 @@ module.exports = {
secondary: '#f39325',
'secondary-content': 'white'
},
dark: {
...require('daisyui/src/theming/themes').dark
},
cyberpunk: {
...require('daisyui/src/theming/themes').cyberpunk
}
"dark",
"cyberpunk"
}
]
},
...
}
```

The full list of themes can be found at the [daisyUI](https://daisyui.com/docs/themes/) website.
The full list of themes can be found at the [daisyUI website](https://daisyui.com/docs/themes/).

**2. Set the assign and the default daisyUI theme in your layout.**

We fetch the theme from the assigns and set the `data-theme` attribute on the `html` tag. If no theme is set, we default to the `light` theme.

```elixir
# root.html.heex
<html data-theme={assigns[:theme] || "light"}>
...
</html>
```

2. Explicitly set the daisyUI theme in your layout.
If you just want to use a single theme, you can set the `data-theme` attribute to the theme name. You can skip the next steps and are done with the theme setup.

```elixir
# root.html.heex
Expand All @@ -322,6 +331,128 @@ The full list of themes can be found at the [daisyUI](https://daisyui.com/docs/t
</html>
```

**3. Add `Backpex.ThemeSelectorPlug` to the pipeline in the router**

To add the saved theme to the assigns, you can add the `Backpex.ThemeSelectorPlug` to the pipeline in your router. This plug will fetch the selected theme from the session and put it in the assigns.

```elixir
# router.ex
pipeline :browser do
...
# Add this plug
plug Backpex.ThemeSelectorPlug
end
```

**4. Add the theme selector component to the app shell**

You can add a theme selector to your layout to allow users to change the theme. The following example shows how to add a theme selector to the `admin.html.heex` layout. The list of themes should match the themes you added to your `tailwind.config.js` file.

```elixir
# admin.html.heex
<Backpex.HTML.Layout.app_shell fluid={@fluid?}>
<:topbar>
<Backpex.HTML.Layout.topbar_branding />
# Add this
<Backpex.HTML.Layout.theme_selector
socket={@socket}
themes={[
{"Light", "light"},
{"Dark", "dark"},
{"Cyberpunk", "cyberpunk"}
]}
/>
<Backpex.HTML.Layout.topbar_dropdown>
<:label>
<label tabindex="0" class="btn btn-square btn-ghost">
<.icon name="hero-user" class="h-8 w-8" />
</label>
</:label>
<li>
<.link navigate={~p"/"} class="flex justify-between text-red-600 hover:bg-gray-100">
<p>Logout</p>
<.icon name="hero-arrow-right-on-rectangle" class="h-5 w-5" />
</.link>
</li>
</Backpex.HTML.Layout.topbar_dropdown>
</:topbar>
<:sidebar>
<Backpex.HTML.Layout.sidebar_item current_url={@current_url} navigate={~p"/admin/posts"}>
<.icon name="hero-book-open" class="h-5 w-5" /> Posts
</Backpex.HTML.Layout.sidebar_item>
</:sidebar>
<Backpex.HTML.Layout.flash_messages flash={@flash} />
<%= @inner_content %>
</Backpex.HTML.Layout.app_shell>
```

**5. Add a hook to persist the selected theme**

To persist the selected theme, you can add a hook to your `app.js` file. This hook will listen for the `backpex:theme-change` event and store the selected theme in the session and in the local storage. The hook will also send a request to the server to store the selected theme in the session.

```js
// app.js
// We want this to run as soon as possible to minimize
// flashes with the old theme in some situations
const storedTheme = window.localStorage.getItem('backpexTheme')
if (storedTheme != null) {
document.documentElement.setAttribute('data-theme', storedTheme)
}

const Hooks = {}

Hooks.BackpexThemeSelector = {
mounted () {
const form = document.querySelector('#backpex-theme-selector-form')
const storedTheme = window.localStorage.getItem('backpexTheme')

// Marking current theme as active
if (storedTheme != null) {
const activeThemeRadio = form.querySelector(
`input[name='theme-selector'][value='${storedTheme}']`
)
activeThemeRadio.checked = true
}

// Event listener that handles the theme changes and store
// the selected theme in the session and also in localStorage
window.addEventListener('backpex:theme-change', async (event) => {
const cookiePath = form.dataset.cookiePath
const selectedTheme = form.querySelector(
'input[name="theme-selector"]:checked'
)
if (selectedTheme) {
window.localStorage.setItem('backpexTheme', selectedTheme.value)
document.documentElement.setAttribute(
'data-theme',
selectedTheme.value
)
await fetch(cookiePath, {
body: `select_theme=${selectedTheme.value}`,
method: 'POST',
headers: {
'Content-type': 'application/x-www-form-urlencoded',
'x-csrf-token': csrfToken
}
})
}
})
}
}

let liveSocket = new LiveSocket("/live", Socket, {
hooks: Hooks,
dom: {
onBeforeElUpdated (from, to) {
if (from._x_dataStack) {
window.Alpine.clone(from, to);
}
},
},
params: { _csrf_token: csrfToken },
});
```

## Remove `@tailwindcss/forms` plugin

There is a conflict between the `@tailwindcss/forms` plugin and daisyUI. You should remove the `@tailwindcss/forms` plugin from your `tailwind.config.js` to prevent styling issues.
Expand Down
10 changes: 10 additions & 0 deletions lib/backpex/controllers/cookie_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Backpex.CookieController do
@backpex_key "backpex"
@toggle_columns_key "column_toggle"
@toggle_metrics_key "metric_visibility"
@select_theme_key "theme"

def update(conn, %{"toggle_columns" => form_data}) do
resource = Map.get(form_data, @form_resource_key)
Expand Down Expand Up @@ -47,6 +48,15 @@ defmodule Backpex.CookieController do
|> redirect(to: to)
end

def update(conn, %{"select_theme" => theme_name}) do
backpex_session = get_session(conn, @backpex_key) || %{}
value = Map.put(backpex_session, @select_theme_key, theme_name)

conn
|> put_session(@backpex_key, value)
|> json(%{})
end

defp redirect_path(%URI{path: path, query: nil}) do
URI.new!(path)
end
Expand Down
Loading

0 comments on commit d66dccc

Please sign in to comment.