diff --git a/backend/assets/tailwind.config.js b/backend/assets/tailwind.config.js index 8fd2ef318..31ac4a661 100644 --- a/backend/assets/tailwind.config.js +++ b/backend/assets/tailwind.config.js @@ -14,7 +14,7 @@ module.exports = { ], safelist: [ { - pattern: /(bg|text)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-.*/, + pattern: /(bg|text|border|from|via|to)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-.*/, }, ], theme: { diff --git a/backend/config/config.exs b/backend/config/config.exs index 60c276a27..a3cacdabd 100644 --- a/backend/config/config.exs +++ b/backend/config/config.exs @@ -30,10 +30,10 @@ config :azimutt, free_plan_seats: 3, # MUST stay in sync with frontend/src/Conf.elm (`features`) free_plan_layouts: 3, - free_plan_memos: 5, + free_plan_memos: 3, free_plan_groups: 1, free_plan_colors: false, - free_plan_private_links: true, + free_plan_private_links: false, free_plan_sql_export: false, free_plan_db_analysis: false, free_plan_db_access: false, diff --git a/backend/lib/azimutt/organizations.ex b/backend/lib/azimutt/organizations.ex index 3ad2cb2a6..c64d2916b 100644 --- a/backend/lib/azimutt/organizations.ex +++ b/backend/lib/azimutt/organizations.ex @@ -14,6 +14,7 @@ defmodule Azimutt.Organizations do alias Azimutt.Projects.ProjectToken alias Azimutt.Repo alias Azimutt.Services.StripeSrv + alias Azimutt.Tracking alias Azimutt.Utils.Enumx alias Azimutt.Utils.Result @@ -329,7 +330,7 @@ defmodule Azimutt.Organizations do |> Repo.update() end - def get_organization_plan(%Organization{} = organization) do + def get_organization_plan(%Organization{} = organization, maybe_current_user) do plans = Azimutt.config(:instance_plans) || ["free"] cond do @@ -338,7 +339,7 @@ defmodule Azimutt.Organizations do organization.stripe_subscription_id && StripeSrv.stripe_configured?() -> stripe_plan(plans, organization.stripe_subscription_id) true -> default_plan(plans) end - |> Result.map(fn plan -> plan_overrides(plans, organization, plan) end) + |> Result.map(fn plan -> plan_overrides(plans, organization, plan, maybe_current_user) end) end defp clever_cloud_plan(plans, %CleverCloud.Resource{} = resource) do @@ -378,7 +379,7 @@ defmodule Azimutt.Organizations do end end - defp plan_overrides(plans, %Organization{} = organization, %OrganizationPlan{} = plan) do + defp plan_overrides(plans, %Organization{} = organization, %OrganizationPlan{} = plan, maybe_current_user) do if organization.data != nil && plans |> Enum.member?("pro") do plan |> override_layouts(organization.data) @@ -389,6 +390,7 @@ defmodule Azimutt.Organizations do else plan end + |> override_streak(maybe_current_user) end defp override_layouts(%OrganizationPlan{} = plan, %Organization.Data{} = data) do @@ -431,6 +433,22 @@ defmodule Azimutt.Organizations do end end + defp override_streak(%OrganizationPlan{} = plan, %User{} = maybe_current_user) do + # MUST stay sync with backend/lib/azimutt_web/templates/partials/_streak.html.heex + streak = Tracking.get_streak(maybe_current_user) |> Result.or_else(0) + plan = %{plan | streak: streak} + plan = if(streak >= 4, do: %{plan | colors: true}, else: plan) + plan = if(streak >= 6, do: %{plan | memos: nil}, else: plan) + plan = if(streak >= 10, do: %{plan | layouts: nil}, else: plan) + plan = if(streak >= 15, do: %{plan | groups: nil}, else: plan) + plan = if(streak >= 25, do: %{plan | sql_export: true}, else: plan) + plan = if(streak >= 40, do: %{plan | db_analysis: true}, else: plan) + plan = if(streak >= 60, do: %{plan | private_links: true}, else: plan) + plan + end + + defp override_streak(%OrganizationPlan{} = plan, maybe_current_user) when is_nil(maybe_current_user), do: plan + defp best_limit(a, b) do cond do a == nil || b == nil -> nil diff --git a/backend/lib/azimutt/organizations/organization_plan.ex b/backend/lib/azimutt/organizations/organization_plan.ex index 9a65e5024..f34b865ff 100644 --- a/backend/lib/azimutt/organizations/organization_plan.ex +++ b/backend/lib/azimutt/organizations/organization_plan.ex @@ -3,7 +3,7 @@ defmodule Azimutt.Organizations.OrganizationPlan do use TypedStruct alias Azimutt.Organizations.OrganizationPlan - # MUST stay in sync with frontend/src/Models/Plan.elm + # MUST stay in sync with frontend/ts-src/types/organization.ts & frontend/src/Models/Plan.elm typedstruct enforce: true do @derive Jason.Encoder field :id, atom() @@ -16,6 +16,7 @@ defmodule Azimutt.Organizations.OrganizationPlan do field :sql_export, boolean() field :db_analysis, boolean() field :db_access, boolean() + field :streak, integer() end def free do @@ -30,7 +31,8 @@ defmodule Azimutt.Organizations.OrganizationPlan do private_links: Azimutt.config(:free_plan_private_links), sql_export: Azimutt.config(:free_plan_sql_export), db_analysis: Azimutt.config(:free_plan_db_analysis), - db_access: Azimutt.config(:free_plan_db_access) + db_access: Azimutt.config(:free_plan_db_access), + streak: 0 } end @@ -45,7 +47,8 @@ defmodule Azimutt.Organizations.OrganizationPlan do private_links: true, sql_export: true, db_analysis: true, - db_access: true + db_access: true, + streak: 0 } end end diff --git a/backend/lib/azimutt/projects.ex b/backend/lib/azimutt/projects.ex index 9fc5fbecf..a0e322363 100644 --- a/backend/lib/azimutt/projects.ex +++ b/backend/lib/azimutt/projects.ex @@ -197,7 +197,7 @@ defmodule Azimutt.Projects do end) end - def access_project(project_id, token_id, now) do + def access_project(project_id, token_id, maybe_current_user, now) do ProjectToken |> where( [pt], @@ -210,14 +210,14 @@ defmodule Azimutt.Projects do |> where([p, _, _], p.id == ^project_id and p.storage_kind == :remote) |> Repo.one() |> Result.from_nillable() - |> Result.filter(fn p -> Organizations.get_organization_plan(p.organization) |> Result.exists(& &1.private_links) end, :not_found) + |> Result.filter(fn p -> Organizations.get_organization_plan(p.organization, maybe_current_user) |> Result.exists(& &1.private_links) end, :not_found) |> Result.tap(fn _ -> token |> ProjectToken.access_changeset(now) |> Repo.update() end) end) end def load_project(project_id, maybe_current_user, token_id, now) do if token_id do - access_project(project_id, token_id, now) + access_project(project_id, token_id, maybe_current_user, now) else get_project(project_id, maybe_current_user) end diff --git a/backend/lib/azimutt/tracking.ex b/backend/lib/azimutt/tracking.ex index 438d7cca2..7925688df 100644 --- a/backend/lib/azimutt/tracking.ex +++ b/backend/lib/azimutt/tracking.ex @@ -13,6 +13,27 @@ defmodule Azimutt.Tracking do alias Azimutt.Utils.Nil alias Azimutt.Utils.Result + def get_streak(%User{} = current_user) do + now = DateTime.utc_now() + months_ago = Timex.shift(now, months: -4) + + Event + |> where([e], e.created_by_id == ^current_user.id and e.created_at >= ^months_ago) + |> select([e], {fragment("to_char(?, 'yyyy-mm-dd')", e.created_at), count(e.id, :distinct)}) + |> group_by([e], fragment("to_char(?, 'yyyy-mm-dd')", e.created_at)) + |> Repo.all() + |> Result.from_nillable() + |> Result.map(fn res -> compute_streak(Map.new(res), now, 0) end) + end + + defp compute_streak(activity, now, streak) do + if Map.has_key?(activity, Date.to_string(now)) do + compute_streak(activity, Timex.shift(now, days: -1), streak + 1) + else + streak + end + end + def last_used_project(%User{} = current_user) do Event |> where( @@ -99,6 +120,9 @@ defmodule Azimutt.Tracking do def user_onboarding(%User{} = current_user, step, data), do: create_event("user_onboarding", user_data(current_user), data |> Map.put("step", step), current_user, nil, nil) + def organization_loaded(%User{} = current_user, %Organization{} = org), + do: create_event("organization_loaded", org_data(org), nil, current_user, org.id, nil) + def project_created(%User{} = current_user, %Project{} = project), do: create_event("project_created", project_data(project), nil, current_user, project.organization.id, project.id) diff --git a/backend/lib/azimutt_web/admin/organization/organization_controller.ex b/backend/lib/azimutt_web/admin/organization/organization_controller.ex index b7d45b5cc..bbec508fd 100644 --- a/backend/lib/azimutt_web/admin/organization/organization_controller.ex +++ b/backend/lib/azimutt_web/admin/organization/organization_controller.ex @@ -20,7 +20,7 @@ defmodule AzimuttWeb.Admin.OrganizationController do {:ok, start_stats} = "2022-11-01" |> Timex.parse("{YYYY}-{0M}-{0D}") with {:ok, %Organization{} = organization} <- Admin.get_organization(organization_id), - {:ok, %OrganizationPlan{} = plan} <- Organizations.get_organization_plan(organization) do + {:ok, %OrganizationPlan{} = plan} <- Organizations.get_organization_plan(organization, nil) do conn |> render("show.html", now: now, diff --git a/backend/lib/azimutt_web/controllers/api/organization_controller.ex b/backend/lib/azimutt_web/controllers/api/organization_controller.ex index 990031837..fdeb7adf0 100644 --- a/backend/lib/azimutt_web/controllers/api/organization_controller.ex +++ b/backend/lib/azimutt_web/controllers/api/organization_controller.ex @@ -13,7 +13,7 @@ defmodule AzimuttWeb.Api.OrganizationController do current_user = conn.assigns.current_user ctx = CtxParams.from_params(params) organizations = Organizations.list_organizations(current_user) - conn |> render("index.json", organizations: organizations, ctx: ctx) + conn |> render("index.json", organizations: organizations, current_user: current_user, ctx: ctx) end def table_colors(conn, %{"organization_organization_id" => organization_id, "tweet_url" => tweet_url}) do diff --git a/backend/lib/azimutt_web/controllers/api/project_controller.ex b/backend/lib/azimutt_web/controllers/api/project_controller.ex index cb9777bbe..96acdad88 100644 --- a/backend/lib/azimutt_web/controllers/api/project_controller.ex +++ b/backend/lib/azimutt_web/controllers/api/project_controller.ex @@ -23,7 +23,7 @@ defmodule AzimuttWeb.Api.ProjectController do current_user = conn.assigns.current_user with {:ok, %Organization{} = organization} <- Organizations.get_organization(organization_id, current_user), - do: conn |> render("index.json", projects: organization.projects) + do: conn |> render("index.json", projects: organization.projects, current_user: current_user) end def show(conn, %{"organization_id" => _organization_id, "project_id" => project_id} = params) do @@ -32,7 +32,7 @@ defmodule AzimuttWeb.Api.ProjectController do ctx = CtxParams.from_params(params) with {:ok, %Project{} = project} <- Projects.load_project(project_id, maybe_current_user, params["token"], now), - do: conn |> render("show.json", project: project, ctx: ctx) + do: conn |> render("show.json", project: project, maybe_current_user: maybe_current_user, ctx: ctx) end swagger_path :create do diff --git a/backend/lib/azimutt_web/controllers/organization_billing_controller.ex b/backend/lib/azimutt_web/controllers/organization_billing_controller.ex index e28b480a7..6a5b5c909 100644 --- a/backend/lib/azimutt_web/controllers/organization_billing_controller.ex +++ b/backend/lib/azimutt_web/controllers/organization_billing_controller.ex @@ -2,6 +2,7 @@ defmodule AzimuttWeb.OrganizationBillingController do use AzimuttWeb, :controller require Logger alias Azimutt.Accounts + alias Azimutt.Accounts.User alias Azimutt.CleverCloud alias Azimutt.Heroku alias Azimutt.Organizations @@ -28,42 +29,42 @@ defmodule AzimuttWeb.OrganizationBillingController do cond do organization.clever_cloud_resource -> conn |> redirect(external: CleverCloud.app_addons_url()) organization.heroku_resource -> conn |> redirect(external: Heroku.app_addons_url(organization.heroku_resource.app)) - organization.stripe_subscription_id -> conn |> stripe_subscription_view(organization) - true -> generate_billing_view(conn, "subscribe.html", organization, "You haven't got subscribe yet !") + organization.stripe_subscription_id -> conn |> stripe_subscription_view(organization, current_user) + true -> generate_billing_view(conn, "subscribe.html", organization, current_user, "You haven't got subscribe yet !") end end end - defp stripe_subscription_view(conn, %Organization{} = organization) do + defp stripe_subscription_view(conn, %Organization{} = organization, %User{} = current_user) do case Organizations.get_subscription_status(organization.stripe_subscription_id) do {:ok, :active} -> - generate_billing_view(conn, "billing.html", organization, "Your subscription is active !") + generate_billing_view(conn, "billing.html", organization, current_user, "Your subscription is active !") {:ok, :past_due} -> - generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription") + generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription") {:ok, :unpaid} -> - generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription") + generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription") {:ok, :canceled} -> - generate_billing_view(conn, "subscribe.html", organization, "Your subscription is canceled") + generate_billing_view(conn, "subscribe.html", organization, current_user, "Your subscription is canceled") {:ok, :incomplete} -> - generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription") + generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription") {:ok, :incomplete_expired} -> - generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription") + generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription") {:ok, :trialing} -> - generate_billing_view(conn, "billing.html", organization, "You are in free trial") + generate_billing_view(conn, "billing.html", organization, current_user, "You are in free trial") {:error, err} -> conn |> put_flash(:error, "Can't show view: #{err}.") |> redirect(to: Routes.organization_path(conn, :show, organization)) end end - defp generate_billing_view(conn, file, organization, message) do - with {:ok, plan} <- Organizations.get_organization_plan(organization) do + defp generate_billing_view(conn, file, organization, %User{} = current_user, message) do + with {:ok, plan} <- Organizations.get_organization_plan(organization, current_user) do conn |> put_view(AzimuttWeb.OrganizationView) |> render(file, organization: organization, plan: plan, message: message) diff --git a/backend/lib/azimutt_web/controllers/organization_controller.ex b/backend/lib/azimutt_web/controllers/organization_controller.ex index cb93471f8..afe44aba1 100644 --- a/backend/lib/azimutt_web/controllers/organization_controller.ex +++ b/backend/lib/azimutt_web/controllers/organization_controller.ex @@ -43,9 +43,10 @@ defmodule AzimuttWeb.OrganizationController do end with {:ok, organization} <- Organizations.get_organization(organization_id, current_user), + {:ok, _event} = Tracking.organization_loaded(current_user, organization), projects = Projects.list_projects(organization, current_user), {:ok, organization_events} = Tracking.recent_organization_events(organization), - {:ok, plan} <- Organizations.get_organization_plan(organization), + {:ok, plan} <- Organizations.get_organization_plan(organization, current_user), do: render(conn, "show.html", organization: organization, projects: projects, plan: plan, organization_events: organization_events) end @@ -58,7 +59,7 @@ defmodule AzimuttWeb.OrganizationController do end with {:ok, organization} <- Organizations.get_organization(organization_id, current_user), - {:ok, plan} <- Organizations.get_organization_plan(organization) do + {:ok, plan} <- Organizations.get_organization_plan(organization, current_user) do changeset = Organization.update_changeset(organization, %{}, current_user) render(conn, "edit.html", organization: organization, plan: plan, changeset: changeset) end @@ -75,7 +76,7 @@ defmodule AzimuttWeb.OrganizationController do |> redirect(to: Routes.organization_path(conn, :show, organization)) {:error, %Ecto.Changeset{} = changeset} -> - with {:ok, plan} <- Organizations.get_organization_plan(organization), + with {:ok, plan} <- Organizations.get_organization_plan(organization, current_user), do: render(conn, "edit.html", organization: organization, plan: plan, changeset: changeset) end end diff --git a/backend/lib/azimutt_web/controllers/organization_invitation_controller.ex b/backend/lib/azimutt_web/controllers/organization_invitation_controller.ex index b0e05f6bd..57fedd262 100644 --- a/backend/lib/azimutt_web/controllers/organization_invitation_controller.ex +++ b/backend/lib/azimutt_web/controllers/organization_invitation_controller.ex @@ -7,10 +7,11 @@ defmodule AzimuttWeb.OrganizationInvitationController do def show(conn, %{"invitation_id" => invitation_id}) do now = DateTime.utc_now() + current_user = conn.assigns.current_user # FIXME: remove `get_organization_plan` {:ok, invitation} = Organizations.get_organization_invitation(invitation_id) organization = invitation.organization - {:ok, plan} = Organizations.get_organization_plan(organization) + {:ok, plan} = Organizations.get_organization_plan(organization, current_user) render(conn, "show.html", now: now, diff --git a/backend/lib/azimutt_web/controllers/organization_member_controller.ex b/backend/lib/azimutt_web/controllers/organization_member_controller.ex index af5f05b4e..7651e1406 100644 --- a/backend/lib/azimutt_web/controllers/organization_member_controller.ex +++ b/backend/lib/azimutt_web/controllers/organization_member_controller.ex @@ -1,6 +1,7 @@ defmodule AzimuttWeb.OrganizationMemberController do use AzimuttWeb, :controller alias Azimutt.Accounts + alias Azimutt.Accounts.User alias Azimutt.Organizations alias Azimutt.Organizations.Organization alias Azimutt.Organizations.OrganizationInvitation @@ -20,7 +21,7 @@ defmodule AzimuttWeb.OrganizationMemberController do with {:ok, %Organization{} = organization} <- Organizations.get_organization(organization_id, current_user) do organization_invitation_changeset = OrganizationInvitation.create_changeset(%OrganizationInvitation{}, %{}, organization.id, current_user, now) - render_index(conn, organization, organization_invitation_changeset) + render_index(conn, organization, current_user, organization_invitation_changeset) end end @@ -45,7 +46,7 @@ defmodule AzimuttWeb.OrganizationMemberController do |> redirect(to: Routes.organization_member_path(conn, :index, invitation.organization_id)) {:error, %Ecto.Changeset{} = changeset} -> - render_index(conn, organization, changeset) + render_index(conn, organization, current_user, changeset) end end @@ -90,8 +91,8 @@ defmodule AzimuttWeb.OrganizationMemberController do end end - defp render_index(conn, organization, changeset) do - with {:ok, plan} <- Organizations.get_organization_plan(organization) do + defp render_index(conn, organization, %User{} = current_user, changeset) do + with {:ok, plan} <- Organizations.get_organization_plan(organization, current_user) do # FIXME: create a `Organizations.get_pending_invitations(organization.id)` organization_invitations = organization.invitations diff --git a/backend/lib/azimutt_web/templates/layout/_organization_right_bar.html.heex b/backend/lib/azimutt_web/templates/layout/_organization_right_bar.html.heex index 436736840..9158a89c5 100644 --- a/backend/lib/azimutt_web/templates/layout/_organization_right_bar.html.heex +++ b/backend/lib/azimutt_web/templates/layout/_organization_right_bar.html.heex @@ -1,24 +1,4 @@ -
- - <%= AzimuttWeb.OrganizationView.generate_html_event_description(event) %> - -
- -WoW, you are serious π€©
Contact us, we have a gift for you π₯³
Your current streak give you table colors, unlimited memos, layouts and groups, SQL exports, schema analysis and private links.
+ <% @value == 60 -> %>Well done! You just unlocked private links!
+ <% @value > 40 -> %>Your current streak give you table colors, unlimited memos, layouts and groups, SQL exports and schema analysis.
+ <% @value == 40 -> %>Well done! You just unlocked schema analysis!
+ <% @value > 25 -> %>Your current streak give you table colors, unlimited memos, layouts and groups and SQL exports.
+ <% @value == 25 -> %>Well done! You just unlocked SQL export!
+ <% @value > 15 -> %>Your current streak give you table colors and unlimited memos, layouts and groups.
+ <% @value == 15 -> %>Well done! You just unlocked unlimited groups!
+ <% @value > 10 -> %>Your current streak give you table colors, unlimited memos and layouts.
+ <% @value == 10 -> %>Well done! You just unlocked unlimited layouts!
+ <% @value > 6 -> %>Your current streak give you table colors and unlimited memos.
+ <% @value == 6 -> %>Well done! You just unlocked unlimited memos!
+ <% @value > 4 -> %>Your current streak give you table colors.
+ <% @value == 4 -> %>Well done! You just unlocked table colors!
+ <% true -> %> + <% end %> +