Skip to content

Commit

Permalink
Initial CMSUI baseline, for PoCs and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
sneridagh committed Feb 15, 2025
1 parent 6155ad7 commit 74090c1
Show file tree
Hide file tree
Showing 27 changed files with 1,353 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@ docs/_build/
docs/source/news

tsconfig.tsbuildinfo

# @plone/registry generated files
registry.loader.js
addons.styles.css
84 changes: 84 additions & 0 deletions packages/cmsui/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* 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', 'plugin:storybook/recommended'],

// Ignore Cypress folder
ignorePatterns: ['cypress/', '.react-router/**/*', 'registry.config.ts'],

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,
},
},
],
};
9 changes: 9 additions & 0 deletions packages/cmsui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/node_modules/

# React Router
/.react-router/
/build/

registry.loader.js
addons.styles.css
4 changes: 4 additions & 0 deletions packages/cmsui/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"trailingComma": "all",
"singleQuote": true
}
8 changes: 8 additions & 0 deletions packages/cmsui/.stylelintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": ["stylelint-config-idiomatic-order"],
"plugins": ["stylelint-prettier"],
"rules": {
"prettier/prettier": true,
"order/properties-alphabetical-order": null
}
}
100 changes: 100 additions & 0 deletions packages/cmsui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Welcome to React Router!

A modern, production-ready template for building full-stack React applications using React Router.

[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)

## Features

- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)

## Getting Started

### Installation

Install the dependencies:

```bash
npm install
```

### Development

Start the development server with HMR:

```bash
npm run dev
```

Your application will be available at `http://localhost:5173`.

## Building for Production

Create a production build:

```bash
npm run build
```

## Deployment

### Docker Deployment

This template includes three Dockerfiles optimized for different package managers:

- `Dockerfile` - for npm
- `Dockerfile.pnpm` - for pnpm
- `Dockerfile.bun` - for bun

To build and run using Docker:

```bash
# For npm
docker build -t my-app .

# For pnpm
docker build -f Dockerfile.pnpm -t my-app .

# For bun
docker build -f Dockerfile.bun -t my-app .

# Run the container
docker run -p 3000:3000 my-app
```

The containerized application can be deployed to any platform that supports Docker, including:

- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway

### DIY Deployment

If you're familiar with deploying Node applications, the built-in app server is production-ready.

Make sure to deploy the output of `npm run build`

```
├── package.json
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```

## Styling

This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.

---

Built with ❤️ using React Router.
12 changes: 12 additions & 0 deletions packages/cmsui/app/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@layer theme, base, components, plone-components, utilities, custom; /* Add custom layer before tailwind defines them */
@import 'tailwindcss';
@import '../addons.styles.css';

html,
body {
@apply bg-white dark:bg-gray-950;

@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
70 changes: 70 additions & 0 deletions packages/cmsui/app/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createCookie, redirect } from 'react-router';
import type { Route } from './+types/login';

let secret = process.env.COOKIE_SECRET || 'default';
if (secret === 'default') {
console.warn(
'🚨 No COOKIE_SECRET environment variable set, using default. The app is insecure in production.',
);
secret = 'default-secret';
}

const cookie = createCookie('auth_seven', {
secrets: [secret],
// 30 days
maxAge: 30 * 24 * 60 * 60,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});

export async function getAuthFromRequest(request: Request): Promise {
let token;
try {
token = await cookie.parse(request.headers.get('Cookie'));
} catch (error) {
// asd
}
return token ?? null;
}

export async function setAuthOnResponse(
response: Response,
token: string,
): Promise {
const header = await cookie.serialize(token);
response.headers.append('Set-Cookie', header);
return response;
}

export async function requireAuthCookie(request: Request) {
const token = await getAuthFromRequest(request);
if (!token) {
throw redirect('/login', {
headers: {
'Set-Cookie': await cookie.serialize('', {
maxAge: 0,
}),
},
});
}
return token;
}

export async function redirectIfLoggedInLoader({ request }: Route.LoaderArgs) {
const token = await getAuthFromRequest(request);
if (token) {
throw redirect('/');
}
return null;
}

export async function redirectWithClearedCookie(): Promise {
return redirect('/', {
headers: {
'Set-Cookie': await cookie.serialize(null, {
expires: new Date(0),
}),
},
});
}
90 changes: 90 additions & 0 deletions packages/cmsui/app/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Route } from './+types/login';
import { data, Form, Link, useActionData, redirect } from 'react-router';

Check failure on line 2 in packages/cmsui/app/auth/login.tsx

View workflow job for this annotation

GitHub Actions / ESlint

'Link' is defined but never used

import { redirectIfLoggedInLoader, setAuthOnResponse } from './auth';
import { Button, TextField } from '@plone/components';

import PloneClient from '@plone/client';
import config from '@plone/registry';

export const loader = redirectIfLoggedInLoader;

export const meta = () => {
return [{ title: 'Plone Login' }];
};

export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const username = String(formData.get('username') || '');
const password = String(formData.get('password') || '');

const ploneClient = config
.getUtility({
name: 'ploneClient',
type: 'client',
})
.method();

const { login } = ploneClient as PloneClient;

const token = await login({ username, password });
if (!token) {
return data(
{ ok: false, errors: { password: 'Invalid credentials' } },
400,
);
}

const response = redirect('/');
return setAuthOnResponse(response, token);
}

export default function Login() {
const actionResult = useActionData<typeof action>();

return (
<div className="mt-20 flex min-h-full flex-1 flex-col sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2
id="login-header"
className="mt-6 text-center text-2xl leading-9 font-bold tracking-tight text-gray-900"
>
Log in
</h2>
</div>

<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
<div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
<Form className="space-y-6" method="post">
<div>
<TextField
autoFocus

Check failure on line 61 in packages/cmsui/app/auth/login.tsx

View workflow job for this annotation

GitHub Actions / ESlint

The autoFocus prop should not be used, as it can reduce usability and accessibility for users
id="username"
name="username"
aria-describedby={
actionResult?.errors?.email ? 'email-error' : 'login-header'
}
isRequired
/>
</div>

<div>
<TextField
id="password"
name="password"
type="password"
autoComplete="current-password"
aria-describedby="password-error"
isRequired
/>
</div>

<div>
<Button type="submit">Sign in</Button>
</div>
</Form>
</div>
</div>
</div>
);
}
Loading

0 comments on commit 74090c1

Please sign in to comment.