diff --git a/assets/css/app.css b/assets/css/app.css index 5767450e..bb4fd916 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -24,5 +24,9 @@ input:-webkit-autofill:active { @font-face { font-family: "Terminal"; - src: url("/fonts/TerminalGrotesque.ttf") format("truetype"); + src: url("/fonts/Terminal/TerminalGrotesque.ttf") format("truetype"); +} +@font-face { + font-family: "Inter-Regular"; + src: url("/fonts/Inter/Inter-Regular.ttf") format("truetype"); } \ No newline at end of file diff --git a/assets/css/components.css b/assets/css/components.css index df1e329d..3e31ca02 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -1,2 +1,3 @@ @import "components/avatar.css"; -@import "components/field.css"; \ No newline at end of file +@import "components/field.css"; +@import "components/dropdown.css"; \ No newline at end of file diff --git a/assets/css/components/dropdown.css b/assets/css/components/dropdown.css new file mode 100644 index 00000000..6f021975 --- /dev/null +++ b/assets/css/components/dropdown.css @@ -0,0 +1,35 @@ +/* Dropdown */ + +.safira-dropdown { + @apply relative inline-block text-left; +} +.safira-dropdown__chevron { + @apply w-5 h-5 ml-2 -mr-1 dark:text-gray-100; +} +.safira-dropdown__menu-items-wrapper { + @apply absolute z-30 w-56 mt-2 bg-white rounded-md shadow-lg dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none; +} +.safira-dropdown__menu-items-wrapper-placement--left { + @apply right-0 origin-top-right; +} +.safira-dropdown__menu-items-wrapper-placement--right { + @apply left-0 origin-top-left; +} +.safira-dropdown__menu-item { + @apply flex items-center self-start justify-start w-full gap-2 px-4 py-2 text-sm text-left text-gray-700 transition duration-150 ease-in-out dark:hover:bg-gray-700 dark:text-gray-300 dark:bg-gray-800 hover:bg-gray-100; +} +.safira-dropdown__trigger-button--no-label { + @apply flex items-center text-gray-400 rounded-full hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-blue-500; +} +.safira-dropdown__trigger-button--with-label { + @apply inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm dark:text-gray-300 dark:bg-gray-800 hover:dark:bg-gray-700 dark:border-gray-700 dark:focus:bg-gray-800 hover:bg-gray-50 focus:outline-none; +} +.safira-dropdown__trigger-button--with-label-and-trigger-element { + @apply align-middle; +} +.safira-dropdown__menu-item--disabled { + @apply text-gray-500 hover:bg-transparent; +} +.safira-dropdown__ellipsis { + @apply w-5 h-5; +} \ No newline at end of file diff --git a/assets/css/components/field.css b/assets/css/components/field.css index 2b49bb09..22a032cc 100644 --- a/assets/css/components/field.css +++ b/assets/css/components/field.css @@ -5,7 +5,7 @@ } .safira-checkbox { - @apply w-5 h-5 transition-all duration-150 ease-linear border-gray-300 rounded cursor-pointer text-primary-500 dark:bg-gray-800 dark:border-gray-600 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed focus:ring-primary-500; + @apply w-5 h-5 transition-all duration-150 ease-linear border-gray-300 rounded cursor-pointer text-secondary-500 dark:bg-gray-800 dark:border-gray-600 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed focus:ring-secondary-500; } .safira-checkbox-label { @@ -47,7 +47,7 @@ /* Text input */ .safira-text-input { - @apply block w-full border-gray-300 rounded-md shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500 sm:text-sm disabled:cursor-not-allowed dark:text-gray-300 focus:outline-none; + @apply block w-full border-gray-300 rounded-md shadow-sm focus:border-secondary-500 focus:ring-secondary-500 dark:border-gray-600 dark:focus:border-secondary-500 sm:text-sm disabled:cursor-not-allowed dark:text-gray-300 focus:outline-none; } /* Switch */ @@ -75,7 +75,7 @@ /* Radio */ .safira-radio { - @apply w-4 h-4 border-gray-300 cursor-pointer text-primary-600 focus:ring-primary-500 dark:border-gray-600; + @apply w-4 h-4 border-gray-300 cursor-pointer text-secondary-600 focus:ring-secondary-500 dark:border-gray-600; } .safira-radio-label { @@ -111,19 +111,19 @@ /* Color */ .safira-color-input { - @apply border-gray-300 cursor-pointer focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500; + @apply border-gray-300 cursor-pointer focus:border-secondary-500 focus:ring-secondary-500 dark:border-gray-600 dark:focus:border-secondary-500; } /* File */ .safira-file-input { - @apply text-sm rounded-md cursor-pointer focus:outline-none file:border-0 text-slate-500 file:text-primary-700 file:font-semibold file:px-4 file:py-2 file:mr-6 file:rounded-md hover:file:bg-primary-100 file:bg-primary-200 dark:file:bg-primary-300 hover:dark:file:bg-primary-200; + @apply text-sm rounded-md cursor-pointer focus:outline-none file:border-0 text-slate-500 file:text-secondary-700 file:font-semibold file:px-4 file:py-2 file:mr-6 file:rounded-md hover:file:bg-secondary-100 file:bg-secondary-200 dark:file:bg-secondary-300 hover:dark:file:bg-secondary-200; } /* Range */ .safira-range-input { - @apply w-full border-gray-300 cursor-pointer focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:focus:border-primary-500; + @apply w-full border-gray-300 cursor-pointer focus:border-secondary-500 focus:ring-secondary-500 dark:border-gray-600 dark:focus:border-secondary-500; } /* Text */ diff --git a/assets/js/app.js b/assets/js/app.js index dc5601ed..560d0a82 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -41,7 +41,7 @@ let liveSocket = new LiveSocket("/live", Socket, { }) // Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) +topbar.config({barColors: {0: "#ffdb0d"}, 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()) diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 30fee28b..24eebd0b 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -4,7 +4,7 @@ const path = require("path") const colors = require("tailwindcss/colors"); module.exports = { - //darkMode: "selector", + darkMode: "selector", content: [ "./js/**/*.js", "../lib/safira_web.ex", @@ -13,9 +13,8 @@ module.exports = { theme: { extend: { colors: { - primary: colors.blue, - primaryDark: "#04041C", - accent: "#ff800d", + primary: "#04041C", + accent: "#ffdb0d", light: "#ffffff", lightMuted: "#a1a1aa", lightShade: "#e5e7eb", @@ -30,8 +29,24 @@ module.exports = { gray: colors.gray }, fontFamily: { - terminal: ["Terminal"] - } + terminal: ["Terminal"], + iregular: ["Inter-Regular"] + }, + animation: { + "slide-in": "slide-in 1.5s ease-in-out", + "fade-in": "fade-in 0.5s ease-in-out", + "fade-in-slow": "fade-in 1.5s ease-in-out" + }, + keyframes: { + "slide-in": { + "0%": { transform: "translateY(20%)", opacity: 0}, + "100%": { transform: "translateY(0)", opacity: 1}, + }, + "fade-in": { + "0%": { opacity: 0}, + "100%": { opacity: 1}, + } + }, }, }, plugins: [ @@ -86,6 +101,39 @@ module.exports = { } } }, {values}) + }), + + // Embeds FontAwesome icons (https://fontawesome.com/) into app.css bundle + plugin(function ({ matchComponents, theme }) { + let iconsDir = path.join(__dirname, "../deps/fontawesome/svgs") + let values = {} + let icons = [ + ["", "", "/regular"], + ["-solid", "", "/solid"], + ["", "brand-", "/brands"] + ] + icons.forEach(([suffix, prefix, dir]) => { + fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { + let name = prefix + path.basename(file, ".svg") + suffix + values[name] = { name, fullPath: path.join(iconsDir, dir, file) } + }) + }) + matchComponents({ + "fa": ({ name, fullPath }) => { + let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") + return { + [`--fa-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, + "-webkit-mask": `var(--fa-${name})`, + "mask": `var(--fa-${name})`, + "mask-repeat": "no-repeat", + "background-color": "currentColor", + "vertical-align": "middle", + "display": "inline-block", + "width": theme("spacing.5"), + "height": theme("spacing.5") + } + } + }, { values }) }) ] -} +} \ No newline at end of file diff --git a/config/config.exs b/config/config.exs index 34e890e1..c05ea83b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -27,7 +27,7 @@ config :safira, SafiraWeb.Endpoint, adapter: Bandit.PhoenixAdapter, render_errors: [ formats: [html: SafiraWeb.ErrorHTML, json: SafiraWeb.ErrorJSON], - layout: false + layout: {SafiraWeb.Layouts, :landing} ], pubsub_server: Safira.PubSub, live_view: [signing_salt: "TzWGKiXG"] diff --git a/lib/safira/accounts/roles/permissions.ex b/lib/safira/accounts/roles/permissions.ex index 92db3f58..12357ced 100644 --- a/lib/safira/accounts/roles/permissions.ex +++ b/lib/safira/accounts/roles/permissions.ex @@ -13,7 +13,7 @@ defmodule Safira.Accounts.Roles.Permissions do "purchases" => ["show", "redeem", "refund"], "badges" => ["show", "edit", "delete", "give", "revoke", "give_without_restrictions"], "minigames" => ["show", "edit", "simulate"], - "event" => ["show", "edit"], + "event" => ["show", "edit", "edit_faqs"], "spotlights" => ["edit"], "schedule" => ["edit"], "statistics" => ["show"], diff --git a/lib/safira/activities.ex b/lib/safira/activities.ex index 4828f52a..c2114418 100644 --- a/lib/safira/activities.ex +++ b/lib/safira/activities.ex @@ -42,6 +42,37 @@ defmodule Safira.Activities do |> Flop.validate_and_run(params, for: Activity) end + @doc """ + Returns the count of activities. + + ## Examples + + iex> get_activities_count() + 42 + + """ + def get_activities_count do + Activity + |> Repo.aggregate(:count, :id) + end + + @doc """ + Returns the list of daily activities. + + ## Examples + + iex> list_daily_activities(~D[2022-01-01]) + [%Activity{}, ...] + + """ + def list_daily_activities(day) do + Activity + |> where([a], a.date == ^day) + |> order_by([a], a.time_start) + |> preload([:speakers, :category]) + |> Repo.all() + end + @doc """ Gets a single activity. @@ -374,4 +405,20 @@ defmodule Safira.Activities do def change_speaker(%Speaker{} = speaker, attrs \\ %{}) do Speaker.changeset(speaker, attrs) end + + @doc """ + Returns the list of highlighted speakers. + + ## Examples + + iex> list_highlighted_speakers() + [%Speaker{}, ...] + + """ + def list_highlighted_speakers(opts \\ []) do + Speaker + |> apply_filters(opts) + |> where([s], s.highlighted) + |> Repo.all() + end end diff --git a/lib/safira/companies.ex b/lib/safira/companies.ex index ea3b21e2..90423a89 100644 --- a/lib/safira/companies.ex +++ b/lib/safira/companies.ex @@ -42,6 +42,20 @@ defmodule Safira.Companies do |> Flop.validate_and_run(params, for: Company) end + @doc """ + Returns the count of companies. + + ## Examples + + iex> get_companies_count() + 42 + + """ + def get_companies_count do + Company + |> Repo.aggregate(:count, :id) + end + @doc """ Gets a single company. @@ -248,4 +262,14 @@ defmodule Safira.Companies do def get_next_tier_priority do (Repo.aggregate(from(t in Tier), :max, :priority) || -1) + 1 end + + @doc """ + Returns the list of tiers with companies. + """ + def list_tiers_with_companies do + Tier + |> order_by(:priority) + |> preload(:companies) + |> Repo.all() + end end diff --git a/lib/safira/event.ex b/lib/safira/event.ex index 796d4fb9..cdb2b127 100644 --- a/lib/safira/event.ex +++ b/lib/safira/event.ex @@ -2,7 +2,10 @@ defmodule Safira.Event do @moduledoc """ The event context. """ + use Safira.Context + alias Safira.Constants + alias Safira.Event.Faq @pubsub Safira.PubSub @@ -144,4 +147,73 @@ defmodule Safira.Event do defp ensure_date(string) when is_binary(string), do: Date.from_iso8601!(string) defp ensure_date(date), do: date + + @doc """ + Gets a single FAQ. + + Raises `Ecto.NoResultsError` if the FAQ does not exist. + + ## Examples + + iex> get_faq!(123) + %Faq{} + + iex> get_faq!(456) + ** (Ecto.NoResultsError) + + """ + def get_faq!(id), do: Repo.get!(Faq, id) + + @doc """ + Returns the list of FAQs. + + ## Examples + + iex> list_faqs() + [%Faq{}, %Faq{}] + """ + def list_faqs do + Repo.all(Faq) + end + + @doc """ + Creates a new FAQ. + + ## Examples + + iex> create_faq(%{question: "Is SEI free?", answer: "Yes! SEI is completly free."}) + {:ok, %Faq{}} + """ + def create_faq(attrs \\ %{}) do + %Faq{} + |> Faq.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a FAQ. + + ## Examples + + iex> update_faq(faq, %{question: "Is SEI free?", answer: "Yes! SEI is completly free."}) + {:ok, %Faq{}} + """ + def update_faq(%Faq{} = faq, attrs) do + faq + |> Faq.changeset(attrs) + |> Repo.update() + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking FAQ changes. + + ## Examples + + iex> change_faq(faq) + %Ecto.Changeset{data: %Faq{}} + + """ + def change_faq(%Faq{} = faq, attrs \\ %{}) do + Faq.changeset(faq, attrs) + end end diff --git a/lib/safira/event/faq.ex b/lib/safira/event/faq.ex new file mode 100644 index 00000000..ca39950c --- /dev/null +++ b/lib/safira/event/faq.ex @@ -0,0 +1,22 @@ +defmodule Safira.Event.Faq do + @moduledoc """ + A frequently asked question. + """ + use Safira.Schema + + @required_fields ~w(answer question)a + + schema "faqs" do + field :answer, :string + field :question, :string + + timestamps() + end + + @doc false + def changeset(faq, attrs) do + faq + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/safira_web.ex b/lib/safira_web.ex index bf643546..8f9ad5ff 100644 --- a/lib/safira_web.ex +++ b/lib/safira_web.ex @@ -17,7 +17,7 @@ defmodule SafiraWeb do those modules here. """ - def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) + def static_paths, do: ~w(assets fonts images docs favicon.ico robots.txt) def router do quote do @@ -83,6 +83,13 @@ defmodule SafiraWeb do end end + def landing_view do + quote do + use Phoenix.LiveView + unquote(html_helpers()) + end + end + def live_component do quote do use Phoenix.LiveComponent diff --git a/lib/safira_web/components/avatar.ex b/lib/safira_web/components/avatar.ex index 8dfec01c..fc3b7615 100644 --- a/lib/safira_web/components/avatar.ex +++ b/lib/safira_web/components/avatar.ex @@ -16,27 +16,9 @@ defmodule SafiraWeb.Components.Avatar do default: :user, doc: "The type of entity associated with the avatar." - attr :color, :atom, - default: :light_gray, - values: [ - :primary, - :secondary, - :info, - :success, - :warning, - :danger, - :gray, - :light_gray, - :pure_white, - :white, - :light, - :dark - ], - doc: "Avatar color." - attr :class, :string, default: nil, doc: "Additional classes to be added to the avatar." - attr :handle, :string, default: nil, doc: "The handle of the user." + attr :handle, :string, doc: "The handle of the user." def avatar(assigns) do ~H""" diff --git a/lib/safira_web/components/button.ex b/lib/safira_web/components/button.ex index 104e1734..f59ab1dc 100644 --- a/lib/safira_web/components/button.ex +++ b/lib/safira_web/components/button.ex @@ -8,6 +8,7 @@ defmodule SafiraWeb.Components.Button do attr :subtitle, :string, default: "" attr :disabled, :boolean, default: false attr :class, :string, default: "" + attr :title_class, :string, default: "" attr :rest, :global, include: @@ -17,11 +18,11 @@ defmodule SafiraWeb.Components.Button do def action_button(assigns) do ~H""" """ diff --git a/lib/safira_web/components/core_components.ex b/lib/safira_web/components/core_components.ex index 804d1318..97aab6f2 100644 --- a/lib/safira_web/components/core_components.ex +++ b/lib/safira_web/components/core_components.ex @@ -500,22 +500,7 @@ defmodule SafiraWeb.CoreComponents do end @doc """ - Renders a [Heroicon](https://heroicons.com). - - Heroicons come in three styles – outline, solid, and mini. - By default, the outline style is used, but solid and mini may - be applied by using the `-solid` and `-mini` suffix. - - You can customize the size and colors of the icons by setting - width, height, and background color classes. - - Icons are extracted from the `deps/heroicons` directory and bundled within - your compiled app.css by the plugin in your `assets/tailwind.config.js`. - - ## Examples - - <.icon name="hero-x-mark-solid" /> - <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + Renders an icon. """ attr :name, :string, required: true attr :class, :string, default: nil @@ -526,6 +511,12 @@ defmodule SafiraWeb.CoreComponents do """ end + def icon(%{name: "fa-" <> _} = assigns) do + ~H""" + + """ + end + ## JS Commands def show(js \\ %JS{}, selector) do diff --git a/lib/safira_web/components/dropdown.ex b/lib/safira_web/components/dropdown.ex new file mode 100644 index 00000000..d2ccdc9b --- /dev/null +++ b/lib/safira_web/components/dropdown.ex @@ -0,0 +1,165 @@ +defmodule SafiraWeb.Components.Dropdown do + @moduledoc """ + A dropdown component. + """ + use Phoenix.Component + + alias Phoenix.LiveView.JS + alias SafiraWeb.Components.Link + import SafiraWeb.CoreComponents + + @transition_in_base "transition transform ease-out duration-100" + @transition_in_start "transform opacity-0 scale-95" + @transition_in_end "transform opacity-100 scale-100" + + @transition_out_base "transition ease-in duration-75" + @transition_out_start "transform opacity-100 scale-100" + @transition_out_end "transform opacity-0 scale-95" + + attr :options_container_id, :string + attr :label, :string, default: nil, doc: "Labels your dropdown option" + attr :class, :any, default: nil, doc: "Any extra CSS class for the parent container" + + attr :trigger_class, :string, + default: nil, + doc: "Additional classes for the trigger button" + + attr :menu_items_wrapper_class, :any, + default: nil, + doc: "Any extra CSS class for menu item wrapper container" + + attr :placement, :string, default: "left", values: ["left", "right"] + attr :rest, :global + + slot :trigger_element + slot :inner_block, required: false + + @doc """ + <.dropdown label="Dropdown"> + <.dropdown_menu_item link_type="button"> + <.icon name="hero-home" class="w-5 h-5 text-gray-500" /> + Button item with icon + + <.dropdown_menu_item link_type="a" to="/" label="a item" /> + <.dropdown_menu_item link_type="a" to="/" disabled label="disabled item" /> + <.dropdown_menu_item link_type="live_patch" to="/" label="Live Patch item" /> + <.dropdown_menu_item link_type="live_redirect" to="/" label="Live Redirect item" /> + + """ + def dropdown(assigns) do + assigns = + assigns + |> assign_new(:options_container_id, fn -> "dropdown_#{Ecto.UUID.generate()}" end) + + ~H""" +