mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 22:02:11 +03:00
Created Post analytics spike React app
This commit is contained in:
parent
60ff94943b
commit
9fcfa25452
2
.github/scripts/dev.js
vendored
2
.github/scripts/dev.js
vendored
@ -74,7 +74,7 @@ const COMMAND_TYPESCRIPT = {
|
|||||||
env: {}
|
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 = [{
|
const COMMANDS_ADMINX = [{
|
||||||
name: 'adminXDeps',
|
name: 'adminXDeps',
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-x-base {
|
.admin-x-settings {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
animation-name: openAnimation;
|
animation-name: openAnimation;
|
||||||
animation-iteration-count: 1;
|
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/', '');
|
const relativePath = packageName.replace('@tryghost/', '');
|
||||||
|
|
||||||
|
console.log('Relative path: ', relativePath);
|
||||||
|
|
||||||
const configKey = camelize(relativePath);
|
const configKey = camelize(relativePath);
|
||||||
|
|
||||||
if (!config[`${configKey}Filename`] || !config[`${configKey}Hash`]) {
|
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);
|
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`]}`);
|
let url = new URL(`${baseUrl}${relativePath}/${config[`${configKey}Filename`]}?v=${config[`${configKey}Hash`]}`);
|
||||||
|
|
||||||
|
console.log('url: ', url);
|
||||||
|
|
||||||
const customUrl = config[`${configKey}CustomUrl`];
|
const customUrl = config[`${configKey}CustomUrl`];
|
||||||
|
|
||||||
|
console.log('customUrl: ', customUrl);
|
||||||
if (customUrl) {
|
if (customUrl) {
|
||||||
url = new URL(customUrl);
|
url = new URL(customUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('url: ', url);
|
||||||
|
|
||||||
if (url.protocol === 'http:') {
|
if (url.protocol === 'http:') {
|
||||||
window[packageName] = await import(`http://${url.host}${url.pathname}${url.search}`);
|
window[packageName] = await import(`http://${url.host}${url.pathname}${url.search}`);
|
||||||
} else {
|
} else {
|
||||||
window[packageName] = await import(`https://${url.host}${url.pathname}${url.search}`);
|
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];
|
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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
<li>
|
||||||
|
<LinkTo @route="post-analytics-spike" @current-when="post-analytics-spike">{{svg-jar "chart"}}Post analytics spike</LinkTo>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="gh-nav-list gh-nav-manage">
|
<ul class="gh-nav-list gh-nav-manage">
|
||||||
<li class="gh-nav-list-new relative">
|
<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('activitypub-x', {path: '/*sub'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route('post-analytics-spike', function () {
|
||||||
|
this.route('post-analytics-spike', {path: '/*sub'});
|
||||||
|
});
|
||||||
|
|
||||||
this.route('explore', function () {
|
this.route('explore', function () {
|
||||||
// actual Ember route, not rendered in iframe
|
// actual Ember route, not rendered in iframe
|
||||||
this.route('connect');
|
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 path = require('path');
|
||||||
const camelCase = require('lodash/camelCase');
|
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) {
|
function generateHash(filePath) {
|
||||||
const fileContents = fs.readFileSync(filePath, 'utf8');
|
const fileContents = fs.readFileSync(filePath, 'utf8');
|
||||||
|
@ -190,7 +190,8 @@
|
|||||||
"projects": [
|
"projects": [
|
||||||
"@tryghost/admin-x-demo",
|
"@tryghost/admin-x-demo",
|
||||||
"@tryghost/admin-x-settings",
|
"@tryghost/admin-x-settings",
|
||||||
"@tryghost/admin-x-activitypub"
|
"@tryghost/admin-x-activitypub",
|
||||||
|
"@tryghost/post-analytics-spike"
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
@ -207,7 +208,8 @@
|
|||||||
"projects": [
|
"projects": [
|
||||||
"@tryghost/admin-x-demo",
|
"@tryghost/admin-x-demo",
|
||||||
"@tryghost/admin-x-settings",
|
"@tryghost/admin-x-settings",
|
||||||
"@tryghost/admin-x-activitypub"
|
"@tryghost/admin-x-activitypub",
|
||||||
|
"@tryghost/post-analytics-spike"
|
||||||
],
|
],
|
||||||
"target": "build"
|
"target": "build"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user