improved a lot of things
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface WorkspaceSettingsFormProps {
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
memberRole: string;
|
||||
}
|
||||
|
||||
const SECTION: React.CSSProperties = {
|
||||
background: '#FFFFFF',
|
||||
border: '1px solid rgba(0,0,0,0.07)',
|
||||
borderRadius: 14,
|
||||
padding: '24px 28px',
|
||||
marginBottom: 20,
|
||||
};
|
||||
|
||||
const LABEL: React.CSSProperties = {
|
||||
display: 'block',
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: 600,
|
||||
color: '#0A0A0A',
|
||||
marginBottom: 6,
|
||||
};
|
||||
|
||||
const INPUT: React.CSSProperties = {
|
||||
width: '100%',
|
||||
fontSize: '0.875rem',
|
||||
padding: '9px 12px',
|
||||
border: '1px solid rgba(0,0,0,0.12)',
|
||||
borderRadius: 9,
|
||||
outline: 'none',
|
||||
fontFamily: "'Inter', -apple-system, sans-serif",
|
||||
color: '#0A0A0A',
|
||||
background: '#FAFAFA',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const BTN_PRIMARY: React.CSSProperties = {
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: '#0A0A0A', color: '#FFFFFF',
|
||||
fontSize: '0.875rem', fontWeight: 600,
|
||||
padding: '9px 20px', borderRadius: 9,
|
||||
border: 'none', cursor: 'pointer', letterSpacing: '-0.01em',
|
||||
};
|
||||
|
||||
export function WorkspaceSettingsForm({ workspaceId, workspaceName, memberRole }: WorkspaceSettingsFormProps) {
|
||||
const router = useRouter();
|
||||
const isOwner = memberRole === 'OWNER';
|
||||
|
||||
/* ── Rename ── */
|
||||
const [name, setName] = useState(workspaceName);
|
||||
const [renameStatus, setRenameStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||
|
||||
async function saveName() {
|
||||
if (!name.trim() || name.trim() === workspaceName) return;
|
||||
setRenameStatus('saving');
|
||||
try {
|
||||
const res = await fetch(`/api/workspace/${workspaceId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Rename failed');
|
||||
setRenameStatus('saved');
|
||||
router.refresh();
|
||||
setTimeout(() => setRenameStatus('idle'), 2000);
|
||||
} catch {
|
||||
setRenameStatus('error');
|
||||
setTimeout(() => setRenameStatus('idle'), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── IDE Token ── */
|
||||
const [agentType, setAgentType] = useState<'CURSOR' | 'CLAUDE_CODE' | 'GENERIC'>('CLAUDE_CODE');
|
||||
const [tokenResult, setTokenResult] = useState<{
|
||||
token: string;
|
||||
cursorConfig: { cursorrules: string; settings: unknown };
|
||||
claudeCodeConfig: { claudeMd: string; settings: unknown };
|
||||
} | null>(null);
|
||||
const [tokenLoading, setTokenLoading] = useState(false);
|
||||
const [tokenError, setTokenError] = useState('');
|
||||
const [copiedToken, setCopiedToken] = useState(false);
|
||||
|
||||
async function issueToken() {
|
||||
setTokenLoading(true);
|
||||
setTokenError('');
|
||||
setTokenResult(null);
|
||||
try {
|
||||
const res = await fetch(`/api/workspace/${workspaceId}/tokens`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ agentType, workspaceName }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Token issuance failed');
|
||||
const data = await res.json() as typeof tokenResult;
|
||||
setTokenResult(data);
|
||||
} catch (e) {
|
||||
setTokenError(e instanceof Error ? e.message : 'Failed to issue token');
|
||||
} finally {
|
||||
setTokenLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToken() {
|
||||
if (!tokenResult) return;
|
||||
void navigator.clipboard.writeText(tokenResult.token);
|
||||
setCopiedToken(true);
|
||||
setTimeout(() => setCopiedToken(false), 2000);
|
||||
}
|
||||
|
||||
const configText = tokenResult
|
||||
? agentType === 'CURSOR'
|
||||
? tokenResult.cursorConfig.cursorrules
|
||||
: tokenResult.claudeCodeConfig.claudeMd
|
||||
: '';
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* General */}
|
||||
<div style={SECTION}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, color: '#0A0A0A', margin: '0 0 20px' }}>
|
||||
General
|
||||
</h2>
|
||||
<label style={LABEL} htmlFor="ws-name">Workspace name</label>
|
||||
<div style={{ display: 'flex', gap: 10, marginBottom: 4 }}>
|
||||
<input
|
||||
id="ws-name"
|
||||
style={INPUT}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') void saveName(); }}
|
||||
disabled={!isOwner}
|
||||
/>
|
||||
<button
|
||||
onClick={() => void saveName()}
|
||||
disabled={!isOwner || renameStatus === 'saving' || name.trim() === workspaceName}
|
||||
style={{
|
||||
...BTN_PRIMARY,
|
||||
opacity: (!isOwner || name.trim() === workspaceName) ? 0.4 : 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{renameStatus === 'saving' ? 'Saving…' : renameStatus === 'saved' ? '✓ Saved' : 'Rename'}
|
||||
</button>
|
||||
</div>
|
||||
{renameStatus === 'error' && (
|
||||
<span style={{ fontSize: '0.75rem', color: '#EF4444' }}>Rename failed — try again</span>
|
||||
)}
|
||||
{!isOwner && (
|
||||
<span style={{ fontSize: '0.75rem', color: '#71717A' }}>Only workspace owners can rename.</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* IDE Integration */}
|
||||
<div style={SECTION}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, color: '#0A0A0A', margin: '0 0 6px' }}>
|
||||
IDE Integration
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.8125rem', color: '#71717A', margin: '0 0 20px', lineHeight: 1.6 }}>
|
||||
Issue a signed workspace token and copy the config snippet into your editor. Tokens expire after 30 days.
|
||||
</p>
|
||||
|
||||
<label style={LABEL}>Agent type</label>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
|
||||
{(['CLAUDE_CODE', 'CURSOR', 'GENERIC'] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setAgentType(t)}
|
||||
style={{
|
||||
fontSize: '0.75rem', fontWeight: 600, padding: '6px 14px', borderRadius: 8,
|
||||
border: `1px solid ${agentType === t ? '#0066FF' : 'rgba(0,0,0,0.12)'}`,
|
||||
background: agentType === t ? 'rgba(0,102,255,0.08)' : '#FFFFFF',
|
||||
color: agentType === t ? '#0066FF' : '#52525B',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{t === 'CLAUDE_CODE' ? 'Claude Code' : t === 'CURSOR' ? 'Cursor' : 'Generic'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => void issueToken()}
|
||||
disabled={tokenLoading}
|
||||
style={{ ...BTN_PRIMARY, opacity: tokenLoading ? 0.6 : 1, marginBottom: tokenResult ? 16 : 0 }}
|
||||
>
|
||||
{tokenLoading ? 'Generating…' : 'Generate token'}
|
||||
</button>
|
||||
|
||||
{tokenError && (
|
||||
<p style={{ fontSize: '0.75rem', color: '#EF4444', marginTop: 8 }}>{tokenError}</p>
|
||||
)}
|
||||
|
||||
{tokenResult && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{/* Token display */}
|
||||
<label style={{ ...LABEL, marginBottom: 6 }}>Workspace token</label>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<input
|
||||
readOnly
|
||||
value={tokenResult.token}
|
||||
style={{ ...INPUT, fontFamily: 'monospace', fontSize: '0.75rem', color: '#0066FF' }}
|
||||
/>
|
||||
<button
|
||||
onClick={copyToken}
|
||||
style={{
|
||||
...BTN_PRIMARY, background: copiedToken ? '#10B981' : '#0066FF', flexShrink: 0,
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
{copiedToken ? '✓ Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Config snippet */}
|
||||
<label style={{ ...LABEL, marginBottom: 6 }}>
|
||||
{agentType === 'CURSOR' ? '.cursorrules snippet' : 'CLAUDE.md snippet'}
|
||||
</label>
|
||||
<pre style={{
|
||||
background: '#0A0A0A', color: 'rgba(255,255,255,0.75)',
|
||||
borderRadius: 10, padding: '14px 16px', fontSize: '0.625rem',
|
||||
fontFamily: 'monospace', overflowX: 'auto', lineHeight: 1.7,
|
||||
margin: 0, maxHeight: 260, overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
||||
}}>
|
||||
{configText}
|
||||
</pre>
|
||||
<p style={{ fontSize: '0.75rem', color: '#71717A', marginTop: 8, lineHeight: 1.5 }}>
|
||||
Paste this into your{' '}
|
||||
{agentType === 'CURSOR' ? (
|
||||
<code style={{ fontFamily: 'monospace', background: 'rgba(0,0,0,0.06)', padding: '1px 5px', borderRadius: 4 }}>.cursorrules</code>
|
||||
) : (
|
||||
<code style={{ fontFamily: 'monospace', background: 'rgba(0,0,0,0.06)', padding: '1px 5px', borderRadius: 4 }}>CLAUDE.md</code>
|
||||
)}{' '}
|
||||
file to enable the Originmain MCP server in your editor.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
{isOwner && (
|
||||
<div style={{ ...SECTION, borderColor: 'rgba(239,68,68,0.2)', background: 'rgba(239,68,68,0.02)' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 600, color: '#EF4444', margin: '0 0 8px' }}>
|
||||
Danger zone
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.8125rem', color: '#71717A', margin: '0 0 16px', lineHeight: 1.6 }}>
|
||||
Deleting this workspace is permanent and cannot be undone. All projects and artboards will be lost.
|
||||
</p>
|
||||
<button
|
||||
style={{
|
||||
fontSize: '0.875rem', fontWeight: 600, padding: '9px 20px', borderRadius: 9,
|
||||
border: '1px solid rgba(239,68,68,0.35)', background: 'transparent',
|
||||
color: '#EF4444', cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (window.confirm(`Delete workspace "${workspaceName}"? This cannot be undone.`)) {
|
||||
// TODO: call DELETE /api/workspace/:id when implemented
|
||||
window.alert('Delete endpoint not yet implemented — coming soon.');
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete workspace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user