Initial commit for project Spoon!
Build and Push Next App / quality (push) Failing after 45s
Build and Push Next App / build-next (push) Has been skipped

This commit is contained in:
Gabriel Brown
2026-06-21 17:52:02 -05:00
commit cf7ff2ee4e
268 changed files with 32981 additions and 0 deletions
@@ -0,0 +1,245 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useMutation } from 'convex/react';
import { toast } from 'sonner';
import { api } from '@spoon/backend/convex/_generated/api.js';
import {
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
} from '@spoon/ui';
const options = {
provider: ['github', 'gitea', 'gitlab', 'other'],
visibility: ['unknown', 'public', 'private', 'internal'],
maintenanceMode: ['watch', 'auto_pr', 'paused'],
syncCadence: ['daily', 'weekly', 'manual'],
productionRefStrategy: ['default_branch', 'latest_release', 'tag_pattern'],
} as const;
type FormState = {
name: string;
description: string;
provider: (typeof options.provider)[number];
upstreamOwner: string;
upstreamRepo: string;
upstreamDefaultBranch: string;
upstreamUrl: string;
forkOwner: string;
forkRepo: string;
forkUrl: string;
visibility: (typeof options.visibility)[number];
maintenanceMode: (typeof options.maintenanceMode)[number];
syncCadence: (typeof options.syncCadence)[number];
productionRefStrategy: (typeof options.productionRefStrategy)[number];
};
const initialState: FormState = {
name: '',
description: '',
provider: 'github',
upstreamOwner: '',
upstreamRepo: '',
upstreamDefaultBranch: 'main',
upstreamUrl: '',
forkOwner: '',
forkRepo: '',
forkUrl: '',
visibility: 'unknown',
maintenanceMode: 'watch',
syncCadence: 'daily',
productionRefStrategy: 'default_branch',
};
const TextField = ({
id,
label,
value,
onChange,
required,
}: {
id: keyof FormState;
label: string;
value: string;
onChange: (value: string) => void;
required?: boolean;
}) => (
<div className='grid gap-2'>
<Label htmlFor={id}>{label}</Label>
<Input
id={id}
value={value}
required={required}
onChange={(event) => onChange(event.target.value)}
/>
</div>
);
export const NewSpoonForm = () => {
const router = useRouter();
const createManual = useMutation(api.spoons.createManual);
const [form, setForm] = useState<FormState>(initialState);
const [submitting, setSubmitting] = useState(false);
const update = <K extends keyof FormState>(key: K, value: FormState[K]) => {
setForm((current) => ({ ...current, [key]: value }));
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmitting(true);
try {
await createManual({
...form,
description: form.description || undefined,
forkOwner: form.forkOwner || undefined,
forkRepo: form.forkRepo || undefined,
forkUrl: form.forkUrl || undefined,
});
toast.success('Spoon created.');
router.push('/spoons');
} catch (error) {
console.error(error);
toast.error('Could not create Spoon.');
} finally {
setSubmitting(false);
}
};
return (
<form
onSubmit={handleSubmit}
className='border-border bg-card grid gap-6 border p-5'
>
<div className='grid gap-4 md:grid-cols-2'>
<TextField
id='name'
label='Spoon name'
value={form.name}
required
onChange={(value) => update('name', value)}
/>
<Select
value={form.provider}
onValueChange={(value) =>
update('provider', value as FormState['provider'])
}
>
<div className='grid gap-2'>
<Label>Provider</Label>
<SelectTrigger className='w-full'>
<SelectValue />
</SelectTrigger>
</div>
<SelectContent>
{options.provider.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='grid gap-2'>
<Label htmlFor='description'>Description</Label>
<Textarea
id='description'
value={form.description}
onChange={(event) => update('description', event.target.value)}
/>
</div>
<div className='grid gap-4 md:grid-cols-2'>
<TextField
id='upstreamOwner'
label='Upstream owner'
value={form.upstreamOwner}
required
onChange={(value) => update('upstreamOwner', value)}
/>
<TextField
id='upstreamRepo'
label='Upstream repository'
value={form.upstreamRepo}
required
onChange={(value) => update('upstreamRepo', value)}
/>
<TextField
id='upstreamDefaultBranch'
label='Upstream default branch'
value={form.upstreamDefaultBranch}
required
onChange={(value) => update('upstreamDefaultBranch', value)}
/>
<TextField
id='upstreamUrl'
label='Upstream URL'
value={form.upstreamUrl}
required
onChange={(value) => update('upstreamUrl', value)}
/>
<TextField
id='forkOwner'
label='Fork owner'
value={form.forkOwner}
onChange={(value) => update('forkOwner', value)}
/>
<TextField
id='forkRepo'
label='Fork repository'
value={form.forkRepo}
onChange={(value) => update('forkRepo', value)}
/>
<TextField
id='forkUrl'
label='Fork URL'
value={form.forkUrl}
onChange={(value) => update('forkUrl', value)}
/>
</div>
<div className='grid gap-4 md:grid-cols-4'>
{(
[
'visibility',
'maintenanceMode',
'syncCadence',
'productionRefStrategy',
] as const
).map((key) => (
<Select
key={key}
value={form[key]}
onValueChange={(value) => update(key, value as never)}
>
<div className='grid gap-2'>
<Label>{key.replace(/([A-Z])/g, ' $1')}</Label>
<SelectTrigger className='w-full'>
<SelectValue />
</SelectTrigger>
</div>
<SelectContent>
{options[key].map((value) => (
<SelectItem key={value} value={value}>
{value.replaceAll('_', ' ')}
</SelectItem>
))}
</SelectContent>
</Select>
))}
</div>
<div className='flex justify-end'>
<Button type='submit' disabled={submitting}>
{submitting ? 'Creating...' : 'Create Spoon'}
</Button>
</div>
</form>
);
};