diff --git a/src/components/pages/plugins/bounties/BountyGrid/BountyFilters/BountyFilters.tsx b/src/components/pages/plugins/bounties/BountyGrid/BountyFilters/BountyFilters.tsx index 91ad7360..8a318cdb 100644 --- a/src/components/pages/plugins/bounties/BountyGrid/BountyFilters/BountyFilters.tsx +++ b/src/components/pages/plugins/bounties/BountyGrid/BountyFilters/BountyFilters.tsx @@ -30,8 +30,8 @@ export const STATUS_FILTERS: Array<{id: StatusFilter; label: string}> = [ ]; export const SORTS: Array<{id: BountySort; label: string}> = [ - {id: 'status', label: 'By status'}, {id: 'votes', label: 'Most voted'}, + {id: 'status', label: 'By status'}, ]; export const DIFFICULTY_FILTERS: Array<{id: DifficultyFilter; label: string}> = [ diff --git a/src/components/pages/plugins/bounties/BountyGrid/BountyGrid.tsx b/src/components/pages/plugins/bounties/BountyGrid/BountyGrid.tsx index e5f9ffee..3c1d180b 100644 --- a/src/components/pages/plugins/bounties/BountyGrid/BountyGrid.tsx +++ b/src/components/pages/plugins/bounties/BountyGrid/BountyGrid.tsx @@ -41,7 +41,7 @@ export function BountyGrid() { const [focus, setFocus] = useState('all'); const [difficulty, setDifficulty] = useState('all'); const [query, setQuery] = useState(''); - const [sort, setSort] = useState('status'); + const [sort, setSort] = useState('votes'); const [selectedId, setSelectedId] = useState(null); const [baked] = useState(getActiveBounties); const live = useLiveBounties(baked, useCallback((slug: string) => setSelectedId(slug), [])); diff --git a/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.module.scss b/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.module.scss index e2add697..185305a4 100644 --- a/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.module.scss +++ b/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.module.scss @@ -109,3 +109,35 @@ overflow-wrap: anywhere; } } + +.modes { + display: flex; + width: fit-content; + max-width: 100%; + gap: 0.25rem; + padding: 0.3rem; + border: 0.1rem solid var(--color-border); + border-radius: var(--radius-full); +} + +.mode { + padding: 0.4rem 0.875rem; + border-radius: var(--radius-full); + font-size: 0.825rem; + font-weight: 600; + color: var(--color-text-secondary); + transition: all 0.2s ease; + + &:hover { + color: var(--color-text-primary); + } +} + +.modeActive { + background-color: var(--color-text-primary); + color: var(--color-bg); + + &:hover { + color: var(--color-bg); + } +} diff --git a/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.tsx b/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.tsx index 55a632ba..e6e01af8 100644 --- a/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.tsx +++ b/src/components/pages/plugins/bounties/BountyGrid/ScaffoldForm/ScaffoldForm.tsx @@ -1,16 +1,21 @@ 'use client'; -import {useState, type FormEvent} from 'react'; -import {ArrowUpRightIcon, CopyIcon, GitBranchIcon, KeyRoundIcon} from 'lucide-react'; +import {useEffect, useState, type FormEvent} from 'react'; +import clsx from 'clsx'; +import {ArrowUpRightIcon, CopyIcon, GitBranchIcon, KeyRoundIcon, LinkIcon} from 'lucide-react'; import {Button} from '@/components/ui/Button/Button'; import {CopyButton} from '@/components/ui/CopyButton/CopyButton'; import {trackEvent} from '@/lib/analytics'; import {APP_VERSION} from '@/lib/download/version'; import { + fetchMaintainedRepos, + fetchRepoOwners, + linkRepository, localScaffoldCommand, scaffoldRepository, type LiveRequest, - type ScaffoldedRepo, + type MaintainedRepo, + type RepoOwner, } from '@/lib/plugins/requests'; import type {BoardBounty} from '../useLiveBounties'; import styles from './ScaffoldForm.module.scss'; @@ -34,9 +39,44 @@ export function ScaffoldForm({bounty, onScaffolded}: ScaffoldFormProps) { const [minRuntimeVersion, setMinRuntimeVersion] = useState(APP_VERSION); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState<{message: string; reauthUrl?: string} | null>(null); - const [created, setCreated] = useState(null); + const [created, setCreated] = useState<{repoUrl: string; fullName: string; submitUrl: string; linked: boolean} | null>( + null, + ); + const [mode, setMode] = useState<'create' | 'link'>(canScaffold ? 'create' : 'link'); + const [owners, setOwners] = useState(null); + const [linkUrl, setLinkUrl] = useState(''); + const [repos, setRepos] = useState(null); const command = localScaffoldCommand(bounty.id, bounty.kind); + // Accounts the repository can be created in; the field stays free text if this fails. + useEffect(() => { + if (!canScaffold) return; + let cancelled = false; + fetchRepoOwners().then((list) => { + if (!cancelled && list && list.length > 0) setOwners(list); + }); + return () => { + cancelled = true; + }; + }, [canScaffold]); + + // Suggestions for the link field, loaded the first time it is shown; retried on the next visit if that fails. + useEffect(() => { + if (mode !== 'link' || repos !== null) return; + let cancelled = false; + fetchMaintainedRepos().then((list) => { + if (!cancelled && list) setRepos(list); + }); + return () => { + cancelled = true; + }; + }, [mode, repos]); + + function switchMode(next: 'create' | 'link') { + setMode(next); + setError(null); + } + async function handleSubmit(event: FormEvent) { event.preventDefault(); if (!bounty.requestId) return; @@ -54,15 +94,44 @@ export function ScaffoldForm({bounty, onScaffolded}: ScaffoldFormProps) { return; } trackEvent('Bounty', 'Scaffold', bounty.id); - setCreated(result.data); + setCreated({...result.data, linked: false}); + onScaffolded(result.data.claimers); + } + + async function handleLink(event: FormEvent) { + event.preventDefault(); + if (!bounty.requestId) return; + setSubmitting(true); + setError(null); + const result = await linkRepository(bounty.requestId, linkUrl.trim()); + setSubmitting(false); + if (!result.ok) { + setError({message: result.error, reauthUrl: result.reauthUrl}); + return; + } + trackEvent('Bounty', 'Link repository', bounty.id); + setCreated({...result.data, linked: true}); onScaffolded(result.data.claimers); } + const errorBox = error && ( +
+

{error.message}

+ {error.reauthUrl && ( + + )} +
+ ); + if (created) { return (

- {created.fullName} is ready. Build the plugin, publish a release, then submit the + {created.fullName} {created.linked ? 'is linked to your claim' : 'is ready'}. Build + the plugin, publish a release, then submit the repository to the registry: the bounty ships as soon as {bounty.id} is listed.

@@ -82,6 +151,59 @@ export function ScaffoldForm({bounty, onScaffolded}: ScaffoldFormProps) { return (
{canScaffold && ( +
+ + +
+ )} + {mode === 'link' && bounty.requestId && ( +
+ + + Pick one of your repositories or paste its URL. You need maintain access, as when submitting a + plugin. + + {errorBox} + +
+ )} + {canScaffold && mode === 'create' && (
{isTheme ? (
- {error && ( -
-

{error.message}

- {error.reauthUrl && ( - - )} -
- )} + {errorBox}