diff --git a/.env.dev.sample b/.env.dev.sample index 745cd4f..610a4bf 100644 --- a/.env.dev.sample +++ b/.env.dev.sample @@ -1,7 +1 @@ -DB_USERNAME=postgres -DB_PASSWORD=postgres -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=gallium_dev -HOST_URL=http://localhost:4000 -ASSET_HOST=http://localhost:4000 +DB_USERNAME=postgres \ No newline at end of file diff --git a/.gitignore b/.gitignore index e1c8b1f..b6e05d3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ gallium-*.tar # In case you use Node.js/npm, you want to ignore these. npm-debug.log /assets/node_modules/ - +.env.dev diff --git a/assets/css/app.css b/assets/css/app.css index 5b4ee36..7ed0ebb 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -105,71 +105,76 @@ /* This file is for your main application CSS */ @font-face { - font-family: 'Amarante'; - src: url('/fonts/Amarante/Amarante-Regular.ttf') format('truetype'); + font-family: 'Roby Soho'; + src: url('/fonts/RobySoho-Regular.ttf') format('truetype'); font-weight: 400; + font-style: normal; font-display: swap; } @font-face { - font-family: 'Cormorant Garamond'; - src: url('/fonts/Cormorant/CormorantGaramond-VariableFont_wght.ttf') format('truetype'); + font-family: 'Instrument Sans'; + src: url('/fonts/InstrumentSans-Variable.ttf') format('truetype'); font-weight: 100 900; + font-style: normal; font-display: swap; } -@font-face { - font-family: "Glacial Indifference"; - src: url("/fonts/Glacial/GlacialIndifference-Regular.otf") - format("truetype"); +@theme { + --font-amarante: "Roby Soho", serif; + --font-cormorant: "Instrument Sans", sans-serif; + --font-glacial: "Instrument Sans", sans-serif; + + --color-light-muted: #EFC7C2; + --color-beige: #E7D8B5; + + --color-blue: #39448B; + --color-blue-50: #F1F1F9; + --color-blue-100: #DBDCF0; + --color-blue-200: #B7BBE1; + --color-blue-300: #9397D2; + --color-blue-400: #6F74C3; + --color-blue-500: #5D67BC; + --color-blue-700: #2A3065; + --color-blue-800: #1E2348; + --color-blue-900: #111429; + --color-blue-950: #0A0B19; + + --color-bronze: #7C0B2B; + --color-bronze-50: #FDEDF1; + --color-bronze-100: #F5D1DA; + --color-bronze-200: #EBA3B5; + --color-bronze-300: #E07590; + --color-bronze-400: #D6476B; + --color-bronze-500: #D31249; + --color-bronze-700: #54071D; + --color-bronze-800: #3A0514; + --color-bronze-900: #20030B; + --color-bronze-950: #140207; + + --color-olive: #45503B; + --color-olive-50: #F5F6F3; + --color-olive-100: #E8EAE5; + --color-olive-200: #CDD3C8; + --color-olive-300: #B1BCAA; + --color-olive-400: #95A58B; + --color-olive-500: #7E936C; + --color-olive-700: #384130; + --color-olive-800: #262C21; + --color-olive-900: #141710; + --color-olive-950: #0B0E0A; + + --color-golden: #B88B4A; +} + +/* Roby Soho — títulos apenas, Regular, line-height 100% */ +.font-amarante { + line-height: 1; font-weight: 400; - font-display: swap; -} +} -@theme { - --font-amarante: "Amarante", serif; - --font-cormorant: "Cormorant Garamond", serif; - --font-glacial: "Glacial Indifference", serif; - - --color-light-muted: #EEEEE5; - --color-beige: #F5F3E8; - - --color-blue: #467486; - --color-blue-50: #eef4f6; - --color-blue-100: #dee9ed; - --color-blue-200: #bcd3dc; - --color-blue-300: #9bbdca; - --color-blue-400: #79a7b9; - --color-blue-500: #5891a7; - --color-blue-700: #355764; - --color-blue-800: #233a43; - --color-blue-900: #121d21; - --color-blue-950: #0c1417; - - --color-bronze: #a86824; - --color-bronze-50: #fbf3ea; - --color-bronze-100: #f6e6d5; - --color-bronze-200: #edcdab; - --color-bronze-300: #e4b481; - --color-bronze-400: #db9b57; - --color-bronze-500: #d2822d; - --color-bronze-700: #7e4e1b; - --color-bronze-800: #543412; - --color-bronze-900: #2a1a09; - --color-bronze-950: #1d1206; - - --color-olive: #7b8a42; - --color-olive-50: #f5f7ee; - --color-olive-100: #ebeedd; - --color-olive-200: #d7deba; - --color-olive-300: #c3cd98; - --color-olive-400: #aebd75; - --color-olive-500: #9aac53; - --color-olive-700: #5d6732; - --color-olive-800: #3e4521; - --color-olive-900: #1f2211; - --color-olive-950: #16180c; - - --color-golden: #cc8235; - --color-green: #a3af71; +/* Instrument Sans — corpo/cabeçalhos, line-height ~137% */ +.font-cormorant, +.font-glacial { + line-height: 1.37; } diff --git a/config/dev.exs b/config/dev.exs index 17747d0..383fae9 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,5 +1,20 @@ import Config +# Auto-load .env.dev variables so they're available via System.get_env/1 below +if File.exists?(".env.dev") do + File.read!(".env.dev") + |> String.split("\n", trim: true) + |> Enum.each(fn line -> + line = String.replace_prefix(line, "export ", "") + + case String.split(line, "=", parts: 2) do + ["#" <> _, _] -> :ok + [key, value] when byte_size(key) > 0 -> System.put_env(String.trim(key), String.trim(value)) + _ -> :ok + end + end) +end + # Configure your database config :gallium, Gallium.Repo, username: "postgres", @@ -90,3 +105,8 @@ config :phoenix_live_view, # Disable swoosh api client as it is only required for production adapters. config :swoosh, :api_client, false + +config :gallium, :midas, + api_key: System.get_env("GALLIUM_API_KEY") || System.get_env("PEARL_API_KEY", "dev_key"), + midas_api_url: System.get_env("MIDAS_API_URL"), + midas_api_key: System.get_env("MIDAS_API_KEY") diff --git a/config/prod.exs b/config/prod.exs index fd76330..9c611ba 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -17,12 +17,27 @@ config :gallium, GalliumWeb.Endpoint, hosts: ["localhost", "127.0.0.1"] ] +config :ex_aws, + json_codec: Jason, + access_key_id: {:system, "AWS_ACCESS_KEY_ID"}, + secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"}, + region: {:system, "AWS_REGION"}, + s3: [ + scheme: "https://", + host: {:system, "ASSET_HOST"}, + region: {:system, "AWS_REGION"}, + access_key_id: {:system, "AWS_ACCESS_KEY_ID"}, + secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"} + ] + # Configure Swoosh API Client config :swoosh, api_client: Swoosh.ApiClient.Req # Disable Swoosh Local Memory Storage config :swoosh, local: false +config :gallium, Gallium.Mailer, adapter: Swoosh.Adapters.ExAwsAmazonSES + # Do not print debug messages in production config :logger, level: :info diff --git a/config/runtime.exs b/config/runtime.exs index 1171010..f1ea5d6 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -20,10 +20,25 @@ if System.get_env("PHX_SERVER") do config :gallium, GalliumWeb.Endpoint, server: true end +config :gallium, + from_email_name: System.get_env("FROM_EMAIL_NAME") || "Jantar de Gala", + from_email_address: System.get_env("FROM_EMAIL_ADDRESS") || "no-reply@cesium.pt" + config :gallium, GalliumWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))] if config_env() == :prod do + config :gallium, :midas, + api_key: + System.get_env("GALLIUM_API_KEY") || + raise("environment variable GALLIUM_API_KEY is missing."), + midas_api_url: + System.get_env("MIDAS_API_URL") || + raise("environment variable MIDAS_API_URL is missing."), + midas_api_key: + System.get_env("MIDAS_API_KEY") || + raise("environment variable MIDAS_API_KEY is missing.") + database_url = System.get_env("DATABASE_URL") || raise """ diff --git a/lib/gallium/accounts/user.ex b/lib/gallium/accounts/user.ex index 6f19efd..a40ff40 100644 --- a/lib/gallium/accounts/user.ex +++ b/lib/gallium/accounts/user.ex @@ -14,6 +14,7 @@ defmodule Gallium.Accounts.User do field :authenticated_at, :utc_datetime, virtual: true has_one :attendee, Gallium.Ticketing.Attendee + has_one :ticket, Gallium.Ticketing.Ticket timestamps(type: :utc_datetime) end @@ -39,8 +40,10 @@ defmodule Gallium.Accounts.User do changeset = changeset |> validate_required([:email]) - |> validate_format(:email, ~r/^[^@,;\s]+@[^@,;\s]+$/, - message: "must have the @ sign and no spaces" + |> validate_format( + :email, + ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/, + message: "formato inválido" ) |> validate_length(:email, max: 160) diff --git a/lib/gallium/accounts/user_notifier.ex b/lib/gallium/accounts/user_notifier.ex index 48b6ec6..70efb99 100644 --- a/lib/gallium/accounts/user_notifier.ex +++ b/lib/gallium/accounts/user_notifier.ex @@ -1,44 +1,67 @@ defmodule Gallium.Accounts.UserNotifier do @moduledoc """ - Handles authentication, session management, and route protection for users. + Module responsible for sending account-related emails to users. """ import Swoosh.Email alias Gallium.Accounts.User alias Gallium.Mailer + alias Gallium.Repo - # Delivers the email using the application mailer. - defp deliver(recipient, subject, body) do - email = - new() - |> to(recipient) - |> from({"Gallium", "contact@example.com"}) - |> subject(subject) - |> text_body(body) - - with {:ok, _metadata} <- Mailer.deliver(email) do - {:ok, email} - end + use Phoenix.Swoosh, view: GalliumWeb.EmailView + + defp base_html_email(recipient, subject) do + sender = {Mailer.get_sender_name(), Mailer.get_sender_address()} + + phx_host = + if System.get_env("PHX_HOST") != nil do + "https://" <> System.get_env("PHX_HOST") + else + "" + end + + new() + |> to(recipient) + |> from(sender) + |> subject("[#{elem(sender, 0)}] #{subject}") + |> assign(:phx_host, phx_host) end + defp user_full_name(%User{} = user) do + user + |> Repo.preload(:attendee) + |> attendee_full_name() + end + + defp attendee_full_name(%User{attendee: %{full_name: full_name}}) when is_binary(full_name) do + full_name + end + + defp attendee_full_name(%User{email: email}), do: email + @doc """ Deliver instructions to update a user email. """ def deliver_update_email_instructions(user, url) do - deliver(user.email, "Update email instructions", """ - - ============================== + user_name = user_full_name(user) - Hi #{user.email}, - - You can change your email by visiting the URL below: + email = + base_html_email(user.email, "Update your email address") + |> assign(:user_name, user_name) + |> assign(:confirm_email_link, url) + |> render_body("confirm_email.html") + |> text_body(""" + Olá #{user_name}, - #{url} + Confirma o teu email através do link abaixo: - If you didn't request this change, please ignore this. + #{url} + """) - ============================== - """) + case Mailer.deliver(email) do + {:ok, _metadata} -> {:ok, email} + {:error, reason} -> {:error, reason} + end end @doc """ @@ -52,36 +75,46 @@ defmodule Gallium.Accounts.UserNotifier do end defp deliver_magic_link_instructions(user, url) do - deliver(user.email, "Log in instructions", """ + user_name = user_full_name(user) - ============================== - - Hi #{user.email}, - - You can log into your account by visiting the URL below: + email = + base_html_email(user.email, "Log in to your account") + |> assign(:user_name, user_name) + |> assign(:magic_link, url) + |> render_body("magic_link.html") + |> text_body(""" + Olá #{user_name}, - #{url} + Usa o link abaixo para entrares na tua conta: - If you didn't request this email, please ignore this. + #{url} + """) - ============================== - """) + case Mailer.deliver(email) do + {:ok, _metadata} -> {:ok, email} + {:error, reason} -> {:error, reason} + end end defp deliver_confirmation_instructions(user, url) do - deliver(user.email, "Confirmation instructions", """ - - ============================== - - Hi #{user.email}, + user_name = user_full_name(user) - You can confirm your account by visiting the URL below: + email = + base_html_email(user.email, "Confirm your email") + |> assign(:user_name, user_name) + |> assign(:confirm_email_link, url) + |> render_body("confirm_email.html") + |> text_body(""" + Olá #{user_name}, - #{url} + Confirma o teu email através do link abaixo: - If you didn't create an account with us, please ignore this. + #{url} + """) - ============================== - """) + case Mailer.deliver(email) do + {:ok, _metadata} -> {:ok, email} + {:error, reason} -> {:error, reason} + end end end diff --git a/lib/gallium/mailer.ex b/lib/gallium/mailer.ex index a4aaf16..2c1b5fa 100644 --- a/lib/gallium/mailer.ex +++ b/lib/gallium/mailer.ex @@ -1,3 +1,11 @@ defmodule Gallium.Mailer do use Swoosh.Mailer, otp_app: :gallium + + def get_sender_name do + Application.get_env(:gallium, :from_email_name) + end + + def get_sender_address do + Application.get_env(:gallium, :from_email_address) + end end diff --git a/lib/gallium/ticketing.ex b/lib/gallium/ticketing.ex index 03cc65c..074d817 100644 --- a/lib/gallium/ticketing.ex +++ b/lib/gallium/ticketing.ex @@ -6,7 +6,19 @@ defmodule Gallium.Ticketing do import Ecto.Query, warn: false alias Gallium.Repo - alias Gallium.Ticketing.{Accompany, Attendee, Payment} + alias Gallium.Ticketing.{Accompany, Attendee, Payment, Ticket, TicketType} + + @pubsub Gallium.PubSub + + @doc """ + Returns all attendees with their payment and accompany preloaded. + """ + def list_attendees_with_details do + Attendee + |> order_by([a], desc: a.inserted_at) + |> Repo.all() + |> Repo.preload([:payment, :accompany, user: :ticket]) + end @doc """ Returns the list of accompanies. @@ -290,7 +302,11 @@ defmodule Gallium.Ticketing do Attendee.changeset(attendee, attrs) end - def process_ticket_purchase(form_data, amount_to_pay, has_accompany?, user_id) do + def get_attendee_by_user_id(user_id) do + Repo.get_by(Attendee, user_id: user_id) + end + + def create_booking(form_data, has_accompany?, user_id) do data = form_data |> Map.from_struct() @@ -298,15 +314,129 @@ defmodule Gallium.Ticketing do Ecto.Multi.new() |> Ecto.Multi.insert(:attendee, Attendee.changeset(%Attendee{}, data)) + |> Ecto.Multi.run(:ticket, fn repo, _changes -> + insert_ticket(repo, user_id, form_data) + end) |> Ecto.Multi.run(:accompany, fn repo, %{attendee: attendee} -> insert_accompany(repo, attendee, form_data, has_accompany?) end) - |> Ecto.Multi.run(:payment, fn repo, %{attendee: attendee} -> - insert_payment(repo, attendee, form_data, amount_to_pay) - end) |> Repo.transaction() end + def start_payment(:mbway, attendee, user, form_data, amount, has_accompany?) do + ticket_type_name = if form_data.is_cesium_member, do: "member", else: "non_member" + ticket_type = Repo.get_by!(TicketType, type: ticket_type_name) + + with :ok <- validate_midas_config(ticket_type) do + quantity = if has_accompany?, do: 2, else: 1 + nif = if form_data.nif in [nil, ""], do: nil, else: form_data.nif + + case Req.post(midas_api_url() <> "/orders", + headers: [{"authorization", "Bearer " <> midas_api_key()}], + json: %{ + order: %{ + lines: [ + %{ + product_id: ticket_type.product_id, + quantity: quantity, + discount: 0 + } + ], + customer_email: user.email, + customer_name: attendee.full_name, + customer_tax_id: nif, + payment: %{ + method: "mbway", + phone_number: form_data.mbway_number + }, + message: "CeSIUM - Midas 2026 Tickets", + extra_fields: %{attendee_id: attendee.id} + } + } + ) do + {:ok, %Req.Response{status: 201, body: body}} -> + to_string(body["id"]) + |> upsert_payment(attendee.id, amount) + |> broadcast_payment_order_update() + + {:ok, %Req.Response{status: status, body: body}} -> + {:error, %{status: status, body: body}} + + {:error, reason} -> + {:error, reason} + end + end + end + + defp validate_midas_config(ticket_type) do + cond do + not is_binary(midas_api_url()) -> + {:error, :missing_midas_api_url} + + is_nil(ticket_type.product_id) -> + {:error, :missing_ticket_product_id} + + not is_binary(midas_api_key()) or midas_api_key() == "" -> + {:error, :missing_midas_api_key} + + true -> + :ok + end + end + + def get_payment_by_order_id(order_id) do + Repo.get_by(Payment, order_id: order_id) + end + + def mark_payment_paid(order_id) do + case get_payment_by_order_id(order_id) do + nil -> + {:error, :not_found} + + payment -> + payment + |> Ecto.Changeset.change(status: :paid) + |> Repo.update() + |> broadcast_payment_order_update() + end + end + + def subscribe_to_payment_order_updates(order_id) do + Phoenix.PubSub.subscribe(@pubsub, "payment_order:#{order_id}") + end + + defp upsert_payment(order_id, attendee_id, amount) do + case Repo.get_by(Payment, attendee_id: attendee_id) do + nil -> + create_payment(%{ + attendee_id: attendee_id, + amount: amount, + order_id: order_id, + status: :pending + }) + + existing -> + existing + |> Ecto.Changeset.change(order_id: order_id, amount: amount, status: :pending) + |> Repo.update() + end + end + + defp broadcast_payment_order_update({:ok, %Payment{} = payment} = result) do + Phoenix.PubSub.broadcast( + @pubsub, + "payment_order:#{payment.order_id}", + {:payment_order_updated, payment} + ) + + result + end + + defp broadcast_payment_order_update({:error, _} = result), do: result + + defp midas_api_url, do: Application.fetch_env!(:gallium, :midas)[:midas_api_url] + defp midas_api_key, do: Application.fetch_env!(:gallium, :midas)[:midas_api_key] + defp insert_accompany(repo, attendee, %{accompany: accompany}, true) when not is_nil(accompany) do accompany_attrs = %{ @@ -322,15 +452,14 @@ defmodule Gallium.Ticketing do defp insert_accompany(_repo, _attendee, _form_data, true), do: {:error, :missing_accompany} defp insert_accompany(_repo, _attendee, _form_data, false), do: {:ok, nil} - defp insert_payment(repo, attendee, form_data, amount_to_pay) do - payment_attrs = %{ - attendee_id: attendee.id, - amount: amount_to_pay, - status: "paid", - mbway_phone: form_data.mbway_number, - order_id: "MOCK_" <> Ecto.UUID.generate() + defp insert_ticket(repo, user_id, form_data) do + ticket_type = if form_data.is_cesium_member, do: :member, else: :non_member + + ticket_attrs = %{ + user_id: user_id, + type: ticket_type } - repo.insert(Payment.changeset(%Payment{}, payment_attrs)) + repo.insert(Ticket.changeset(%Ticket{}, ticket_attrs)) end end diff --git a/lib/gallium/ticketing/attendee.ex b/lib/gallium/ticketing/attendee.ex index eccad54..34b420a 100644 --- a/lib/gallium/ticketing/attendee.ex +++ b/lib/gallium/ticketing/attendee.ex @@ -2,8 +2,8 @@ defmodule Gallium.Ticketing.Attendee do use Ecto.Schema import Ecto.Changeset - @required_fields ~w(full_name phone_number is_cesium_member user_id)a - @optional_fields ~w(student_number nif)a + @required_fields ~w(full_name phone_number is_cesium_member wants_transport user_id)a + @optional_fields ~w(student_number nif table_preference allergies)a @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @@ -13,6 +13,9 @@ defmodule Gallium.Ticketing.Attendee do field :student_number, :string field :nif, :string field :is_cesium_member, :boolean, default: false + field :wants_transport, :boolean, default: false + field :table_preference, :string + field :allergies, :string belongs_to :user, Gallium.Accounts.User, type: :binary_id has_one :accompany, Gallium.Ticketing.Accompany diff --git a/lib/gallium/ticketing/checkout_form.ex b/lib/gallium/ticketing/checkout_form.ex index fd28f8c..acac0a9 100644 --- a/lib/gallium/ticketing/checkout_form.ex +++ b/lib/gallium/ticketing/checkout_form.ex @@ -12,6 +12,9 @@ defmodule Gallium.Ticketing.CheckoutForm do field :nif, :string field :mbway_number, :string field :is_cesium_member, :boolean + field :wants_transport, :boolean, default: false + field :table_preference, :string + field :allergies, :string embeds_one :accompany, AccompanyForm, primary_key: false, on_replace: :delete do field :full_name, :string @@ -22,14 +25,23 @@ defmodule Gallium.Ticketing.CheckoutForm do def changeset_personal_data(schema, attrs \\ %{}) do schema - |> cast(attrs, [:full_name, :student_number, :phone_number, :nif, :is_cesium_member]) + |> cast(attrs, [ + :full_name, + :student_number, + :phone_number, + :nif, + :is_cesium_member, + :wants_transport, + :table_preference, + :allergies + ]) |> validate_required([:full_name, :student_number, :phone_number, :is_cesium_member], message: "Este campo é obrigatório" ) |> validate_length(:full_name, min: 3, message: "O nome tem de ter pelo menos 3 letras") |> validate_format(:phone_number, ~r/^\+?\d{9,15}$/, message: "Número de telefone inválido") - |> validate_format(:student_number, ~r/^(A|PG|a|pg)\d+$/, - message: "Formato inválido (ex: A12345)" + |> validate_format(:student_number, ~r/^(a\d{1,6}|pg\d{1,5}|e\d{1,6})$/i, + message: "Formato inválido (ex: A12345, PG1234, E12345)" ) |> validate_cesium_member(:student_number, :is_cesium_member) |> validate_format(:nif, ~r/^\d{9}$/, message: "O NIF tem de ter exatamente 9 números") diff --git a/lib/gallium/ticketing/payment.ex b/lib/gallium/ticketing/payment.ex index 1735eb9..c52fdaa 100644 --- a/lib/gallium/ticketing/payment.ex +++ b/lib/gallium/ticketing/payment.ex @@ -2,7 +2,7 @@ defmodule Gallium.Ticketing.Payment do use Ecto.Schema import Ecto.Changeset - @required_fields ~w(amount status order_id mbway_phone attendee_id)a + @required_fields ~w(amount status order_id attendee_id)a @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @@ -10,7 +10,6 @@ defmodule Gallium.Ticketing.Payment do field :amount, :decimal field :status, Ecto.Enum, values: [:pending, :paid, :failed], default: :pending field :order_id, :string - field :mbway_phone, :string belongs_to :attendee, Gallium.Ticketing.Attendee, type: :binary_id timestamps(type: :utc_datetime) end diff --git a/lib/gallium/ticketing/ticket.ex b/lib/gallium/ticketing/ticket.ex new file mode 100644 index 0000000..54acf3a --- /dev/null +++ b/lib/gallium/ticketing/ticket.ex @@ -0,0 +1,23 @@ +defmodule Gallium.Ticketing.Ticket do + use Ecto.Schema + import Ecto.Changeset + + @required_fields ~w(type user_id)a + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "tickets" do + field :type, Ecto.Enum, values: [:member, :non_member] + belongs_to :user, Gallium.Accounts.User, type: :binary_id + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(ticket, attrs) do + ticket + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :tickets_user_id_index) + end +end diff --git a/lib/gallium/ticketing/ticket_type.ex b/lib/gallium/ticketing/ticket_type.ex new file mode 100644 index 0000000..d9e6c3f --- /dev/null +++ b/lib/gallium/ticketing/ticket_type.ex @@ -0,0 +1,36 @@ +defmodule Gallium.Ticketing.TicketType do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + + alias Gallium.Repo + + @required_fields ~w(product_id price type)a + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + schema "ticket_types" do + field :product_id, :binary_id + field :price, :decimal + field :type, :string + + timestamps(type: :utc_datetime) + end + + def price_for_member do + Repo.one!(from t in __MODULE__, where: t.type == "member", select: t.price) + |> Decimal.to_integer() + end + + def price_for_non_member do + Repo.one!(from t in __MODULE__, where: t.type == "non_member", select: t.price) + |> Decimal.to_integer() + end + + @doc false + def changeset(ticket_type, attrs) do + ticket_type + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/gallium_web.ex b/lib/gallium_web.ex index 376c468..df78d84 100644 --- a/lib/gallium_web.ex +++ b/lib/gallium_web.ex @@ -57,6 +57,21 @@ defmodule GalliumWeb do end end + def view do + quote do + use Phoenix.View, + root: "lib/gallium_web/templates", + namespace: AresWeb + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [view_module: 1, view_template: 1] + + # Include shared imports and aliases for views + unquote(html_helpers()) + end + end + def live_component do quote do use Phoenix.LiveComponent diff --git a/lib/gallium_web/components/about_section.ex b/lib/gallium_web/components/about_section.ex index ed285e9..3577993 100644 --- a/lib/gallium_web/components/about_section.ex +++ b/lib/gallium_web/components/about_section.ex @@ -14,15 +14,15 @@ defmodule GalliumWeb.Components.AboutSection do def about_section(assigns) do ~H"""
-
- <.frame style={:style2} mode={:bottom} color={:olive} class="scale-75" /> +
+ <.frame style={:style2} mode={:bottom} color={:blue} class="scale-75 md:scale-100" />
-

+

{raw( @title |> String.replace("\\n", "\n") @@ -30,13 +30,13 @@ defmodule GalliumWeb.Components.AboutSection do )}

-

+

{@description}

-
- <.frame style={:style2} mode={:bottom} color={:olive} class="scale-75" /> +
+ <.frame style={:style2} mode={:bottom} color={:blue} class="scale-75 md:scale-100" />
""" diff --git a/lib/gallium_web/components/button.ex b/lib/gallium_web/components/button.ex index 51a72cd..e7ae848 100644 --- a/lib/gallium_web/components/button.ex +++ b/lib/gallium_web/components/button.ex @@ -2,7 +2,7 @@ defmodule GalliumWeb.Components.Button do @moduledoc """ Button Component. """ - use Phoenix.Component + use GalliumWeb, :component import GalliumWeb.CoreComponents attr :text, :string, doc: "Button text" @@ -38,6 +38,7 @@ defmodule GalliumWeb.Components.Button do doc: "Additional attributes for the button" attr :disabled, :boolean, default: false + attr :class, :string, default: nil def primary_button(%{link: nil} = assigns) do ~H""" @@ -47,9 +48,9 @@ defmodule GalliumWeb.Components.Button do def primary_button(assigns) do ~H""" - + <.link navigate={@link}> {button_content(assigns)} - + """ end @@ -58,14 +59,14 @@ defmodule GalliumWeb.Components.Button do