Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
sneridagh committed Jan 16, 2025
1 parent 5dc98e1 commit 2353602
Show file tree
Hide file tree
Showing 16 changed files with 530 additions and 0 deletions.
81 changes: 81 additions & 0 deletions packages/revolto/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/

/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},

// Base config
extends: ['eslint:recommended'],

overrides: [
// React
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: ['react', 'jsx-a11y'],
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
],
settings: {
react: {
version: 'detect',
},
'import/core-modules': ['@plone/registry/addons-loader'],
formComponents: ['Form'],
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
],
},
},

// Typescript
{
files: ['**/*.{ts,tsx}'],
plugins: ['@typescript-eslint', 'import'],
parser: '@typescript-eslint/parser',
settings: {
'import/internal-regex': '^~/',
'import/resolver': {
node: {
extensions: ['.ts', '.tsx'],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
],
},

// Node
{
files: ['.eslintrc.js'],
env: {
node: true,
},
},
],
};
7 changes: 7 additions & 0 deletions packages/revolto/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules

/.cache
/build
.env
.react-router
.registry.loader.js
30 changes: 30 additions & 0 deletions packages/revolto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Plone on React Router 7

This is a proof of concept of a [React Router](https://reactrouter.com/dev/docs) app, using the `@plone/*` libraries.
This is intended to serve as both a playground for the development of both packages and as a demo of Plone using Remix.

> [!WARNING]
> This package or app is experimental.
> The community offers no support whatsoever for it.
> Breaking changes may occur without notice.
## Development

To start, from the root of the monorepo, issue the following commands.

```shell
pnpm install
pnpm --filter plone-remix run dev
```

Then start the Plone backend.

% TODO MAKEFILE
```shell
make backend-docker-start
```


## About this app

- [Remix Docs](https://remix.run/docs/en/main)
32 changes: 32 additions & 0 deletions packages/revolto/app/config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* This is the server side config entry point
*/
import config from '@plone/registry';
import ploneClient from '@plone/client';
import applyAddonConfiguration from '@plone/registry/addons-loader';

export default function install() {
applyAddonConfiguration(config);

config.settings.apiPath =
process.env.PLONE_API_PATH || 'http://localhost:3000';
config.settings.internalApiPath =
process.env.PLONE_INTERNAL_API_PATH || undefined;

const cli = ploneClient.initialize({
apiPath: config.settings.internalApiPath || config.settings.apiPath,
});

config.registerUtility({
name: 'ploneClient',
type: 'client',
method: () => cli,
});

console.log('API_PATH is:', config.settings.apiPath);
console.log(
'INTERNAL_API_PATH is:',
config.settings.internalApiPath || 'not set',
);
return config;
}
11 changes: 11 additions & 0 deletions packages/revolto/app/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* This is the client side config entry point
*/
import config from '@plone/registry';
import applyAddonConfiguration from '@plone/registry/addons-loader';

export default function install() {
applyAddonConfiguration(config);
config.settings.apiPath = 'http://localhost:3000';
return config;
}
57 changes: 57 additions & 0 deletions packages/revolto/app/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Route } from './+types/content';
import { data, useLoaderData, useLocation } from 'react-router';
import PloneClient from '@plone/client';
import App from '@plone/slots/components/App';
import config from '@plone/registry';

export const meta: Route.MetaFunction = ({ data }) => {
return [
{ title: data?.title },
{ name: 'description', content: data?.description },
];
};

const expand = ['navroot', 'breadcrumbs', 'navigation'];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function loader({ params, request }: Route.LoaderArgs) {
const ploneClient = config
.getUtility({
name: 'ploneClient',
type: 'client',
})
.method();

const { getContent } = ploneClient as PloneClient;

const path = new URL(request.url).pathname;

if (
!(
/^https?:\/\//.test(path) ||
/^favicon.ico\/\//.test(path) ||
/expand/.test(path) ||
/\/@@images\//.test(path) ||
/\/@@download\//.test(path) ||
/^\/assets/.test(path) ||
/\.(css|css\.map)$/.test(path)
)
) {
console.log('prefetching', path);
try {
return await getContent({ path, expand });
} catch (error) {
throw data('Content Not Found', { status: 404 });
}
} else {
console.log('path not prefetched', path);
throw data('Content Not Found', { status: 404 });
}
}

export default function Content() {
const data = useLoaderData<typeof loader>();
const pathname = useLocation().pathname;

return <App content={data} location={{ pathname }} />;
}
5 changes: 5 additions & 0 deletions packages/revolto/app/okroute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function loader() {
return new Response(null, {
status: 200,
});
}
161 changes: 161 additions & 0 deletions packages/revolto/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { LinksFunction } from 'react-router';
import type { Route } from './+types/root';
import { useState } from 'react';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useHref,
useLocation,
useNavigate as useRRNavigate,
useParams,
useLoaderData,
isRouteErrorResponse,
} from 'react-router';

import { QueryClient } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import PloneClient from '@plone/client';
import { PloneProvider } from '@plone/providers';
import { flattenToAppURL } from './utils';
import config from '@plone/registry';
import install from './config';
import installSSR from './config.server';

install();

import themingMain from '@plone/theming/styles/main.css?url';
import slotsMain from '@plone/slots/main.css?url';

function useNavigate() {
const navigate = useRRNavigate();
return (to: string) => navigate(flattenToAppURL(to));
}

function useHrefLocal(to: string) {
return useHref(flattenToAppURL(to));
}

export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: themingMain },
{ rel: 'stylesheet', href: slotsMain },
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossOrigin: 'anonymous',
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap',
},
];

export async function loader() {
const ssrConfig = installSSR();

return {
env: {
PLONE_API_PATH: ssrConfig.settings.apiPath,
PLONE_INTERNAL_API_PATH: ssrConfig.settings.internalApiPath,
},
};
}

export function Layout({ children }: { children: React.ReactNode }) {
const data = useLoaderData<typeof loader>();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<script
dangerouslySetInnerHTML={{
__html: `window.env = ${JSON.stringify(data.env)}`,
}}
/>
<Scripts />
</body>
</html>
);
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!';
let details = 'An unexpected error occurred.';
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error';
details =
error.status === 404
? 'The requested page could not be found.'
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}

return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}

export default function App() {
if (!import.meta.env.SSR) {
config.settings.apiPath = window.env.PLONE_API_PATH;
config.settings.internalApiPath = window.env.PLONE_INTERNAL_API_PATH;
}

const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
}),
);

const [ploneClient] = useState(() =>
PloneClient.initialize({
apiPath: config.settings.apiPath,
}),
);

const navigate = useNavigate();

return (
<PloneProvider
ploneClient={ploneClient}
queryClient={queryClient}
useLocation={useLocation}
useParams={useParams}
useHref={useHrefLocal}
navigate={navigate}
flattenToAppURL={flattenToAppURL}
>
<Outlet />
<ReactQueryDevtools initialIsOpen={false} />
</PloneProvider>
);
}
Loading

0 comments on commit 2353602

Please sign in to comment.