Packages
phoenix_kit
1.7.199
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/settings.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("Settings")}
page_subtitle={gettext("Configure system preferences and options")}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<%!-- Settings Form --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl mb-6">{gettext("System Configuration")}</h2>
<.form
:let={_form}
for={@changeset}
id="settings_form"
phx-submit="save_settings"
phx-change="validate_settings"
class="space-y-6"
>
<%!-- Hidden fields for required settings from Users page --%>
<%!-- Use @saved_settings to preserve database values, not @settings which may have incorrect form state --%>
<input
type="hidden"
name="settings[allow_registration]"
value={@saved_settings["allow_registration"]}
/>
<input
type="hidden"
name="settings[new_user_default_role]"
value={@saved_settings["new_user_default_role"]}
/>
<input
type="hidden"
name="settings[new_user_default_status]"
value={@saved_settings["new_user_default_status"]}
/>
<input
type="hidden"
name="settings[track_registration_geolocation]"
value={@saved_settings["track_registration_geolocation"]}
/>
<.section_header icon="hero-identification" title={gettext("Site Identity")} />
<%!-- Project Title Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Project Title")}</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Application name display")}
</span>
</label>
<input
name="settings[project_title]"
type="text"
value={@settings["project_title"]}
class="input input-bordered w-full focus:input-primary"
placeholder={gettext("Enter project title")}
phx-debounce="500"
/>
<.unsaved_hint
dirty={@settings["project_title"] != @saved_settings["project_title"]}
saved={@saved_settings["project_title"]}
/>
</div>
<%!-- Project Logo --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Project Logo")}</span>
</label>
<input
type="hidden"
name="settings[auth_logo_file_uuid]"
value={@settings["auth_logo_file_uuid"]}
/>
<%= if (@settings["auth_logo_file_uuid"] || "") != "" do %>
<div class="flex items-center gap-3 p-2 bg-base-200 rounded-lg">
<img
src={signed_preview_url(@settings["auth_logo_file_uuid"], "medium")}
alt="Logo preview"
class="h-20 object-contain"
/>
<button
type="button"
class="btn btn-ghost btn-sm"
phx-click="clear_image"
phx-value-target="logo"
>
{gettext("Remove")}
</button>
</div>
<% else %>
<button
type="button"
class="btn btn-outline btn-sm w-fit"
phx-click="open_media_selector"
phx-value-target="logo"
>
{gettext("Select Image")}
</button>
<% end %>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
{gettext(
"Logo displayed across the application. Falls back to the site icon, then the project title text."
)}
<%= if (@settings["auth_logo_file_uuid"] || "") == "" &&
(@settings["site_icon_file_uuid"] || "") != "" do %>
<span class="text-info">{gettext("Currently using the site icon.")}</span>
<% end %>
</span>
</div>
</div>
<%!-- Browser Tab: site icon + default tab title as one identity group,
with a live browser-chrome preview (WordPress site-icon style). --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Browser Tab")}</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Site icon and tab title")}
</span>
</label>
<input
type="hidden"
name="settings[site_icon_file_uuid]"
value={@settings["site_icon_file_uuid"]}
/>
<%!-- The favicon defaults to the project logo when unset (and vice
versa) — mirror that fallback so the preview shows what the
browser will actually render. --%>
<% preview_icon_uuid =
if (@settings["site_icon_file_uuid"] || "") != "",
do: @settings["site_icon_file_uuid"],
else: @settings["auth_logo_file_uuid"] %>
<div class="rounded-box border border-base-300 overflow-hidden">
<%!-- Live preview: tab strip. The title updates as you type
(the form's phx-change refreshes @settings). --%>
<div class="bg-base-200 px-4 pt-3">
<div class="flex items-end gap-2">
<div class="hidden sm:flex items-center gap-1.5 mb-2.5 mr-1">
<span class="w-2.5 h-2.5 rounded-full bg-error/60"></span>
<span class="w-2.5 h-2.5 rounded-full bg-warning/60"></span>
<span class="w-2.5 h-2.5 rounded-full bg-success/60"></span>
</div>
<div class="flex items-center gap-2 bg-base-100 rounded-t-lg px-3 py-2 min-w-0 max-w-[260px] shadow-sm">
<%= if (preview_icon_uuid || "") != "" do %>
<img
src={signed_preview_url(preview_icon_uuid, "thumbnail")}
alt=""
class="w-4 h-4 rounded-sm object-cover shrink-0"
/>
<% else %>
<.icon name="hero-globe-alt" class="w-4 h-4 text-base-content/30 shrink-0" />
<% end %>
<span class="text-xs font-medium truncate">
{if (@settings["default_tab_title"] || "") != "",
do: @settings["default_tab_title"],
else: @settings["project_title"] || "My Site"}
</span>
<.icon
name="hero-x-mark"
class="w-3 h-3 text-base-content/40 shrink-0 ml-auto"
/>
</div>
<div class="mb-2.5 ml-1 text-base-content/40">
<.icon name="hero-plus" class="w-3.5 h-3.5" />
</div>
</div>
</div>
<%!-- Address bar --%>
<div class="bg-base-100 border-b border-base-300 px-4 py-2 flex items-center gap-3">
<div class="hidden sm:flex items-center gap-2 text-base-content/25">
<.icon name="hero-arrow-left" class="w-3.5 h-3.5" />
<.icon name="hero-arrow-right" class="w-3.5 h-3.5" />
<.icon name="hero-arrow-path" class="w-3.5 h-3.5" />
</div>
<div class="flex items-center gap-1.5 bg-base-200 rounded-full px-3 py-1 flex-1 max-w-xs min-w-0">
<.icon name="hero-lock-closed" class="w-3 h-3 text-base-content/40 shrink-0" />
<span class="text-xs text-base-content/50 truncate">{site_host()}</span>
</div>
</div>
<%!-- Controls --%>
<div class="p-4 sm:p-5 bg-base-100 flex flex-col sm:flex-row gap-5">
<div class="flex items-start gap-4">
<%= if (@settings["site_icon_file_uuid"] || "") != "" do %>
<img
src={signed_preview_url(@settings["site_icon_file_uuid"], "thumbnail")}
alt={gettext("Site icon preview")}
class="w-16 h-16 rounded-2xl object-cover ring-1 ring-base-300 shadow-sm shrink-0"
/>
<div class="flex flex-col gap-2">
<button
type="button"
class="btn btn-outline btn-sm"
phx-click="open_media_selector"
phx-value-target="site_icon"
>
{gettext("Change icon")}
</button>
<button
type="button"
class="btn btn-ghost btn-sm text-error"
phx-click="clear_image"
phx-value-target="site_icon"
>
{gettext("Remove")}
</button>
</div>
<% else %>
<button
type="button"
class="w-16 h-16 rounded-2xl border-2 border-dashed border-base-300 hover:border-primary hover:text-primary text-base-content/40 flex items-center justify-center transition-colors shrink-0"
title={gettext("Select site icon")}
phx-click="open_media_selector"
phx-value-target="site_icon"
>
<.icon name="hero-photo" class="w-6 h-6" />
</button>
<div class="flex flex-col gap-2">
<button
type="button"
class="btn btn-outline btn-sm"
phx-click="open_media_selector"
phx-value-target="site_icon"
>
{gettext("Select site icon")}
</button>
<p class="text-xs text-base-content/50 max-w-[16rem]">
{gettext("Square, at least 512×512 pixels.")}
<%= if (@settings["auth_logo_file_uuid"] || "") != "" do %>
{gettext("Currently using the project logo.")}
<% end %>
</p>
</div>
<% end %>
</div>
<div class="divider divider-horizontal m-0 hidden sm:flex"></div>
<div class="flex-1 min-w-0">
<label class="label pt-0 pb-1">
<span class="label-text text-sm font-medium">
{gettext("Default Tab Title")}
</span>
</label>
<input
name="settings[default_tab_title]"
type="text"
value={@settings["default_tab_title"]}
class="input input-bordered w-full focus:input-primary"
placeholder={@settings["project_title"] || gettext("e.g. Welcome to My Site")}
phx-debounce="300"
/>
<p class="text-xs text-base-content/50 mt-2">
{gettext(
"Shown when a page doesn't set its own title. Leave empty to use the project title."
)}
</p>
</div>
</div>
</div>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
{gettext(
"The icon appears in browser tabs, bookmarks, and on phone home screens."
)}
</span>
</div>
</div>
<.section_header icon="hero-globe-alt" title={gettext("Site Address")} />
<%!-- Site Address URL Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("Site Address (URL)")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Public base URL of this application")}
</span>
</label>
<input
name="settings[site_url]"
type="url"
value={@settings["site_url"]}
class="input input-bordered w-full focus:input-primary"
placeholder="https://site.com"
phx-debounce="500"
/>
<.unsaved_hint
dirty={@settings["site_url"] != @saved_settings["site_url"]}
saved={
if @saved_settings["site_url"] != "",
do: @saved_settings["site_url"],
else: gettext("(empty)")
}
/>
</div>
<.section_header icon="hero-adjustments-horizontal" title={gettext("Features")} />
<%!-- Notifications --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("Notifications")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Per-user inbox driven by the activity feed")}
</span>
</label>
<.checkbox
name="settings[notifications_enabled]"
checked={@settings["notifications_enabled"] == "true"}
label={gettext("Enable user notifications")}
>
<:description>
{gettext(
"When on, users receive a bell notification whenever an activity targets them (e.g., their post is liked)."
)}
</:description>
</.checkbox>
<%!-- Default notification destination (catch-all click target) --%>
<label class="label pt-3">
<span class="label-text">{gettext("Default notification link")}</span>
</label>
<input
type="text"
name="settings[notification_default_link]"
value={@settings["notification_default_link"] || "/dashboard"}
placeholder="/dashboard"
class="input input-bordered input-sm w-full max-w-md"
/>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
{gettext(
"Where a notification opens when it has no link of its own. Defaults to /dashboard (every signed-in user can reach it). Clear the field to make such notifications non-clickable instead. The URL prefix and locale are added automatically."
)}
</span>
</div>
</div>
<.section_header icon="hero-clock" title={gettext("Date & Time")} />
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2">
<%!-- Week Start Day Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Week Start On")}</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("First day of the week")}
</span>
</label>
<label class="select w-full focus:select-primary">
<select name="settings[week_start_day]">
<%= for {label, value} <- @setting_options["week_start_day"] do %>
<option value={value} selected={value == @settings["week_start_day"]}>
{label}
</option>
<% end %>
</select>
</label>
<.unsaved_hint
dirty={@settings["week_start_day"] != @saved_settings["week_start_day"]}
saved={
get_option_label(
@saved_settings["week_start_day"],
@setting_options["week_start_day"]
)
}
/>
</div>
<%!-- Time Zone Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Time Zone")}</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("System default timezone")}
</span>
</label>
<label class="select w-full focus:select-primary">
<select name="settings[time_zone]">
<%= for {label, value} <- @setting_options["time_zone"] do %>
<option value={value} selected={value == @settings["time_zone"]}>
{label}
</option>
<% end %>
</select>
</label>
<.unsaved_hint
dirty={@settings["time_zone"] != @saved_settings["time_zone"]}
saved={get_timezone_label(@saved_settings["time_zone"], @setting_options)}
/>
</div>
<%!-- Date Format Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Date Format")}</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("How dates are displayed")}
</span>
</label>
<label class="select w-full focus:select-primary">
<select name="settings[date_format]">
<%= for {label, value} <- @setting_options["date_format"] do %>
<option value={value} selected={value == @settings["date_format"]}>
{label}
</option>
<% end %>
</select>
</label>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
{gettext("Example:")} {get_current_date_example(@settings["date_format"])}
</span>
</div>
<.unsaved_hint
dirty={@settings["date_format"] != @saved_settings["date_format"]}
saved={
get_option_label(
@saved_settings["date_format"],
@setting_options["date_format"]
)
}
/>
</div>
<%!-- Time Format Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">{gettext("Time Format")}</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("How times are displayed")}
</span>
</label>
<label class="select w-full focus:select-primary">
<select name="settings[time_format]">
<%= for {label, value} <- @setting_options["time_format"] do %>
<option value={value} selected={value == @settings["time_format"]}>
{label}
</option>
<% end %>
</select>
</label>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
{gettext("Example:")} {get_current_time_example(@settings["time_format"])}
</span>
</div>
<.unsaved_hint
dirty={@settings["time_format"] != @saved_settings["time_format"]}
saved={
get_option_label(
@saved_settings["time_format"],
@setting_options["time_format"]
)
}
/>
</div>
</div>
<%!-- Action Buttons --%>
<div class="flex gap-3 pt-4">
<button
type="submit"
class={[
"btn btn-primary flex-1",
if(@saving, do: "loading", else: "")
]}
disabled={@saving}
>
<%= if @saving do %>
{gettext("Saving...")}
<% else %>
{gettext("Save All Settings")}
<% end %>
</button>
<button
type="button"
class="btn btn-outline"
phx-click="reset_to_defaults"
data-confirm={
gettext(
"Reset ALL settings to their defaults? Your title, logo, formats and feature toggles will be replaced. This cannot be undone."
)
}
>
<PhoenixKitWeb.Components.Core.Icons.icon_refresh class="w-4 h-4 mr-2" /> {gettext(
"Reset"
)}
</button>
</div>
</.form>
<%!-- Footnote --%>
<div class="flex items-start gap-2 mt-6 text-xs text-base-content/50">
<.icon name="hero-information-circle" class="w-4 h-4 shrink-0 mt-0.5" />
<p>
{gettext(
"Changes apply system-wide after \"Save All Settings\". Registration and user defaults are managed on the Users page."
)}
</p>
</div>
</div>
</div>
</div>
<.live_component
:if={@show_media_selector}
module={PhoenixKitWeb.Live.Components.MediaSelectorModal}
id="settings-media-selector"
show={@show_media_selector}
mode={:single}
selected_uuids={[]}
phoenix_kit_current_user={assigns[:phoenix_kit_current_user]}
/>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>