diff --git a/lib/zip_liner/accounts/member.ex b/lib/zip_liner/accounts/member.ex
index e5c3fe0..96bf4da 100644
--- a/lib/zip_liner/accounts/member.ex
+++ b/lib/zip_liner/accounts/member.ex
@@ -4,6 +4,7 @@ defmodule ZipLiner.Accounts.Member do
@roles ~w(student alumni instructor staff mentor guest)a
@statuses ~w(active suspended deprovisioned)a
+ @theme_preferences ~w(light dark)a
@admin_handles ~w(kristofer)
schema "members" do
@@ -21,6 +22,7 @@ defmodule ZipLiner.Accounts.Member do
field :status, Ecto.Enum, values: @statuses, default: :active
field :is_admin, :boolean, default: false
field :open_to_opportunities, :boolean, default: false
+ field :theme_preference, Ecto.Enum, values: @theme_preferences, default: :light
field :skills, {:array, :string}, default: []
field :avatar_source, Ecto.Enum, values: [:github, :linkedin], default: :github
@@ -57,6 +59,7 @@ defmodule ZipLiner.Accounts.Member do
:role,
:status,
:open_to_opportunities,
+ :theme_preference,
:skills,
:avatar_source,
:cohort_id
diff --git a/lib/zip_liner_web/components/layouts/root.html.heex b/lib/zip_liner_web/components/layouts/root.html.heex
index 73e9958..4199e8f 100644
--- a/lib/zip_liner_web/components/layouts/root.html.heex
+++ b/lib/zip_liner_web/components/layouts/root.html.heex
@@ -1,5 +1,5 @@
-
+
diff --git a/lib/zip_liner_web/controllers/settings_html/edit.html.heex b/lib/zip_liner_web/controllers/settings_html/edit.html.heex
index d1fcceb..bb543c1 100644
--- a/lib/zip_liner_web/controllers/settings_html/edit.html.heex
+++ b/lib/zip_liner_web/controllers/settings_html/edit.html.heex
@@ -61,6 +61,23 @@
+
+
Theme
+
+
+
+
+
+
+
diff --git a/priv/repo/migrations/20260417213600_add_theme_preference_to_members.exs b/priv/repo/migrations/20260417213600_add_theme_preference_to_members.exs
new file mode 100644
index 0000000..e107049
--- /dev/null
+++ b/priv/repo/migrations/20260417213600_add_theme_preference_to_members.exs
@@ -0,0 +1,9 @@
+defmodule ZipLiner.Repo.Migrations.AddThemePreferenceToMembers do
+ use Ecto.Migration
+
+ def change do
+ alter table(:members) do
+ add :theme_preference, :string, default: "light", null: false
+ end
+ end
+end
diff --git a/test/zip_liner/accounts_test.exs b/test/zip_liner/accounts_test.exs
index 8a352a6..666ad39 100644
--- a/test/zip_liner/accounts_test.exs
+++ b/test/zip_liner/accounts_test.exs
@@ -98,6 +98,12 @@ defmodule ZipLiner.AccountsTest do
assert updated.bio == "Hello world"
end
+ test "update_member/2 updates theme_preference" do
+ member = member_fixture()
+ {:ok, updated} = Accounts.update_member(member, %{theme_preference: :dark})
+ assert updated.theme_preference == :dark
+ end
+
test "update_member/2 saves a valid linkedin_url" do
member = member_fixture()
{:ok, updated} = Accounts.update_member(member, %{linkedin_url: "https://www.linkedin.com/in/testuser"})
diff --git a/test/zip_liner_web/controllers/settings_controller_test.exs b/test/zip_liner_web/controllers/settings_controller_test.exs
new file mode 100644
index 0000000..bcd5de3
--- /dev/null
+++ b/test/zip_liner_web/controllers/settings_controller_test.exs
@@ -0,0 +1,29 @@
+defmodule ZipLinerWeb.SettingsControllerTest do
+ use ZipLinerWeb.ConnCase
+
+ alias ZipLiner.Accounts
+
+ describe "settings" do
+ setup :log_in_member
+
+ test "edit renders theme switch with light mode default", %{conn: conn} do
+ conn = get(conn, ~p"/settings")
+ html = html_response(conn, 200)
+
+ assert html =~ "Use dark mode"
+ assert html =~ ~s(name="member[theme_preference]")
+ assert html =~ ~s(data-bs-theme="light")
+ end
+
+ test "update persists dark mode preference", %{conn: conn, member: member} do
+ conn = put(conn, ~p"/settings", member: %{"theme_preference" => "dark"})
+ assert redirected_to(conn) == ~p"/settings"
+
+ updated_member = Accounts.get_member!(member.id)
+ assert updated_member.theme_preference == :dark
+
+ conn = get(recycle(conn), ~p"/settings")
+ assert html_response(conn, 200) =~ ~s(data-bs-theme="dark")
+ end
+ end
+end