feat: developers UI, description
This commit is contained in:
parent
40d9d11f72
commit
b0832740d0
2 changed files with 1003 additions and 412 deletions
|
|
@ -3,7 +3,6 @@
|
|||
import { FC, useCallback, useState } from 'react';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import useSWR from 'swr';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useDecisionModal, useModals } from '@gitroom/frontend/components/layout/new-modal';
|
||||
import { MediaBox } from '@gitroom/frontend/components/media/media.component';
|
||||
|
|
@ -27,6 +26,41 @@ const useOAuthApp = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const CopyButton = ({
|
||||
text,
|
||||
label,
|
||||
}: {
|
||||
text: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const toaster = useToaster();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
copy(text);
|
||||
toaster.show(`${label} copied to clipboard`, 'success');
|
||||
}}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
||||
</svg>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeveloperComponent: FC = () => {
|
||||
const fetch = useFetch();
|
||||
const toaster = useToaster();
|
||||
|
|
@ -175,314 +209,397 @@ export const DeveloperComponent: FC = () => {
|
|||
}
|
||||
}, [decision]);
|
||||
|
||||
const copyToClipboard = useCallback(
|
||||
(text: string, label: string) => {
|
||||
copy(text);
|
||||
toaster.show(`${label} copied to clipboard`, 'success');
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (app === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// No app yet — show create prompt
|
||||
if (!app && !creating) {
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">{t('developer', 'Developer')}</h3>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
{t(
|
||||
'create_an_oauth_application',
|
||||
'Create an OAuth application to allow third-party integrations with Postiz on behalf of your users.'
|
||||
)}
|
||||
<br />
|
||||
<div className="flex flex-col gap-[40px]">
|
||||
<div className="text-[14px] text-textColor leading-[1.7]">
|
||||
{t(
|
||||
'oauth_app_note_line1',
|
||||
'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'oauth_app_note_line2',
|
||||
'After a user completes the OAuth2 flow, you receive a pos_ prefixed token that works everywhere an API Key does — API, MCP, and CLI.'
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('oauth_application', 'OAuth Application')}
|
||||
</div>
|
||||
<div className="text-[13px] text-customColor18 mt-[2px]">
|
||||
{t(
|
||||
'create_an_oauth_application',
|
||||
'Create an OAuth application to allow third-party integrations with Postiz on behalf of your users.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
||||
<a
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
href="https://docs.postiz.com/public-api/oauth"
|
||||
target="_blank"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
||||
{t('read_the_docs', 'Docs')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-[20px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreating(true)}
|
||||
className="cursor-pointer px-[20px] h-[44px] bg-[#612BD3] hover:bg-[#5520CB] transition-colors text-white rounded-[8px] text-[15px] font-[600]"
|
||||
>
|
||||
{t('create_oauth_app', 'Create OAuth App')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Create form
|
||||
if (creating && !app) {
|
||||
return (
|
||||
<div className="flex flex-col gap-[40px]">
|
||||
<div className="text-[14px] text-textColor leading-[1.7]">
|
||||
{t(
|
||||
'oauth_app_note_line1',
|
||||
'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'oauth_app_note_line2',
|
||||
'After a user completes the OAuth2 flow, you receive a pos_ prefixed token that works everywhere an API Key does — API, MCP, and CLI.'
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder">
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('create_oauth_app', 'Create OAuth App')}
|
||||
</div>
|
||||
<div className="text-[13px] text-customColor18 mt-[2px]">
|
||||
{t(
|
||||
'fill_in_the_details_for_your_oauth_application',
|
||||
'Fill in the details for your OAuth application.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('app_name', 'App Name')} *
|
||||
</label>
|
||||
<input
|
||||
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My Application"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('description', 'Description')}
|
||||
</label>
|
||||
<textarea
|
||||
className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-textColor outline-none min-h-[80px]"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Describe what your app does"
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('profile_picture', 'Profile Picture')}
|
||||
</label>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
{picturePath ? (
|
||||
<img
|
||||
src={picturePath}
|
||||
alt="App picture"
|
||||
className="w-[48px] h-[48px] rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-btnSimple flex items-center justify-center text-customColor18">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openMedia}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600]"
|
||||
>
|
||||
{t('choose_image', 'Choose Image')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('redirect_url', 'Redirect URL')} *
|
||||
</label>
|
||||
<input
|
||||
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
||||
value={redirectUrl}
|
||||
onChange={(e) => setRedirectUrl(e.target.value)}
|
||||
placeholder="https://yourapp.com/callback"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={createApp}
|
||||
className="cursor-pointer px-[20px] h-[44px] bg-[#612BD3] hover:bg-[#5520CB] transition-colors text-white rounded-[8px] text-[15px] font-[600]"
|
||||
>
|
||||
{t('create', 'Create')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreating(false)}
|
||||
className="cursor-pointer px-[20px] h-[44px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[15px] font-[600]"
|
||||
>
|
||||
{t('cancel', 'Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// App exists — show details
|
||||
return (
|
||||
<div className="flex flex-col gap-[40px]">
|
||||
<div className="text-[14px] text-textColor leading-[1.7]">
|
||||
{t(
|
||||
'oauth_app_note_line1',
|
||||
'Create an OAuth App to let other Postiz users authorize your product to post on their behalf.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'oauth_app_note_line2',
|
||||
'After a user completes the OAuth2 flow, you receive a pos_ prefixed token that works everywhere an API Key does — API, MCP, and CLI.'
|
||||
)}
|
||||
</div>
|
||||
{/* App details / edit */}
|
||||
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('oauth_application', 'OAuth Application')}
|
||||
</div>
|
||||
<div className="text-[13px] text-customColor18 mt-[2px]">
|
||||
{t(
|
||||
'manage_your_oauth_application',
|
||||
'Manage your OAuth application for third-party integrations.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
||||
<a
|
||||
className="underline hover:font-bold hover:underline"
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
href="https://docs.postiz.com/public-api/oauth"
|
||||
target="_blank"
|
||||
>
|
||||
{t(
|
||||
'read_the_oauth_documentation',
|
||||
'Read the OAuth documentation.'
|
||||
)}
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
||||
{t('read_the_docs', 'Docs')}
|
||||
</a>
|
||||
</div>
|
||||
<div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px]">
|
||||
<Button onClick={() => setCreating(true)}>
|
||||
{t('create_oauth_app', 'Create OAuth App')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (creating && !app) {
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">
|
||||
{t('create_oauth_app', 'Create OAuth App')}
|
||||
</h3>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
{t(
|
||||
'fill_in_the_details_for_your_oauth_application',
|
||||
'Fill in the details for your OAuth application.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">{t('app_name', 'App Name')} *</label>
|
||||
<input
|
||||
className="bg-input border border-fifth rounded-[4px] p-[8px] text-textColor outline-none"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My Application"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">
|
||||
{t('description', 'Description')}
|
||||
</label>
|
||||
<textarea
|
||||
className="bg-input border border-fifth rounded-[4px] p-[8px] text-textColor outline-none min-h-[80px]"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Describe what your app does"
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">
|
||||
{t('profile_picture', 'Profile Picture')}
|
||||
</label>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
{picturePath ? (
|
||||
<img
|
||||
src={picturePath}
|
||||
alt="App picture"
|
||||
className="w-[48px] h-[48px] rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-fifth flex items-center justify-center text-customColor18">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={openMedia}>
|
||||
{t('choose_image', 'Choose Image')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">
|
||||
{t('redirect_url', 'Redirect URL')} *
|
||||
</label>
|
||||
<input
|
||||
className="bg-input border border-fifth rounded-[4px] p-[8px] text-textColor outline-none"
|
||||
value={redirectUrl}
|
||||
onChange={(e) => setRedirectUrl(e.target.value)}
|
||||
placeholder="https://yourapp.com/callback"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-[10px]">
|
||||
<Button onClick={createApp}>
|
||||
{t('create', 'Create')}
|
||||
</Button>
|
||||
<Button onClick={() => setCreating(false)}>
|
||||
{t('cancel', 'Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">{t('developer', 'Developer')}</h3>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
{t(
|
||||
'manage_your_oauth_application',
|
||||
'Manage your OAuth application for third-party integrations.'
|
||||
)}
|
||||
<br />
|
||||
<a
|
||||
className="underline hover:font-bold hover:underline"
|
||||
href="https://docs.postiz.com/public-api/oauth"
|
||||
target="_blank"
|
||||
>
|
||||
{t(
|
||||
'read_the_oauth_documentation',
|
||||
'Read the OAuth documentation.'
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">{t('app_name', 'App Name')} *</label>
|
||||
<input
|
||||
className="bg-input border border-fifth rounded-[4px] p-[8px] text-textColor outline-none"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My Application"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">
|
||||
{t('description', 'Description')}
|
||||
</label>
|
||||
<textarea
|
||||
className="bg-input border border-fifth rounded-[4px] p-[8px] text-textColor outline-none min-h-[80px]"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Describe what your app does"
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">
|
||||
{t('profile_picture', 'Profile Picture')}
|
||||
</label>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
{picturePath ? (
|
||||
<img
|
||||
src={picturePath}
|
||||
alt="App picture"
|
||||
className="w-[48px] h-[48px] rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-fifth flex items-center justify-center text-customColor18">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={openMedia}>
|
||||
{t('choose_image', 'Choose Image')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px]">
|
||||
{t('redirect_url', 'Redirect URL')} *
|
||||
</label>
|
||||
<input
|
||||
className="bg-input border border-fifth rounded-[4px] p-[8px] text-textColor outline-none"
|
||||
value={redirectUrl}
|
||||
onChange={(e) => setRedirectUrl(e.target.value)}
|
||||
placeholder="https://yourapp.com/callback"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-[10px]">
|
||||
<Button onClick={updateApp}>
|
||||
{t('save', 'Save')}
|
||||
</Button>
|
||||
<Button onClick={() => setEditing(false)}>
|
||||
{t('cancel', 'Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[16px]">
|
||||
<div className="flex items-center gap-[12px]">
|
||||
{app.picture?.path ? (
|
||||
<img
|
||||
src={app.picture.path}
|
||||
alt={app.name}
|
||||
className="w-[48px] h-[48px] rounded-full object-cover"
|
||||
{editing ? (
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('app_name', 'App Name')} *
|
||||
</label>
|
||||
<input
|
||||
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My Application"
|
||||
maxLength={100}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-fifth flex items-center justify-center text-customColor18">
|
||||
{app.name?.[0]?.toUpperCase() || '?'}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('description', 'Description')}
|
||||
</label>
|
||||
<textarea
|
||||
className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-textColor outline-none min-h-[80px]"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Describe what your app does"
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('profile_picture', 'Profile Picture')}
|
||||
</label>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
{picturePath ? (
|
||||
<img
|
||||
src={picturePath}
|
||||
alt="App picture"
|
||||
className="w-[48px] h-[48px] rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-btnSimple flex items-center justify-center text-customColor18">
|
||||
?
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openMedia}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600]"
|
||||
>
|
||||
{t('choose_image', 'Choose Image')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-[16px] font-bold">{app.name}</div>
|
||||
{app.description && (
|
||||
<div className="text-customColor18 text-[14px]">
|
||||
{app.description}
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<label className="text-[13px] font-[600] text-customColor18">
|
||||
{t('redirect_url', 'Redirect URL')} *
|
||||
</label>
|
||||
<input
|
||||
className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] text-textColor outline-none"
|
||||
value={redirectUrl}
|
||||
onChange={(e) => setRedirectUrl(e.target.value)}
|
||||
placeholder="https://yourapp.com/callback"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={updateApp}
|
||||
className="cursor-pointer px-[20px] h-[44px] bg-[#612BD3] hover:bg-[#5520CB] transition-colors text-white rounded-[8px] text-[15px] font-[600]"
|
||||
>
|
||||
{t('save', 'Save')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditing(false)}
|
||||
className="cursor-pointer px-[20px] h-[44px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[15px] font-[600]"
|
||||
>
|
||||
{t('cancel', 'Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
<div className="flex items-center gap-[12px]">
|
||||
{app.picture?.path ? (
|
||||
<img
|
||||
src={app.picture.path}
|
||||
alt={app.name}
|
||||
className="w-[48px] h-[48px] rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[48px] h-[48px] rounded-full bg-btnSimple flex items-center justify-center text-customColor18 text-[18px] font-[600]">
|
||||
{app.name?.[0]?.toUpperCase() || '?'}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-[15px] font-[600]">{app.name}</div>
|
||||
{app.description && (
|
||||
<div className="text-customColor18 text-[13px]">
|
||||
{app.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<div className="text-[13px] font-[600] text-customColor18">
|
||||
{t('redirect_url', 'Redirect URL')}
|
||||
</div>
|
||||
<div className="text-[14px]">{app.redirectUrl}</div>
|
||||
</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={startEditing}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7" /><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z" /></svg>
|
||||
{t('edit_app', 'Edit App')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px] text-customColor18">
|
||||
{t('redirect_url', 'Redirect URL')}
|
||||
</label>
|
||||
<div className="text-[14px]">{app.redirectUrl}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button onClick={startEditing}>
|
||||
{t('edit_app', 'Edit App')}
|
||||
</Button>
|
||||
{/* Credentials */}
|
||||
<div className="bg-newBgColorInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder">
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('credentials', 'Credentials')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-[12px]">
|
||||
<h4 className="text-[16px]">{t('credentials', 'Credentials')}</h4>
|
||||
|
||||
<div className="bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px] text-customColor18">
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="text-[13px] font-[600] text-customColor18">
|
||||
{t('client_id', 'Client ID')}
|
||||
</label>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
<code className="text-[14px] break-all">{app.clientId}</code>
|
||||
<Button onClick={() => copyToClipboard(app.clientId, 'Client ID')}>
|
||||
{t('copy', 'Copy')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] flex items-center overflow-hidden">
|
||||
<code className="text-[14px] flex-1 truncate">{app.clientId}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[4px]">
|
||||
<label className="text-[14px] text-customColor18">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="text-[13px] font-[600] text-customColor18">
|
||||
{t('client_secret', 'Client Secret')}
|
||||
</label>
|
||||
<div className="flex items-center gap-[12px]">
|
||||
</div>
|
||||
<div className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] flex items-center overflow-hidden">
|
||||
{plaintextSecret ? (
|
||||
<code className="text-[14px] break-all">
|
||||
<code className="text-[14px] flex-1 truncate">
|
||||
{plaintextSecret}
|
||||
</code>
|
||||
) : (
|
||||
<span className="text-customColor18 text-[14px]">
|
||||
<span className="text-customColor18 text-[13px]">
|
||||
{t(
|
||||
'secret_only_shown_on_creation',
|
||||
'Secret is only shown on creation or rotation'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{plaintextSecret && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
copyToClipboard(plaintextSecret, 'Client Secret')
|
||||
}
|
||||
>
|
||||
{t('copy', 'Copy')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<CopyButton text={app.clientId} label={t('copy_id', 'Copy ID')} />
|
||||
{plaintextSecret && (
|
||||
<CopyButton
|
||||
text={plaintextSecret}
|
||||
label={t('copy_secret', 'Copy Secret')}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={rotateSecret}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.5 2v6h-6" /><path d="M21.34 15.57a10 10 0 11-.57-8.38L21.5 8" /></svg>
|
||||
{t('rotate_secret', 'Rotate Secret')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteApp}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-red-600 hover:bg-red-700 text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" /></svg>
|
||||
{t('delete_app', 'Delete App')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-[10px]">
|
||||
<Button onClick={rotateSecret}>
|
||||
{t('rotate_secret', 'Rotate Secret')}
|
||||
</Button>
|
||||
<Button onClick={deleteApp}>
|
||||
{t('delete_app', 'Delete App')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useSWRConfig } from 'swr';
|
||||
import { useUser } from '../layout/user.context';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
|
@ -13,6 +12,447 @@ import { useDecisionModal } from '@gitroom/frontend/components/layout/new-modal'
|
|||
import { DeveloperComponent } from '@gitroom/frontend/components/developer/developer.component';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const mcpClients = [
|
||||
'Claude Code',
|
||||
'Cursor',
|
||||
'VS Code / Copilot',
|
||||
'Windsurf',
|
||||
'Amp',
|
||||
'Codex',
|
||||
'Gemini CLI',
|
||||
'Warp',
|
||||
] as const;
|
||||
|
||||
type McpClient = (typeof mcpClients)[number];
|
||||
|
||||
const getMcpConfig = (
|
||||
client: McpClient,
|
||||
method: 'header' | 'path',
|
||||
mcpBase: string,
|
||||
apiKey: string
|
||||
): { config: string; hint: string } => {
|
||||
const urlWithKey = `${mcpBase}/mcp/${apiKey}`;
|
||||
const urlBase = `${mcpBase}/mcp`;
|
||||
const bearer = `Bearer ${apiKey}`;
|
||||
|
||||
const json = (obj: object) => JSON.stringify(obj, null, 2);
|
||||
|
||||
if (method === 'path') {
|
||||
switch (client) {
|
||||
case 'Claude Code':
|
||||
return {
|
||||
config: `claude mcp add postiz --transport http "${urlWithKey}"`,
|
||||
hint: 'Run this command in your terminal.',
|
||||
};
|
||||
case 'Cursor':
|
||||
return {
|
||||
config: json({ mcpServers: { postiz: { url: urlWithKey } } }),
|
||||
hint: 'Add to .cursor/mcp.json in your project root.',
|
||||
};
|
||||
case 'VS Code / Copilot':
|
||||
return {
|
||||
config: json({
|
||||
servers: { postiz: { type: 'http', url: urlWithKey } },
|
||||
}),
|
||||
hint: 'Add to .vscode/mcp.json in your project root.',
|
||||
};
|
||||
case 'Windsurf':
|
||||
return {
|
||||
config: json({
|
||||
mcpServers: { postiz: { serverUrl: urlWithKey } },
|
||||
}),
|
||||
hint: 'Add to ~/.codeium/windsurf/mcp_config.json',
|
||||
};
|
||||
case 'Amp':
|
||||
return {
|
||||
config: `amp mcp add postiz ${urlWithKey}`,
|
||||
hint: 'Run this command in your terminal.',
|
||||
};
|
||||
case 'Codex':
|
||||
return {
|
||||
config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlWithKey}"`,
|
||||
hint: 'Add to ~/.codex/config.toml',
|
||||
};
|
||||
case 'Gemini CLI':
|
||||
return {
|
||||
config: json({ mcpServers: { postiz: { url: urlWithKey } } }),
|
||||
hint: 'Add to ~/.gemini/settings.json',
|
||||
};
|
||||
case 'Warp':
|
||||
return {
|
||||
config: json({ postiz: { url: urlWithKey } }),
|
||||
hint: 'Settings > MCP Servers > + Add, then paste this config.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (client) {
|
||||
case 'Claude Code':
|
||||
return {
|
||||
config: `claude mcp add postiz \\\n --transport http \\\n --header "Authorization: ${bearer}" \\\n "${urlBase}"`,
|
||||
hint: 'Run this command in your terminal.',
|
||||
};
|
||||
case 'Cursor':
|
||||
return {
|
||||
config: json({
|
||||
mcpServers: {
|
||||
postiz: { url: urlBase, headers: { Authorization: bearer } },
|
||||
},
|
||||
}),
|
||||
hint: 'Add to .cursor/mcp.json in your project root.',
|
||||
};
|
||||
case 'VS Code / Copilot':
|
||||
return {
|
||||
config: json({
|
||||
servers: {
|
||||
postiz: {
|
||||
type: 'http',
|
||||
url: urlBase,
|
||||
headers: { Authorization: bearer },
|
||||
},
|
||||
},
|
||||
}),
|
||||
hint: 'Add to .vscode/mcp.json in your project root.',
|
||||
};
|
||||
case 'Windsurf':
|
||||
return {
|
||||
config: json({
|
||||
mcpServers: {
|
||||
postiz: {
|
||||
serverUrl: urlBase,
|
||||
headers: { Authorization: bearer },
|
||||
},
|
||||
},
|
||||
}),
|
||||
hint: 'Add to ~/.codeium/windsurf/mcp_config.json',
|
||||
};
|
||||
case 'Amp':
|
||||
return {
|
||||
config: json({
|
||||
'amp.mcpServers': {
|
||||
postiz: { url: urlBase, headers: { Authorization: bearer } },
|
||||
},
|
||||
}),
|
||||
hint: 'Add to your Amp settings.json',
|
||||
};
|
||||
case 'Codex':
|
||||
return {
|
||||
config: `# ~/.codex/config.toml\n\n[mcp_servers.postiz]\nurl = "${urlBase}"\nhttp_headers = { "Authorization" = "${bearer}" }`,
|
||||
hint: 'Add to ~/.codex/config.toml',
|
||||
};
|
||||
case 'Gemini CLI':
|
||||
return {
|
||||
config: json({
|
||||
mcpServers: {
|
||||
postiz: { url: urlBase, headers: { Authorization: bearer } },
|
||||
},
|
||||
}),
|
||||
hint: 'Add to ~/.gemini/settings.json',
|
||||
};
|
||||
case 'Warp':
|
||||
return {
|
||||
config: json({
|
||||
postiz: { url: urlBase, headers: { Authorization: bearer } },
|
||||
}),
|
||||
hint: 'Settings > MCP Servers > + Add, then paste this config.',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const CopyButton = ({
|
||||
text,
|
||||
label,
|
||||
}: {
|
||||
text: string;
|
||||
label: string;
|
||||
}) => {
|
||||
const toaster = useToaster();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
copy(text);
|
||||
toaster.show(`${label} copied to clipboard`, 'success');
|
||||
}}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
||||
</svg>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const McpSection = ({
|
||||
user,
|
||||
mcpBase,
|
||||
}: {
|
||||
user: { publicApi: string };
|
||||
mcpBase: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [activeClient, setActiveClient] = useState<McpClient>('Claude Code');
|
||||
const [method, setMethod] = useState<'header' | 'path'>('header');
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const { config, hint } = getMcpConfig(
|
||||
activeClient,
|
||||
method,
|
||||
mcpBase,
|
||||
user.publicApi
|
||||
);
|
||||
|
||||
const maskedConfig = revealed
|
||||
? config
|
||||
: config.replace(new RegExp(user.publicApi.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '*'.repeat(user.publicApi.length));
|
||||
|
||||
return (
|
||||
<div className="bg-newBgColorInnerInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('mcp_client_configuration', 'MCP Client Configuration')}
|
||||
</div>
|
||||
<div className="text-[13px] text-customColor18 mt-[2px]">
|
||||
{t(
|
||||
'connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster',
|
||||
'Connect Postiz MCP server to your client (Http streaming) to schedule your posts faster.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
||||
<a
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
href="https://docs.postiz.com/mcp/introduction"
|
||||
target="_blank"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
||||
{t('read_the_docs', 'Docs')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="text-[13px] font-[600] text-customColor18">
|
||||
{t('auth_method', 'Authentication')}
|
||||
</div>
|
||||
<div className="flex gap-[6px]">
|
||||
{(['header', 'path'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'cursor-pointer px-[14px] h-[36px] text-[13px] font-[500] rounded-[8px] transition-colors',
|
||||
method === m
|
||||
? 'bg-[#612BD3] text-white'
|
||||
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
|
||||
)}
|
||||
onClick={() => setMethod(m)}
|
||||
>
|
||||
{m === 'header'
|
||||
? t('authorization_header', 'Authorization Header')
|
||||
: t('api_key_in_url', 'API Key in URL')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className="text-[13px] font-[600] text-customColor18">
|
||||
{t('mcp_client', 'Client')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-[6px]">
|
||||
{mcpClients.map((client) => (
|
||||
<button
|
||||
key={client}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'cursor-pointer px-[14px] h-[36px] text-[13px] font-[500] rounded-[8px] transition-colors',
|
||||
activeClient === client
|
||||
? 'bg-[#612BD3] text-white'
|
||||
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
|
||||
)}
|
||||
onClick={() => setActiveClient(client)}
|
||||
>
|
||||
{client}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[8px]">
|
||||
<div className="text-[12px] text-customColor18 font-[500]">
|
||||
{hint}
|
||||
</div>
|
||||
<pre className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-[13px] whitespace-pre-wrap break-all overflow-x-auto leading-[1.6]">
|
||||
{maskedConfig}
|
||||
</pre>
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRevealed(!revealed)}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{revealed ? (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
|
||||
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
{revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')}
|
||||
</button>
|
||||
<CopyButton text={config} label={t('copy', 'Copy')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const cliSteps = [
|
||||
{
|
||||
label: 'Install the CLI',
|
||||
code: 'npm install -g postiz',
|
||||
},
|
||||
{
|
||||
label: 'Set your API key, copy it to your secret files',
|
||||
code: 'export POSTIZ_API_KEY="{API_KEY}"',
|
||||
},
|
||||
{
|
||||
label: 'Install the Postiz skill for your AI agent',
|
||||
code: 'npx skills add gitroomhq/postiz-agent',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const CliSection = ({
|
||||
apiKey,
|
||||
backendUrl,
|
||||
}: {
|
||||
apiKey: string;
|
||||
backendUrl: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const toaster = useToaster();
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const steps = cliSteps.map((step) => ({
|
||||
...step,
|
||||
code: step.code.replace('{API_KEY}', apiKey),
|
||||
}));
|
||||
|
||||
const maskedSteps = steps.map((step) => ({
|
||||
...step,
|
||||
code: revealed
|
||||
? step.code
|
||||
: step.code.replace(
|
||||
new RegExp(apiKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
|
||||
'*'.repeat(apiKey.length)
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="bg-newBgColorInnerInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('cli_and_skills', 'CLI & AI Skills')}
|
||||
</div>
|
||||
<div className="text-[13px] text-customColor18 mt-[2px]">
|
||||
{t(
|
||||
'cli_description',
|
||||
'Use the Postiz CLI to automate posting from your terminal, or install the skill to let your AI agent schedule posts for you.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
||||
<a
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
href="https://docs.postiz.com/cli/introduction"
|
||||
target="_blank"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
||||
{t('read_the_docs', 'Docs')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
{maskedSteps.map((step, i) => (
|
||||
<div key={i} className="flex flex-col gap-[6px]">
|
||||
<div className="text-[13px] font-[600] text-customColor18">
|
||||
{i + 1}. {step.label}
|
||||
</div>
|
||||
<pre className="bg-newBgColorInner border border-newBorder rounded-[8px] p-[16px] text-[13px] whitespace-pre-wrap break-all overflow-x-auto leading-[1.6]">
|
||||
{step.code}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRevealed(!revealed)}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{revealed ? (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
|
||||
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
{revealed ? t('hide', 'Hide') : t('reveal', 'Reveal')}
|
||||
</button>
|
||||
<CopyButton
|
||||
text={steps.map((s) => s.code).join(' && ')}
|
||||
label={t('copy_all', 'Copy All')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PublicApiContent = () => {
|
||||
const user = useUser();
|
||||
const { backendUrl, frontEndUrl, mcpUrl } = useVariables();
|
||||
|
|
@ -21,150 +461,191 @@ const PublicApiContent = () => {
|
|||
const decision = useDecisionModal();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [reveal, setReveal] = useState(false);
|
||||
const [reveal2, setReveal2] = useState(false);
|
||||
const copyToClipboard = useCallback(() => {
|
||||
toaster.show('API Key copied to clipboard', 'success');
|
||||
copy(user?.publicApi!);
|
||||
}, [user]);
|
||||
const copyToClipboard2 = useCallback(() => {
|
||||
toaster.show('MCP copied to clipboard', 'success');
|
||||
copy(`${mcpUrl || backendUrl}/mcp/` + user?.publicApi);
|
||||
}, [user]);
|
||||
const t = useT();
|
||||
|
||||
const rotateKey = useCallback(async () => {
|
||||
const approved = await decision.open({
|
||||
title: 'Rotate API Key?',
|
||||
description:
|
||||
'This will generate a new API key and invalidate the current one. Any integrations using the old key will stop working.',
|
||||
approveLabel: 'Rotate',
|
||||
cancelLabel: 'Cancel',
|
||||
title: t('rotate_api_key', 'Rotate API Key?'),
|
||||
description: t(
|
||||
'rotate_api_key_description',
|
||||
'This will generate a new API key and invalidate the current one. Any integrations using the old key will stop working.'
|
||||
),
|
||||
approveLabel: t('rotate', 'Rotate'),
|
||||
cancelLabel: t('cancel', 'Cancel'),
|
||||
});
|
||||
if (!approved) return;
|
||||
await fetch('/user/api-key/rotate', { method: 'POST' });
|
||||
await mutate('/user/self');
|
||||
setReveal(false);
|
||||
setReveal2(false);
|
||||
toaster.show('API Key rotated successfully', 'success');
|
||||
toaster.show(
|
||||
t('api_key_rotated', 'API Key rotated successfully'),
|
||||
'success'
|
||||
);
|
||||
}, [decision, fetch, mutate, toaster]);
|
||||
|
||||
const t = useT();
|
||||
|
||||
if (!user || !user.publicApi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mcpBase = mcpUrl || backendUrl;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">{t('public_api', 'Public API')}</h3>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
{t(
|
||||
'use_postiz_api_to_integrate_with_your_tools',
|
||||
'Use Postiz API to integrate with your tools.'
|
||||
)}
|
||||
<br />
|
||||
<a
|
||||
className="underline hover:font-bold hover:underline"
|
||||
href="https://docs.postiz.com/public-api"
|
||||
target="_blank"
|
||||
>
|
||||
{t(
|
||||
'read_how_to_use_it_over_the_documentation',
|
||||
'Read how to use it over the documentation.'
|
||||
)}
|
||||
</a>
|
||||
<a
|
||||
className="underline hover:font-bold hover:underline"
|
||||
href="https://www.npmjs.com/package/n8n-nodes-postiz"
|
||||
target="_blank"
|
||||
>
|
||||
<br />
|
||||
{t('check_n8n', 'Check out our N8N custom node for Postiz.')}
|
||||
</a>
|
||||
<div className="flex flex-col gap-[40px]">
|
||||
<div className="text-[14px] text-textColor leading-[1.7]">
|
||||
{t(
|
||||
'api_auth_note_line1',
|
||||
'Use your API Key to automate your own account.'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'api_auth_note_line2',
|
||||
'If you are building a product that schedules posts on behalf of other Postiz users,'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'api_auth_note_line3',
|
||||
'create an OAuth App under the "Apps" tab. Your users will authorize your app via OAuth2,'
|
||||
)}
|
||||
<br />
|
||||
{t(
|
||||
'api_auth_note_line4',
|
||||
'and you will receive a pos_ prefixed token that works with the API, MCP, and CLI — just like an API Key.'
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-newBgColorInnerInner rounded-[12px] border border-newBorder overflow-hidden">
|
||||
<div className="bg-newBgColorInner px-[20px] py-[14px] border-b border-newBorder flex items-start justify-between gap-[12px]">
|
||||
<div>
|
||||
<div className="text-[15px] font-[600]">
|
||||
{t('api_key', 'API Key')}
|
||||
</div>
|
||||
<div className="text-[13px] text-customColor18 mt-[2px]">
|
||||
{t(
|
||||
'use_postiz_api_to_integrate_with_your_tools',
|
||||
'Use Postiz API to integrate with your tools.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[6px] shrink-0 pt-[2px]">
|
||||
<a
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
href="https://docs.postiz.com/public-api"
|
||||
target="_blank"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
||||
{t('read_the_docs', 'Docs')}
|
||||
</a>
|
||||
<a
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-[#612BD3] hover:bg-[#5520CB] text-white transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
href="https://www.npmjs.com/package/n8n-nodes-postiz"
|
||||
target="_blank"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
|
||||
{t('n8n_node', 'N8N Node')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
|
||||
<div className="flex items-center">
|
||||
<div className="p-[20px] flex flex-col gap-[16px]">
|
||||
<div className="bg-newBgColorInner border border-newBorder rounded-[8px] px-[16px] h-[44px] flex items-center overflow-hidden">
|
||||
<code className="text-[14px] flex-1 truncate">
|
||||
{reveal ? (
|
||||
user.publicApi
|
||||
) : (
|
||||
<>
|
||||
<div className="blur-sm">{user.publicApi.slice(0, -5)}</div>
|
||||
<div>{user.publicApi.slice(-5)}</div>
|
||||
</>
|
||||
<span className="flex items-center">
|
||||
<span className="blur-sm select-none">
|
||||
{user.publicApi.slice(0, -5)}
|
||||
</span>
|
||||
<span>{user.publicApi.slice(-5)}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!reveal ? (
|
||||
<Button onClick={() => setReveal(true)}>
|
||||
{t('reveal', 'Reveal')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={copyToClipboard}>
|
||||
{t('copy_key', 'Copy Key')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={rotateKey}>
|
||||
<div className="flex gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReveal(!reveal)}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{reveal ? (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94" />
|
||||
<path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
{reveal ? t('hide', 'Hide') : t('reveal', 'Reveal')}
|
||||
</button>
|
||||
<CopyButton text={user.publicApi} label={t('copy', 'Copy')} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={rotateKey}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21.5 2v6h-6" />
|
||||
<path d="M21.34 15.57a10 10 0 11-.57-8.38L21.5 8" />
|
||||
</svg>
|
||||
{t('rotate_key', 'Rotate Key')}
|
||||
</Button>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip-id="tooltip"
|
||||
data-tooltip-content={t(
|
||||
'payload_wizard_description',
|
||||
'Building a POST request to /posts can be complex. Use the wizard to schedule a post with the UI, then copy the generated payload.'
|
||||
)}
|
||||
onClick={() =>
|
||||
window.open(`${frontEndUrl}/modal/dark/all`, '_blank')
|
||||
}
|
||||
className="cursor-pointer px-[16px] h-[36px] bg-btnSimple hover:bg-boxHover transition-colors rounded-[8px] text-[13px] font-[600] flex items-center gap-[6px]"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
{t('open_wizard', 'Open Wizard')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">{t('mcp', 'MCP')}</h3>
|
||||
<div className="text-customColor18 mt-[4px]">
|
||||
{t(
|
||||
'connect_your_mcp_client_to_postiz_to_schedule_your_posts_faster',
|
||||
'Connect Postiz MCP server to your client (Http streaming) to schedule your posts faster.'
|
||||
)}
|
||||
</div>
|
||||
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
|
||||
<div className="flex items-center">
|
||||
{reveal2 ? (
|
||||
`${mcpUrl || backendUrl}/mcp/` + user.publicApi
|
||||
) : (
|
||||
<>
|
||||
<div className="blur-sm">
|
||||
{(`${mcpUrl || backendUrl}/mcp/` + user.publicApi).slice(0, -5)}
|
||||
</div>
|
||||
<div>{(`${mcpUrl || backendUrl}/mcp/` + user.publicApi).slice(-5)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!reveal2 ? (
|
||||
<Button onClick={() => setReveal2(true)}>
|
||||
{t('reveal', 'Reveal')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={copyToClipboard2}>
|
||||
{t('copy_key', 'Copy Key')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CliSection apiKey={user.publicApi} backendUrl={backendUrl} />
|
||||
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-[20px]">Building your Postiz payload</h3>
|
||||
<div className="text-customColor18 mt-[4px] whitespace-pre-line">
|
||||
Sending a POST request to <strong className="text-textColor">/posts</strong> might feel a bit overwhelming as many
|
||||
platforms have different requirements.{'\n'}
|
||||
We have created an easy way to build your Postiz payload to schedule
|
||||
posts. {'\n'}
|
||||
You can use the Postiz wizard, and schedule a post with our UI, after
|
||||
you added all your text and settings, the wizard will generate the
|
||||
payload for you.{'\n'}
|
||||
</div>
|
||||
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
|
||||
<Button onClick={() => window.open(`${frontEndUrl}/modal/dark/all`, '_blank')}>
|
||||
Open the payload wizard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<McpSection user={user} mcpBase={mcpBase} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -175,31 +656,24 @@ export const PublicComponent = () => {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col gap-[20px]">
|
||||
<div className="flex gap-[4px] border-b border-fifth">
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'px-[16px] py-[8px] text-[14px] rounded-t-[4px] transition-colors',
|
||||
subTab === 'api'
|
||||
? 'bg-sixth text-textColor border border-fifth border-b-0'
|
||||
: 'text-customColor18 hover:text-textColor'
|
||||
)}
|
||||
onClick={() => setSubTab('api')}
|
||||
>
|
||||
{t('public_api', 'Public API')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'px-[16px] py-[8px] text-[14px] rounded-t-[4px] transition-colors',
|
||||
subTab === 'developer'
|
||||
? 'bg-sixth text-textColor border border-fifth border-b-0'
|
||||
: 'text-customColor18 hover:text-textColor'
|
||||
)}
|
||||
onClick={() => setSubTab('developer')}
|
||||
>
|
||||
{t('apps', 'Apps')}
|
||||
</button>
|
||||
<div className="flex gap-[6px]">
|
||||
{(['api', 'developer'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'cursor-pointer px-[20px] h-[44px] text-[15px] font-[600] rounded-[8px] transition-colors',
|
||||
subTab === tab
|
||||
? 'bg-[#612BD3] text-white'
|
||||
: 'bg-btnSimple text-customColor18 hover:bg-boxHover hover:text-textColor'
|
||||
)}
|
||||
onClick={() => setSubTab(tab)}
|
||||
>
|
||||
{tab === 'api'
|
||||
? t('access', 'Access')
|
||||
: t('apps', 'Apps')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{subTab === 'api' && <PublicApiContent />}
|
||||
{subTab === 'developer' && <DeveloperComponent />}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue