Skip to content

Commit a630041

Browse files
committed
adding new contract flow
1 parent 690152b commit a630041

File tree

8 files changed

+243
-85
lines changed

8 files changed

+243
-85
lines changed

lib/algora/bounties/bounties.ex

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ defmodule Algora.Bounties do
5252
ticket: Ticket.t(),
5353
visibility: Bounty.visibility(),
5454
shared_with: [String.t()],
55-
hours_per_week: integer() | nil
55+
hours_per_week: integer() | nil,
56+
hourly_rate: Money.t() | nil
5657
}) ::
5758
{:ok, Bounty.t()} | {:error, atom()}
5859
defp do_create_bounty(%{creator: creator, owner: owner, amount: amount, ticket: ticket} = params) do
@@ -64,7 +65,8 @@ defmodule Algora.Bounties do
6465
creator_id: creator.id,
6566
visibility: params[:visibility] || owner.bounty_mode,
6667
shared_with: params[:shared_with] || [],
67-
hours_per_week: params[:hours_per_week]
68+
hours_per_week: params[:hours_per_week],
69+
hourly_rate: params[:hourly_rate]
6870
})
6971

7072
changeset
@@ -112,6 +114,7 @@ defmodule Algora.Bounties do
112114
command_source: :ticket | :comment,
113115
visibility: Bounty.visibility() | nil,
114116
shared_with: [String.t()] | nil,
117+
hourly_rate: Money.t() | nil,
115118
hours_per_week: integer() | nil
116119
]
117120
) ::
@@ -144,6 +147,7 @@ defmodule Algora.Bounties do
144147
ticket: ticket,
145148
visibility: opts[:visibility],
146149
shared_with: shared_with,
150+
hourly_rate: opts[:hourly_rate],
147151
hours_per_week: opts[:hours_per_week]
148152
})
149153

@@ -195,7 +199,8 @@ defmodule Algora.Bounties do
195199
strategy: strategy(),
196200
visibility: Bounty.visibility() | nil,
197201
shared_with: [String.t()] | nil,
198-
hours_per_week: integer() | nil
202+
hours_per_week: integer() | nil,
203+
hourly_rate: Money.t() | nil
199204
]
200205
) ::
201206
{:ok, Bounty.t()} | {:error, atom()}
@@ -215,7 +220,8 @@ defmodule Algora.Bounties do
215220
ticket: ticket,
216221
visibility: opts[:visibility],
217222
shared_with: shared_with,
218-
hours_per_week: opts[:hours_per_week]
223+
hours_per_week: opts[:hours_per_week],
224+
hourly_rate: opts[:hourly_rate]
219225
}),
220226
{:ok, _job} <- notify_bounty(%{owner: owner, bounty: bounty}) do
221227
broadcast()

lib/algora/bounties/schemas/bounty.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ defmodule Algora.Bounties.Bounty do
44

55
alias Algora.Accounts.User
66
alias Algora.Bounties.Bounty
7+
alias Algora.Types.Money
78

89
@type visibility :: :community | :exclusive | :public
910

1011
typed_schema "bounties" do
11-
field :amount, Algora.Types.Money
12+
field :amount, Money
1213
field :status, Ecto.Enum, values: [:open, :cancelled, :paid]
1314
field :number, :integer, default: 0
1415
field :autopay_disabled, :boolean, default: false
1516
field :visibility, Ecto.Enum, values: [:community, :exclusive, :public], null: false, default: :community
1617
field :shared_with, {:array, :string}, null: false, default: []
1718
field :deadline, :utc_datetime_usec
1819
field :hours_per_week, :integer
20+
field :hourly_rate, Money
1921

2022
belongs_to :ticket, Algora.Workspace.Ticket
2123
belongs_to :owner, User
@@ -34,7 +36,7 @@ defmodule Algora.Bounties.Bounty do
3436

3537
def changeset(bounty, attrs) do
3638
bounty
37-
|> cast(attrs, [:amount, :ticket_id, :owner_id, :creator_id, :visibility, :shared_with, :hours_per_week])
39+
|> cast(attrs, [:amount, :ticket_id, :owner_id, :creator_id, :visibility, :shared_with, :hours_per_week, :hourly_rate])
3840
|> validate_required([:amount, :ticket_id, :owner_id, :creator_id])
3941
|> generate_id()
4042
|> foreign_key_constraint(:ticket)

lib/algora/shared/types/usd.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ defmodule Algora.Types.USD do
1717
end
1818
end
1919

20+
def cast(money) when is_struct(money, Money) do
21+
{:ok, money}
22+
end
23+
2024
def cast(_), do: :error
2125

2226
@impl true

lib/algora_web/forms/contract_form.ex

Lines changed: 149 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule AlgoraWeb.Forms.ContractForm do
1313
field :amount, USD
1414
field :hourly_rate, USD
1515
field :hours_per_week, :integer
16+
field :marketplace?, :boolean, default: false
1617
field :type, Ecto.Enum, values: [:fixed, :hourly], default: :fixed
1718
field :title, :string
1819
field :description, :string
@@ -56,68 +57,160 @@ defmodule AlgoraWeb.Forms.ContractForm do
5657
phx-change="validate_contract_main"
5758
>
5859
<div class="space-y-4">
60+
<%= if contractor = get_field(@form.source, :contractor) do %>
61+
<.card>
62+
<.card_content>
63+
<div class="flex items-center gap-4">
64+
<.avatar class="h-16 w-16 rounded-full">
65+
<.avatar_image src={contractor.avatar_url} alt={contractor.name} />
66+
<.avatar_fallback class="rounded-lg">
67+
{Algora.Util.initials(contractor.name)}
68+
</.avatar_fallback>
69+
</.avatar>
70+
71+
<div>
72+
<div class="flex items-center gap-1 text-base text-foreground">
73+
<span class="font-semibold">{contractor.name}</span>
74+
{Algora.Misc.CountryEmojis.get(contractor.country)}
75+
</div>
76+
77+
<div
78+
:if={contractor.provider_meta}
79+
class="pt-0.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground sm:text-sm"
80+
>
81+
<.link
82+
:if={contractor.provider_login}
83+
href={"https://github.com/#{contractor.provider_login}"}
84+
target="_blank"
85+
class="flex items-center gap-1 hover:underline"
86+
>
87+
<.icon name="github" class="h-4 w-4" />
88+
<span class="whitespace-nowrap">{contractor.provider_login}</span>
89+
</.link>
90+
<.link
91+
:if={contractor.provider_meta["twitter_handle"]}
92+
href={"https://x.com/#{contractor.provider_meta["twitter_handle"]}"}
93+
target="_blank"
94+
class="flex items-center gap-1 hover:underline"
95+
>
96+
<.icon name="tabler-brand-x" class="h-4 w-4" />
97+
<span class="whitespace-nowrap">
98+
{contractor.provider_meta["twitter_handle"]}
99+
</span>
100+
</.link>
101+
<div :if={contractor.provider_meta["location"]} class="flex items-center gap-1">
102+
<.icon name="tabler-map-pin" class="h-4 w-4" />
103+
<span class="whitespace-nowrap">
104+
{contractor.provider_meta["location"]}
105+
</span>
106+
</div>
107+
<div :if={contractor.provider_meta["company"]} class="flex items-center gap-1">
108+
<.icon name="tabler-building" class="h-4 w-4" />
109+
<span class="whitespace-nowrap">
110+
{contractor.provider_meta["company"] |> String.trim_leading("@")}
111+
</span>
112+
</div>
113+
</div>
114+
</div>
115+
</div>
116+
<div class="pt-6 flex flex-wrap gap-2 line-clamp-1">
117+
<%= for tech <- contractor.tech_stack do %>
118+
<div class="rounded-lg bg-foreground/5 px-2 py-1 text-xs font-medium text-foreground ring-1 ring-inset ring-foreground/25">
119+
{tech}
120+
</div>
121+
<% end %>
122+
</div>
123+
</.card_content>
124+
</.card>
125+
<% end %>
126+
59127
<.input label="Title" field={@form[:title]} />
60128
<.input label="Description (optional)" field={@form[:description]} type="textarea" />
61-
<div>
62-
<label class="block text-sm font-semibold leading-6 text-foreground mb-2">
63-
Payment
64-
</label>
65-
<div class="grid grid-cols-2 gap-4" phx-update="ignore" id="main-contract-form-tabs">
66-
<%= for {label, value} <- type_options() do %>
67-
<label class={[
68-
"group relative flex cursor-pointer rounded-lg px-3 py-2 shadow-sm focus:outline-none",
69-
"border-2 bg-background transition-all duration-200 hover:border-primary hover:bg-primary/10",
70-
"border-border has-[:checked]:border-primary has-[:checked]:bg-primary/10"
71-
]}>
72-
<.input
73-
id={"main-contract-form-type-#{value}"}
74-
type="radio"
75-
field={@form[:type]}
76-
checked={@form[:type].value == value}
77-
value={value}
78-
class="sr-only"
79-
phx-click={
80-
%JS{}
81-
|> JS.hide(to: "#main-contract-form [data-tab]:not([data-tab=#{value}])")
82-
|> JS.show(to: "#main-contract-form [data-tab=#{value}]")
83-
}
84-
/>
85-
<span class="flex flex-1 items-center justify-between">
86-
<span class="text-sm font-medium">{label}</span>
87-
<.icon
88-
name="tabler-check"
89-
class="invisible size-5 text-primary group-has-[:checked]:visible"
129+
130+
<%= if not get_field(@form.source, :marketplace?) do %>
131+
<div>
132+
<label class="block text-sm font-semibold leading-6 text-foreground mb-2">
133+
Payment
134+
</label>
135+
<div class="grid grid-cols-2 gap-4" phx-update="ignore" id="main-contract-form-tabs">
136+
<%= for {label, value} <- type_options() do %>
137+
<label class={[
138+
"group relative flex cursor-pointer rounded-lg px-3 py-2 shadow-sm focus:outline-none",
139+
"border-2 bg-background transition-all duration-200 hover:border-primary hover:bg-primary/10",
140+
"border-border has-[:checked]:border-primary has-[:checked]:bg-primary/10"
141+
]}>
142+
<.input
143+
id={"main-contract-form-type-#{value}"}
144+
type="radio"
145+
field={@form[:type]}
146+
checked={@form[:type].value == value}
147+
value={value}
148+
class="sr-only"
149+
phx-click={
150+
%JS{}
151+
|> JS.hide(to: "#main-contract-form [data-tab]:not([data-tab=#{value}])")
152+
|> JS.show(to: "#main-contract-form [data-tab=#{value}]")
153+
}
90154
/>
91-
</span>
92-
</label>
93-
<% end %>
155+
<span class="flex flex-1 items-center justify-between">
156+
<span class="text-sm font-medium">{label}</span>
157+
<.icon
158+
name="tabler-check"
159+
class="invisible size-5 text-primary group-has-[:checked]:visible"
160+
/>
161+
</span>
162+
</label>
163+
<% end %>
164+
</div>
94165
</div>
95-
</div>
96-
97-
<div data-tab="fixed">
98-
<.input label="Amount" icon="tabler-currency-dollar" field={@form[:amount]} />
99-
</div>
100-
<div data-tab="hourly" class="hidden">
101-
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
102-
<.input label="Hourly rate" icon="tabler-currency-dollar" field={@form[:hourly_rate]} />
103-
<.input label="Hours per week" field={@form[:hours_per_week]} />
166+
167+
<div data-tab="fixed">
168+
<.input label="Amount" icon="tabler-currency-dollar" field={@form[:amount]} />
169+
</div>
170+
<div data-tab="hourly" class="hidden">
171+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
172+
<.input label="Hourly rate" icon="tabler-currency-dollar" field={@form[:hourly_rate]} />
173+
<.input label="Hours per week" field={@form[:hours_per_week]} />
174+
</div>
104175
</div>
105-
</div>
106-
107-
<div class="relative">
108-
<.input
109-
label="GitHub handle"
110-
field={@form[:contractor_handle]}
111-
phx-debounce="500"
112-
class="pl-10"
113-
/>
114-
<div class="pointer-events-none absolute left-0 top-9 flex items-center justify-center pl-3 h-7 w-7">
115-
<.avatar :if={get_field(@form.source, :contractor)} class="h-7 w-7">
116-
<.avatar_image src={get_field(@form.source, :contractor).avatar_url} />
117-
</.avatar>
118-
<.icon name="github" class="h-7 w-7 text-muted-foreground" />
176+
<div class="relative">
177+
<.input
178+
label="GitHub handle"
179+
field={@form[:contractor_handle]}
180+
phx-debounce="500"
181+
class="pl-10"
182+
/>
183+
<div class="pointer-events-none absolute left-0 top-9 flex items-center justify-center pl-3 h-7 w-7">
184+
<.avatar :if={get_field(@form.source, :contractor)} class="h-7 w-7">
185+
<.avatar_image src={get_field(@form.source, :contractor).avatar_url} />
186+
</.avatar>
187+
<.icon name="github" class="h-7 w-7 text-muted-foreground" />
188+
</div>
119189
</div>
120-
</div>
190+
<% end %>
191+
192+
<%= if get_field(@form.source, :marketplace?) do %>
193+
<.input type="hidden" field={@form[:amount]} />
194+
<.input type="hidden" field={@form[:hourly_rate]} />
195+
<.input type="hidden" field={@form[:hours_per_week]} />
196+
<.input type="hidden" field={@form[:contractor_handle]} />
197+
198+
<dl class="space-y-4">
199+
<div class="flex justify-between">
200+
<dt class="text-foreground">
201+
Total amount for
202+
<span class="font-semibold">{get_change(@form.source, :hours_per_week)}</span>
203+
hours
204+
<div class="text-xs text-muted-foreground">
205+
(includes all platform and payment processing fees)
206+
</div>
207+
</dt>
208+
<dd class="font-display font-semibold tabular-nums text-lg">
209+
{Money.to_string!(Money.mult!(get_change(@form.source, :amount), Decimal.new("1.13")))}
210+
</dd>
211+
</div>
212+
</dl>
213+
<% end %>
121214
</div>
122215
<div class="pt-4 ml-auto flex gap-4">
123216
<.button variant="secondary" phx-click="close_share_drawer" type="button">

lib/algora_web/live/contract_live.ex

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -278,20 +278,6 @@ defmodule AlgoraWeb.ContractLive do
278278
</div>
279279
</div>
280280
</div>
281-
282-
<div class="flex flex-col gap-4">
283-
<div class="font-display tabular-nums text-5xl text-success-400 font-bold">
284-
{Money.to_string!(@bounty.amount)}<span
285-
:if={@bounty.hours_per_week && @bounty.hours_per_week > 0}
286-
class="text-base"
287-
>
288-
/hr
289-
</span>
290-
</div>
291-
<.button :if={@can_create_bounty} phx-click="reward">
292-
Pay
293-
</.button>
294-
</div>
295281
</div>
296282
</.card_content>
297283
</.card>
@@ -307,6 +293,51 @@ defmodule AlgoraWeb.ContractLive do
307293
</div>
308294
</.card_content>
309295
</.card>
296+
<.card :if={length(@transactions) == 0}>
297+
<.card_header>
298+
<.card_title>
299+
Finalize offer
300+
</.card_title>
301+
</.card_header>
302+
<.card_content class="pt-0">
303+
<div class="flex flex-col xl:flex-row xl:justify-between gap-4">
304+
<ul class="space-y-2">
305+
<li class="flex items-center gap-2">
306+
<.icon name="tabler-circle-number-1" class="size-8 text-success-400" />
307+
Authorize the payment to share the contract offer with {@contractor.name}
308+
</li>
309+
<li class="flex items-center gap-2">
310+
<.icon name="tabler-circle-number-2" class="size-8 text-success-400" />
311+
When {@contractor.name} accepts, you will be charged {Money.to_string!(
312+
@bounty.amount
313+
)} into escrow
314+
</li>
315+
<li class="flex items-center gap-2">
316+
<.icon name="tabler-circle-number-3" class="size-8 text-success-400" />
317+
At the end of the week, release or withhold the funds based on {@contractor.name}'s performance
318+
</li>
319+
</ul>
320+
321+
<dl class="-mt-12 space-y-4">
322+
<dd class="font-display tabular-nums text-5xl text-success-400 font-bold">
323+
{Money.to_string!(Money.mult!(@bounty.amount, Decimal.new("1.13")))}
324+
</dd>
325+
<div class="flex justify-between">
326+
<dt class="text-foreground">
327+
Total amount for <span class="font-semibold">{@bounty.hours_per_week}</span>
328+
hours
329+
<div class="text-xs text-muted-foreground">
330+
(includes all platform and payment processing fees)
331+
</div>
332+
</dt>
333+
</div>
334+
<.button :if={@can_create_bounty} phx-click="reward">
335+
Authorize
336+
</.button>
337+
</dl>
338+
</div>
339+
</.card_content>
340+
</.card>
310341
<.card :if={length(@transactions) > 0}>
311342
<.card_header>
312343
<.card_title>

0 commit comments

Comments
 (0)