Compare commits

...

2 commits

Author SHA1 Message Date
9d86060d49 Merge pull request 'feat: add dark mode' (#2) from dark-mode into main
All checks were successful
Checks / check (push) Successful in -2m55s
Build and Push Docker Image / Build and Push Image (push) Successful in -2m38s
Reviewed-on: #2
2024-12-01 19:13:24 +02:00
6f47a56653
feat: add dark mode
All checks were successful
Checks / check (pull_request) Successful in -2m56s
2024-12-01 18:48:39 +02:00
11 changed files with 165 additions and 40 deletions

View file

@ -16,29 +16,35 @@
// //
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. // Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html" import "phoenix_html";
// Establish Phoenix Socket and LiveView configuration. // Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix" import { Socket } from "phoenix";
import {LiveSocket} from "phoenix_live_view" import { LiveSocket } from "phoenix_live_view";
import topbar from "../vendor/topbar" 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, { let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500, longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken} params: { _csrf_token: csrfToken },
}) hooks: Hooks,
});
// Show progress bar on live navigation and form submits // Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 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-start", (_info) => topbar.show(300));
window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());
// connect if there are any LiveViews on the page // 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: // expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug() // >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket;

View file

@ -7,6 +7,7 @@ const path = require("path");
module.exports = { module.exports = {
content: ["./js/**/*.js", "../lib/exmr_web.ex", "../lib/exmr_web/**/*.*ex"], content: ["./js/**/*.js", "../lib/exmr_web.ex", "../lib/exmr_web/**/*.*ex"],
darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {

46
assets/vendor/dark_mode.js vendored Normal file
View 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;

View file

@ -202,9 +202,12 @@ defmodule ExmrWeb.CoreComponents do
def simple_form(assigns) do def simple_form(assigns) do
~H""" ~H"""
<.form :let={f} for={@for} as={@as} {@rest}> <.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) %> <%= 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) %> <%= render_slot(action, f) %>
</div> </div>
</div> </div>
@ -232,7 +235,7 @@ defmodule ExmrWeb.CoreComponents do
type={@type} type={@type}
class={[ class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3", "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 @class
]} ]}
{@rest} {@rest}
@ -310,7 +313,7 @@ defmodule ExmrWeb.CoreComponents do
~H""" ~H"""
<div> <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="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input <input
type="checkbox" type="checkbox"
@ -318,7 +321,7 @@ defmodule ExmrWeb.CoreComponents do
name={@name} name={@name}
value="true" value="true"
checked={@checked} 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} {@rest}
/> />
<%= @label %> <%= @label %>
@ -396,7 +399,7 @@ defmodule ExmrWeb.CoreComponents do
def label(assigns) do def label(assigns) do
~H""" ~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) %> <%= render_slot(@inner_block) %>
</label> </label>
""" """
@ -429,10 +432,10 @@ defmodule ExmrWeb.CoreComponents do
~H""" ~H"""
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}> <header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
<div> <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) %> <%= render_slot(@inner_block) %>
</h1> </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) %> <%= render_slot(@subtitle) %>
</p> </p>
</div> </div>

View 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

View file

@ -8,19 +8,27 @@
<%= assigns[:page_title] || "Exmr" %> <%= assigns[:page_title] || "Exmr" %>
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <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 defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script> </script>
</head> </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"> <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 %> <%= 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 %> <%= @current_user.email %>
</li> </li>
<li> <li>
<.link <.link
href={~p"/users/settings"} 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 Settings
</.link> </.link>
@ -29,7 +37,7 @@
<.link <.link
href={~p"/users/log_out"} href={~p"/users/log_out"}
method="delete" 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 Log out
</.link> </.link>
@ -38,7 +46,7 @@
<li> <li>
<.link <.link
href={~p"/users/register"} 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 Register
</.link> </.link>
@ -46,7 +54,7 @@
<li> <li>
<.link <.link
href={~p"/users/log_in"} 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 Log in
</.link> </.link>

View file

@ -47,22 +47,22 @@
v<%= Application.spec(:exmr, :vsn) %> v<%= Application.spec(:exmr, :vsn) %>
</small> </small>
</h1> </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 hell yeah
</p> </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 y'all really though i'm gonna be serious? lol
</p> </p>
<div class="flex"> <div class="flex">
<div class="w-full sm:w-auto"> <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"> <div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
<a <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" 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>
<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"> <svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
@ -79,9 +79,9 @@
href="https://mas.to/@vavakado" 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" 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>
<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"> <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" /> <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> </svg>

View file

@ -8,7 +8,9 @@
</div> </div>
<:actions> <:actions>
<.link patch={~p"/exams/new"}> <.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> </.link>
</:actions> </:actions>
</.header> </.header>

View file

@ -14,7 +14,7 @@ defmodule ExmrWeb.UserForgotPasswordLive do
<.simple_form for={@form} id="reset_password_form" phx-submit="send_email"> <.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
<.input field={@form[:email]} type="email" placeholder="Email" required /> <.input field={@form[:email]} type="email" placeholder="Email" required />
<:actions> <: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 Send password reset instructions
</.button> </.button>
</:actions> </:actions>

View file

@ -26,7 +26,7 @@ defmodule ExmrWeb.UserLoginLive do
</.link> </.link>
</:actions> </:actions>
<: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> Log in <span aria-hidden="true"></span>
</.button> </.button>
</:actions> </:actions>

View file

@ -6,7 +6,7 @@ defmodule ExmrWeb.UserRegistrationLive do
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="mx-auto max-w-sm"> <div class="mx-auto max-w-sm dark:bg-zinc-950">
<.header class="text-center"> <.header class="text-center">
Register for an account Register for an account
<:subtitle> <:subtitle>
@ -35,7 +35,9 @@ defmodule ExmrWeb.UserRegistrationLive do
<.input field={@form[:password]} type="password" label="Password" required /> <.input field={@form[:password]} type="password" label="Password" required />
<:actions> <: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> </:actions>
</.simple_form> </.simple_form>
</div> </div>