This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/* eslint-env node */
|
||||
const tailwindCssConfig = `${__dirname}/../admin/src/index.css`;
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'plugin:ghost/ts',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
plugins: [
|
||||
'ghost',
|
||||
'react-refresh',
|
||||
'tailwindcss'
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
tailwindcss: {
|
||||
config: tailwindCssConfig
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// Sort multiple import lines into alphabetical groups
|
||||
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
|
||||
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
|
||||
}],
|
||||
'no-restricted-imports': ['error', {
|
||||
paths: [{
|
||||
name: '@tryghost/shade',
|
||||
message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).'
|
||||
}]
|
||||
}],
|
||||
|
||||
// Enforce kebab-case (lowercase with hyphens) for all filenames
|
||||
'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false],
|
||||
|
||||
// TODO: re-enable this (maybe fixed fast refresh?)
|
||||
'react-refresh/only-export-components': 'off',
|
||||
|
||||
// Suppress errors for missing 'import React' in JSX files, as we don't need it
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// ignore prop-types for now
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// TODO: re-enable these if deemed useful
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
|
||||
// Custom react rules
|
||||
'react/jsx-sort-props': ['error', {
|
||||
reservedFirst: true,
|
||||
callbacksLast: true,
|
||||
shorthandLast: true,
|
||||
locale: 'en'
|
||||
}],
|
||||
'react/button-has-type': 'error',
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/jsx-key': 'off',
|
||||
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/enforces-negative-arbitrary-values': 'warn',
|
||||
'tailwindcss/enforces-shorthand': 'warn',
|
||||
'tailwindcss/migration-from-tailwind-2': 'warn',
|
||||
'tailwindcss/no-arbitrary-value': 'off',
|
||||
'tailwindcss/no-custom-classname': 'off',
|
||||
'tailwindcss/no-contradicting-classname': 'error'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
types
|
||||
playwright-report
|
||||
test-results
|
||||
@@ -0,0 +1,16 @@
|
||||
<!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>Posts</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/standalone.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "@tryghost/posts",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TryGhost/Ghost/tree/main/apps/posts"
|
||||
},
|
||||
"author": "Ghost Foundation",
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"dist/"
|
||||
],
|
||||
"main": "./dist/posts.umd.cjs",
|
||||
"module": "./dist/posts.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/posts.js",
|
||||
"require": "./dist/posts.umd.cjs"
|
||||
},
|
||||
"./api": "./src/api.ts",
|
||||
"./members": "./src/views/members/members.tsx"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"dev:start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test:unit": "vitest run test/unit",
|
||||
"lint": "pnpm run lint:code && pnpm run lint:test",
|
||||
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src",
|
||||
"lint:code:fix": "eslint --ext .js,.ts,.cjs,.tsx --cache --fix src",
|
||||
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test",
|
||||
"lint:fix": "eslint --ext .js,.ts,.cjs,.tsx --cache src --fix",
|
||||
"preview": "vite preview",
|
||||
"test": "pnpm test:unit --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.59.1",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"@tanstack/react-virtual": "3.13.23",
|
||||
"@testing-library/jest-dom": "^6",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/react": "18.3.28",
|
||||
"@vitest/coverage-v8": "^1.6.1",
|
||||
"msw": "2.12.14",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"vite": "5.4.21",
|
||||
"vitest": "1.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/admin-x-framework": "workspace:*",
|
||||
"@tryghost/nql-lang": "0.6.4",
|
||||
"@tryghost/shade": "workspace:*",
|
||||
"i18n-iso-countries": "7.14.0",
|
||||
"moment": "2.24.0",
|
||||
"moment-timezone": "0.5.45",
|
||||
"papaparse": "5.5.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router": "7.14.0",
|
||||
"sonner": "2.0.7",
|
||||
"use-debounce": "10.1.1",
|
||||
"zod": "4.1.12"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"dev": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:unit": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"test:acceptance": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright';
|
||||
|
||||
export default adminXPlaywrightConfig();
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Public API for cross-package imports.
|
||||
* Admin uses these exports instead of reaching into src/ directly.
|
||||
*/
|
||||
export {default as PostsAppContextProvider} from './providers/posts-app-context';
|
||||
export {routes} from './routes';
|
||||
export {parseAllSharedViewsJSON} from './views/members/shared-views';
|
||||
export type {SharedView, AllSharedViewsParseResult} from './views/members/shared-views';
|
||||
@@ -0,0 +1,43 @@
|
||||
import PostsAppContextProvider, {PostsAppContextType} from './providers/posts-app-context';
|
||||
import PostsErrorBoundary from '@components/errors/posts-error-boundary';
|
||||
import React from 'react';
|
||||
import {APP_ROUTE_PREFIX, routes} from '@src/routes';
|
||||
import {BaseAppProps, FrameworkProvider, Outlet, RouterProvider} from '@tryghost/admin-x-framework';
|
||||
import {ShadeApp} from '@tryghost/shade/app';
|
||||
|
||||
interface AppProps extends BaseAppProps {
|
||||
fromAnalytics?: boolean;
|
||||
}
|
||||
|
||||
const App: React.FC<AppProps> = ({framework, designSystem, fromAnalytics = false, appSettings}) => {
|
||||
const appContextValue: PostsAppContextType = {
|
||||
appSettings,
|
||||
externalNavigate: (url: string) => {
|
||||
window.location.href = url;
|
||||
},
|
||||
fromAnalytics
|
||||
};
|
||||
|
||||
return (
|
||||
<FrameworkProvider
|
||||
{...framework}
|
||||
queryClientOptions={{
|
||||
staleTime: 0, // Always consider data stale (matches Ember admin route behavior)
|
||||
refetchOnMount: true, // Always refetch when component mounts (matches Ember route model)
|
||||
refetchOnWindowFocus: false // Disable window focus refetch (Ember admin doesn't have this)
|
||||
}}
|
||||
>
|
||||
<PostsAppContextProvider value={appContextValue}>
|
||||
<RouterProvider prefix={APP_ROUTE_PREFIX} routes={routes}>
|
||||
<PostsErrorBoundary>
|
||||
<ShadeApp className="shade-posts app-container" darkMode={designSystem.darkMode} fetchKoenigLexical={null}>
|
||||
<Outlet />
|
||||
</ShadeApp>
|
||||
</PostsErrorBoundary>
|
||||
</RouterProvider>
|
||||
</PostsAppContextProvider>
|
||||
</FrameworkProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,6 @@
|
||||
import './styles/index.css';
|
||||
import App from './app';
|
||||
|
||||
export {
|
||||
App as AdminXApp
|
||||
};
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
declare module '@tryghost/nql-lang' {
|
||||
const nql: {
|
||||
parse(input: string): unknown;
|
||||
};
|
||||
|
||||
export default nql;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {ErrorPage} from '@tryghost/shade/primitives';
|
||||
import {RouteObject, lazyComponent} from '@tryghost/admin-x-framework';
|
||||
// import {withFeatureFlag} from '@src/hooks/with-feature-flag';
|
||||
|
||||
export const APP_ROUTE_PREFIX = '/';
|
||||
|
||||
// Wrap components with feature flag protection where needed
|
||||
// e.g.
|
||||
// const ProtectedNewsletter = withFeatureFlag(Newsletter, 'trafficAnalyticsAlpha', '/analytics/', 'Newsletter');
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
{
|
||||
// Root route configuration
|
||||
path: '',
|
||||
errorElement: <ErrorPage onBackToDashboard={() => {}} />, // @TODO: add back to dashboard click handle
|
||||
children: [
|
||||
{
|
||||
// Post Analytics
|
||||
path: 'posts/analytics/:postId',
|
||||
lazy: async () => {
|
||||
const [
|
||||
{default: PostAnalyticsProvider},
|
||||
{default: PostAnalytics}
|
||||
] = await Promise.all([
|
||||
import('./providers/post-analytics-context'),
|
||||
import('./views/PostAnalytics/post-analytics')
|
||||
]);
|
||||
return {
|
||||
element: (
|
||||
<PostAnalyticsProvider>
|
||||
<PostAnalytics />
|
||||
</PostAnalyticsProvider>
|
||||
)
|
||||
};
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
lazy: lazyComponent(() => import('@views/PostAnalytics/Overview/overview'))
|
||||
},
|
||||
{
|
||||
path: 'web',
|
||||
lazy: lazyComponent(() => import('@views/PostAnalytics/Web/web'))
|
||||
},
|
||||
{
|
||||
path: 'growth',
|
||||
lazy: lazyComponent(() => import('@views/PostAnalytics/Growth/growth'))
|
||||
},
|
||||
{
|
||||
path: 'newsletter',
|
||||
lazy: lazyComponent(() => import('@views/PostAnalytics/Newsletter/newsletter'))
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'tags',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
lazy: lazyComponent(() => import('@views/Tags/tags'))
|
||||
},
|
||||
{
|
||||
path: ':tagSlug',
|
||||
element: null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'comments',
|
||||
lazy: lazyComponent(() => import('@views/comments/comments'))
|
||||
},
|
||||
|
||||
// Error handling
|
||||
{
|
||||
path: '*',
|
||||
element: <ErrorPage onBackToDashboard={() => {}} /> // @TODO: add back to dashboard click handle
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
import './styles/index.css';
|
||||
import App from './app';
|
||||
import renderShadeApp from '@tryghost/admin-x-framework/test/render-shade';
|
||||
|
||||
renderShadeApp(App, {});
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare module 'papaparse';
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/ts-test'
|
||||
],
|
||||
rules: {
|
||||
// We aren't using Mocha so we can disable some Ghost test rules.
|
||||
'ghost/mocha/no-mocha-arrows': 'off'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import {setupShadeMocks} from '@tryghost/admin-x-framework/test/setup';
|
||||
|
||||
// Set up common mocks for shade components
|
||||
setupShadeMocks();
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"declarationDir": "./types",
|
||||
"emitDeclarationOnly": true,
|
||||
"tsBuildInfoFile": "./types/tsconfig.tsbuildinfo",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path aliases */
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@src/*": ["*"],
|
||||
"@assets/*": ["assets/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@hooks/*": ["hooks/*"],
|
||||
"@utils/*": ["utils/*"],
|
||||
"@views/*": ["views/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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'),
|
||||
overrides: {
|
||||
test: {
|
||||
include: [
|
||||
'./test/unit/**/*',
|
||||
'./src/**/*.test.ts'
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@src': resolve(__dirname, './src'),
|
||||
'@assets': resolve(__dirname, './src/assets'),
|
||||
'@components': resolve(__dirname, './src/components'),
|
||||
'@hooks': resolve(__dirname, './src/hooks'),
|
||||
'@utils': resolve(__dirname, './src/utils'),
|
||||
'@views': resolve(__dirname, './src/views')
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import {createVitestConfig} from '@tryghost/admin-x-framework/test/vitest-config';
|
||||
|
||||
export default createVitestConfig();
|
||||
Reference in New Issue
Block a user