Skip to content

Commit a4164f8

Browse files
committed
wip
1 parent 9cfa371 commit a4164f8

File tree

10 files changed

+809
-12
lines changed

10 files changed

+809
-12
lines changed

lib/algora/bounties/bounties.ex

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ defmodule Algora.Bounties do
5151
amount: Money.t(),
5252
ticket: Ticket.t(),
5353
visibility: Bounty.visibility(),
54-
shared_with: [String.t()]
54+
shared_with: [String.t()],
55+
hours_per_week: integer() | nil
5556
}) ::
5657
{:ok, Bounty.t()} | {:error, atom()}
5758
defp do_create_bounty(%{creator: creator, owner: owner, amount: amount, ticket: ticket} = params) do
@@ -62,7 +63,8 @@ defmodule Algora.Bounties do
6263
owner_id: owner.id,
6364
creator_id: creator.id,
6465
visibility: params[:visibility] || owner.bounty_mode,
65-
shared_with: params[:shared_with] || []
66+
shared_with: params[:shared_with] || [],
67+
hours_per_week: params[:hours_per_week]
6668
})
6769

6870
changeset
@@ -109,7 +111,8 @@ defmodule Algora.Bounties do
109111
command_id: integer(),
110112
command_source: :ticket | :comment,
111113
visibility: Bounty.visibility() | nil,
112-
shared_with: [String.t()] | nil
114+
shared_with: [String.t()] | nil,
115+
hours_per_week: integer() | nil
113116
]
114117
) ::
115118
{:ok, Bounty.t()} | {:error, atom()}
@@ -140,7 +143,8 @@ defmodule Algora.Bounties do
140143
amount: amount,
141144
ticket: ticket,
142145
visibility: opts[:visibility],
143-
shared_with: shared_with
146+
shared_with: shared_with,
147+
hours_per_week: opts[:hours_per_week]
144148
})
145149

146150
:set ->
@@ -190,7 +194,8 @@ defmodule Algora.Bounties do
190194
opts :: [
191195
strategy: strategy(),
192196
visibility: Bounty.visibility() | nil,
193-
shared_with: [String.t()] | nil
197+
shared_with: [String.t()] | nil,
198+
hours_per_week: integer() | nil
194199
]
195200
) ::
196201
{:ok, Bounty.t()} | {:error, atom()}
@@ -209,7 +214,8 @@ defmodule Algora.Bounties do
209214
amount: amount,
210215
ticket: ticket,
211216
visibility: opts[:visibility],
212-
shared_with: shared_with
217+
shared_with: shared_with,
218+
hours_per_week: opts[:hours_per_week]
213219
}),
214220
{:ok, _job} <- notify_bounty(%{owner: owner, bounty: bounty}) do
215221
broadcast()

lib/algora/bounties/schemas/bounty.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule Algora.Bounties.Bounty do
1515
field :visibility, Ecto.Enum, values: [:community, :exclusive, :public], null: false, default: :community
1616
field :shared_with, {:array, :string}, null: false, default: []
1717
field :deadline, :utc_datetime_usec
18+
field :hours_per_week, :integer
1819

1920
belongs_to :ticket, Algora.Workspace.Ticket
2021
belongs_to :owner, User
@@ -33,7 +34,7 @@ defmodule Algora.Bounties.Bounty do
3334

3435
def changeset(bounty, attrs) do
3536
bounty
36-
|> cast(attrs, [:amount, :ticket_id, :owner_id, :creator_id, :visibility, :shared_with])
37+
|> cast(attrs, [:amount, :ticket_id, :owner_id, :creator_id, :visibility, :shared_with, :hours_per_week])
3738
|> validate_required([:amount, :ticket_id, :owner_id, :creator_id])
3839
|> generate_id()
3940
|> foreign_key_constraint(:ticket)

lib/algora/organizations/schemas/member.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ defmodule Algora.Organizations.Member do
3636
end
3737

3838
def can_create_bounty?(role), do: role in [:admin, :mod]
39+
40+
def can_create_contract?(role), do: role in [:admin, :mod]
3941
end

lib/algora/shared/validations.ex

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,33 @@ defmodule Algora.Validations do
4040
end
4141
end
4242

43+
def validate_github_handle(changeset, field, embed_field \\ nil) do
44+
case get_change(changeset, field) do
45+
handle when not is_nil(handle) ->
46+
# Check if user is already embedded with matching provider_login
47+
existing_user = embed_field && get_field(changeset, embed_field)
48+
49+
if existing_user && existing_user.provider_login == handle do
50+
changeset
51+
else
52+
case Algora.Workspace.ensure_user(Algora.Admin.token!(), handle) do
53+
{:ok, user} ->
54+
if embed_field do
55+
put_embed(changeset, embed_field, user)
56+
else
57+
changeset
58+
end
59+
60+
{:error, error, _, _, _, _} ->
61+
add_error(changeset, field, error)
62+
end
63+
end
64+
65+
_ ->
66+
changeset
67+
end
68+
end
69+
4370
def validate_date_in_future(changeset, field) do
4471
validate_change(changeset, field, fn _, date ->
4572
if date && Date.before?(date, DateTime.utc_now()) do

lib/algora_web/components/layouts/user.html.heex

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,35 @@
135135
</ul>
136136
<% end %>
137137
</nav>
138+
<%= if main_contract_form = Map.get(assigns, :main_contract_form) do %>
139+
<div class="mt-auto mx-auto">
140+
<.button
141+
phx-click="open_main_contract_form"
142+
class="h-9 w-9 rounded-md flex items-center justify-center relative"
143+
>
144+
<.icon name="tabler-user-dollar" class="h-6 w-6 shrink-0" />
145+
<.icon
146+
name="tabler-plus"
147+
class="h-[0.8rem] w-[0.8rem] shrink-0 absolute bottom-[0.2rem] right-[0.2rem]"
148+
/>
149+
</.button>
150+
<.drawer
151+
show={@main_contract_form_open?}
152+
direction="right"
153+
on_cancel="close_main_contract_form"
154+
>
155+
<.drawer_header>
156+
<.drawer_title>Create new contract</.drawer_title>
157+
<.drawer_description>
158+
Engage a developer for ongoing work
159+
</.drawer_description>
160+
</.drawer_header>
161+
<.drawer_content class="mt-4">
162+
<AlgoraWeb.Forms.ContractForm.contract_form form={main_contract_form} />
163+
</.drawer_content>
164+
</.drawer>
165+
</div>
166+
<% end %>
138167
<%= if main_bounty_form = Map.get(assigns, :main_bounty_form) do %>
139168
<div class="mt-auto mx-auto">
140169
<.button
@@ -237,6 +266,26 @@
237266
</div>
238267
</.link>
239268
<%= if @current_user do %>
269+
<.button
270+
phx-click="open_main_contract_form"
271+
class="h-9 w-9 rounded-md flex items-center justify-center relative"
272+
>
273+
<.icon name="tabler-user-dollar" class="h-6 w-6 shrink-0" />
274+
<%!-- <.icon
275+
name="tabler-plus"
276+
class="h-[0.8rem] w-[0.8rem] shrink-0 absolute bottom-[0.2rem] right-[0.2rem]"
277+
/> --%>
278+
</.button>
279+
<.button
280+
phx-click="open_main_bounty_form"
281+
class="h-9 w-9 rounded-md flex items-center justify-center relative"
282+
>
283+
<.icon name="tabler-diamond" class="h-6 w-6 shrink-0" />
284+
<.icon
285+
name="tabler-plus"
286+
class="h-[0.8rem] w-[0.8rem] shrink-0 absolute bottom-[0.2rem] right-[0.2rem]"
287+
/>
288+
</.button>
240289
<%!-- {live_render(@socket, AlgoraWeb.Activity.UserNavTimelineLive,
241290
id: "activity-timeline",
242291
session: %{},
Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,122 @@
11
defmodule AlgoraWeb.Forms.ContractForm do
22
@moduledoc false
33
use Ecto.Schema
4+
use AlgoraWeb, :html
45

56
import Ecto.Changeset
67

8+
alias Algora.Accounts.User
79
alias Algora.Types.USD
810
alias Algora.Validations
911

1012
embedded_schema do
11-
field :hourly_rate, USD
13+
field :amount, USD
1214
field :hours_per_week, :integer
15+
field :type, Ecto.Enum, values: [:fixed, :hourly], default: :fixed
16+
field :title, :string
17+
field :description, :string
18+
field :contractor_handle, :string
19+
20+
embeds_one :contractor, User
21+
end
22+
23+
def type_options do
24+
[{"Fixed", :fixed}, {"Hourly", :hourly}]
1325
end
1426

1527
def changeset(form, attrs) do
1628
form
17-
|> cast(attrs, [:hourly_rate, :hours_per_week])
18-
|> validate_required([:hourly_rate, :hours_per_week])
19-
|> Validations.validate_money_positive(:hourly_rate)
20-
|> validate_number(:hours_per_week, greater_than: 0, less_than_or_equal_to: 40)
29+
|> cast(attrs, [:amount, :hours_per_week, :type, :title, :description, :contractor_handle])
30+
|> validate_required([:contractor_handle])
31+
|> validate_type_fields()
32+
|> Validations.validate_money_positive(:amount)
33+
|> Validations.validate_github_handle(:contractor_handle, :contractor)
34+
end
35+
36+
defp validate_type_fields(changeset) do
37+
case get_field(changeset, :type) do
38+
:hourly -> validate_required(changeset, [:amount, :hours_per_week])
39+
_ -> validate_required(changeset, [:amount])
40+
end
41+
end
42+
43+
def contract_form(assigns) do
44+
~H"""
45+
<.form
46+
id="main-contract-form"
47+
for={@form}
48+
phx-submit="create_contract_main"
49+
phx-change="validate_contract_main"
50+
>
51+
<div class="space-y-4">
52+
<div class="grid grid-cols-2 gap-4" phx-update="ignore" id="main-contract-form-tabs">
53+
<%= for {label, value} <- type_options() do %>
54+
<label class={[
55+
"group relative flex cursor-pointer rounded-lg px-3 py-2 shadow-sm focus:outline-none",
56+
"border-2 bg-background transition-all duration-200 hover:border-primary hover:bg-primary/10",
57+
"border-border has-[:checked]:border-primary has-[:checked]:bg-primary/10"
58+
]}>
59+
<.input
60+
id={"main-contract-form-type-#{value}"}
61+
type="radio"
62+
field={@form[:type]}
63+
checked={@form[:type].value == value}
64+
value={value}
65+
class="sr-only"
66+
phx-click={
67+
%JS{}
68+
|> JS.hide(to: "#main-contract-form [data-tab]:not([data-tab=#{value}])")
69+
|> JS.show(to: "#main-contract-form [data-tab=#{value}]")
70+
}
71+
/>
72+
<span class="flex flex-1 items-center justify-between">
73+
<span class="text-sm font-medium">{label}</span>
74+
<.icon
75+
name="tabler-check"
76+
class="invisible size-5 text-primary group-has-[:checked]:visible"
77+
/>
78+
</span>
79+
</label>
80+
<% end %>
81+
</div>
82+
83+
<.input label="Title" field={@form[:title]} placeholder="Brief description of the contract" />
84+
<.input
85+
label="Description (optional)"
86+
field={@form[:description]}
87+
type="textarea"
88+
placeholder="Requirements and acceptance criteria"
89+
/>
90+
<.input label="Amount" icon="tabler-currency-dollar" field={@form[:amount]} />
91+
92+
<div data-tab="hourly" class="hidden">
93+
<.input label="Hours per week" field={@form[:hours_per_week]} />
94+
</div>
95+
96+
<div class="relative">
97+
<.input
98+
label="GitHub handle"
99+
field={@form[:contractor_handle]}
100+
phx-debounce="500"
101+
class="pl-10"
102+
/>
103+
<div class="pointer-events-none absolute left-0 top-9 flex items-center justify-center pl-3 h-7 w-7">
104+
<.avatar :if={get_field(@form.source, :contractor)} class="h-7 w-7">
105+
<.avatar_image src={get_field(@form.source, :contractor).avatar_url} />
106+
</.avatar>
107+
<.icon name="github" class="h-7 w-7 text-muted-foreground" />
108+
</div>
109+
</div>
110+
</div>
111+
<div class="pt-4 ml-auto flex gap-4">
112+
<.button variant="secondary" phx-click="close_share_drawer" type="button">
113+
Cancel
114+
</.button>
115+
<.button type="submit">
116+
Share Contract <.icon name="tabler-arrow-right" class="-mr-1 ml-2 h-4 w-4" />
117+
</.button>
118+
</div>
119+
</.form>
120+
"""
21121
end
22122
end

0 commit comments

Comments
 (0)