-
-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add InsiderBenefitsForm * Add component * Add migration * Progress * Sort things out better * Permit editing hide thingy * Pass down props * Disable form for non insiders * Remove unused import * Create an insiders settings page * Add new components * Add API endpoints * Improve code generation services * Simplify things * Refine things * Finalise affiliate form * Rename everything, add free coupon form * Add rendering tests * Add a WIP generate test * Stub affiliate generation in test * Restore schema * Fix missing cols from schema * Refine user changes hide adverts test * Tweaks for copy * Fix typo * Fix wrong prop * Adjust all tests * Add missing fields * Fix wrong method call --------- Co-authored-by: Jeremy Walker <jez.walker@gmail.com>
- Loading branch information
Showing
26 changed files
with
711 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
app/helpers/react_components/settings/bootcamp_affiliate_coupon_form.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module ReactComponents | ||
module Settings | ||
class BootcampAffiliateCouponForm < ReactComponent | ||
def to_s | ||
super("settings-bootcamp-affiliate-coupon-form", { | ||
insiders_status: current_user.insiders_status, | ||
bootcamp_affiliate_coupon_code: current_user.bootcamp_affiliate_coupon_code, | ||
links: { | ||
insiders_path: Exercism::Routes.insiders_path, | ||
bootcamp_affiliate_coupon_code: Exercism::Routes.bootcamp_affiliate_coupon_code_api_settings_user_preferences_url | ||
} | ||
}) | ||
end | ||
end | ||
end | ||
end |
15 changes: 15 additions & 0 deletions
15
app/helpers/react_components/settings/bootcamp_free_coupon_form.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module ReactComponents | ||
module Settings | ||
class BootcampFreeCouponForm < ReactComponent | ||
def to_s | ||
super("settings-bootcamp-free-coupon-form", { | ||
insiders_status: current_user.insiders_status, | ||
bootcamp_free_coupon_code: current_user.bootcamp_free_coupon_code, | ||
links: { | ||
bootcamp_free_coupon_code: Exercism::Routes.bootcamp_free_coupon_code_api_settings_user_preferences_url | ||
} | ||
}) | ||
end | ||
end | ||
end | ||
end |
18 changes: 18 additions & 0 deletions
18
app/helpers/react_components/settings/insider_benefits_form.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module ReactComponents | ||
module Settings | ||
class InsiderBenefitsForm < ReactComponent | ||
def to_s | ||
super("settings-insider-benefits-form", { | ||
preferences:, | ||
insiders_status: current_user.insiders_status, | ||
links: { | ||
update: Exercism::Routes.api_settings_user_preferences_url, | ||
insiders_path: Exercism::Routes.insiders_path | ||
} | ||
}) | ||
end | ||
|
||
def preferences = current_user.preferences.slice(:hide_website_adverts) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
app/javascript/components/settings/BootcampAffiliateCouponForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import React, { useState, useCallback } from 'react' | ||
import { fetchJSON } from '@/utils/fetch-json' | ||
import CopyToClipboardButton from '../common/CopyToClipboardButton' | ||
|
||
type Links = { | ||
bootcampAffiliateCouponCode: string | ||
insidersPath: string | ||
} | ||
|
||
export default function BootcampAffiliateCouponForm({ | ||
insidersStatus, | ||
bootcampAffiliateCouponCode, | ||
links, | ||
}: { | ||
insidersStatus: string | ||
bootcampAffiliateCouponCode: string | ||
links: Links | ||
}): JSX.Element { | ||
const [couponCode, setCouponCode] = useState(bootcampAffiliateCouponCode) | ||
const [error, setError] = useState<string | null>(null) | ||
const [loading, setLoading] = useState(false) | ||
|
||
const generateCouponCode = useCallback(async () => { | ||
setLoading(true) | ||
try { | ||
const data = await fetchJSON<{ couponCode: string }>( | ||
links.bootcampAffiliateCouponCode, | ||
{ | ||
method: 'POST', | ||
body: null, | ||
} | ||
) | ||
setCouponCode(data.couponCode) | ||
setError(null) | ||
} catch (err) { | ||
console.error('Error generating coupon code:', err) | ||
setError('Failed to generate coupon code. Please try again.') | ||
} finally { | ||
setLoading(false) | ||
} | ||
}, [links]) | ||
|
||
const isInsider = | ||
insidersStatus == 'active' || insidersStatus == 'active_lifetime' | ||
|
||
return ( | ||
<div> | ||
<h2 className="!mb-8">Bootcamp Affiliate Coupon</h2> | ||
<InfoMessage | ||
isInsider={isInsider} | ||
insidersStatus={insidersStatus} | ||
insidersPath={links.insidersPath} | ||
couponCode={couponCode} | ||
/> | ||
|
||
{couponCode ? ( | ||
<CopyToClipboardButton textToCopy={couponCode} /> | ||
) : ( | ||
<button | ||
id="generate-affiliate-coupon-code-button" | ||
onClick={generateCouponCode} | ||
disabled={!isInsider || loading} | ||
type="button" | ||
className="btn btn-primary" | ||
> | ||
{loading | ||
? 'Generating code...' | ||
: 'Generate your Affiliate Discount code'} | ||
</button> | ||
)} | ||
<ErrorMessage error={error} /> | ||
</div> | ||
) | ||
} | ||
|
||
export function InfoMessage({ | ||
insidersStatus, | ||
insidersPath, | ||
isInsider, | ||
couponCode, | ||
}: { | ||
insidersStatus: string | ||
insidersPath: string | ||
isInsider: boolean | ||
couponCode: string | ||
}): JSX.Element { | ||
if (isInsider) { | ||
return ( | ||
<> | ||
<p className="text-p-base mb-12"> | ||
To thank you for being an Insider and to help increase the amount of | ||
people signing up to Exercism's{' '} | ||
<a href="https://bootcamp.exercism/org">Learn to Code Bootcamp</a>, we | ||
are giving all Insiders an{' '} | ||
<strong className="font-semibold">Discount Affiliate code</strong>. | ||
</p> | ||
<p className="text-p-base mb-12"> | ||
This code gives a 20% discount for the bootcamp (on top of any | ||
geographical discount). And for everyone that signs up,{' '} | ||
<strong>we'll give you 20%</strong> of whatever they pay. | ||
</p> | ||
<p className="text-p-base mb-16"> | ||
Please help us spread the word. Send this code to your friends, post | ||
it on social media. Maybe even print it out on postcards and put it | ||
through your neighbours doors? | ||
</p> | ||
</> | ||
) | ||
} | ||
|
||
switch (insidersStatus) { | ||
case 'eligible': | ||
case 'eligible_lifetime': | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
You're eligible to join Insiders.{' '} | ||
<a href={insidersPath}>Get started here.</a> | ||
</p> | ||
) | ||
default: | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
Exercism Insiders can access 20% off Exercism's{' '} | ||
<a href="https://bootcamp.exercism/org">Learn to Code Bootcamp</a>, | ||
and receive 20% of all sales when someone uses their voucher code. | ||
</p> | ||
) | ||
} | ||
} | ||
|
||
function ErrorMessage({ error }: { error: string | null }) { | ||
if (!error) return null | ||
return ( | ||
<div className="c-alert--danger text-15 font-body mt-10 normal-case py-8 px-16"> | ||
{error} | ||
</div> | ||
) | ||
} |
78 changes: 78 additions & 0 deletions
78
app/javascript/components/settings/BootcampFreeCouponForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import React, { useState, useCallback } from 'react' | ||
import { fetchJSON } from '@/utils/fetch-json' | ||
import CopyToClipboardButton from '../common/CopyToClipboardButton' | ||
|
||
type Links = { | ||
bootcampFreeCouponCode: string | ||
} | ||
|
||
export default function BootcampFreeCouponForm({ | ||
bootcampFreeCouponCode, | ||
links, | ||
}: { | ||
bootcampFreeCouponCode: string | ||
links: Links | ||
}): JSX.Element { | ||
const [couponCode, setCouponCode] = useState(bootcampFreeCouponCode) | ||
const [error, setError] = useState<string | null>(null) | ||
const [loading, setLoading] = useState(false) | ||
|
||
const generateCouponCode = useCallback(async () => { | ||
setLoading(true) | ||
try { | ||
const data = await fetchJSON<{ couponCode: string }>( | ||
links.bootcampFreeCouponCode, | ||
{ | ||
method: 'POST', | ||
body: null, | ||
} | ||
) | ||
setCouponCode(data.couponCode) | ||
setError(null) | ||
} catch (err) { | ||
console.error('Error generating coupon code:', err) | ||
setError('Failed to generate coupon code. Please try again.') | ||
} finally { | ||
setLoading(false) | ||
} | ||
}, [links]) | ||
|
||
return ( | ||
<div> | ||
<h2 className="!mb-8">Free Seat on the Bootcamp</h2> | ||
<p className="text-p-base mb-12"> | ||
As a lifetime insider you're eligible for a free seat on Exercism's{' '} | ||
<a href="https://bootcamp.exercism/org">Learn to Code Bootcamp</a>. | ||
</p> | ||
<p className="text-p-base mb-16"> | ||
To claim your free seat, we're providing you with a discount code that | ||
you can use at the checkout for a 100% discount. You can use it for | ||
yourself, give it to a friend, offer it to a charity, post it on social | ||
media, or anything else you feel appropriate. | ||
</p> | ||
|
||
{couponCode ? ( | ||
<CopyToClipboardButton textToCopy={couponCode} /> | ||
) : ( | ||
<button | ||
onClick={generateCouponCode} | ||
disabled={loading} | ||
type="button" | ||
className="btn btn-primary" | ||
> | ||
{loading ? 'Generating code...' : 'Click to generate code'} | ||
</button> | ||
)} | ||
<ErrorMessage error={error} /> | ||
</div> | ||
) | ||
} | ||
|
||
function ErrorMessage({ error }: { error: string | null }) { | ||
if (!error) return null | ||
return ( | ||
<div className="c-alert--danger text-15 font-body mt-10 normal-case py-8 px-16"> | ||
{error} | ||
</div> | ||
) | ||
} |
Oops, something went wrong.