diff --git a/assets/css/app.css b/assets/css/app.css index bb9a4cc5da89c87d8b7976af6058b8510b77e4f1..c547b430fa3527b16635d836615852fc1f9dcc15 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -2,6 +2,148 @@ @tailwind components; @tailwind utilities; +/* Theme variables */ +:root[data-theme="light"] { + --color-background: #ffffff; + --color-text: #1a1a1a; + --color-primary: #4a90e2; + --color-secondary: #f5f5f5; + --color-border: #e2e8f0; + --color-accent: #3182ce; + --color-card: #ffffff; + --color-card-hover: #f7fafc; + --heading-color: #1a1a1a; +} + +:root[data-theme="dark"] { + --color-background: #0f172a; + --color-text: #e2e8f0; + --color-primary: #60a5fa; + --color-secondary: #1e293b; + --color-border: #334155; + --color-accent: #93c5fd; + --color-card: #1e293b; + --color-card-hover: #334155; + --heading-color: #f1f5f9; +} + +/* Apply theme colors */ +body { + background-color: var(--color-background); + color: var(--color-text); + transition: background-color 0.3s ease, color 0.3s ease; +} + +h1, h2, h3, h4, h5, h6 { + color: var(--heading-color); +} + +/* Card styles */ +.property-card { + background-color: var(--color-card); + border: 1px solid var(--color-border); + transition: all 0.3s ease; +} + +.property-card:hover { + background-color: var(--color-card-hover); +} + +/* Form element styles */ +input, textarea, select { + background-color: var(--color-card); + color: var(--color-text); + border-color: var(--color-border); +} + +input:focus, textarea:focus, select:focus { + border-color: var(--color-accent); +} + +/* Button styles */ +.button-primary { + background-color: var(--color-primary); + color: white; +} + +.button-secondary { + background-color: var(--color-secondary); + color: var(--color-text); +} + +/* Theme toggle */ +.theme-toggle-button { + background: none; + border: none; + cursor: pointer; + padding: 8px; + border-radius: 50%; + color: var(--color-text); + transition: background-color 0.3s ease; +} + +.theme-toggle-button:hover { + background-color: var(--color-secondary); +} + +/* Dark mode specific overrides */ +.dark { + /* Form inputs */ + input, textarea, select { + @apply bg-gray-800 border-gray-700 text-gray-200; + } + + /* Card styles */ + .card, .property-card { + @apply bg-gray-800 border-gray-700; + } + + /* Headers and text */ + h1, h2, h3, h4, h5, h6 { + @apply text-gray-100; + } + + /* Buttons */ + .button, button[type="submit"] { + @apply bg-gray-700 hover:bg-gray-600 text-white; + } + + /* Links */ + a { + @apply text-blue-400 hover:text-blue-300; + } + + /* Tables */ + table { + @apply bg-gray-800 border-gray-700; + } + + td, th { + @apply border-gray-700 text-gray-200; + } +} + +.link[href*="/edit"] { + @apply text-blue-400 hover:text-blue-300; +} + +.link[href*="/properties"][data-confirm] { + @apply bg-red-600 text-white hover:bg-red-700; +} + +.interest-menu-button { + @apply text-gray-900 dark:text-white; +} + +.interest-menu { + @apply bg-white dark:bg-gray-800 border dark:border-gray-700; +} + +.interest-option { + @apply text-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700; +} + +/* Previous code */ .favorite-button { background: none; border: none; diff --git a/assets/js/app.js b/assets/js/app.js index f839f9dc433050099ca74ef9eaa36c19cde1db98..db386ebcaa2c7e5f1002179f1e80ac0266caf92e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -7,6 +7,9 @@ import "./favorites"; import "./status"; import "./pricecalculation"; import "./interest"; +import initTheme from "./theme"; + +initTheme(); let csrfToken = document .querySelector("meta[name='csrf-token']") diff --git a/assets/js/theme.js b/assets/js/theme.js new file mode 100644 index 0000000000000000000000000000000000000000..395251dda3d2b6499ea6a0404a711468dd33e8e4 --- /dev/null +++ b/assets/js/theme.js @@ -0,0 +1,47 @@ +const initTheme = () => { + // Check for saved theme preference or default to system preference + const savedTheme = localStorage.getItem('theme') || + (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); + + // Apply initial theme + document.documentElement.setAttribute('data-theme', savedTheme); + if (savedTheme === 'dark') { + document.documentElement.classList.add('dark'); + } + + // Add click handler to theme toggle button + document.addEventListener('click', (e) => { + const themeToggle = e.target.closest('[data-theme-toggle]'); + if (themeToggle) { + const currentTheme = localStorage.getItem('theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + + // Update localStorage + localStorage.setItem('theme', newTheme); + + // Update document classes and data attributes + document.documentElement.setAttribute('data-theme', newTheme); + if (newTheme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } + }); + + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', e => { + if (!localStorage.getItem('theme')) { + const newTheme = e.matches ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', newTheme); + if (newTheme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } + }); + }; + + export default initTheme; \ No newline at end of file diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index a5db4f88202a3b620350801e9f2fed5e97c68511..3975fec5474158e8711ad21ffae787a5629a0697 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -6,15 +6,38 @@ const fs = require("fs") const path = require("path") module.exports = { + darkMode: 'class', content: [ "./js/**/*.js", "../lib/proptrackr_web.ex", - "../lib/proptrackr_web/**/*.*ex" + "../lib/proptrackr_web/**/*.*ex", + "../lib/proptrackr_web/components/**/*.*ex" ], theme: { extend: { colors: { brand: "#FD4F00", + dark: { + background: '#0f172a', + card: '#1e293b', + text: '#e2e8f0', + heading: '#f1f5f9', + border: '#334155', + primary: '#60a5fa', + accent: '#93c5fd' + } + }, + backgroundColor: { + 'dark-form': '#1e293b', + 'dark-card': '#1e293b', + 'dark-hover': '#334155' + }, + textColor: { + 'dark-primary': '#f1f5f9', + 'dark-secondary': '#e2e8f0' + }, + borderColor: { + 'dark-border': '#334155' } }, }, @@ -71,4 +94,4 @@ module.exports = { }, {values}) }) ] -} +} \ No newline at end of file diff --git a/features/config.exs b/features/config.exs index 43d1aec2bf5afa093a8eb950162a0fb779e29bfc..7c34e90539bb8b9d78b9933fcc92a969c1fc2650 100644 --- a/features/config.exs +++ b/features/config.exs @@ -73,4 +73,8 @@ defmodule WhiteBreadConfig do context: PropertySearchContext, feature_paths: ["features/property_search.feature"] + suite name: "FR-21: Dark/Light mode appearance switch", + context: DarkModeContext, + feature_paths: ["features/dark_mode.feature"] + end diff --git a/features/contexts/dark_mode_context.exs b/features/contexts/dark_mode_context.exs new file mode 100644 index 0000000000000000000000000000000000000000..1c7e324fa4076986a593a82588cb26c7a55db15a --- /dev/null +++ b/features/contexts/dark_mode_context.exs @@ -0,0 +1,187 @@ +defmodule DarkModeContext do + use WhiteBread.Context + use Hound.Helpers + alias PropTrackr.Properties.Property + alias PropTrackr.Accounts.User + alias PropTrackr.Repo + + scenario_starting_state fn _state -> + Ecto.Adapters.SQL.Sandbox.checkout(PropTrackr.Repo) + Ecto.Adapters.SQL.Sandbox.mode(PropTrackr.Repo, {:shared, self()}) + Hound.start_session() + %{} + end + + scenario_finalize fn _status, _state -> + Ecto.Adapters.SQL.Sandbox.checkin(PropTrackr.Repo) + Hound.end_session() + end + + given_ ~r/^there exists following accounts$/, fn state, %{table_data: table} -> + table + |> Enum.map(fn user_details -> User.changeset(%User{}, user_details) end) + |> Enum.each(fn changeset -> Repo.insert!(changeset) end) + + user = List.last(table) + + { + :ok, + state + |> Map.put(:user_email, user[:email]) + |> Map.put(:user_password, user[:password]) + } + end + + and_ ~r/^the following properties exist$/, fn state, %{ table_data: table } -> + owner = Repo.get_by(User, email: state[:user_email]) + + advertisements = + table + |> Enum.map(fn details -> details |> Map.put(:reference, Ecto.UUID.generate()) end) + |> Enum.map(fn details -> {Ecto.build_assoc(owner, :properties, details), details} end) + |> Enum.map(fn {assoc, details} -> Property.changeset(assoc, details) end) + |> Enum.map(fn changeset -> Repo.insert!(changeset) end) + + { + :ok, + state + |> Map.put(:advertisements, advertisements) + } + end + + # Navigation steps + when_ ~r/^I visit the home page$/, fn state -> + navigate_to("/") + {:ok, state} + end + + given_ ~r/^I am on the home page$/, fn state -> + navigate_to("/") + {:ok, state} + end + + when_ ~r/^I click the theme toggle button$/, fn state -> + find_element(:css, "[data-theme-toggle]") |> click() + :timer.sleep(500) + {:ok, state} + end + + given_ ~r/^I have enabled dark mode$/, fn state -> + navigate_to("/") + find_element(:css, "[data-theme-toggle]") |> click() + :timer.sleep(500) + {:ok, state} + end + + when_ ~r/^I navigate to the (?<page>[^"]+) page$/, fn state, %{page: page} -> + path = case page do + "profile" -> "/me" + "login" -> "/login" + "home" -> "/" + end + navigate_to(path) + :timer.sleep(500) + {:ok, state} + end + + when_ ~r/^I refresh the page$/, fn state -> + refresh_page() + :timer.sleep(500) + {:ok, state} + end + + when_ ~r/^I visit the property details page$/, fn state -> + property = hd(state.advertisements) + navigate_to("/properties/#{property.reference}") + :timer.sleep(1000) + {:ok, state} + end + + # Light mode assertions + then_ ~r/^I should see the application in light mode$/, fn state -> + assert get_css_variable("--color-background") == "#ffffff" + assert get_css_variable("--color-text") == "#1a1a1a" + assert get_css_variable("--color-card") == "#ffffff" + {:ok, state} + end + + # Dark mode assertions + then_ ~r/^I should see the application in dark mode$/, fn state -> + assert get_css_variable("--color-background") == "#0f172a" + assert get_css_variable("--color-text") == "#e2e8f0" + assert get_css_variable("--color-card") == "#1e293b" + {:ok, state} + end + + then_ ~r/^I should see the theme toggle button shows (?<icon>[^"]+) icon$/, fn state, %{icon: icon} -> + icon_class = case icon do + "moon" -> "hero-moon-solid" + "sun" -> "hero-sun-solid" + end + assert element?(:css, ".#{icon_class}") + {:ok, state} + end + + then_ ~r/^the background should be dark$/, fn state -> + assert get_css_variable("--color-background") == "#0f172a" + {:ok, state} + end + + and_ ~r/^I am logged in$/, fn state -> + setup_session(state[:user_email], state[:user_password]) + :timer.sleep(500) + {:ok, state} + end + + and_ ~r/^the theme toggle should show the moon icon$/, fn state -> + moon_icon = find_element(:css, ".theme-toggle-button .hero-moon-solid") + assert element_displayed?(moon_icon) + {:ok, state} + end + + and_ ~r/^the theme toggle should show the sun icon$/, fn state -> + sun_icon = find_element(:css, ".theme-toggle-button .hero-sun-solid") + assert element_displayed?(sun_icon) + {:ok, state} + end + + then_ ~r/^the text should be light colored$/, fn state -> + assert get_css_variable("--color-text") == "#e2e8f0" + {:ok, state} + end + + then_ ~r/^I should see the (?<page>[^"]+) page in dark mode$/, fn state, %{page: page} -> + assert get_css_variable("--color-background") == "#0f172a" + {:ok, state} + end + + then_ ~r/^I should see dark mode colors for:$/, fn state, %{table_data: table} -> + variables = %{ + "Background" => "--color-background", + "Text" => "--color-text", + "Cards" => "--color-card", + "Buttons" => "--color-primary" + } + + {:ok, Map.put(state, :colors, + Enum.reduce(table, %{}, fn row, acc -> + css_var = variables[row["Element"]] + value = get_css_variable(css_var) + Map.put(acc, row["Element"], value) + end))} + end + + defp get_css_variable(variable_name) do + script = """ + var style = getComputedStyle(document.documentElement); + return style.getPropertyValue('#{variable_name}').trim(); + """ + execute_script(script) + end + + defp setup_session(email, password) do + fill_field({:id, "email"}, email) + fill_field({:id, "password"}, password) + click({:id, "login_button"}) + end + end diff --git a/features/dark_mode.feature b/features/dark_mode.feature new file mode 100644 index 0000000000000000000000000000000000000000..d8528033e1938954dc81cb2b079fd16deb12b761 --- /dev/null +++ b/features/dark_mode.feature @@ -0,0 +1,48 @@ +Feature: FR-21: Dark/Light mode appearance switch + + Scenario: Default theme is light + When I visit the home page + Then I should see the application in light mode + And the theme toggle should show the moon icon + + Scenario: Switching to dark mode + Given I am on the home page + When I click the theme toggle button + Then I should see the application in dark mode + And the theme toggle should show the sun icon + And the background should be dark + And the text should be light colored + + Scenario: Dark mode persists across pages + Given there exists following accounts + | name | surname | birth_date | phone_number | email | password | confirm_password | + | Existing | Account | 2000-01-01 | 000 | existing.account@gmail.com | password | password | + And I have enabled dark mode + When I navigate to the login page + Then I should see the login page in dark mode + And I am logged in + When I navigate to the profile page + Then I should see the profile page in dark mode + When I navigate to the home page + Then I should see the home page in dark mode + + Scenario: Theme preference persists after refresh + Given I have enabled dark mode + When I refresh the page + Then I should see the application in dark mode + + Scenario: Dark mode affects all UI elements + Given there exists following accounts + | name | surname | birth_date | phone_number | email | password | confirm_password | + | Existing | Account | 2000-01-01 | 000 | existing.account@gmail.com | password | password | + And the following properties exist + | title | description | type | property_type | state | location | room_count | area | floor | floor_count | price | + | Really cool property | Selling this really really house | sell | house | available | London | 3 | 100.0 | 2 | 5 | 500000 | + And I have enabled dark mode + When I visit the property details page + Then I should see dark mode colors for: + | Element | Color | + | Background | #0f172a | + # | Text | #e2e8f0 | + # | Cards | #1e293b | + # | Buttons | #60a5fa | \ No newline at end of file diff --git a/lib/proptrackr_web/components/core_components.ex b/lib/proptrackr_web/components/core_components.ex index 922d3f25c6b838026b4c68173e9de9c98ed2d23f..6ee997e70b49a2d42652e27ffaac97d49b8a70a9 100644 --- a/lib/proptrackr_web/components/core_components.ex +++ b/lib/proptrackr_web/components/core_components.ex @@ -202,7 +202,7 @@ defmodule PropTrackrWeb.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="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6 space-y-8"> <%= render_slot(@inner_block, f) %> <div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6"> <%= render_slot(action, f) %> @@ -231,7 +231,7 @@ defmodule PropTrackrWeb.CoreComponents do <button type={@type} 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 dark:bg-gray-700 dark:hover:bg-gray-600 py-2 px-3", "text-sm font-semibold leading-6 text-white active:text-white/80", @class ]} @@ -335,7 +335,7 @@ defmodule PropTrackrWeb.CoreComponents do <select id={@id} name={@name} - class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm" + class="mt-2 block w-full rounded-md border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200 shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm" multiple={@multiple} {@rest} > @@ -355,7 +355,8 @@ defmodule PropTrackrWeb.CoreComponents do id={@id} name={@name} class={[ - "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]", + "mt-2 block w-full rounded-lg text-zinc-900 dark:text-gray-200 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]", + "dark:bg-gray-800 dark:border-gray-700", @errors == [] && "border-zinc-300 focus:border-zinc-400", @errors != [] && "border-rose-400 focus:border-rose-400" ]} @@ -377,7 +378,8 @@ defmodule PropTrackrWeb.CoreComponents do id={@id} value={Phoenix.HTML.Form.normalize_value(@type, @value)} class={[ - "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6", + "mt-2 block w-full rounded-lg text-zinc-900 dark:text-gray-200 focus:ring-0 sm:text-sm sm:leading-6", + "dark:bg-gray-800 dark:border-gray-700", @errors == [] && "border-zinc-300 focus:border-zinc-400", @errors != [] && "border-rose-400 focus:border-rose-400" ]} @@ -396,7 +398,7 @@ defmodule PropTrackrWeb.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-dark-text"> <%= render_slot(@inner_block) %> </label> """ @@ -429,10 +431,10 @@ defmodule PropTrackrWeb.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-gray-100"> <%= 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-gray-300"> <%= render_slot(@subtitle) %> </p> </div> @@ -673,4 +675,60 @@ defmodule PropTrackrWeb.CoreComponents do def translate_errors(errors, field) when is_list(errors) do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end + + def theme_toggle(assigns) do + ~H""" + <div class="theme-toggle"> + <button + type="button" + class="theme-toggle-button" + aria-label="Toggle theme" + data-theme-toggle + > + <.icon name="hero-moon-solid" class="h-6 w-6 dark:hidden" /> + <.icon name="hero-sun-solid" class="hidden h-6 w-6 dark:block" /> + </button> + </div> + """ + end + + def content_card(assigns) do + ~H""" + <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6"> + <%= render_slot(@inner_block) %> + </div> + """ + end + + def section_title(assigns) do + ~H""" + <h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4"> + <%= render_slot(@inner_block) %> + </h3> + """ + end + + def detail_item(assigns) do + ~H""" + <div> + <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= @label %></dt> + <dd class="mt-1 text-sm text-gray-900 dark:text-gray-200"> + <%= render_slot(@inner_block) %> + </dd> + </div> + """ + end + + def property_link(assigns) do + assigns = assign_new(assigns, :class, fn -> "text-blue-400 hover:text-blue-300" end) + + ~H""" + <.link + href={@href} + class={@class} + > + <%= render_slot(@inner_block) %> + </.link> + """ + end end diff --git a/lib/proptrackr_web/components/layouts/app.html.heex b/lib/proptrackr_web/components/layouts/app.html.heex index 43b4f6df321c11c72c8191d6dc2501e80ee4f377..d9150299a360687720f908e03c11dc455a6c1e43 100644 --- a/lib/proptrackr_web/components/layouts/app.html.heex +++ b/lib/proptrackr_web/components/layouts/app.html.heex @@ -1,4 +1,40 @@ -<header class="header flex flex-row-reverse gap-x-4 px-4 py-1"> +<header class="header flex items-center justify-between px-4 py-1"> + <span class="logo"></span> + + <div class="flex items-center gap-4"> + <.theme_toggle /> + + <%= if @conn.assigns.current_user do %> + <button + id="logout_button" + phx-click={JS.navigate("/logout")} + class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300" + > + Logout + <.icon name="hero-arrow-right-solid" class="h-3 w-3" /> + </button> + <span class="text-zinc-900 dark:text-zinc-100">Hello, <%= @conn.assigns.current_user.name %>!</span> + <% else %> + <button + id="nav_login_button" + phx-click={JS.navigate("/login")} + class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300" + > + Log in + <.icon name="hero-arrow-right-solid" class="h-3 w-3" /> + </button> + <% end %> + </div> +</header> + +<main class="px-4 py-20 sm:px-6 lg:px-8"> + <div class="mx-auto max-w-2xl"> + <.flash_group flash={@flash} /> + <%= @inner_content %> + </div> +</main> + +<%!-- <header class="header flex flex-row-reverse gap-x-4 px-4 py-1"> <%= if @conn.assigns.current_user do %> <button id="logout_button" @@ -24,4 +60,4 @@ <main class="px-4 py-20 sm:px-6 lg:px-8"> <div class="mx-auto max-w-2xl"><.flash_group flash={@flash} /> <%= @inner_content %></div> -</main> +</main> --%> diff --git a/lib/proptrackr_web/components/layouts/root.html.heex b/lib/proptrackr_web/components/layouts/root.html.heex index e975c72a48d60d756eafa701f7f8375e25bf6319..6c40d4c24b404912496efbe8930ad9575aafb87e 100644 --- a/lib/proptrackr_web/components/layouts/root.html.heex +++ b/lib/proptrackr_web/components/layouts/root.html.heex @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en" class="[scrollbar-gutter:stable]"> +<html lang="en" class="[scrollbar-gutter:stable] dark:dark" data-theme="light"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> @@ -11,7 +11,7 @@ <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-dark-background text-zinc-900 dark:text-dark-text transition-colors duration-300"> <%= @inner_content %> </body> </html> diff --git a/lib/proptrackr_web/controllers/my_favorites_html/index.html.heex b/lib/proptrackr_web/controllers/my_favorites_html/index.html.heex index 2a26060ec1303201328b7912b1281691dacba9e9..1b5bf30e7986f6b893f5cf3b83d8fe5a8a5fc0a4 100644 --- a/lib/proptrackr_web/controllers/my_favorites_html/index.html.heex +++ b/lib/proptrackr_web/controllers/my_favorites_html/index.html.heex @@ -1,12 +1,14 @@ <.header> - My Favorite Properties + <span>My Favorite Properties</span> </.header> + <div id="favorites" class="flex flex-col gap-y-4 mt-8"> <%= if @favorites == [] do %> - <p>You haven't favorited any properties yet.</p> + <p class="text-gray-600 dark:text-gray-300">You haven't favorited any properties yet.</p> <% end %> + <%= for favorite <- @favorites do %> - <div class="bg-white border-black border rounded px-4 py-2"> + <.content_card> <div class="flex gap-4"> <div class="w-48 h-48 flex-shrink-0"> <%= if first_photo = Enum.at(favorite.property.photos, 0) do %> @@ -16,33 +18,39 @@ class="w-full h-full object-cover rounded" /> <% else %> - <div class="w-full h-full bg-gray-200 flex items-center justify-center rounded"> - <span class="text-gray-400">No photo</span> + <div class="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center rounded"> + <span class="text-gray-400 dark:text-gray-300">No photo</span> </div> <% end %> </div> <div class="flex-grow"> <div class="flex justify-between items-start"> - <h2 class="font-bold"><%= favorite.property.title %></h2> + <.section_title><%= favorite.property.title %></.section_title> <button type="button" - class="favorite-button text-2xl" + class="favorite-button text-2xl text-gray-900 dark:text-white" data-property-reference={favorite.property.reference} data-favorited="true" > âک… </button> </div> - <p><%= favorite.property.description %></p> - <p class="italic"><%= favorite.property.location %></p> + <p class="text-gray-600 dark:text-gray-300"><%= favorite.property.description %></p> + <p class="italic text-gray-600 dark:text-gray-300"><%= favorite.property.location %></p> <div class="flex flex-row gap-x-2"> - <span><%= favorite.property.price %> €</span> - <span><%= favorite.property.room_count %> rooms</span> - <span><%= favorite.property.area %> m<sup>2</sup></span> + <.detail_item label="Price"> + <%= favorite.property.price %> € + </.detail_item> + <.detail_item label="Rooms"> + <%= favorite.property.room_count %> + </.detail_item> + <.detail_item label="Area"> + <%= favorite.property.area %> m<sup>2</sup> + </.detail_item> </div> - <div class="flex flex-row justify-end"> - <.link href={~p"/properties/#{favorite.property.reference}"}> + <div class="flex flex-row justify-end mt-4"> + <.property_link href={~p"/properties/#{favorite.property.reference}"}> <.button type="button" class="text-white rounded px-4 py-2" @@ -50,10 +58,10 @@ > View more </.button> - </.link> + </.property_link> </div> </div> </div> - </div> + </.content_card> <% end %> </div> diff --git a/lib/proptrackr_web/controllers/my_properties_html/index.html.heex b/lib/proptrackr_web/controllers/my_properties_html/index.html.heex index 60ada05ad49640bc4ec779f61e41217e99ebdc9b..68541e507c50941c33c0d6a77757488ddbb662db 100644 --- a/lib/proptrackr_web/controllers/my_properties_html/index.html.heex +++ b/lib/proptrackr_web/controllers/my_properties_html/index.html.heex @@ -3,7 +3,7 @@ </.header> <div id="properties" class="flex flex-col gap-y-4 mt-20"> <%= for property <- @properties do %> - <div class="bg-white border-black border rounded px-4 py-2"> + <.content_card> <div class="flex gap-4"> <div class="w-48 h-48 flex-shrink-0"> <%= if first_photo = Enum.at(property.photos, 0) do %> @@ -20,7 +20,7 @@ </div> <div class="flex-grow"> - <h2 class="font-bold"><%= property.title %></h2> + <h2 class="font-bold text-gray-900 dark:text-white"><%= property.title %></h2> <p><%= property.description %></p> <p class="italic"><%= property.location %></p> <div class="flex flex-row gap-x-2"> @@ -41,6 +41,6 @@ </div> </div> </div> - </div> + </.content_card> <% end %> </div> diff --git a/lib/proptrackr_web/controllers/properties_html/new.html.heex b/lib/proptrackr_web/controllers/properties_html/new.html.heex index af7113149acec3eca2458dec7cb1839c40f76ea7..90c502f793ccfe22383ef8a54e45f4ac70ff0dde 100644 --- a/lib/proptrackr_web/controllers/properties_html/new.html.heex +++ b/lib/proptrackr_web/controllers/properties_html/new.html.heex @@ -56,7 +56,7 @@ <.input field={f[:price]} id="price" type="text" label="Price" required /> <div phx-feedback-for="property[photos]"> - <label for="property_photos" class="block text-sm font-medium text-gray-700"> + <label for="property_photos" class="block text-sm font-medium text-gray-700 dark:text-dark-text"> Photos (Upload 1-5 images) </label> <input diff --git a/lib/proptrackr_web/controllers/properties_html/show.html.heex b/lib/proptrackr_web/controllers/properties_html/show.html.heex index 2e2fd10fdeec55eb86c51fd625a1eeace894c396..f4515d5d00885b856804bb48389b44033750a242 100644 --- a/lib/proptrackr_web/controllers/properties_html/show.html.heex +++ b/lib/proptrackr_web/controllers/properties_html/show.html.heex @@ -1,12 +1,12 @@ <.flash_group flash={@flash} /> <.header> - <%= @property.title %> + <span><%= @property.title %></span> <:actions> <%= if @can_edit do %> <.link href={~p"/properties/#{@property.reference}/edit"} - class="bg-zinc-900 hover:bg-zinc-700 text-white px-4 py-2 rounded-md mr-4" + class="bg-zinc-900 hover:bg-zinc-700 text-white hover:text-white bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-md mr-4" id="edit-property" > Edit Property @@ -16,7 +16,7 @@ href={~p"/properties/#{@property.reference}"} method="delete" data-confirm="Are you sure you want to delete this property?" - class="rounded-lg bg-red-600 px-4 py-2 text-white hover:bg-red-700 ml-2" + class="text-white hover:text-white bg-red-600 hover:bg-red-700 rounded-lg px-4 py-2" id="delete-property" > Delete Property @@ -26,7 +26,7 @@ <%= if @conn.assigns[:current_user] && !@can_edit do %> <button type="button" - class="favorite-button text-2xl mr-4" + class="favorite-button text-2xl mr-4 dark:text-white" data-property-reference={@property.reference} data-favorited={if @property.id in @favorites, do: "true", else: "false"} > @@ -34,17 +34,16 @@ </button> <% end %> - <.link href={~p"/"}> + <.property_link href={~p"/"}> Back to listings <.icon name="hero-arrow-right-solid" class="h-3 w-3" /> - </.link> + </.property_link> </:actions> </.header> -<div class="mt-8 bg-white shadow rounded-lg p-6"> - <h3 class="text-lg font-medium text-gray-900 mb-4">Property Photos</h3> +<.content_card class="flex flex-col gap-y-4 mt-20"> + <.section_title>Property Photos</.section_title> <div class="relative"> - <div class="w-full h-[500px] relative overflow-hidden rounded-lg"> <%= if length(@property.photos) > 0 do %> <%= for {photo, index} <- Enum.with_index(@property.photos) do %> @@ -61,8 +60,8 @@ </div> <% end %> <% else %> - <div class="w-full h-full bg-gray-200 flex items-center justify-center"> - <span class="text-gray-400">No photos available</span> + <div class="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center"> + <span class="text-gray-400 dark:text-gray-300">No photos available</span> </div> <% end %> </div> @@ -105,26 +104,21 @@ <% end %> </div> <% end %> -</div> +</.content_card> <div class="mt-8 space-y-8"> - <div class="bg-white shadow rounded-lg p-6"> + <.content_card> <dl class="grid grid-cols-1 md:grid-cols-2 gap-4"> - <div> - <dt class="text-sm font-medium text-gray-500">Type</dt> - <dd class="mt-1 text-sm text-gray-900"> - <%= String.capitalize(to_string(@property.type)) %> - </dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Property Type</dt> - <dd class="mt-1 text-sm text-gray-900"> - <%= String.capitalize(to_string(@property.property_type)) %> - </dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Status</dt> - <dd class="mt-1 text-sm text-gray-900 flex items-center justify-between"> + <.detail_item label="Type"> + <%= String.capitalize(to_string(@property.type)) %> + </.detail_item> + + <.detail_item label="Property Type"> + <%= String.capitalize(to_string(@property.property_type)) %> + </.detail_item> + + <.detail_item label="Status"> + <div class="flex items-center justify-between"> <span id="property-status-text"> <%= cond do %> <% @property.state == :unavailable and @property.type == :rent -> %> @@ -140,7 +134,7 @@ <div> <button type="button" - class="status-dropdown-button inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500" + class="status-dropdown-button inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600" id="status-menu-button" aria-expanded="false" aria-haspopup="true" @@ -163,7 +157,7 @@ </button> </div> <div - class="status-dropdown-menu hidden origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 focus:outline-none" + class="status-dropdown-menu hidden origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-700 ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 dark:divide-gray-600" role="menu" aria-orientation="vertical" aria-labelledby="status-menu-button" @@ -171,21 +165,21 @@ > <div class="py-1" role="none"> <button - class="status-option text-gray-700 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100" + class="status-option text-gray-700 dark:text-gray-200 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem" data-status="available" > Available </button> <button - class="status-option text-gray-700 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100" + class="status-option text-gray-700 dark:text-gray-200 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem" data-status="reserved" > Reserved </button> <button - class="status-option text-gray-700 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100" + class="status-option text-gray-700 dark:text-gray-200 block px-4 py-2 text-sm w-full text-left hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem" data-status="unavailable" > @@ -195,56 +189,54 @@ </div> </div> <% end %> - </dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Location</dt> - <dd class="mt-1 text-sm text-gray-900"><%= @property.location %></dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Price</dt> - <dd class="mt-1 text-sm text-gray-900"> - €<%= :erlang.float_to_binary(@property.price, decimals: 2) %> - </dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Area</dt> - <dd class="mt-1 text-sm text-gray-900"><%= @property.area %> mآ²</dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Rooms</dt> - <dd class="mt-1 text-sm text-gray-900"><%= @property.room_count %></dd> - </div> - <div> - <dt class="text-sm font-medium text-gray-500">Floor</dt> - <dd class="mt-1 text-sm text-gray-900"> - <%= @property.floor %>/<%= @property.floor_count %> - </dd> - </div> + </div> + </.detail_item> + + <.detail_item label="Location"> + <%= @property.location %> + </.detail_item> + + <.detail_item label="Price"> + €<%= :erlang.float_to_binary(@property.price, decimals: 2) %> + </.detail_item> + + <.detail_item label="Area"> + <%= @property.area %> mآ² + </.detail_item> + + <.detail_item label="Rooms"> + <%= @property.room_count %> + </.detail_item> + + <.detail_item label="Floor"> + <%= @property.floor %>/<%= @property.floor_count %> + </.detail_item> </dl> - </div> + </.content_card> - <div class="bg-white shadow rounded-lg p-6"> - <h3 class="text-lg font-medium text-gray-900">Description</h3> - <p class="mt-4 text-sm text-gray-600"><%= @property.description %></p> - </div> + <.content_card> + <.section_title>Description</.section_title> + <p class="mt-4 text-sm text-gray-600 dark:text-gray-300"><%= @property.description %></p> + </.content_card> - <div class="bg-white shadow rounded-lg p-6"> - <h3 class="text-lg font-medium text-gray-900">Contact Information</h3> + <.content_card> + <.section_title>Contact Information</.section_title> <div class="mt-4"> - <p class="text-sm text-gray-600"> + <p class="text-sm text-gray-600 dark:text-gray-300"> Listed by <%= @property.user.name %> <%= @property.user.surname %> </p> - <p class="text-sm text-gray-600">Phone: <%= @property.user.phone_number %></p> - <p class="text-sm text-gray-600">Email: <%= @property.user.email %></p> + <p class="text-sm text-gray-600 dark:text-gray-300"> + Phone: <%= @property.user.phone_number %> + </p> + <p class="text-sm text-gray-600 dark:text-gray-300">Email: <%= @property.user.email %></p> </div> - </div> + </.content_card> - <div class="bg-white shadow rounded-lg p-6"> - <h3 class="text-lg font-medium text-gray-900">Similar properties</h3> + <.content_card> + <.section_title>Similar properties</.section_title> <div class="mt-4"> <%= if @similar_properties == [] do %> - <p class="text-sm text-gray-600">No similar properties found</p> + <p class="text-sm text-gray-600 dark:text-gray-300">No similar properties found</p> <% else %> <%= for property <- @similar_properties do %> <div @@ -253,12 +245,15 @@ data-price={property.price} > <div> - <p class="text-sm text-gray-600"><%= property.title %></p> - <p class="text-sm text-gray-600"> + <p class="text-sm text-gray-600 dark:text-gray-300"><%= property.title %></p> + <p class="text-sm text-gray-600 dark:text-gray-300"> <%= :erlang.float_to_binary(property.price, decimals: 2) %>€ </p> - <p class="text-sm text-gray-600"><%= property.location %></p> + <p class="text-sm text-gray-600 dark:text-gray-300"><%= property.location %></p> </div> + <%!-- <.property_link href={~p"/properties/#{property.reference}"} id={"view-similar-property-#{property.reference}"}> + View + </.property_link> --%> <a href={~p"/properties/#{property.reference}"} class="text-sm text-blue-600 hover:underline" @@ -270,7 +265,7 @@ <% end %> <% end %> </div> - </div> + </.content_card> </div> <script>