mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 11:16:01 +03:00
Created Post analytics spike React app
This commit is contained in:
parent
46bdbaa3b8
commit
c229042d33
2
.github/scripts/dev.js
vendored
2
.github/scripts/dev.js
vendored
@ -74,7 +74,7 @@ const COMMAND_TYPESCRIPT = {
|
||||
env: {}
|
||||
};
|
||||
|
||||
const adminXApps = '@tryghost/admin-x-demo,@tryghost/admin-x-settings,@tryghost/admin-x-activitypub';
|
||||
const adminXApps = '@tryghost/admin-x-demo,@tryghost/admin-x-settings,@tryghost/admin-x-activitypub,@tryghost/post-analytics-spike';
|
||||
|
||||
const COMMANDS_ADMINX = [{
|
||||
name: 'adminXDeps',
|
||||
|
@ -42,7 +42,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.admin-x-base {
|
||||
.admin-x-settings {
|
||||
opacity: 1;
|
||||
animation-name: openAnimation;
|
||||
animation-iteration-count: 1;
|
||||
|
5621
apps/post-analytics-spike/dist/index-b060828d.mjs
vendored
Normal file
5621
apps/post-analytics-spike/dist/index-b060828d.mjs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
apps/post-analytics-spike/dist/index-b060828d.mjs.map
vendored
Normal file
1
apps/post-analytics-spike/dist/index-b060828d.mjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1128
apps/post-analytics-spike/dist/modals-8550f7b5.mjs
vendored
Normal file
1128
apps/post-analytics-spike/dist/modals-8550f7b5.mjs
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/post-analytics-spike/dist/modals-8550f7b5.mjs.map
vendored
Normal file
1
apps/post-analytics-spike/dist/modals-8550f7b5.mjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
apps/post-analytics-spike/dist/post-analytics-spike.js
vendored
Normal file
6
apps/post-analytics-spike/dist/post-analytics-spike.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/post-analytics-spike/dist/post-analytics-spike.js.map
vendored
Normal file
1
apps/post-analytics-spike/dist/post-analytics-spike.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"post-analytics-spike.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
13
apps/post-analytics-spike/index.html
Normal file
13
apps/post-analytics-spike/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Post Analytics Standalone</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/standalone.tsx"></script>
|
||||
</body>
|
||||
</html>
|
55
apps/post-analytics-spike/package.json
Normal file
55
apps/post-analytics-spike/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@tryghost/post-analytics-spike",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TryGhost/Ghost/tree/main/apps/post-analytics"
|
||||
},
|
||||
"author": "Ghost Foundation",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"dist/"
|
||||
],
|
||||
"main": "./dist/post-analytics-spike.umd.cjs",
|
||||
"module": "./dist/post-analytics-spike.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"dev:start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "yarn run lint:code && yarn run lint:test",
|
||||
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src",
|
||||
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@tryghost/admin-x-design-system": "0.0.0",
|
||||
"@tryghost/admin-x-framework": "0.0.0",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"dev": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:acceptance": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
apps/post-analytics-spike/playwright.config.mjs
Normal file
3
apps/post-analytics-spike/playwright.config.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright';
|
||||
|
||||
export default adminXPlaywrightConfig();
|
1
apps/post-analytics-spike/postcss.config.cjs
Normal file
1
apps/post-analytics-spike/postcss.config.cjs
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs');
|
30
apps/post-analytics-spike/src/App.tsx
Normal file
30
apps/post-analytics-spike/src/App.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import MainContent from './MainContent';
|
||||
import {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system';
|
||||
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
|
||||
import {RoutingProvider} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
interface AppProps {
|
||||
framework: TopLevelFrameworkProps;
|
||||
designSystem: DesignSystemAppProps;
|
||||
}
|
||||
|
||||
const modals = {
|
||||
paths: {
|
||||
'demo-modal': 'DemoModal'
|
||||
},
|
||||
load: async () => import('./components/modals')
|
||||
};
|
||||
|
||||
const App: React.FC<AppProps> = ({framework, designSystem}) => {
|
||||
return (
|
||||
<FrameworkProvider {...framework}>
|
||||
<RoutingProvider basePath='post-analytics-spike' modals={modals}>
|
||||
<DesignSystemApp className='post-analytics-spike' {...designSystem}>
|
||||
<MainContent />
|
||||
</DesignSystemApp>
|
||||
</RoutingProvider>
|
||||
</FrameworkProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
147
apps/post-analytics-spike/src/DetailPage.tsx
Normal file
147
apps/post-analytics-spike/src/DetailPage.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import {Avatar, Breadcrumbs, Button, Heading, Page, Toggle, ViewContainer} from '@tryghost/admin-x-design-system';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
const DetailPage: React.FC = () => {
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
return (
|
||||
<Page
|
||||
breadCrumbs={
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{
|
||||
label: 'Members',
|
||||
onClick: () => {
|
||||
updateRoute('');
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Emerson Vaccaro'
|
||||
}
|
||||
]}
|
||||
onBack={() => {
|
||||
updateRoute('');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
fullBleedPage={false}
|
||||
>
|
||||
<ViewContainer
|
||||
firstOnPage={false}
|
||||
headerContent={
|
||||
<div>
|
||||
<Avatar bgColor='#A5D5F7' image='https://i.pravatar.cc/150' label='EV' labelColor='white' size='2xl' />
|
||||
<Heading className='mt-2' level={1}>Emerson Vaccaro</Heading>
|
||||
<div className=''>Colombus, OH</div>
|
||||
</div>
|
||||
}
|
||||
primaryAction={
|
||||
{
|
||||
icon: 'ellipsis',
|
||||
color: 'outline'
|
||||
}
|
||||
}
|
||||
type='page'
|
||||
>
|
||||
<div className='grid grid-cols-3 border-b border-grey-200 pb-5 tablet:grid-cols-4'>
|
||||
<div className='col-span-3 -ml-5 mb-5 hidden h-full gap-4 px-5 tablet:!visible tablet:col-span-1 tablet:mb-0 tablet:!flex tablet:flex-col tablet:gap-0'>
|
||||
<span>Last seen on <strong>22 June 2023</strong></span>
|
||||
<span className='tablet:mt-2'>Created on <strong>27 Jan 2021</strong></span>
|
||||
</div>
|
||||
<div className='flex h-full flex-col tablet:px-5'>
|
||||
<Heading level={6}>Emails received</Heading>
|
||||
<span className='mt-1 text-4xl font-bold leading-none'>181</span>
|
||||
</div>
|
||||
<div className='flex h-full flex-col tablet:px-5'>
|
||||
<Heading level={6}>Emails opened</Heading>
|
||||
<span className='mt-1 text-4xl font-bold leading-none'>104</span>
|
||||
</div>
|
||||
<div className='-mr-5 flex h-full flex-col tablet:px-5'>
|
||||
<Heading level={6}>Average open rate</Heading>
|
||||
<span className='mt-1 text-4xl font-bold leading-none'>57%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 items-baseline border-b border-grey-200 py-5 tablet:grid-cols-4'>
|
||||
<div className='-ml-5 flex h-full flex-col gap-6 border-r border-grey-200 px-5'>
|
||||
<div className='flex justify-between'>
|
||||
<Heading level={5}>Member data</Heading>
|
||||
<Button color='green' label='Edit' link />
|
||||
</div>
|
||||
<div>
|
||||
<Heading level={6}>Name</Heading>
|
||||
<div>Emerson Vaccaro</div>
|
||||
</div>
|
||||
<div>
|
||||
<Heading level={6}>Email</Heading>
|
||||
<div>emerson@vaccaro.com</div>
|
||||
</div>
|
||||
<div>
|
||||
<Heading level={6}>Labels</Heading>
|
||||
<div className='mt-2 flex gap-1'>
|
||||
<div className='inline-block rounded-sm bg-grey-200 px-1.5 text-xs font-medium'>VIP</div>
|
||||
<div className='inline-block rounded-sm bg-grey-200 px-1.5 text-xs font-medium'>Inner Circle</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Heading level={6}>Notes</Heading>
|
||||
<div className='text-grey-500'>No notes.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex h-full flex-col gap-6 border-grey-200 px-5 tablet:border-r'>
|
||||
<Heading level={5}>Newsletters</Heading>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Toggle />
|
||||
<span>Daily news</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Toggle />
|
||||
<span>Weekly roundup</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Toggle checked />
|
||||
<span>The Inner Circle</span>
|
||||
</div>
|
||||
<div className='mt-5 rounded border border-red p-4 text-sm text-red'>
|
||||
This member cannot receive emails due to permanent failure (bounce).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='-ml-5 flex h-full flex-col gap-6 border-r border-grey-200 px-5 pt-10 tablet:ml-0 tablet:pt-0'>
|
||||
<Heading level={5}>Subscriptions</Heading>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex h-16 w-16 flex-col items-center justify-center rounded-md bg-grey-200'>
|
||||
<Heading level={5}>$5</Heading>
|
||||
<span className='text-xs text-grey-700'>Yearly</span>
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-semibold'>Gold</span>
|
||||
<span className='text-sm text-grey-500'>Renews 21 Jan 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='-mr-5 flex h-full flex-col gap-6 px-5 pt-10 tablet:pt-0'>
|
||||
<div className='flex justify-between'>
|
||||
<Heading level={5}>Activity</Heading>
|
||||
<Button color='green' label='View all' link />
|
||||
</div>
|
||||
<div className='flex flex-col text-sm'>
|
||||
<span className='font-semibold'>Logged in</span>
|
||||
<span className='text-sm text-grey-500'>13 days ago</span>
|
||||
</div>
|
||||
<div className='flex flex-col text-sm'>
|
||||
<span className='font-semibold'>Subscribed to Daily News</span>
|
||||
<span className='text-sm text-grey-500'>17 days ago</span>
|
||||
</div>
|
||||
<div className='flex flex-col text-sm'>
|
||||
<span className='font-semibold'>Logged in</span>
|
||||
<span className='text-sm text-grey-500'>21 days ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ViewContainer>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPage;
|
246
apps/post-analytics-spike/src/ListPage.tsx
Normal file
246
apps/post-analytics-spike/src/ListPage.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import {Avatar, Button, ButtonGroup, DynamicTable, DynamicTableColumn, DynamicTableRow, Heading, Hint, Page, SortMenu, Tooltip, ViewContainer, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
import {useState} from 'react';
|
||||
|
||||
const ListPage = () => {
|
||||
const {updateRoute} = useRouting();
|
||||
const [view, setView] = useState<string>('list');
|
||||
|
||||
const dummyActions = [
|
||||
<Button label='Filter' onClick={() => {
|
||||
showToast({message: 'Were you really expecting a filter? 😛'});
|
||||
}} />,
|
||||
<SortMenu
|
||||
direction='desc'
|
||||
items={[
|
||||
{
|
||||
id: 'date-added',
|
||||
label: 'Date added',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name'
|
||||
},
|
||||
{
|
||||
id: 'redemptions',
|
||||
label: 'Open Rate'
|
||||
}
|
||||
]}
|
||||
position="start"
|
||||
onDirectionChange={() => {}}
|
||||
onSortChange={() => {}}
|
||||
/>,
|
||||
<Tooltip content="Search members">
|
||||
<Button icon='magnifying-glass' size='sm' onClick={() => {
|
||||
alert('Clicked search');
|
||||
}} />
|
||||
</Tooltip>,
|
||||
<ButtonGroup buttons={[
|
||||
{
|
||||
icon: 'listview',
|
||||
size: 'sm',
|
||||
iconColorClass: (view === 'list' ? 'text-black' : 'text-grey-500'),
|
||||
onClick: () => {
|
||||
setView('list');
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'cardview',
|
||||
size: 'sm',
|
||||
iconColorClass: (view === 'card' ? 'text-black' : 'text-grey-500'),
|
||||
onClick: () => {
|
||||
setView('card');
|
||||
}
|
||||
}
|
||||
]} clearBg={false} link />
|
||||
];
|
||||
|
||||
const testColumns: DynamicTableColumn[] = [
|
||||
{
|
||||
title: 'Member',
|
||||
noWrap: true,
|
||||
minWidth: '1%',
|
||||
maxWidth: '1%'
|
||||
},
|
||||
{
|
||||
title: 'Status'
|
||||
},
|
||||
{
|
||||
title: 'Open rate'
|
||||
},
|
||||
{
|
||||
title: 'Location',
|
||||
noWrap: true
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
noWrap: true
|
||||
},
|
||||
{
|
||||
title: 'Signed up on post',
|
||||
noWrap: true,
|
||||
maxWidth: '150px'
|
||||
},
|
||||
{
|
||||
title: 'Newsletter'
|
||||
},
|
||||
{
|
||||
title: 'Billing period'
|
||||
},
|
||||
{
|
||||
title: 'Email sent'
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
hidden: true,
|
||||
disableRowClick: true
|
||||
}
|
||||
];
|
||||
|
||||
const testRows = (noOfRows: number) => {
|
||||
const data: DynamicTableRow[] = [];
|
||||
for (let i = 0; i < noOfRows; i++) {
|
||||
data.push(
|
||||
{
|
||||
onClick: () => {
|
||||
updateRoute('detail');
|
||||
},
|
||||
cells: [
|
||||
(<div className='flex items-center gap-3 whitespace-nowrap pr-10'>
|
||||
<Avatar image={`https://i.pravatar.cc/150?img=${i}`} />
|
||||
<div>
|
||||
{i % 3 === 0 && <div className='whitespace-nowrap text-md'>Jamie Larson</div>}
|
||||
{i % 3 === 1 && <div className='whitespace-nowrap text-md'>Giana Septimus</div>}
|
||||
{i % 3 === 2 && <div className='whitespace-nowrap text-md'>Zaire Bator</div>}
|
||||
<div className='text-grey-700'>jamie@larson.com</div>
|
||||
</div>
|
||||
</div>),
|
||||
'Free',
|
||||
'40%',
|
||||
'London, UK',
|
||||
<div>
|
||||
<div>22 June 2023</div>
|
||||
<div className='text-grey-500'>5 months ago</div>
|
||||
</div>,
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
'Subscribed',
|
||||
'Monthly',
|
||||
'1,303',
|
||||
<Button color='green' label='Edit' link onClick={() => {
|
||||
alert('Clicked Edit in row:' + i);
|
||||
}} />
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const dummyCards = (noOfCards: number) => {
|
||||
const cards = [];
|
||||
|
||||
for (let i = 0; i < noOfCards; i++) {
|
||||
cards.push(
|
||||
<div className='flex min-h-[20vh] cursor-pointer flex-col items-center gap-5 rounded-sm bg-grey-100 p-7 pt-9 transition-all hover:bg-grey-200' onClick={() => {
|
||||
updateRoute('detail');
|
||||
}}>
|
||||
<Avatar image={`https://i.pravatar.cc/150?img=${i}`} size='xl' />
|
||||
<div className='flex flex-col items-center'>
|
||||
<Heading level={5}>
|
||||
{i % 3 === 0 && 'Jamie Larson'}
|
||||
{i % 3 === 1 && 'Giana Septimus'}
|
||||
{i % 3 === 2 && 'Zaire Bator'}
|
||||
</Heading>
|
||||
<div className='mt-1 text-sm text-grey-700'>
|
||||
{i % 3 === 0 && 'jamie@larson.com'}
|
||||
{i % 3 === 1 && 'giana@septimus.com'}
|
||||
{i % 3 === 2 && 'zaire@bator.com'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex w-full flex-col gap-4 border-t border-grey-300 pt-5'>
|
||||
{i % 3 === 0 && (<>
|
||||
<div className='flex gap-4'>
|
||||
<div className='basis-1/2 text-center'>
|
||||
<Heading level={6}>Open rate</Heading>
|
||||
<div className='text-lg'>83%</div>
|
||||
</div>
|
||||
<div className='basis-1/2 text-center'>
|
||||
<Heading level={6}>Click rate</Heading>
|
||||
<div className='text-lg'>19%</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
{i % 3 === 1 && (<>
|
||||
<div className='flex gap-4'>
|
||||
<div className='basis-1/2 text-center'>
|
||||
<Heading level={6}>Open rate</Heading>
|
||||
<div className='text-lg'>68%</div>
|
||||
</div>
|
||||
<div className='basis-1/2 text-center'>
|
||||
<Heading level={6}>Click rate</Heading>
|
||||
<div className='text-lg'>21%</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
{i % 3 === 2 && (<>
|
||||
<div className='flex gap-4'>
|
||||
<div className='basis-1/2 text-center'>
|
||||
<Heading level={6}>Open rate</Heading>
|
||||
<div className='text-lg'>89%</div>
|
||||
</div>
|
||||
<div className='basis-1/2 text-center'>
|
||||
<Heading level={6}>Click rate</Heading>
|
||||
<div className='text-lg'>34%</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return cards;
|
||||
};
|
||||
|
||||
let contents = <></>;
|
||||
switch (view) {
|
||||
case 'list':
|
||||
contents = <DynamicTable
|
||||
cellClassName='text-sm'
|
||||
columns={testColumns}
|
||||
footer={
|
||||
<Hint>30 members</Hint>
|
||||
}
|
||||
rows={testRows(30)}
|
||||
stickyFooter
|
||||
stickyHeader
|
||||
/>;
|
||||
break;
|
||||
case 'card':
|
||||
contents = <div className='grid grid-cols-4 gap-8 py-8'>{dummyCards(30)}</div>;
|
||||
break;
|
||||
}
|
||||
|
||||
const demoPage = (
|
||||
<Page>
|
||||
<ViewContainer
|
||||
actions={dummyActions}
|
||||
primaryAction={{
|
||||
title: 'About',
|
||||
onClick: () => {
|
||||
updateRoute('demo-modal');
|
||||
}
|
||||
}}
|
||||
title='AdminX Demo App'
|
||||
toolbarBorder={view === 'card'}
|
||||
type='page'
|
||||
>
|
||||
{contents}
|
||||
</ViewContainer>
|
||||
</Page>
|
||||
);
|
||||
|
||||
return demoPage;
|
||||
};
|
||||
|
||||
export default ListPage;
|
15
apps/post-analytics-spike/src/MainContent.tsx
Normal file
15
apps/post-analytics-spike/src/MainContent.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
// import DetailPage from './DetailPage';
|
||||
// import ListPage from './ListPage';
|
||||
// import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
const MainContent = () => {
|
||||
// const {route} = useRouting();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Post analytics spike</h1>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainContent;
|
33
apps/post-analytics-spike/src/components/DemoModal.tsx
Normal file
33
apps/post-analytics-spike/src/components/DemoModal.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import {Heading, Modal} from '@tryghost/admin-x-design-system';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
const DemoModal = NiceModal.create(() => {
|
||||
const {updateRoute} = useRouting();
|
||||
const modal = NiceModal.useModal();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
updateRoute('');
|
||||
}}
|
||||
cancelLabel=''
|
||||
okLabel='Close'
|
||||
size='sm'
|
||||
title='About'
|
||||
onOk={() => {
|
||||
updateRoute('');
|
||||
modal.remove();
|
||||
}}
|
||||
>
|
||||
<div className='mt-3 flex flex-col gap-4'>
|
||||
<p>{`You're looking at a React app inside Ghost Admin. It uses common AdminX framework and Design System packages, and works seamlessly with the current Admin's routing.`}</p>
|
||||
<p>{`At the moment the look and feel follows the current Admin's style to blend in with existing pages. However the system is built in a very flexible way to allow easy updates in the future.`}</p>
|
||||
<Heading className='-mb-2 mt-4' level={5}>Contents</Heading>
|
||||
<p>{`The demo uses a mocked list of members — it's `}<strong>not</strong> {`the actual or future design of members in Ghost Admin. Instead, the pages showcase common design patterns like a list and detail, navigation, modals and toasts.`}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default DemoModal;
|
9
apps/post-analytics-spike/src/components/modals.tsx
Normal file
9
apps/post-analytics-spike/src/components/modals.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import DemoModal from './DemoModal';
|
||||
import {ModalComponent} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const modals = {DemoModal} satisfies {[key: string]: ModalComponent<any>};
|
||||
|
||||
export default modals;
|
||||
|
||||
export type ModalName = keyof typeof modals;
|
6
apps/post-analytics-spike/src/index.tsx
Normal file
6
apps/post-analytics-spike/src/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import './styles/index.css';
|
||||
import App from './App.tsx';
|
||||
|
||||
export {
|
||||
App as AdminXApp
|
||||
};
|
5
apps/post-analytics-spike/src/standalone.tsx
Normal file
5
apps/post-analytics-spike/src/standalone.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import './styles/index.css';
|
||||
import App from './App.tsx';
|
||||
import renderStandaloneApp from '@tryghost/admin-x-framework/test/render';
|
||||
|
||||
renderStandaloneApp(App, {});
|
1
apps/post-analytics-spike/src/styles/index.css
Normal file
1
apps/post-analytics-spike/src/styles/index.css
Normal file
@ -0,0 +1 @@
|
||||
@import '@tryghost/admin-x-design-system/styles.css';
|
6
apps/post-analytics-spike/tailwind.config.cjs
Normal file
6
apps/post-analytics-spike/tailwind.config.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
const adminXPreset = require('@tryghost/admin-x-design-system/tailwind.cjs');
|
||||
|
||||
module.exports = {
|
||||
presets: [adminXPreset('.post-analytics-spike')],
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', '../../node_modules/@tryghost/admin-x-design-system/es/**/*.{js,ts,jsx,tsx}']
|
||||
};
|
6
apps/post-analytics-spike/test/.eslintrc.cjs
Normal file
6
apps/post-analytics-spike/test/.eslintrc.cjs
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/ts-test'
|
||||
]
|
||||
};
|
18
apps/post-analytics-spike/test/acceptance/example.test.ts
Normal file
18
apps/post-analytics-spike/test/acceptance/example.test.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {mockApi, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance';
|
||||
|
||||
test.describe.skip('Demo', async () => {
|
||||
test('Renders the list page', async ({page}) => {
|
||||
await mockApi({page, requests: {
|
||||
browseSettings: {
|
||||
method: 'GET',
|
||||
path: /^\/settings\/\?group=/,
|
||||
response: responseFixtures.settings
|
||||
}
|
||||
}});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.locator('body')).toContainText('AdminX Demo App');
|
||||
});
|
||||
});
|
10
apps/post-analytics-spike/test/unit/example.test.tsx
Normal file
10
apps/post-analytics-spike/test/unit/example.test.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import ListPage from '../../src/ListPage';
|
||||
import {render, screen} from '@testing-library/react';
|
||||
|
||||
describe.skip('Demo', function () {
|
||||
it('renders a component', async function () {
|
||||
render(<ListPage />);
|
||||
|
||||
expect(screen.getAllByRole('heading')[0].textContent).toEqual('AdminX Demo App');
|
||||
});
|
||||
});
|
23
apps/post-analytics-spike/tsconfig.json
Normal file
23
apps/post-analytics-spike/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
10
apps/post-analytics-spike/vite.config.mjs
Normal file
10
apps/post-analytics-spike/vite.config.mjs
Normal file
@ -0,0 +1,10 @@
|
||||
import adminXViteConfig from '@tryghost/admin-x-framework/vite';
|
||||
import pkg from './package.json';
|
||||
import {resolve} from 'path';
|
||||
|
||||
export default (function viteConfig() {
|
||||
return adminXViteConfig({
|
||||
packageName: pkg.name,
|
||||
entry: resolve(__dirname, 'src/index.tsx')
|
||||
});
|
||||
});
|
@ -51,6 +51,9 @@ export const importComponent = async (packageName) => {
|
||||
}
|
||||
|
||||
const relativePath = packageName.replace('@tryghost/', '');
|
||||
|
||||
console.log('Relative path: ', relativePath);
|
||||
|
||||
const configKey = camelize(relativePath);
|
||||
|
||||
if (!config[`${configKey}Filename`] || !config[`${configKey}Hash`]) {
|
||||
@ -58,19 +61,30 @@ export const importComponent = async (packageName) => {
|
||||
}
|
||||
|
||||
const baseUrl = (config.cdnUrl ? `${config.cdnUrl}assets/` : ghostPaths().assetRootWithHost);
|
||||
|
||||
console.log('Base URL: ', baseUrl);
|
||||
|
||||
let url = new URL(`${baseUrl}${relativePath}/${config[`${configKey}Filename`]}?v=${config[`${configKey}Hash`]}`);
|
||||
|
||||
console.log('url: ', url);
|
||||
|
||||
const customUrl = config[`${configKey}CustomUrl`];
|
||||
|
||||
console.log('customUrl: ', customUrl);
|
||||
if (customUrl) {
|
||||
url = new URL(customUrl);
|
||||
}
|
||||
|
||||
console.log('url: ', url);
|
||||
|
||||
if (url.protocol === 'http:') {
|
||||
window[packageName] = await import(`http://${url.host}${url.pathname}${url.search}`);
|
||||
} else {
|
||||
window[packageName] = await import(`https://${url.host}${url.pathname}${url.search}`);
|
||||
}
|
||||
|
||||
console.log('import URL: ', `http://${url.host}${url.pathname}${url.search}`);
|
||||
|
||||
return window[packageName];
|
||||
};
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
<div {{react-render this.ReactComponent}}></div>
|
@ -0,0 +1,8 @@
|
||||
import AdminXComponent from './admin-x-component';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class AdminXDemo extends AdminXComponent {
|
||||
@service upgradeStatus;
|
||||
|
||||
static packageName = '@tryghost/post-analytics-spike';
|
||||
}
|
@ -49,6 +49,9 @@
|
||||
</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<LinkTo @route="post-analytics-spike" @current-when="post-analytics-spike">{{svg-jar "chart"}}Post analytics spike</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="gh-nav-list gh-nav-manage">
|
||||
<li class="gh-nav-list-new relative">
|
||||
|
6
ghost/admin/app/controllers/post-analytics-spike.js
Normal file
6
ghost/admin/app/controllers/post-analytics-spike.js
Normal file
@ -0,0 +1,6 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class ActivitypubXController extends Controller {
|
||||
@service upgradeStatus;
|
||||
}
|
@ -64,6 +64,10 @@ Router.map(function () {
|
||||
this.route('activitypub-x', {path: '/*sub'});
|
||||
});
|
||||
|
||||
this.route('post-analytics-spike', function () {
|
||||
this.route('post-analytics-spike', {path: '/*sub'});
|
||||
});
|
||||
|
||||
this.route('explore', function () {
|
||||
// actual Ember route, not rendered in iframe
|
||||
this.route('connect');
|
||||
|
3
ghost/admin/app/routes/post-analytics-spike.js
Normal file
3
ghost/admin/app/routes/post-analytics-spike.js
Normal file
@ -0,0 +1,3 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default class PostAnalyticsSpikeRoute extends AuthenticatedRoute {}
|
1
ghost/admin/app/templates/post-analytics-spike.hbs
Normal file
1
ghost/admin/app/templates/post-analytics-spike.hbs
Normal file
@ -0,0 +1 @@
|
||||
<AdminX::PostAnalyticsSpike />
|
@ -6,7 +6,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const camelCase = require('lodash/camelCase');
|
||||
|
||||
const adminXApps = ['admin-x-demo', 'admin-x-settings', 'admin-x-activitypub'];
|
||||
const adminXApps = ['admin-x-demo', 'admin-x-settings', 'admin-x-activitypub', 'post-analytics-spike'];
|
||||
|
||||
function generateHash(filePath) {
|
||||
const fileContents = fs.readFileSync(filePath, 'utf8');
|
||||
|
@ -190,7 +190,8 @@
|
||||
"projects": [
|
||||
"@tryghost/admin-x-demo",
|
||||
"@tryghost/admin-x-settings",
|
||||
"@tryghost/admin-x-activitypub"
|
||||
"@tryghost/admin-x-activitypub",
|
||||
"@tryghost/post-analytics-spike"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
@ -207,7 +208,8 @@
|
||||
"projects": [
|
||||
"@tryghost/admin-x-demo",
|
||||
"@tryghost/admin-x-settings",
|
||||
"@tryghost/admin-x-activitypub"
|
||||
"@tryghost/admin-x-activitypub",
|
||||
"@tryghost/post-analytics-spike"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user