Merge pull request 'feat: add dark mode' (#2) from dark-mode into main
Reviewed-on: #2
This commit is contained in:
commit
eee0b548e9
11 changed files with 165 additions and 40 deletions
|
@ -16,29 +16,35 @@
|
|||
//
|
||||
|
||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||
import "phoenix_html"
|
||||
import "phoenix_html";
|
||||
// Establish Phoenix Socket and LiveView configuration.
|
||||
import {Socket} from "phoenix"
|
||||
import {LiveSocket} from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import { Socket } from "phoenix";
|
||||
import { LiveSocket } from "phoenix_live_view";
|
||||
import topbar from "../vendor/topbar";
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
import darkModeHook from "../vendor/dark_mode";
|
||||
let Hooks = {};
|
||||
Hooks.DarkThemeToggle = darkModeHook;
|
||||
|
||||
let csrfToken = document
|
||||
.querySelector("meta[name='csrf-token']")
|
||||
.getAttribute("content");
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: {_csrf_token: csrfToken}
|
||||
})
|
||||
longPollFallbackMs: 2500,
|
||||
params: { _csrf_token: csrfToken },
|
||||
hooks: Hooks,
|
||||
});
|
||||
|
||||
// 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())
|
||||
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()
|
||||
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
|
||||
|
||||
window.liveSocket = liveSocket;
|
||||
|
|
|
@ -7,6 +7,7 @@ const path = require("path");
|
|||
|
||||
module.exports = {
|
||||
content: ["./js/**/*.js", "../lib/exmr_web.ex", "../lib/exmr_web/**/*.*ex"],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
|
46
assets/vendor/dark_mode.js
vendored
Normal file
46
assets/vendor/dark_mode.js
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
const localStorageKey = "theme";
|
||||
|
||||
const isDark = () => {
|
||||
if (localStorage.getItem(localStorageKey) === "dark") return true;
|
||||
if (localStorage.getItem(localStorageKey) === "light") return false;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
};
|
||||
|
||||
const setupThemeToggle = () => {
|
||||
toggleVisibility = (dark) => {
|
||||
const themeToggleDarkIcon = document.getElementById(
|
||||
"theme-toggle-dark-icon",
|
||||
);
|
||||
const themeToggleLightIcon = document.getElementById(
|
||||
"theme-toggle-light-icon",
|
||||
);
|
||||
if (themeToggleDarkIcon == null || themeToggleLightIcon == null) return;
|
||||
const show = dark ? themeToggleDarkIcon : themeToggleLightIcon;
|
||||
const hide = dark ? themeToggleLightIcon : themeToggleDarkIcon;
|
||||
show.classList.remove("hidden", "text-transparent");
|
||||
hide.classList.add("hidden", "text-transparent");
|
||||
if (dark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(localStorageKey, dark ? "dark" : "light");
|
||||
} catch (_err) { }
|
||||
};
|
||||
toggleVisibility(isDark());
|
||||
document
|
||||
.getElementById("theme-toggle")
|
||||
.addEventListener("click", function() {
|
||||
toggleVisibility(!isDark());
|
||||
});
|
||||
};
|
||||
|
||||
const darkModeHook = {
|
||||
mounted() {
|
||||
setupThemeToggle();
|
||||
},
|
||||
updated() { },
|
||||
};
|
||||
|
||||
export default darkModeHook;
|
|
@ -202,9 +202,12 @@ defmodule ExmrWeb.CoreComponents do
|
|||
def simple_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||
<div class="mt-10 space-y-8 bg-white">
|
||||
<div class="mt-10 space-y-8 bg-white dark:bg-zinc-950">
|
||||
<%= render_slot(@inner_block, f) %>
|
||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||
<div
|
||||
:for={action <- @actions}
|
||||
class="mt-2 flex items-center justify-between gap-6 dark:text-zinc-100"
|
||||
>
|
||||
<%= render_slot(action, f) %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -232,7 +235,7 @@ defmodule ExmrWeb.CoreComponents do
|
|||
type={@type}
|
||||
class={[
|
||||
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
|
||||
"text-sm font-semibold leading-6 text-white active:text-white/80",
|
||||
"text-sm font-semibold leading-6",
|
||||
@class
|
||||
]}
|
||||
{@rest}
|
||||
|
@ -310,7 +313,7 @@ defmodule ExmrWeb.CoreComponents do
|
|||
|
||||
~H"""
|
||||
<div>
|
||||
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
|
||||
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600 dark:text-zinc-200">
|
||||
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -318,7 +321,7 @@ defmodule ExmrWeb.CoreComponents do
|
|||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||
{@rest}
|
||||
/>
|
||||
<%= @label %>
|
||||
|
@ -396,7 +399,7 @@ defmodule ExmrWeb.CoreComponents do
|
|||
|
||||
def label(assigns) do
|
||||
~H"""
|
||||
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
|
||||
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800 dark:text-zinc-200">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</label>
|
||||
"""
|
||||
|
@ -429,10 +432,10 @@ defmodule ExmrWeb.CoreComponents do
|
|||
~H"""
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
||||
<h1 class="text-lg font-semibold leading-8 text-zinc-800 dark:text-zinc-200">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</h1>
|
||||
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
||||
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600 dark:text-zinc-100">
|
||||
<%= render_slot(@subtitle) %>
|
||||
</p>
|
||||
</div>
|
||||
|
|
57
lib/exmr_web/components/dark_mode.ex
Normal file
57
lib/exmr_web/components/dark_mode.ex
Normal file
|
@ -0,0 +1,57 @@
|
|||
defmodule DarkMode do
|
||||
@moduledoc """
|
||||
A component to toggle dark mode.
|
||||
"""
|
||||
|
||||
use Phoenix.Component
|
||||
|
||||
def button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
id="theme-toggle"
|
||||
type="button"
|
||||
phx-update="ignore"
|
||||
phx-hook="DarkThemeToggle"
|
||||
class="text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded-lg text-sm p-2"
|
||||
>
|
||||
<svg
|
||||
id="theme-toggle-dark-icon"
|
||||
class="w-5 h-5 text-transparent hidden"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
id="theme-toggle-light-icon"
|
||||
class="w-5 h-5 text-transparent"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
// Toggle early based on <html class="dark">
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
if (themeToggleDarkIcon != null && themeToggleLightIcon != null) {
|
||||
let dark = document.documentElement.classList.contains('dark');
|
||||
const show = dark ? themeToggleDarkIcon : themeToggleLightIcon
|
||||
const hide = dark ? themeToggleLightIcon : themeToggleDarkIcon
|
||||
show.classList.remove('hidden', 'text-transparent');
|
||||
hide.classList.add('hidden', 'text-transparent');
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -8,19 +8,27 @@
|
|||
<%= assigns[:page_title] || "Exmr" %>
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<script>
|
||||
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
</script>
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white">
|
||||
<body class="bg-white dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
|
||||
<DarkMode.button />
|
||||
<%= if @current_user do %>
|
||||
<li class="text-[0.8125rem] leading-6 text-zinc-900">
|
||||
<li class="text-[0.8125rem] leading-6 text-zinc-900 dark:text-zinc-300">
|
||||
<%= @current_user.email %>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
href={~p"/users/settings"}
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Settings
|
||||
</.link>
|
||||
|
@ -29,7 +37,7 @@
|
|||
<.link
|
||||
href={~p"/users/log_out"}
|
||||
method="delete"
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Log out
|
||||
</.link>
|
||||
|
@ -38,7 +46,7 @@
|
|||
<li>
|
||||
<.link
|
||||
href={~p"/users/register"}
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Register
|
||||
</.link>
|
||||
|
@ -46,7 +54,7 @@
|
|||
<li>
|
||||
<.link
|
||||
href={~p"/users/log_in"}
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
|
||||
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700 dark:text-zinc-300 dark:hover:text-zinc-100"
|
||||
>
|
||||
Log in
|
||||
</.link>
|
||||
|
|
|
@ -47,22 +47,22 @@
|
|||
v<%= Application.spec(:exmr, :vsn) %>
|
||||
</small>
|
||||
</h1>
|
||||
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 text-balance">
|
||||
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900 dark:text-zinc-200/90 text-balance">
|
||||
hell yeah
|
||||
</p>
|
||||
<p class="mt-4 text-base leading-7 text-zinc-600">
|
||||
<p class="mt-4 text-base leading-7 text-zinc-600 dark:text-zinc-300">
|
||||
y'all really though i'm gonna be serious? lol
|
||||
</p>
|
||||
<div class="flex">
|
||||
<div class="w-full sm:w-auto">
|
||||
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
|
||||
<a
|
||||
href="https://github.com/phoenixframework/phoenix"
|
||||
href="https://git.vavakado.com/vavakado/exmr"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 dark:bg-zinc-800 dark:group-hover:bg-zinc-900/95 sm:group-hover:scale-105">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<span class="relative flex items-center gap-4 sm:flex-col dark:text-white">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
@ -79,9 +79,9 @@
|
|||
href="https://mas.to/@vavakado"
|
||||
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
|
||||
>
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105">
|
||||
<span class="absolute inset-0 rounded-2xl bg-zinc-50 transition group-hover:bg-zinc-100 sm:group-hover:scale-105 dark:bg-zinc-800 dark:group-hover:bg-zinc-900/95">
|
||||
</span>
|
||||
<span class="relative flex items-center gap-4 sm:flex-col">
|
||||
<span class="relative flex items-center gap-4 sm:flex-col dark:text-white">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
|
||||
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a4 4 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522q0-1.288.66-2.046c.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764q.662.757.661 2.046z" />
|
||||
</svg>
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
</div>
|
||||
<:actions>
|
||||
<.link patch={~p"/exams/new"}>
|
||||
<.button>New Exam</.button>
|
||||
<.button class="dark:bg-sky-300 text-white dark:text-black/80 dark:hover:bg-sky-400">
|
||||
New Exam
|
||||
</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
|
|
@ -14,7 +14,7 @@ defmodule ExmrWeb.UserForgotPasswordLive do
|
|||
<.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
|
||||
<.input field={@form[:email]} type="email" placeholder="Email" required />
|
||||
<:actions>
|
||||
<.button phx-disable-with="Sending..." class="w-full">
|
||||
<.button phx-disable-with="Sending..." class="w-full dark:text-black dark:bg-red-600">
|
||||
Send password reset instructions
|
||||
</.button>
|
||||
</:actions>
|
||||
|
|
|
@ -26,7 +26,7 @@ defmodule ExmrWeb.UserLoginLive do
|
|||
</.link>
|
||||
</:actions>
|
||||
<:actions>
|
||||
<.button phx-disable-with="Logging in..." class="w-full">
|
||||
<.button phx-disable-with="Logging in..." class="w-full dark:bg-teal-600">
|
||||
Log in <span aria-hidden="true">→</span>
|
||||
</.button>
|
||||
</:actions>
|
||||
|
|
|
@ -6,7 +6,7 @@ defmodule ExmrWeb.UserRegistrationLive do
|
|||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="mx-auto max-w-sm">
|
||||
<div class="mx-auto max-w-sm dark:bg-zinc-950">
|
||||
<.header class="text-center">
|
||||
Register for an account
|
||||
<:subtitle>
|
||||
|
@ -35,7 +35,9 @@ defmodule ExmrWeb.UserRegistrationLive do
|
|||
<.input field={@form[:password]} type="password" label="Password" required />
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
|
||||
<.button phx-disable-with="Creating account..." class="w-full dark:bg-green-800">
|
||||
Create an account
|
||||
</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue