Add agent workflows & stuff
This commit is contained in:
@@ -120,7 +120,12 @@ const AgentsPage = () => {
|
||||
<div key={request._id} className='border-border border p-4'>
|
||||
<p className='font-medium'>{request.prompt}</p>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
{request.status.replaceAll('_', ' ')}
|
||||
{request.status.replaceAll('_', ' ')} ·{' '}
|
||||
{(request.requestType ?? 'future_code_change').replaceAll(
|
||||
'_',
|
||||
' ',
|
||||
)}{' '}
|
||||
· {request.source ?? 'user'}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
import Link from 'next/link';
|
||||
import { MetricCard } from '@/components/dashboard/metric-card';
|
||||
import { SpoonCard } from '@/components/spoons/spoon-card';
|
||||
import { MaintenanceQueue } from '@/components/updates/maintenance-queue';
|
||||
import { useQuery } from 'convex/react';
|
||||
import { Bot, GitBranch, GitPullRequest, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
Bot,
|
||||
GitBranch,
|
||||
GitPullRequest,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import { Button, Card, CardContent, CardHeader, CardTitle } from '@spoon/ui';
|
||||
@@ -14,12 +21,18 @@ const DashboardPage = () => {
|
||||
const syncRuns = useQuery(api.syncRuns.listRecent, { limit: 5 }) ?? [];
|
||||
const agentRequests =
|
||||
useQuery(api.agentRequests.listRecent, { limit: 5 }) ?? [];
|
||||
const aiReviews = useQuery(api.aiReviews.listRecent, { limit: 5 }) ?? [];
|
||||
const activeSpoons = spoons.filter(
|
||||
(spoon) => spoon.status === 'active',
|
||||
).length;
|
||||
const needsReview = syncRuns.filter(
|
||||
(run) => run.status === 'needs_review',
|
||||
const behind = spoons.filter((spoon) => spoon.syncStatus === 'behind').length;
|
||||
const diverged = spoons.filter(
|
||||
(spoon) => spoon.syncStatus === 'diverged',
|
||||
).length;
|
||||
const openPullRequests = spoons.reduce(
|
||||
(total, spoon) => total + (spoon.upstreamAheadBy ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
@@ -49,9 +62,9 @@ const DashboardPage = () => {
|
||||
icon={GitPullRequest}
|
||||
/>
|
||||
<MetricCard
|
||||
label='Needs review'
|
||||
value={needsReview}
|
||||
note='Upstream updates'
|
||||
label='Behind upstream'
|
||||
value={behind}
|
||||
note={`${diverged} diverged`}
|
||||
icon={RefreshCw}
|
||||
/>
|
||||
<MetricCard
|
||||
@@ -60,8 +73,19 @@ const DashboardPage = () => {
|
||||
note='Queued and recent'
|
||||
icon={Bot}
|
||||
/>
|
||||
<MetricCard
|
||||
label='Upstream commits'
|
||||
value={openPullRequests}
|
||||
note='Waiting across Spoons'
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className='space-y-3'>
|
||||
<h2 className='text-lg font-semibold'>Maintenance queue</h2>
|
||||
<MaintenanceQueue spoons={spoons} />
|
||||
</section>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-2'>
|
||||
<section className='space-y-3'>
|
||||
<h2 className='text-lg font-semibold'>Recent Spoons</h2>
|
||||
@@ -112,6 +136,35 @@ const DashboardPage = () => {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='mt-4 shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>AI reviews</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{aiReviews.length ? (
|
||||
<div className='space-y-3'>
|
||||
{aiReviews.map((review) => (
|
||||
<div
|
||||
key={review._id}
|
||||
className='border-border border p-3 text-sm'
|
||||
>
|
||||
<p className='font-medium capitalize'>
|
||||
{review.risk} risk
|
||||
</p>
|
||||
<p className='text-muted-foreground'>
|
||||
{review.outputSummary ?? review.inputSummary}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
OpenAI compatibility reviews will appear here after you run
|
||||
them on a Spoon.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { GitHubConnectClient } from '@/components/github/github-connect-client';
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ installation_id?: string }>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>
|
||||
Connect GitHub
|
||||
</h1>
|
||||
<p className='text-muted-foreground mt-2 max-w-2xl'>
|
||||
Spoon stores the GitHub App installation ID and uses short-lived
|
||||
installation tokens for repository automation.
|
||||
</p>
|
||||
</div>
|
||||
<GitHubConnectClient installationId={params.installation_id} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { OpenAiStatusPanel } from '@/components/integrations/openai-status-panel';
|
||||
|
||||
const AiSettingsPage = () => (
|
||||
<section className='max-w-3xl space-y-4'>
|
||||
<div>
|
||||
<h2 className='text-xl font-semibold'>AI</h2>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
Configure the OpenAI key, review model, and thinking level used for
|
||||
compatibility reviews.
|
||||
</p>
|
||||
</div>
|
||||
<OpenAiStatusPanel />
|
||||
</section>
|
||||
);
|
||||
|
||||
export default AiSettingsPage;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { GithubIntegrationPanel } from '@/components/integrations/github-integration-panel';
|
||||
|
||||
const IntegrationsPage = () => (
|
||||
<section className='max-w-3xl space-y-4'>
|
||||
<div>
|
||||
<h2 className='text-xl font-semibold'>Integrations</h2>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
Provider access used by Spoon maintenance workflows.
|
||||
</p>
|
||||
</div>
|
||||
<GithubIntegrationPanel />
|
||||
</section>
|
||||
);
|
||||
|
||||
export default IntegrationsPage;
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Brain, Github, Shield, User } from 'lucide-react';
|
||||
|
||||
import { cn } from '@spoon/ui';
|
||||
|
||||
const settingsItems = [
|
||||
{ href: '/settings/profile', label: 'Profile', icon: User },
|
||||
{ href: '/settings/integrations', label: 'Integrations', icon: Github },
|
||||
{ href: '/settings/ai', label: 'AI', icon: Brain },
|
||||
{ href: '/settings/security', label: 'Security', icon: Shield },
|
||||
];
|
||||
|
||||
const SettingsLayout = ({ children }: { children: ReactNode }) => {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<div className='flex flex-col justify-between gap-4 border-b pb-5 lg:flex-row lg:items-end'>
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>Settings</h1>
|
||||
<p className='text-muted-foreground mt-2'>
|
||||
Account, provider, AI, and security controls for this Spoon
|
||||
workspace.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid gap-6 xl:grid-cols-[13rem_1fr]'>
|
||||
<nav className='border-border bg-card flex gap-1 overflow-x-auto border p-2 xl:flex-col xl:self-start'>
|
||||
{settingsItems.map(({ href, label, icon: Icon }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className={cn(
|
||||
'hover:bg-muted flex min-w-fit items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
pathname === href
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className='size-4' />
|
||||
{label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className='min-w-0'>{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsLayout;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const SettingsPage = () => {
|
||||
redirect('/settings/profile');
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -0,0 +1,42 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
AvatarUpload,
|
||||
ProfileHeader,
|
||||
ResetPasswordForm,
|
||||
UserInfoForm,
|
||||
} from '@/components/layout/auth/profile';
|
||||
import { preloadQuery } from 'convex/nextjs';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import { Card, Separator } from '@spoon/ui';
|
||||
|
||||
const SettingsProfilePage = async () => {
|
||||
const preloadedUser = await preloadQuery(api.auth.getUser, {});
|
||||
const preloadedUserProvider = await preloadQuery(
|
||||
api.auth.getUserProvider,
|
||||
{},
|
||||
);
|
||||
return (
|
||||
<section className='max-w-3xl space-y-4'>
|
||||
<div>
|
||||
<h2 className='text-xl font-semibold'>Profile</h2>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
Manage your identity, avatar, and account email.
|
||||
</p>
|
||||
</div>
|
||||
<Card className='shadow-none'>
|
||||
<ProfileHeader />
|
||||
<AvatarUpload preloadedUser={preloadedUser} />
|
||||
<Separator className='my-6' />
|
||||
<UserInfoForm
|
||||
preloadedUser={preloadedUser}
|
||||
preloadedProvider={preloadedUserProvider}
|
||||
/>
|
||||
<ResetPasswordForm preloadedProvider={preloadedUserProvider} />
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsProfilePage;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SignOutForm } from '@/components/layout/auth/profile';
|
||||
|
||||
import { Card } from '@spoon/ui';
|
||||
|
||||
const SecuritySettingsPage = () => (
|
||||
<section className='max-w-3xl space-y-4'>
|
||||
<div>
|
||||
<h2 className='text-xl font-semibold'>Security</h2>
|
||||
<p className='text-muted-foreground mt-1 text-sm'>
|
||||
Session controls and security-sensitive account actions.
|
||||
</p>
|
||||
</div>
|
||||
<Card className='shadow-none'>
|
||||
<SignOutForm />
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
|
||||
export default SecuritySettingsPage;
|
||||
@@ -0,0 +1,281 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AgentJobList } from '@/components/agents/agent-job-list';
|
||||
import { AgentRequestForm } from '@/components/agents/agent-request-form';
|
||||
import { SpoonActivityTimeline } from '@/components/spoons/spoon-activity-timeline';
|
||||
import { SpoonAgentSettingsForm } from '@/components/spoons/spoon-agent-settings-form';
|
||||
import { SpoonAiReviewPanel } from '@/components/spoons/spoon-ai-review-panel';
|
||||
import { SpoonClonePanel } from '@/components/spoons/spoon-clone-panel';
|
||||
import { SpoonCommitList } from '@/components/spoons/spoon-commit-list';
|
||||
import { SpoonDetailHeader } from '@/components/spoons/spoon-detail-header';
|
||||
import { SpoonMetrics } from '@/components/spoons/spoon-metrics';
|
||||
import { SpoonPrList } from '@/components/spoons/spoon-pr-list';
|
||||
import { SpoonSecretsForm } from '@/components/spoons/spoon-secrets-form';
|
||||
import { SpoonSettingsForm } from '@/components/spoons/spoon-settings-form';
|
||||
import { useQuery } from 'convex/react';
|
||||
|
||||
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@spoon/ui';
|
||||
|
||||
const SpoonDetailPage = () => {
|
||||
const params = useParams<{ spoonId: string }>();
|
||||
const spoonId = params.spoonId as Id<'spoons'>;
|
||||
const details = useQuery(api.spoons.getDetails, { spoonId });
|
||||
const upstreamCommits =
|
||||
useQuery(api.spoonCommits.listForSpoon, {
|
||||
spoonId,
|
||||
side: 'upstream',
|
||||
limit: 100,
|
||||
}) ?? [];
|
||||
const forkCommits =
|
||||
useQuery(api.spoonCommits.listForSpoon, {
|
||||
spoonId,
|
||||
side: 'fork',
|
||||
limit: 100,
|
||||
}) ?? [];
|
||||
const pullRequests =
|
||||
useQuery(api.spoonPullRequests.listForSpoon, { spoonId, limit: 100 }) ?? [];
|
||||
const reviews =
|
||||
useQuery(api.aiReviews.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
||||
const syncRuns =
|
||||
useQuery(api.syncRuns.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
||||
const agentRequests =
|
||||
useQuery(api.agentRequests.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
||||
const agentSettings = useQuery(api.spoonAgentSettings.getForSpoon, {
|
||||
spoonId,
|
||||
});
|
||||
const agentJobs =
|
||||
useQuery(api.agentJobs.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
||||
|
||||
if (details === undefined) {
|
||||
return <main className='text-muted-foreground p-6'>Loading Spoon...</main>;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className='space-y-6'>
|
||||
<SpoonDetailHeader spoon={details.spoon} state={details.state} />
|
||||
<SpoonMetrics
|
||||
spoon={details.spoon}
|
||||
state={details.state}
|
||||
latestReview={details.latestReview}
|
||||
/>
|
||||
{details.spoon.lastError ? (
|
||||
<Card className='border-destructive shadow-none'>
|
||||
<CardContent className='p-4 text-sm'>
|
||||
{details.spoon.lastError}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Tabs defaultValue='overview' className='flex flex-col gap-5'>
|
||||
<TabsList
|
||||
variant='line'
|
||||
className='border-border flex h-auto w-full justify-start overflow-x-auto rounded-none border-b p-0'
|
||||
>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='overview'>
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='upstream'>
|
||||
Upstream
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='fork'>
|
||||
Fork changes
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='pulls'>
|
||||
Pull requests
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='ai'>
|
||||
AI review
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='agent'>
|
||||
Agent work
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='activity'>
|
||||
Activity
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className='h-9 flex-none px-3' value='settings'>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='overview' className='space-y-4'>
|
||||
<div className='grid gap-4 xl:grid-cols-[1.15fr_0.85fr]'>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader className='pb-3'>
|
||||
<CardTitle className='text-base'>Repository health</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-4 text-sm md:grid-cols-2'>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Drift state</p>
|
||||
<p className='mt-1 text-xl font-semibold capitalize'>
|
||||
{(
|
||||
details.state?.status ??
|
||||
details.spoon.syncStatus ??
|
||||
'unknown'
|
||||
).replaceAll('_', ' ')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Default branches</p>
|
||||
<p className='mt-1 font-medium'>
|
||||
{details.state?.upstreamDefaultBranch ??
|
||||
details.spoon.upstreamDefaultBranch}{' '}
|
||||
→{' '}
|
||||
{details.state?.forkDefaultBranch ??
|
||||
details.spoon.forkDefaultBranch ??
|
||||
details.spoon.upstreamDefaultBranch}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Merge base</p>
|
||||
<p className='mt-1 truncate font-mono text-xs'>
|
||||
{details.state?.mergeBaseSha ??
|
||||
details.spoon.lastMergeBaseCommit ??
|
||||
'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Cadence</p>
|
||||
<p className='mt-1 font-medium capitalize'>
|
||||
{details.spoon.syncCadence}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='shadow-none'>
|
||||
<CardHeader className='pb-3'>
|
||||
<CardTitle className='text-base'>Latest AI review</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-3 text-sm'>
|
||||
{details.latestReview ? (
|
||||
<>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Risk</p>
|
||||
<p className='mt-1 font-semibold capitalize'>
|
||||
{details.latestReview.risk}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className='text-muted-foreground'>Action</p>
|
||||
<p className='mt-1 font-semibold capitalize'>
|
||||
{details.latestReview.recommendedAction.replaceAll(
|
||||
'_',
|
||||
' ',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className='text-muted-foreground'>
|
||||
{details.latestReview.outputSummary ??
|
||||
details.latestReview.inputSummary}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className='text-muted-foreground'>
|
||||
Run a refresh and AI review to get a compatibility summary
|
||||
for upstream changes.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SpoonClonePanel spoon={details.spoon} />
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 xl:grid-cols-2'>
|
||||
<section className='space-y-3'>
|
||||
<div>
|
||||
<h2 className='text-base font-semibold'>Upstream waiting</h2>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Commits upstream has that your fork does not.
|
||||
</p>
|
||||
</div>
|
||||
<SpoonCommitList
|
||||
commits={upstreamCommits.slice(0, 5)}
|
||||
empty='No upstream-only commits are cached. Refresh from GitHub to check drift.'
|
||||
/>
|
||||
</section>
|
||||
<section className='space-y-3'>
|
||||
<div>
|
||||
<h2 className='text-base font-semibold'>Fork changes</h2>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
Custom commits Spoon should preserve during maintenance.
|
||||
</p>
|
||||
</div>
|
||||
<SpoonCommitList
|
||||
commits={forkCommits.slice(0, 5)}
|
||||
empty='No fork-only commits are cached.'
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='upstream'>
|
||||
<SpoonCommitList
|
||||
commits={upstreamCommits}
|
||||
empty='No upstream changes are waiting, or this Spoon has not been refreshed yet.'
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='fork'>
|
||||
<SpoonCommitList
|
||||
commits={forkCommits}
|
||||
empty='No fork-only commits are cached. Your customizations will appear here after refresh.'
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='pulls'>
|
||||
<SpoonPrList pullRequests={pullRequests} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='ai' className='space-y-4'>
|
||||
<SpoonAiReviewPanel
|
||||
latestReview={details.latestReview}
|
||||
reviews={reviews}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='agent' className='space-y-4'>
|
||||
<AgentRequestForm
|
||||
spoon={details.spoon}
|
||||
agentSettings={agentSettings}
|
||||
/>
|
||||
<AgentJobList jobs={agentJobs} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='activity'>
|
||||
<SpoonActivityTimeline
|
||||
syncRuns={syncRuns}
|
||||
reviews={reviews}
|
||||
requests={agentRequests}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='settings' className='space-y-4'>
|
||||
<SpoonSettingsForm
|
||||
spoon={details.spoon}
|
||||
settings={details.settings}
|
||||
/>
|
||||
<SpoonAgentSettingsForm
|
||||
spoon={details.spoon}
|
||||
settings={agentSettings}
|
||||
/>
|
||||
<SpoonSecretsForm spoonId={spoonId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpoonDetailPage;
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GitHubConnectionPanel } from '@/components/github/github-connection-panel';
|
||||
import { GitHubForkForm } from '@/components/spoons/github-fork-form';
|
||||
import { NewSpoonForm } from '@/components/spoons/new-spoon-form';
|
||||
|
||||
const NewSpoonPage = () => (
|
||||
@@ -5,8 +7,18 @@ const NewSpoonPage = () => (
|
||||
<div>
|
||||
<h1 className='text-3xl font-semibold tracking-normal'>New Spoon</h1>
|
||||
<p className='text-muted-foreground mt-2 max-w-2xl'>
|
||||
Create a provider-neutral managed fork record. This does not call a Git
|
||||
provider yet; it prepares the dashboard surface for future automation.
|
||||
Connect GitHub to create real forks, or add an existing managed fork
|
||||
manually.
|
||||
</p>
|
||||
</div>
|
||||
<GitHubConnectionPanel />
|
||||
<GitHubForkForm />
|
||||
<div>
|
||||
<h2 className='text-xl font-semibold tracking-normal'>
|
||||
Add an existing fork manually
|
||||
</h2>
|
||||
<p className='text-muted-foreground mt-2 max-w-2xl text-sm'>
|
||||
Use this path for non-GitHub providers or forks that already exist.
|
||||
</p>
|
||||
</div>
|
||||
<NewSpoonForm />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { MaintenanceQueue } from '@/components/updates/maintenance-queue';
|
||||
import { useQuery } from 'convex/react';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
@@ -76,6 +77,10 @@ const UpdatesPage = () => {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section className='space-y-3'>
|
||||
<h2 className='text-lg font-semibold'>Maintenance queue</h2>
|
||||
<MaintenanceQueue spoons={spoons} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
'use server';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import {
|
||||
AvatarUpload,
|
||||
ProfileHeader,
|
||||
ResetPasswordForm,
|
||||
SignOutForm,
|
||||
UserInfoForm,
|
||||
} from '@/components/layout/auth/profile';
|
||||
import { preloadQuery } from 'convex/nextjs';
|
||||
|
||||
import { api } from '@spoon/backend/convex/_generated/api.js';
|
||||
import { Card, Separator } from '@spoon/ui';
|
||||
|
||||
const Profile = async () => {
|
||||
const preloadedUser = await preloadQuery(api.auth.getUser, {});
|
||||
const preloadedUserProvider = await preloadQuery(
|
||||
api.auth.getUserProvider,
|
||||
{},
|
||||
);
|
||||
return (
|
||||
<main className='container mx-auto px-4 py-12 md:py-16'>
|
||||
<div className='mx-auto max-w-3xl'>
|
||||
{/* Page Header */}
|
||||
<div className='mb-8 text-center'>
|
||||
<h1 className='mb-2 text-3xl font-bold tracking-tight sm:text-4xl'>
|
||||
Your Profile
|
||||
</h1>
|
||||
<p className='text-muted-foreground'>
|
||||
Manage your personal information and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Card */}
|
||||
<Card className='border-border/40'>
|
||||
<ProfileHeader />
|
||||
<AvatarUpload preloadedUser={preloadedUser} />
|
||||
<Separator className='my-6' />
|
||||
<UserInfoForm
|
||||
preloadedUser={preloadedUser}
|
||||
preloadedProvider={preloadedUserProvider}
|
||||
/>
|
||||
<ResetPasswordForm preloadedProvider={preloadedUserProvider} />
|
||||
<Separator className='my-6' />
|
||||
<SignOutForm />
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
const Profile = () => {
|
||||
redirect('/settings/profile');
|
||||
};
|
||||
export default Profile;
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AuthentikSignInButton } from '@/components/layout/auth/buttons';
|
||||
import {
|
||||
AuthentikSignInButton,
|
||||
GitHubSignInButton,
|
||||
} from '@/components/layout/auth/buttons';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ConvexError } from 'convex/values';
|
||||
@@ -342,7 +345,8 @@ const SignIn = () => {
|
||||
<Separator className='ml-3 py-0.5' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<div className='mt-3 flex flex-col items-center gap-3'>
|
||||
<GitHubSignInButton />
|
||||
<AuthentikSignInButton />
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -450,7 +454,8 @@ const SignIn = () => {
|
||||
<Separator className='ml-3 py-0.5' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<div className='mt-3 flex flex-col items-center gap-3'>
|
||||
<GitHubSignInButton type='signUp' />
|
||||
<AuthentikSignInButton type='signUp' />
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -7,9 +7,6 @@ import { Geist, Geist_Mono } from 'next/font/google';
|
||||
import '@/app/styles.css';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Footer from '@/components/layout/footer';
|
||||
import Header from '@/components/layout/header';
|
||||
import { ConvexClientProvider } from '@/components/providers';
|
||||
import { env } from '@/env';
|
||||
import { generateMetadata } from '@/lib/metadata';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
@@ -59,20 +56,13 @@ const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ConvexClientProvider>
|
||||
<main className='flex min-h-screen flex-col items-center'>
|
||||
<Header />
|
||||
<NextError statusCode={0} />
|
||||
{reset !== undefined && (
|
||||
<Button onClick={() => reset()}>Try Again</Button>
|
||||
)}
|
||||
<Toaster />
|
||||
<Footer />
|
||||
</main>
|
||||
<main className='flex min-h-[90vh] flex-col items-center'>
|
||||
<Toaster />
|
||||
</main>
|
||||
</ConvexClientProvider>
|
||||
<main className='flex min-h-screen flex-col items-center justify-center gap-4 p-6'>
|
||||
<NextError statusCode={0} />
|
||||
{reset !== undefined && (
|
||||
<Button onClick={() => reset()}>Try Again</Button>
|
||||
)}
|
||||
<Toaster />
|
||||
</main>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user