Skip to content

Commit 8d2b541

Browse files
Merge pull request #1415 from jordan-ae/self-assign
Add Self Assignment Component to Bounty Modal
2 parents e362201 + 44af604 commit 8d2b541

File tree

7 files changed

+275
-5
lines changed

7 files changed

+275
-5
lines changed

src/people/interfaces.ts

+5
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export interface WantedSummaryProps {
265265
owner_id?: string;
266266
markPaidOrUnpaid?: ReactNode;
267267
isEditButtonDisable?: boolean;
268+
stakeMin?: number;
268269
}
269270

270271
export type LocalPaymeentState = 'UNKNOWN' | 'PAID' | 'UNPAID';
@@ -342,6 +343,8 @@ export interface CodingBountiesProps extends WantedSummaryProps {
342343
assigneeLabel?: { [key: string]: any };
343344
actionButtons?: boolean | JSX.Element;
344345
unlock_code?: string;
346+
stake_min?: number;
347+
is_stakable?: boolean;
345348
}
346349

347350
export interface CodingViewProps extends WantedSummaryProps {
@@ -443,6 +446,8 @@ export interface WantedViews2Props extends WantedViewsProps {
443446
fromBountyPage?: boolean;
444447
activeWorkspace?: string;
445448
isBountyLandingPage?: boolean;
449+
is_stakable?: boolean;
450+
stake_min?: number;
446451
}
447452

448453
export interface AboutViewProps {

src/people/widgetViews/WantedView.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ function WantedView(props: WantedViews2Props) {
125125
return (
126126
<MobileView
127127
{...props}
128+
is_stakable={props.is_stakable}
129+
stake_min={props.stake_min}
128130
labels={labels}
129131
key={ticketUrl}
130132
saving={saving ?? false}

src/people/widgetViews/summaries/WantedSummary.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ function WantedSummary(props: WantedSummaryProps) {
6060
feature_uuid,
6161
phase_uuid,
6262
id,
63-
isEditButtonDisable
63+
isEditButtonDisable,
64+
stakeMin
6465
} = props;
6566
const titleString = one_sentence_summary || title || '';
6667
const bountyPath = `/bounty/${id}`;
@@ -621,6 +622,7 @@ function WantedSummary(props: WantedSummaryProps) {
621622
isEditButtonDisable={isEditButtonDisable}
622623
phase_uuid={phase_uuid}
623624
feature_uuid={feature_uuid}
625+
stakeMin={stakeMin}
624626
/>
625627
);
626628
}

src/people/widgetViews/summaries/wantedSummaries/CodingBounty.tsx

+110-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import {
6161
} from './style';
6262
import { getTwitterLink } from './lib';
6363
import CodingMobile from './CodingMobile';
64-
import { BountyEstimates } from './Components';
64+
import { BountyEstimates, SelfAssignButton } from './Components';
6565
import CodingBountyProofModal from './CodingBountyProofModal';
6666
let interval;
6767

@@ -123,7 +123,9 @@ function MobileView(props: CodingBountiesProps) {
123123
isEditButtonDisable,
124124
completed,
125125
payment_failed,
126-
payment_pending
126+
payment_pending,
127+
stake_min,
128+
is_stakable
127129
} = props;
128130
const color = colors['light'];
129131

@@ -147,6 +149,8 @@ function MobileView(props: CodingBountiesProps) {
147149
const [menuOpen, setMenuOpen] = useState<number | null>(null);
148150
const textareaRef = useRef<HTMLTextAreaElement>(null);
149151
const bountyID = id?.toString() || '';
152+
const [showComingSoonModal, setShowComingSoonModal] = useState(false);
153+
const [featureFlags, setFeatureFlags] = useState<any>({});
150154

151155
useEffect(() => {
152156
if (textareaRef.current) {
@@ -415,7 +419,7 @@ function MobileView(props: CodingBountiesProps) {
415419

416420
const makePayment = async () => {
417421
setPaymentLoading(true);
418-
// If the bounty has a commitment fee, add the fee to the user payment
422+
// If the bounty has a commitment fee, add the fee to the user's payment
419423
const price = Number(props.price);
420424
// if there is an workspace and the workspace's
421425
// buudget is sufficient keysend to the user immediately
@@ -717,6 +721,25 @@ function MobileView(props: CodingBountiesProps) {
717721
let pillColor = color.statusAssigned;
718722
let pillText = 'assigned';
719723

724+
useEffect(() => {
725+
const fetchFeatureFlags = async () => {
726+
try {
727+
const response = await main.getFeatureFlags();
728+
if (response?.success) {
729+
setFeatureFlags(
730+
response.data.reduce((acc: any, flag: any) => {
731+
acc[flag.name] = flag.enabled;
732+
return acc;
733+
}, {})
734+
);
735+
}
736+
} catch (error) {
737+
console.error('Error fetching feature flags:', error);
738+
}
739+
};
740+
fetchFeatureFlags();
741+
}, [main]);
742+
720743
if (isMobile) {
721744
return (
722745
<CodingMobile
@@ -847,6 +870,14 @@ function MobileView(props: CodingBountiesProps) {
847870
/>
848871
)}
849872

873+
{featureFlags.staking && is_stakable && (
874+
<SelfAssignButton
875+
onClick={() => setShowComingSoonModal(true)}
876+
stakeMin={stake_min || 0}
877+
EstimatedSessionLength={props.estimated_session_length || ''}
878+
/>
879+
)}
880+
850881
{isOpenProofModal && (
851882
<CodingBountyProofModal
852883
closeModal={() => setIsOpenProofModal(false)}
@@ -1291,6 +1322,13 @@ function MobileView(props: CodingBountiesProps) {
12911322
completion_date={props.estimated_completion_date}
12921323
session_length={props.estimated_session_length}
12931324
/>
1325+
{featureFlags.staking && is_stakable && (
1326+
<SelfAssignButton
1327+
onClick={() => setShowComingSoonModal(true)}
1328+
stakeMin={stake_min || 0}
1329+
EstimatedSessionLength={props.estimated_session_length || ''}
1330+
/>
1331+
)}
12941332
<div className="buttonSet">
12951333
<ButtonSet
12961334
githubShareAction={() => {
@@ -1889,6 +1927,13 @@ function MobileView(props: CodingBountiesProps) {
18891927
completion_date={props.estimated_completion_date}
18901928
session_length={props.estimated_session_length}
18911929
/>
1930+
{featureFlags.staking && is_stakable && (
1931+
<SelfAssignButton
1932+
onClick={() => setShowComingSoonModal(true)}
1933+
stakeMin={stake_min || 0}
1934+
EstimatedSessionLength={props.estimated_session_length || ''}
1935+
/>
1936+
)}
18921937
<ButtonSet
18931938
showGithubBtn={!!ticket_url}
18941939
githubShareAction={() => {
@@ -1979,6 +2024,13 @@ function MobileView(props: CodingBountiesProps) {
19792024
completion_date={props.estimated_completion_date}
19802025
session_length={props.estimated_session_length}
19812026
/>
2027+
{featureFlags.staking && is_stakable && (
2028+
<SelfAssignButton
2029+
onClick={() => setShowComingSoonModal(true)}
2030+
stakeMin={stake_min || 0}
2031+
EstimatedSessionLength={props.estimated_session_length || ''}
2032+
/>
2033+
)}
19822034
<ButtonSet
19832035
showGithubBtn={!!ticket_url}
19842036
githubShareAction={() => {
@@ -2002,6 +2054,61 @@ function MobileView(props: CodingBountiesProps) {
20022054
</AssigneeProfile>
20032055
</NormalUser>
20042056
)}
2057+
{showComingSoonModal && (
2058+
<Modal
2059+
visible={true}
2060+
envStyle={{
2061+
borderRadius: '12px',
2062+
background: color.pureWhite,
2063+
padding: '32px 24px',
2064+
width: '400px',
2065+
maxWidth: '90vw',
2066+
boxShadow: '0px 8px 24px rgba(0, 0, 0, 0.1)'
2067+
}}
2068+
bigCloseImage={() => setShowComingSoonModal(false)}
2069+
bigCloseImageStyle={{
2070+
top: '-12px',
2071+
right: '-12px',
2072+
background: color.pureWhite,
2073+
borderRadius: '50%',
2074+
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
2075+
padding: '4px'
2076+
}}
2077+
>
2078+
<div
2079+
style={{
2080+
display: 'flex',
2081+
flexDirection: 'column',
2082+
alignItems: 'center',
2083+
textAlign: 'center',
2084+
gap: '16px'
2085+
}}
2086+
>
2087+
<EuiText
2088+
style={{
2089+
fontSize: '24px',
2090+
fontWeight: 600,
2091+
color: color.grayish.G100,
2092+
marginBottom: '8px'
2093+
}}
2094+
>
2095+
Staking Coming Soon!
2096+
</EuiText>
2097+
<Button
2098+
color="primary"
2099+
onClick={() => setShowComingSoonModal(false)}
2100+
text="Got It!"
2101+
style={{
2102+
width: '120px',
2103+
height: '40px',
2104+
fontSize: '16px',
2105+
fontWeight: 600,
2106+
borderRadius: '8px'
2107+
}}
2108+
/>
2109+
</div>
2110+
</Modal>
2111+
)}
20052112
<EuiGlobalToastList toasts={toasts} dismissToast={removeToast} toastLifeTimeMs={6000} />
20062113
</div>
20072114
);

src/people/widgetViews/summaries/wantedSummaries/Components.tsx

+61
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,64 @@ export const ICanHelpButton = ({ onClick }: ICanHelpButtonProps) => {
272272
/>
273273
);
274274
};
275+
276+
type SelfAssignButtonProps = {
277+
onClick: () => void;
278+
stakeMin: number;
279+
EstimatedSessionLength: string;
280+
};
281+
282+
export const SelfAssignButton = ({
283+
onClick,
284+
stakeMin,
285+
EstimatedSessionLength
286+
}: SelfAssignButtonProps) => {
287+
const color = colors['light'];
288+
289+
return (
290+
<div style={{ width: '100%', marginTop: '16px' }}>
291+
<IconButton
292+
text={'Self Assign'}
293+
endingIcon={'arrow_forward'}
294+
width={'100%'}
295+
height={48}
296+
onClick={onClick}
297+
color="primary"
298+
hovercolor={color.button_secondary.hover}
299+
activecolor={color.button_secondary.active}
300+
shadowcolor={color.button_secondary.shadow}
301+
iconSize={'16px'}
302+
iconStyle={{
303+
top: '16px',
304+
right: '14px'
305+
}}
306+
textStyle={{
307+
width: '100%',
308+
display: 'flex',
309+
justifyContent: 'center',
310+
fontFamily: 'Barlow'
311+
}}
312+
/>
313+
314+
<div
315+
style={{
316+
display: 'flex',
317+
flexDirection: 'column',
318+
alignItems: 'center',
319+
marginTop: 16,
320+
marginBottom: 8
321+
}}
322+
>
323+
<span style={{ fontSize: 12, color: color.grayish.G100 }}>
324+
Deposit this amount to Self Assign
325+
</span>
326+
<span style={{ fontSize: 16, fontWeight: 600 }}>{stakeMin?.toLocaleString()} SAT</span>
327+
</div>
328+
329+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
330+
<span style={{ fontSize: 12, color: color.grayish.G100 }}>Time to Complete</span>
331+
<span style={{ fontSize: 16, fontWeight: 600 }}>{EstimatedSessionLength}</span>
332+
</div>
333+
</div>
334+
);
335+
};

src/people/widgetViews/summaries/wantedSummaries/__tests__/CodingBounty.spec.tsx

+46
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-empty */
12
import '@testing-library/jest-dom';
23
import {
34
fireEvent,
@@ -20,6 +21,9 @@ jest.mock('remark-gfm', () => ({}));
2021

2122
jest.mock('rehype-raw', () => ({}));
2223

24+
jest.mock('remark-gfm', () => ({}));
25+
jest.mock('rehype-raw', () => ({}));
26+
2327
describe('MobileView component', () => {
2428
beforeEach(() => {
2529
const mockIntersectionObserver = jest.fn();
@@ -515,4 +519,46 @@ describe('MobileView component', () => {
515519
});
516520
})();
517521
});
522+
523+
it('does not render Self Assignment Component when isStakable is false', async () => {
524+
const mockGetFeatureFlags = jest.spyOn(mainStore, 'getFeatureFlags').mockResolvedValue({
525+
success: true,
526+
data: [{ name: 'staking', enabled: true }]
527+
});
528+
529+
const props = {
530+
...defaultProps,
531+
isStakable: false,
532+
stakeMin: 1000
533+
};
534+
535+
render(<MobileView {...props} />);
536+
537+
await waitFor(() => {
538+
expect(screen.queryByText('Self Assign')).not.toBeInTheDocument();
539+
});
540+
541+
mockGetFeatureFlags.mockRestore();
542+
});
543+
544+
it('does not render Self Assignment Component when staking feature flag is false', async () => {
545+
const mockGetFeatureFlags = jest.spyOn(mainStore, 'getFeatureFlags').mockResolvedValue({
546+
success: true,
547+
data: [{ name: 'staking', enabled: false }]
548+
});
549+
550+
const props = {
551+
...defaultProps,
552+
isStakable: true,
553+
stakeMin: 1000
554+
};
555+
556+
render(<MobileView {...props} />);
557+
558+
await waitFor(() => {
559+
expect(screen.queryByText('Self Assign')).not.toBeInTheDocument();
560+
});
561+
562+
mockGetFeatureFlags.mockRestore();
563+
});
518564
});

0 commit comments

Comments
 (0)