From 0ada1f84d319df1d70fd8f49d202088ad686e98e Mon Sep 17 00:00:00 2001 From: francesco Date: Thu, 25 Feb 2021 10:54:01 +0100 Subject: [PATCH] Landing page implementation --- README.md | 849 +- example/public/index.html | 7 +- src/CMSAppProvider.tsx | 2 +- src/collection/fields/TableNumberInput.tsx | 2 +- src/types_test.tsx | 45 + website/.gitignore | 20 + website/README.md | 33 + website/babel.config.js | 3 + website/blog/2019-05-28-hola.md | 11 + website/blog/2019-05-29-hello-world.md | 17 + website/blog/2019-05-30-welcome.md | 13 + website/docs/collections.md | 124 + website/docs/config_intro.md | 77 + website/docs/contexts/auth_context.md | 38 + .../docs/contexts/side_entity_controller.md | 58 + website/docs/contexts/snackbar.md | 40 + website/docs/custom_fields.md | 167 + website/docs/entity_schemas.md | 112 + website/docs/intro.md | 67 + website/docs/mdx.md | 17 + website/docs/properties/array.md | 19 + website/docs/properties/boolean.md | 10 + website/docs/properties/geopoint.md | 8 + website/docs/properties/map.md | 17 + website/docs/properties/number.md | 23 + website/docs/properties/reference.md | 21 + website/docs/properties/string.md | 64 + website/docs/properties/timestamp.md | 12 + website/docs/quickstart.md | 246 + website/docs/ref.md | 203 + website/docusaurus.config.js | 110 + website/package.json | 61 + .../docusaurus-tailwindcss-loader/index.js | 34 + website/sidebars.js | 47 + .../additional-styles/utility-patterns.css | 38 + website/src/css/custom.css | 60 + website/src/css/tailwind.css | 5 + website/src/pages/index.tsx | 64 + website/src/pages/styles.module.css | 37 + website/src/partials/Features.tsx | 285 + website/src/partials/FeaturesBlocks.tsx | 289 + website/src/partials/FirebaseIntro.tsx | 62 + website/src/partials/Header.tsx | 91 + website/src/partials/HeroButtons.tsx | 34 + website/src/partials/HeroHome.tsx | 61 + website/src/partials/Newsletter.tsx | 141 + website/src/partials/Separator.tsx | 6 + website/src/partials/Testimonials.tsx | 155 + website/src/partials/utils/Transition.tsx | 134 + website/src/shape/Psycodelic.tsx | 397 + website/src/shape/Shape.tsx | 581 + website/src/shape/ThreeJSAnimation.tsx | 646 + website/src/shape/ThreeJSAnimationShader.tsx | 726 ++ website/src/shape/perlin2.js | 261 + website/src/shape/perlin3.js | 64 + website/src/shape/shape_logic.ts | 402 + website/src/shape/test.js | 313 + website/src/shape/useRaf.ts | 39 + website/src/shape/useTween.ts | 26 + website/src/shape/utils.js | 923 ++ website/src/theme/FooterTest.tsx | 317 + website/static/.nojekyll | 0 website/static/img/editing.mp4 | Bin 0 -> 266398 bytes website/static/img/favicon-16x16.png | Bin 0 -> 1754 bytes website/static/img/favicon-32x32.png | Bin 0 -> 2663 bytes website/static/img/favicon-96x96.png | Bin 0 -> 9707 bytes website/static/img/favicon.ico | Bin 0 -> 1150 bytes website/static/img/features-bg.png | Bin 0 -> 45632 bytes website/static/img/features-element.png | Bin 0 -> 7244 bytes website/static/img/firebase.svg | 51 + website/static/img/firecms_logo.svg | 47 + website/static/img/icon-192x192.png | Bin 0 -> 24723 bytes website/static/img/logo_small.png | Bin 0 -> 73076 bytes website/static/img/price.png | Bin 0 -> 25485 bytes website/static/img/reactjs-icon.svg | 15 + website/tailwind.config.js | 161 + website/tsconfig.json | 28 + website/yarn.lock | 10749 ++++++++++++++++ 78 files changed, 18934 insertions(+), 851 deletions(-) create mode 100644 src/types_test.tsx create mode 100644 website/.gitignore create mode 100644 website/README.md create mode 100644 website/babel.config.js create mode 100644 website/blog/2019-05-28-hola.md create mode 100644 website/blog/2019-05-29-hello-world.md create mode 100644 website/blog/2019-05-30-welcome.md create mode 100644 website/docs/collections.md create mode 100644 website/docs/config_intro.md create mode 100644 website/docs/contexts/auth_context.md create mode 100644 website/docs/contexts/side_entity_controller.md create mode 100644 website/docs/contexts/snackbar.md create mode 100644 website/docs/custom_fields.md create mode 100644 website/docs/entity_schemas.md create mode 100644 website/docs/intro.md create mode 100644 website/docs/mdx.md create mode 100644 website/docs/properties/array.md create mode 100644 website/docs/properties/boolean.md create mode 100644 website/docs/properties/geopoint.md create mode 100644 website/docs/properties/map.md create mode 100644 website/docs/properties/number.md create mode 100644 website/docs/properties/reference.md create mode 100644 website/docs/properties/string.md create mode 100644 website/docs/properties/timestamp.md create mode 100644 website/docs/quickstart.md create mode 100644 website/docs/ref.md create mode 100644 website/docusaurus.config.js create mode 100644 website/package.json create mode 100644 website/plugins/docusaurus-tailwindcss-loader/index.js create mode 100644 website/sidebars.js create mode 100644 website/src/css/additional-styles/utility-patterns.css create mode 100644 website/src/css/custom.css create mode 100644 website/src/css/tailwind.css create mode 100644 website/src/pages/index.tsx create mode 100644 website/src/pages/styles.module.css create mode 100644 website/src/partials/Features.tsx create mode 100644 website/src/partials/FeaturesBlocks.tsx create mode 100644 website/src/partials/FirebaseIntro.tsx create mode 100644 website/src/partials/Header.tsx create mode 100644 website/src/partials/HeroButtons.tsx create mode 100644 website/src/partials/HeroHome.tsx create mode 100644 website/src/partials/Newsletter.tsx create mode 100644 website/src/partials/Separator.tsx create mode 100644 website/src/partials/Testimonials.tsx create mode 100644 website/src/partials/utils/Transition.tsx create mode 100644 website/src/shape/Psycodelic.tsx create mode 100644 website/src/shape/Shape.tsx create mode 100644 website/src/shape/ThreeJSAnimation.tsx create mode 100644 website/src/shape/ThreeJSAnimationShader.tsx create mode 100644 website/src/shape/perlin2.js create mode 100644 website/src/shape/perlin3.js create mode 100644 website/src/shape/shape_logic.ts create mode 100644 website/src/shape/test.js create mode 100644 website/src/shape/useRaf.ts create mode 100644 website/src/shape/useTween.ts create mode 100644 website/src/shape/utils.js create mode 100644 website/src/theme/FooterTest.tsx create mode 100644 website/static/.nojekyll create mode 100644 website/static/img/editing.mp4 create mode 100644 website/static/img/favicon-16x16.png create mode 100644 website/static/img/favicon-32x32.png create mode 100644 website/static/img/favicon-96x96.png create mode 100644 website/static/img/favicon.ico create mode 100644 website/static/img/features-bg.png create mode 100644 website/static/img/features-element.png create mode 100644 website/static/img/firebase.svg create mode 100644 website/static/img/firecms_logo.svg create mode 100644 website/static/img/icon-192x192.png create mode 100644 website/static/img/logo_small.png create mode 100644 website/static/img/price.png create mode 100644 website/static/img/reactjs-icon.svg create mode 100644 website/tailwind.config.js create mode 100644 website/tsconfig.json create mode 100644 website/yarn.lock diff --git a/README.md b/README.md index d487d715d..ba50dd4d2 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,7 @@ easy to create your custom form fields, or your complete views. Note that this is a full application, with routing enabled and not a simple component. -It is currently in an alpha state, and we continue working to add features and -expose internal APIs, so it is safe to expect breaking changes. - -[![NPM](https://img.shields.io/npm/v/@camberi/firecms.svg)](https://www.npmjs.com/package/@camberi/firecms) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) +[![NPM](https://img.shields.io/n4pm/v/@camberi/firecms.svg)](https://www.npmjs.com/package/@camberi/firecms) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) ### Core technologies @@ -86,862 +83,18 @@ yarn add @camberi/firecms - [x] Custom fields defined by the developer. - [x] Subcollection support -## Use - -FireCMS is a purely a React app that uses your Firebase project as a backend, so -you do not need a specific backend to make it run. Just build your project -following the installation instructions and deploy it in the way you prefer. A -very easy way is using Firebase Hosting. - -## Firebase requirements - -You need to enable the Firestore database in your Firebase project. If you have -enabled authentication in the CMS config you need to enable Google -authentication in your project. - -Also, if you are using storage fields in your string properties, you need to -enable Firebase Storage. - -### Deployment to Firebase hosting - -If you are deploying this project to firebase hosting, and the app it properly -linked to ir, you can omit the firebaseConfig specification, since it gets -picked up automatically. - -## Quickstart - -- Create a new React app including Typescript: - -```npx create-react-app my-cms --template typescript``` - -- Go into the new directory: - -```cd my-cms``` - -- Install FireCMS and it's peer dependencies: - -```yarn add @camberi/firecms @material-ui/core @material-ui/icons @material-ui/pickers @material-ui/lab firebase``` - -You can replace the content of the file App.tsx with the following sample code. -Remember to replace the Firebase config with the one you get after creating a -webapp in the Firebase console. - -```tsx -import React from "react"; - -import { - Authenticator, - buildCollection, - buildProperty, - buildSchema, - CMSApp, - NavigationBuilder, - NavigationBuilderProps -} from "@camberi/firecms"; - -import firebase from "firebase/app"; - -import "typeface-rubik"; -import "typeface-space-mono"; - -// TODO: Replace with your config -const firebaseConfig = { - apiKey: "", - authDomain: "", - projectId: "", - storageBucket: "", - messagingSenderId: "", - appId: "" -}; - -const locales = { - "de-DE": "German", - "en-US": "English (United States)", - "es-ES": "Spanish (Spain)", - "es-419": "Spanish (South America)" -}; - -const productSchema = buildSchema({ - name: "Product", - properties: { - name: { - title: "Name", - validation: { required: true }, - dataType: "string" - }, - price: { - title: "Price", - validation: { - required: true, - requiredMessage: "You must set a price between 0 and 1000", - min: 0, - max: 1000 - }, - description: "Price with range validation", - dataType: "number" - }, - status: { - title: "Status", - validation: { required: true }, - dataType: "string", - description: "Should this product be visible in the website", - longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", - config: { - enumValues: { - private: "Private", - public: "Public" - } - } - }, - published: ({ values }) => buildProperty({ - title: "Published", - dataType: "boolean", - columnWidth: 100, - disabled: ( - values.status === "public" - ? false - : { - clearOnDisabled: true, - disabledMessage: "Status must be public in order to enable this the published flag" - } - ) - }), - main_image: buildProperty({ - title: "Image", - dataType: "string", - config: { - storageMeta: { - mediaType: "image", - storagePath: "images", - acceptedFiles: ["image/*"] - } - } - }), - tags: { - title: "Tags", - description: "Example of generic array", - validation: { required: true }, - dataType: "array", - of: { - dataType: "string" - } - }, - description: { - title: "Description", - description: "Not mandatory but it'd be awesome if you filled this up", - longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", - dataType: "string", - columnWidth: 300 - }, - categories: { - title: "Categories", - validation: { required: true }, - dataType: "array", - of: { - dataType: "string", - config: { - enumValues: { - electronics: "Electronics", - books: "Books", - furniture: "Furniture", - clothing: "Clothing", - food: "Food" - } - } - } - }, - publisher: { - title: "Publisher", - description: "This is an example of a map property", - dataType: "map", - properties: { - name: { - title: "Name", - dataType: "string" - }, - external_id: { - title: "External id", - dataType: "string" - } - } - }, - expires_on: { - title: "Expires on", - dataType: "timestamp" - } - } -}); - -const localeSchema = buildSchema({ - customId: locales, - name: "Locale", - properties: { - title: { - title: "Title", - validation: { required: true }, - dataType: "string" - }, - selectable: { - title: "Selectable", - description: "Is this locale selectable", - dataType: "boolean" - }, - video: { - title: "Video", - dataType: "string", - validation: { required: false }, - config: { - storageMeta: { - mediaType: "video", - storagePath: "videos", - acceptedFiles: ["video/*"] - } - } - } - } -}); - -export function App() { - - const navigation: NavigationBuilder = ({ user }: NavigationBuilderProps) => ({ - collections: [ - buildCollection({ - relativePath: "products", - schema: productSchema, - name: "Products", - permissions: ({ user }) => ({ - edit: true, - create: true, - delete: true - }), - subcollections: [ - buildCollection({ - name: "Locales", - relativePath: "locales", - schema: localeSchema - }) - ] - }) - ] - }); - - const myAuthenticator: Authenticator = (user?: firebase.User) => { - console.log("Allowing access to", user?.email); - return true; - }; - - return ; -} -``` - #### Included example You can access the code for the demo project under [`example`](https://github.com/Camberi/firecms/tree/master/example). It includes every feature provided by this CMS. -To get going you just need to set you Firebase config in `firebase_config.ts` -and run `yarn start`. - -#### More granular control - -If you don't want to use FireCMS `CMSApp` as a full app but would like to -integrate some of its components you may want to use the `CMSAppProvider` -and `CMSMainView` -components (used internally) directly. - -This will allow you to initialise Firebase on your own and integrate the FireCMS -components into your own app. Just place `CMSAppProvider` on top of the -components that need to use the FireCMS hooks. - -You can see an -example [here](https://github.com/Camberi/firecms/blob/master/example/src/SimpleAppWithProvider.tsx) - -#### Real time support - -Every view in the CMS has real time data support. This makes it suitable for -displaying data that needs to be always updated. - -**Forms** also support this feature, any modified value in the database will be -updated in any currently open form view, as long as it has not been touched by -the user. This makes it suitable for advanced cases where you trigger a Cloud -Function after saving an entity that modifies some values, and you want to get -real time updates. - -## CMSApp level configuration - -The entry point for setting up a FireCMS app is the `CMSApp`, where you can -define the following specs: - -- `name` Name of the app, displayed as the main title and in the tab title. - -- `navigation` Use this prop to specify the views that will be generated in the - CMS. You will usually want to create a `Navigation` object that includes - collection views where you specify the path and the schema. Additionally, you - can add custom views to the root navigation. In you need to customize the - navigation based on the logged user you can use a `NavigationBuilder` - -- `logo` Logo to be displayed in the drawer of the CMS. - -- `authentication` Do the users need to log in to access the CMS. You can - specify an Authenticator function to discriminate which users can access the - CMS or not. If not specified, authentication is enabled but no user - restrictions apply. - -- `signInOptions` List of sign in options that will be displayed in the login - view if `authentication` is enabled. You can pass google providers strings, - such as `firebase.auth.GoogleAuthProvider.PROVIDER_ID` or full configuration - objects such as specified - in https://firebase.google.com/docs/auth/web/firebaseui - Defaults to Google sign in only. - -- `allowSkipLogin` If authentication is enabled, allow the user to access the - content without login. - -- `firebaseConfig` Firebase configuration of the project. If you afe deploying - the app to Firebase hosting, you don't need to specify this value. - -- `onFirebaseInit` An optional callback after Firebase has been initialised. - Useful for using the local emulator or retrieving the used configuration. - -- `primaryColor` Primary color of the theme of the CMS. - -- `secondaryColor` Primary color of the theme of the CMS. - -- `fontFamily` Font family string. e.g. '"Roboto", "Helvetica", "Arial", - sans-serif'. - -- `toolbarExtraWidget` A component that gets rendered on the upper side of the - main toolbar. - -- `dateTimeFormat` Format of the dates in the CMS. Defaults to 'MMMM dd, yyyy, - HH:mm:ss' - -- `locale` Locale of the CMS, currently only affecting dates - -- `schemaResolver` Used to override schemas based on the collection path and - entityId. This resolver allows to override the schema for specific entities, - or specific collections, app wide. This overrides schemas all through the app. - You can also override schemas in place, when using `useSideEntityController` - -## Entities configuration - -The core of the CMS are entities, which are defined by an `EntitySchema`. In the -schema you define the properties, which are related to the Firestore data types. - -- `name` A singular name of the entity as displayed in an Add button. E.g. - Product - -- `description` Description of this entity. - -- `customId` When not specified, Firestore will create a random ID. You can set - the value to `true` to allow the users to choose the ID. You can also pass a - set of values (as an `EnumValues` object) to allow them to pick from only - those. - -- `properties` Object defining the properties for the entity schema. - -### Entity properties - -You can specify the properties of an entity, using the following configuration -fields, common to all data types: - -* `dataType` Firestore datatype of the property. - -* `title` Property title (e.g. Product). - -* `description` Property description. - -* `longDescription` Width in pixels of this column in the collection view. If - not set, the width is inferred based on the other configurations. - -* `columnWidth` Longer description of a field, displayed under a popover. - -* `disabled` Is this a read only property. - -* `config` - * `field`If you need to render a custom field, you can create a component - that takes `FieldProps` as props. You receive the value, a function to - update the value and additional utility props such as if there is an - error. You can customize it by passing custom props that are received in - the component. - - * `preview` Configure how a property is displayed as a preview, e.g. in the - collection view. You can customize it by passing custom props that are - received in the component. - - * `customProps` Additional props that are passed to the components defined - in `field` - or in `preview`. - - -* `onPreSave` Hook called before saving, you need to return the values that will - get saved. If you throw an error in this method the process stops, and an - error snackbar gets displayed. (example bellow) - -* `onSaveSuccess` Hook called when save is successful. - -* `onPreSave` Hook called when saving fails. - -* `defaultValues` Object defining the initial values of the entity on creation. - -#### Conditional fields from properties - -When defining the properties of a schema, you can choose to use a builder -(`PropertyBuilder`), instead of assigning the property configuration directly. -In the builder you receive `PropertyBuilderProps` and return your property. - -This is useful for changing property configurations like available values on the -fly, based on other values. - -#### Property configurations - -Beside the common fields, some properties have specific configurations. - -##### `string` - -* `config` - * `storageMeta` You can specify a `StorageMeta` configuration. It is used to - indicate that this string refers to a path in Google Cloud Storage. - * `mediaType` Media type of this reference, used for displaying the - preview. - * `storagePath` Absolute path in your bucket. You can specify it - directly or use a callback - * `acceptedFiles` File MIME types that can be uploaded to this - reference. - * `metadata` Specific metadata set in your uploaded file. - * `fileName` You can specify a fileName callback if you need to - customize the name of the file - * `storeUrl` When set to `true`, this flag indicates that the download - URL of the file will be saved in Firestore instead of the Cloud - storage path. Note that the generated URL may use a token that, if - disabled, may make the URL unusable and lose the original reference to - Cloud Storage, so it is not encouraged to use this flag. Defaults to - false. - * `url` If the value of this property is a URL, you can set this flag - to `true` - to add a link, or one of the supported media types to render a preview. - * `enumValues` You can use the enum values providing a map of possible - exclusive values the property can take, mapped to the label that it is - displayed in the dropdown. You can use a simple object with the format - `value` => `label`, or with the format `value` => `EnumValueConfig` if you - need extra customization, (like disabling specific options or assigning - colors). If you need to ensure the order of the elements, you can pass - a `Map` instead of a plain object. - * `multiline` Is this string property long enough, so it should be displayed - in a multiple line field. Defaults to false. If set to `true`, the number - of lines adapts to the content. - * `markdown` Should this string property be displayed as a markdown field. - If `true`, the field is rendered as a text editors that supports markdown - highlight syntax. It also includes a preview of the result. - * `previewAsTag` Should this string be rendered as a tag instead of just - text. - -* `validation` Rules for validating this property: - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - * `unique` The value of this field must be unique in this collection. - * `uniqueInArray` If you set it to `true`, the user will only be allowed to - have the value of that property once in the parent - `ArrayProperty`. It works on direct children properties or on first level - children of a `MapProperty` (if set as the `.of` property of - the `ArrayProperty`). - * `length` Set a required length for the string value. - * `min` Set a minimum length limit for the string value. - * `max` Set a maximum length limit for the string value. - * `matches` Provide an arbitrary regex to match the value against. - * `email` Validates the value as an email address via a regex. - * `url` Validates the value as a valid URL via a regex. - * `trim` Transforms string values by removing leading and trailing - whitespace. - * `lowercase` Transforms the string value to lowercase. - * `uppercase` Transforms the string value to uppercase. - -##### `number` - -* `config` - * `enumValues` You can use the enum values providing a map of possible - exclusive values the property can take, mapped to the label that it is - displayed in the dropdown. - -* `validation` Rules for validating this property. - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - * `min` Set the minimum value allowed. - * `max` Set the maximum value allowed. - * `lessThan` Value must be less than. - * `moreThan` Value must be more than. - * `positive` Value must be a positive number. - * `negative` Value must be a negative number. - * `integer` Value must be an integer. - -##### `boolean` - -* `validation` Rules for validating this property. - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - -##### `timestamp` - -* `validation` Rules for validating this property. - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - * `min` Set the minimum date allowed. - * `max` Set the maximum date allowed. - -##### `reference` - -* `collectionPath` Absolute collection path of the collection this reference - points to. The schema of the entity is inferred based on the root navigation, - so the filters and search delegate existing there are applied to this view as - well. - -* `previewProperties` List of properties rendered as this reference preview. - Defaults to first 3. - -* `validation` Rules for validating this property. - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - -##### `array` - -* `of` The property of this array. You can specify any property. You can also - specify an array or properties if you need the array to have a specific - limited shape such as [string, number, string]. - -* `validation` Rules for validating this property. - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - * `min` Set the minimum length allowed. - * `max` Set the maximum length allowed. - -##### `map` - -* `properties` Record of properties included in this map. - -* `previewProperties` List of properties rendered as this map preview. Defaults - to first 3. - -* `validation` Rules for validating this property. - * `required` Should this field be compulsory. - * `requiredMessage` Message to be displayed as a validation error. - -##### `geopoint` - -*THIS PROPERTY IS CURRENTLY NOT SUPPORTED* - -#### Custom fields - -If you need a custom field for your property you can do it by passing a React -component to the `field` prop of a property `config`. The React component must -accept the props of type `CMSFieldProps`, which you can extend with your own -props. The bare minimum you need to implement is a field that displays the -received `value` and uses the `setValue` callback. - -See how it works in this -[sample custom text field](https://github.com/Camberi/firecms/blob/master/example/src/SampleApp/custom_field/CustomColorTextField.tsx) - -You can find the all -the `CMSFieldProps` [here](https://github.com/Camberi/firecms/blob/master/src/models/fields.tsx) - -You can also pass custom props to your custom field, which you then receive in -the `customProps`. - -If you are developing a custom field and need to access the values of the -entity, you can use the `context` field in CMSFieldProps. - -#### Saving callbacks - -When you are saving an entity you can attach different callbacks before and -after it gets saved: `onPreSave`, `onSaveSuccess` and `onSaveFailure`. - -``` -const productSchema = buildSchema({ - customId: true, - name: "Product", - onPreSave: ({ - schema, - collectionPath, - id, - values, - status - }: EntitySaveProps) => { - values.uppercase_name = values.name.toUpperCase(); - return values; -}, - properties: { - name: { - title: "Name", - validation: { required: true }, - dataType: "string" - }, - uppercase_name: { - title: "Uppercase Name", - dataType: "string", - disabled: true, - description: "This field gets updated with a preSave callback" - }, - } -}); - -``` - -## Collection configuration - -Once you have defined at least one entity schema, you can include it in a -collection. You can find collection views as the first level of navigation in -the main menu, or as subcollections inside other collections, following the -Firestore data schema. - -* `name` The plural name of the view. E.g. 'products'. - -* `relativePath` Relative Firestore path of this view to its parent. If this - view is in the root the path is equal to the absolute one. This path also - determines the URL in FireCMS. - -* `defaultSize` Default size of the rendered collection. - -* `group` Optional field used to group top level navigation entries under a - navigation view. If you set this value in a subcollection it has no effect. - -* `description` Optional description of this view. You can use Markdown. - -* `properties` Properties displayed in this collection. If this property is not - set every property is displayed. - -* `excludedProperties` Properties that should NOT get displayed in the - collection view. All the other properties from the entity are displayed. It - has no effect if the `properties` value is set. - -* `filterableProperties` List of properties that include a filter widget. - Defaults to none. - -* `initialFilter` Initial filters applied to this collection. Consider that you - can filter any property, but only those included in - `filterableProperties` will include the corresponding filter widget. Defaults - to none - -* `initialSort` Default sort applied to this collection. It takes tuples in the - shape `["property_name", "asc"]` or `["property_name", "desc"]` - -* `extraActions` Builder for rendering additional components such as buttons in - the collection toolbar. The builder takes an object with - props `entityCollection` and `selectedEntities` if any are set by the end - user. - -* `pagination` If enabled, content is loaded in batches. If `false` all entities - in the collection are loaded. You can specify a number to specify the - pagination size (50 by default) - Defaults to `true` - -* `additionalColumns` You can add additional columns to the collection view by - implementing an additional column delegate. - -* `textSearchDelegate` If a text search delegate is supplied, a search bar is - displayed on top. - -* `permissions` You can specify an object with boolean permissions with the - shape `{edit:boolean; create:boolean; delete:boolean}` to indicate the actions - the user can perform. You can also pass a `PermissionsBuilder` to customize - the permissions based on user or entity. - -* `inlineEditing` Can the elements in this collection be edited inline in the - collection view. If this flag is set to false but `permissions.edit` is `true` - , entities can still be edited in the side panel. - -* `exportable` Should the data in this collection view include an export button. - You can also set an `ExportConfig` configuration object to customize - the export and add additional values. - Defaults to `true` - -* `subcollections` Following the Firestore document and collection schema, you - can add subcollections to your entity in the same way you define the root - collections. - -* `onEntityDelete` Hook called after the entity gets deleted in Firestore. - -### Additional columns - -If you would like to include a column that does not map directly to a property, -you can use the `additionalColumns` field, providing a -`AdditionalColumnDelegate`, which includes an id, a title, and a builder that -receives the corresponding entity. - -In the builder you can return any React Component. - -If you would like to do some async computation, such as fetching a different -entity, you can use the utility component `AsyncPreviewComponent` to show a -loading indicator. - -### Subcollections - -Subcollections are collections of entities that are found under another entity. -For example, you can have a collection named "translations" under the entity -"Article". You just need to use the same format as for defining your collection -using the field `subcollections`. - -Subcollections are easily accessible from the side view while editing an entity. - -### Filters - -Filtering support is currently limited to string, number and boolean values, -including enum types. If you want a property to be filterable, you can mark it -as such in the entity schema. - -Any comments related to this feature are welcome. - -### Text search - -Firestore does not support native text search, so we need to rely on external -solutions. If you specify a `textSearchDelegate` to the collection view, you -will see a search bar on top. The delegate is in charge of returning the -matching ids, from the search string. - -A delegate using AlgoliaSearch is included, where you need to specify your -credentials and index. For this to work you need to set up an AlgoliaSearch -account and manage the indexing of your documents. There is a full backend -example included in the code, which indexes documents with Cloud Functions. - -You can also implement your own `TextSearchDelegate`, and would love to hear how -you come around this problem. - ## Provided hooks FireCMS provides different hooks that allow you to interact with the internal state of the app. Please note that in order to use this hook you **must** be in a component (you can't use them directly from a callback function). -### Auth Context - -`useAuthContext` -For state and operations regarding authentication. - -The props provided by this context are: - -* `loggedUser` The Firebase user currently logged in or null -* `authProviderError` Error dispatched by the auth provider -* `authLoading` Is the login process ongoing -* `loginSkipped` Is the login skipped -* `notAllowedError` The current user was not allowed access -* `skipLogin()` Skip login -* `signOut()` Sign out - -Example: - -```tsx - -import React from "react"; -import { useAuthContext } from "@camberi/firecms"; - -export function ExampleCMSView() { - - const authController = useAuthContext(); - - return ( - authController.loggedUser ? -
Logged in as {authController.loggedUser.displayName}
- : -
You are not logged in
- ); -} -``` - -### Snackbar controller - -`useSnackbarController` -For displaying snackbars - -The props provided by this context are: - -* `isOpen` Is there currently an open snackbar -* `close()` Close the currently open snackbar -* `open ({ type: "success" | "info" | "warning" | "error"; title?: string; message: string; })` - Display a new snackbar. You need to specify the type and message. You can - optionally specify a title - -Example: - -```tsx - -import React from "react"; -import { useSnackbarController } from "@camberi/firecms"; - -export function ExampleCMSView() { - - const snackbarController = useSnackbarController(); - - return ( - - ); -} -``` - -### Side entity controller - -`useSideEntityController` -You can use this controller to open the side entity view used to edit entities. - -The props provided by this context are: - -* `close()` Close the last panel -* `sidePanels` List of side entity panels currently open -* `open (props: SideEntityPanelProps & Partial)` - Open a new entity sideDialog. By default, the schema and configuration of the - view is fetched from the collections you have specified in the navigation. At - least you need to pass the collectionPath of the entity you would like to - edit. You can set an entityId if you would like to edit and existing one - (or a new one with that id). If you wish, you can also override - the `SchemaSidePanelProps` (such as schema or subcollections) and choose to - override the CMSApp level `SchemaResolver`. - -Example: - -```tsx -import React from "react"; -import { useSideEntityController } from "@camberi/firecms"; - -export function ExampleCMSView() { - - const sideEntityController = useSideEntityController(); - - // You don't need to provide a schema if the collection path is mapped in - // the main navigation or you have set a `schemaResolver` - const customProductSchema = buildSchema({ - name: "Product", - properties: { - name: { - title: "Name", - validation: { required: true }, - dataType: "string" - }, - } - }); - - return ( - - ); -} -``` ## Contact and support diff --git a/example/public/index.html b/example/public/index.html index 32dcad11d..2b3930a6c 100644 --- a/example/public/index.html +++ b/example/public/index.html @@ -9,7 +9,12 @@ name="description" content="The next content manager" /> - + + + + + + + +This is a test post. + +A whole bunch of other information. diff --git a/website/blog/2019-05-30-welcome.md b/website/blog/2019-05-30-welcome.md new file mode 100644 index 000000000..d35d57b7d --- /dev/null +++ b/website/blog/2019-05-30-welcome.md @@ -0,0 +1,13 @@ +--- +slug: welcome +title: Welcome +author: Yangshun Tay +author_title: Front End Engineer @ Facebook +author_url: https://github.com/yangshun +author_image_url: https://avatars0.githubusercontent.com/u/1315101?s=400&v=4 +tags: [facebook, hello, docusaurus] +--- + +Blog features are powered by the blog plugin. Simply add files to the `blog` directory. It supports tags as well! + +Delete the whole directory if you don't want the blog features. As simple as that! diff --git a/website/docs/collections.md b/website/docs/collections.md new file mode 100644 index 000000000..18f2972ac --- /dev/null +++ b/website/docs/collections.md @@ -0,0 +1,124 @@ +--- +id: collections +title: Collections +sidebar_label: Collections +--- + + +Once you have defined at least one entity schema, you can include it in a +collection. You can find collection views as the first level of navigation in +the main menu, or as subcollections inside other collections, following the +Firestore data schema. + +* `name` The plural name of the view. E.g. 'products'. + +* `relativePath` Relative Firestore path of this view to its parent. If this + view is in the root the path is equal to the absolute one. This path also + determines the URL in FireCMS. + +* `defaultSize` Default size of the rendered collection. + +* `group` Optional field used to group top level navigation entries under a + navigation view. If you set this value in a subcollection it has no effect. + +* `description` Optional description of this view. You can use Markdown. + +* `properties` Properties displayed in this collection. If this property is not + set every property is displayed. + +* `excludedProperties` Properties that should NOT get displayed in the + collection view. All the other properties from the entity are displayed. It + has no effect if the `properties` value is set. + +* `filterableProperties` List of properties that include a filter widget. + Defaults to none. + +* `initialFilter` Initial filters applied to this collection. Consider that you + can filter any property, but only those included in + `filterableProperties` will include the corresponding filter widget. Defaults + to none + +* `initialSort` Default sort applied to this collection. It takes tuples in the + shape `["property_name", "asc"]` or `["property_name", "desc"]` + +* `extraActions` Builder for rendering additional components such as buttons in + the collection toolbar. The builder takes an object with + props `entityCollection` and `selectedEntities` if any are set by the end + user. + +* `pagination` If enabled, content is loaded in batches. If `false` all entities + in the collection are loaded. You can specify a number to specify the + pagination size (50 by default) + Defaults to `true` + +* `additionalColumns` You can add additional columns to the collection view by + implementing an additional column delegate. + +* `textSearchDelegate` If a text search delegate is supplied, a search bar is + displayed on top. + +* `permissions` You can specify an object with boolean permissions with the + shape `{edit:boolean; create:boolean; delete:boolean}` to indicate the actions + the user can perform. You can also pass a `PermissionsBuilder` to customize + the permissions based on user or entity. + +* `inlineEditing` Can the elements in this collection be edited inline in the + collection view. If this flag is set to false but `permissions.edit` is `true` + , entities can still be edited in the side panel. + +* `exportable` Should the data in this collection view include an export button. + You can also set an `ExportConfig` configuration object to customize + the export and add additional values. + Defaults to `true` + +* `subcollections` Following the Firestore document and collection schema, you + can add subcollections to your entity in the same way you define the root + collections. + +* `onEntityDelete` Hook called after the entity gets deleted in Firestore. + +### Additional columns + +If you would like to include a column that does not map directly to a property, +you can use the `additionalColumns` field, providing a +`AdditionalColumnDelegate`, which includes an id, a title, and a builder that +receives the corresponding entity. + +In the builder you can return any React Component. + +If you would like to do some async computation, such as fetching a different +entity, you can use the utility component `AsyncPreviewComponent` to show a +loading indicator. + +### Subcollections + +Subcollections are collections of entities that are found under another entity. +For example, you can have a collection named "translations" under the entity +"Article". You just need to use the same format as for defining your collection +using the field `subcollections`. + +Subcollections are easily accessible from the side view while editing an entity. + +### Filters + +Filtering support is currently limited to string, number and boolean values, +including enum types. If you want a property to be filterable, you can mark it +as such in the entity schema. + +Any comments related to this feature are welcome. + +### Text search + +Firestore does not support native text search, so we need to rely on external +solutions. If you specify a `textSearchDelegate` to the collection view, you +will see a search bar on top. The delegate is in charge of returning the +matching ids, from the search string. + +A delegate using AlgoliaSearch is included, where you need to specify your +credentials and index. For this to work you need to set up an AlgoliaSearch +account and manage the indexing of your documents. There is a full backend +example included in the code, which indexes documents with Cloud Functions. + +You can also implement your own `TextSearchDelegate`, and would love to hear how +you come around this problem. + diff --git a/website/docs/config_intro.md b/website/docs/config_intro.md new file mode 100644 index 000000000..d7fb9c6eb --- /dev/null +++ b/website/docs/config_intro.md @@ -0,0 +1,77 @@ +--- +id: cms_config +title: CMS main config +sidebar_label: Main config +--- + + +The entry point for setting up a FireCMS app is the `CMSApp`, where you can +define the following specs: + +- `name` Name of the app, displayed as the main title and in the tab title. + +- `navigation` Use this prop to specify the views that will be generated in the + CMS. You will usually want to create a `Navigation` object that includes + collection views where you specify the path and the schema. Additionally, you + can add custom views to the root navigation. In you need to customize the + navigation based on the logged user you can use a `NavigationBuilder` + +- `logo` Logo to be displayed in the drawer of the CMS. + +- `authentication` Do the users need to log in to access the CMS. You can + specify an Authenticator function to discriminate which users can access the + CMS or not. If not specified, authentication is enabled but no user + restrictions apply. + +- `signInOptions` List of sign in options that will be displayed in the login + view if `authentication` is enabled. You can pass google providers strings, + such as `firebase.auth.GoogleAuthProvider.PROVIDER_ID` or full configuration + objects such as specified + in https://firebase.google.com/docs/auth/web/firebaseui + Defaults to Google sign in only. + +- `allowSkipLogin` If authentication is enabled, allow the user to access the + content without login. + +- `firebaseConfig` Firebase configuration of the project. If you afe deploying + the app to Firebase hosting, you don't need to specify this value. + +- `onFirebaseInit` An optional callback after Firebase has been initialised. + Useful for using the local emulator or retrieving the used configuration. + +- `primaryColor` Primary color of the theme of the CMS. + +- `secondaryColor` Primary color of the theme of the CMS. + +- `fontFamily` Font family string. e.g. '"Roboto", "Helvetica", "Arial", + sans-serif'. + +- `toolbarExtraWidget` A component that gets rendered on the upper side of the + main toolbar. + +- `dateTimeFormat` Format of the dates in the CMS. Defaults to 'MMMM dd, yyyy, + HH:mm:ss' + +- `locale` Locale of the CMS, currently only affecting dates + +- `schemaResolver` Used to override schemas based on the collection path and + entityId. This resolver allows to override the schema for specific entities, + or specific collections, app wide. This overrides schemas all through the app. + You can also override schemas in place, when using `useSideEntityController` + + + +#### More granular control + +If you don't want to use FireCMS `CMSApp` as a full app but would like to +integrate some of its components you may want to use the `CMSAppProvider` +and `CMSMainView` +components (used internally) directly. + +This will allow you to initialise Firebase on your own and integrate the FireCMS +components into your own app. Just place `CMSAppProvider` on top of the +components that need to use the FireCMS hooks. + +You can see an +example [here](https://github.com/Camberi/firecms/blob/master/example/src/SimpleAppWithProvider.tsx) + diff --git a/website/docs/contexts/auth_context.md b/website/docs/contexts/auth_context.md new file mode 100644 index 000000000..d0ea2bb32 --- /dev/null +++ b/website/docs/contexts/auth_context.md @@ -0,0 +1,38 @@ +--- +id: auth_context +title: Auth Context +sidebar_label: Auth Context +--- + +`useAuthContext` +For state and operations regarding authentication. + +The props provided by this context are: + +* `loggedUser` The Firebase user currently logged in or null +* `authProviderError` Error dispatched by the auth provider +* `authLoading` Is the login process ongoing +* `loginSkipped` Is the login skipped +* `notAllowedError` The current user was not allowed access +* `skipLogin()` Skip login +* `signOut()` Sign out + +Example: + +```tsx + +import React from "react"; +import { useAuthContext } from "dist/index"; + +export function ExampleCMSView() { + + const authController = useAuthContext(); + + return ( + authController.loggedUser ? +
Logged in as {authController.loggedUser.displayName}
+ : +
You are not logged in
+ ); +} +``` diff --git a/website/docs/contexts/side_entity_controller.md b/website/docs/contexts/side_entity_controller.md new file mode 100644 index 000000000..83af1656a --- /dev/null +++ b/website/docs/contexts/side_entity_controller.md @@ -0,0 +1,58 @@ +--- +id: side_entity_controller +title: Side Entity Controller +sidebar_label: Side Entity Controller +--- + +`useSideEntityController` +You can use this controller to open the side entity view used to edit entities. + +The props provided by this context are: + +* `close()` Close the last panel +* `sidePanels` List of side entity panels currently open +* `open (props: SideEntityPanelProps & Partial)` + Open a new entity sideDialog. By default, the schema and configuration of the + view is fetched from the collections you have specified in the navigation. At + least you need to pass the collectionPath of the entity you would like to + edit. You can set an entityId if you would like to edit and existing one + (or a new one with that id). If you wish, you can also override + the `SchemaSidePanelProps` (such as schema or subcollections) and choose to + override the CMSApp level `SchemaResolver`. + +Example: + +```tsx +import React from "react"; +import { useSideEntityController } from "@camberi/firecms"; + +export function ExampleCMSView() { + + const sideEntityController = useSideEntityController(); + + // You don't need to provide a schema if the collection path is mapped in + // the main navigation or you have set a `schemaResolver` + const customProductSchema = buildSchema({ + name: "Product", + properties: { + name: { + title: "Name", + validation: { required: true }, + dataType: "string" + }, + } + }); + + return ( + + ); +} +``` diff --git a/website/docs/contexts/snackbar.md b/website/docs/contexts/snackbar.md new file mode 100644 index 000000000..5a16b773c --- /dev/null +++ b/website/docs/contexts/snackbar.md @@ -0,0 +1,40 @@ +--- +id: snackbars +title: Snackbars +sidebar_label: Snackbars +--- + +`useSnackbarController` +For displaying snackbars + +The props provided by this context are: + +* `isOpen` Is there currently an open snackbar +* `close()` Close the currently open snackbar +* `open ({ type: "success" | "info" | "warning" | "error"; title?: string; message: string; })` + Display a new snackbar. You need to specify the type and message. You can + optionally specify a title + +Example: + +```tsx + +import React from "react"; +import { useSnackbarController } from "@camberi/firecms"; + +export function ExampleCMSView() { + + const snackbarController = useSnackbarController(); + + return ( + + ); +} +``` diff --git a/website/docs/custom_fields.md b/website/docs/custom_fields.md new file mode 100644 index 000000000..345806197 --- /dev/null +++ b/website/docs/custom_fields.md @@ -0,0 +1,167 @@ +--- +id: custom_fields +title: Custom fields +sidebar_label: Custom fields +--- + +If you need a custom field for your property you can do it by passing a React +component to the `field` prop of a property `config`. The React component must +accept the props of type `CMSFieldProps`. The bare minimum you need to implement is a field that displays the +received `value` and uses the `setValue` callback. + +You can also specify your own props that are passed to your component in `customProps` + +## Custom field props - CMSFieldProps + +* `name` The name of the property, such as `age`. You can use nested and array + indexed such as `address.street` or `people[3]` + +* `property` The CMS property you are binding this field to + +* `context` The context where this field is being rendered. You get a + context as a prop when creating a custom field. + +* `includeDescription` Should the description be included in this field + +* `underlyingValueHasChanged` Has the value of this property been updated + in the database while this field is being edited + +* `tableMode` Is this field being rendered in a table + +* `partOfArray` Is this field part of an array + +* `autoFocus` Should the field take focus when rendered. When opening the + popup view in table mode, it makes sense to put the focus on the only + field rendered. + +* `disabled` Should this field be disabled + +* `dependsOnOtherProperties` This flag is used to avoid using Formik + FastField internally, which prevents being updated from the values + +You can also pass custom props to your custom field, which you then receive in +the `customProps`. + +If you are developing a custom field and need to access the values of the +entity, you can use the `context` field in CMSFieldProps. + +## Example + +This is an example of a custom TextField that takes the background color as a prop + +```tsx +import { TextField, Theme, withStyles } from "@material-ui/core"; +import React, { ReactElement } from "react"; +import { FieldDescription, FieldProps } from "@camberi/firecms"; + +interface CustomColorTextFieldProps { + color: string +} + +export const TextFieldWithStyles = withStyles((theme: Theme) => ({ + root: (props: any) => ({ + "& .MuiFilledInput-root": { + backgroundColor: props.customcolor + } + }) +}))(TextField); + +export default function CustomColorTextField({ + property, + value, + setValue, + customProps, + touched, + error, + isSubmitting, + context, // the rest of the entity values here + ...props + }: FieldProps) + : ReactElement { + + return ( + <> + { + setValue( + evt.target.value + ); + }} + helperText={error} + fullWidth + variant={"filled"} + customcolor={customProps.color}/> + + + + + ); + +} +``` + +...and how it is used: +```tsx +export const blogSchema = buildSchema({ + name: "Blog entry", + properties: { + // ... + gold_text: { + title: "Gold text", + description: "This field is using a custom component defined by the developer", + dataType: "string", + config: { + field: CustomColorTextField, + customProps: { + color: "gold" + } + } + } + } +}); +``` + +## Custom previews +Just as you can customize how your property field is rendered, you can change +how the preview of a property is displayed in collection and other read only +views + +Example of a custom preview for a `boolean` property: +```tsx +import React, { ReactElement } from "react"; +import { PreviewComponentProps } from "@camberi/firecms"; + +import CheckBoxOutlineBlank from "@material-ui/icons/CheckBoxOutlineBlank"; +import CheckBoxOutlined from "@material-ui/icons/CheckBoxOutlined"; + +export default function CustomBooleanPreview({ + value, property, size + }: PreviewComponentProps) + : ReactElement { + return ( + value ? : + ); +} +``` + +...and how it is used: + +```tsx +export const blogSchema = buildSchema({ + name: "Blog entry", + properties: { + // ... + reviewed: { + title: "Reviewed", + dataType: "boolean", + config: { + preview: CustomBooleanPreview + } + }, + } +}); +``` diff --git a/website/docs/entity_schemas.md b/website/docs/entity_schemas.md new file mode 100644 index 000000000..b89e228df --- /dev/null +++ b/website/docs/entity_schemas.md @@ -0,0 +1,112 @@ +--- +id: entity_schemas +title: Entity Schemas +sidebar_label: Entity Schemas +--- + +The core of the CMS are entities, which are defined by an `EntitySchema`. In the +schema you define the properties, which are related to the Firestore data types. + +- `name` A singular name of the entity as displayed in an Add button. E.g. + Product + +- `description` Description of this entity. + +- `customId` When not specified, Firestore will create a random ID. You can set + the value to `true` to allow the users to choose the ID. You can also pass a + set of values (as an `EnumValues` object) to allow them to pick from only + those. + +- `properties` Object defining the properties for the entity schema. + +### Entity properties + +You can specify the properties of an entity, using the following configuration +fields, common to all data types: + +* `dataType` Firestore datatype of the property. + +* `title` Property title (e.g. Product). + +* `description` Property description. + +* `longDescription` Width in pixels of this column in the collection view. If + not set, the width is inferred based on the other configurations. + +* `columnWidth` Longer description of a field, displayed under a popover. + +* `disabled` Is this a read only property. + +* `config` + * `field`If you need to render a custom field, you can create a component + that takes `FieldProps` as props. You receive the value, a function to + update the value and additional utility props such as if there is an + error. You can customize it by passing custom props that are received in + the component. + + * `preview` Configure how a property is displayed as a preview, e.g. in the + collection view. You can customize it by passing custom props that are + received in the component. + + * `customProps` Additional props that are passed to the components defined + in `field` + or in `preview`. + + +* `onPreSave` Hook called before saving, you need to return the values that will + get saved. If you throw an error in this method the process stops, and an + error snackbar gets displayed. (example bellow) + +* `onSaveSuccess` Hook called when save is successful. + +* `onPreSave` Hook called when saving fails. + +* `defaultValues` Object defining the initial values of the entity on creation. + +### Conditional fields from properties + +When defining the properties of a schema, you can choose to use a builder +(`PropertyBuilder`), instead of assigning the property configuration directly. +In the builder you receive `PropertyBuilderProps` and return your property. + +This is useful for changing property configurations like available values on the +fly, based on other values. + + +#### Saving callbacks + +When you are saving an entity you can attach different callbacks before and +after it gets saved: `onPreSave`, `onSaveSuccess` and `onSaveFailure`. + +``` +const productSchema = buildSchema({ + customId: true, + name: "Product", + onPreSave: ({ + schema, + collectionPath, + id, + values, + status + }: EntitySaveProps) => { + values.uppercase_name = values.name.toUpperCase(); + return values; +}, + properties: { + name: { + title: "Name", + validation: { required: true }, + dataType: "string" + }, + uppercase_name: { + title: "Uppercase Name", + dataType: "string", + disabled: true, + description: "This field gets updated with a preSave callback" + }, + } +}); + +``` + + diff --git a/website/docs/intro.md b/website/docs/intro.md new file mode 100644 index 000000000..b6f04a9f4 --- /dev/null +++ b/website/docs/intro.md @@ -0,0 +1,67 @@ +--- +id: intro +title: Introduction +sidebar_label: Introduction +slug: / +--- + +FireCMS is a headless CMS and admin panel built by developers for developers. It +generates CRUD views based on your configuration. You define views that are +mapped to absolute or relative paths in your Firestore database, as well as +schemas for your entities. + +The goal of this CMS is to generate collection and form views that bind nicely +to the Firestore collection/document model. We have built in many basic (and not +so basic) use cases; but FireCMS is build with extensibility in mind, so it is +easy to create your custom form fields, or your complete views. + +### Core technologies + +FireCMS is based on this great technologies: + +- Typescript +- Firebase +- React + React Router +- Material UI +- Formik + Yup + +:::important + +Note that this is a full application, with routing enabled and not a simple +component. + +::: + +## Use + +FireCMS is a purely a React app that uses your Firebase project as a backend, so +you do not need a specific backend to make it run. Just build your project +following the installation instructions and deploy it in the way you prefer. A +very easy way is using Firebase Hosting. + +## Firebase requirements + +You need to enable the Firestore database in your Firebase project. If you have +enabled authentication in the CMS config you need to enable Google +authentication in your project. + +Also, if you are using storage fields in your string properties, you need to +enable Firebase Storage. + +### Deployment to Firebase hosting + +If you are deploying this project to firebase hosting, and the app it properly +linked to it, you can omit the `firebaseConfig` specification, since it gets +picked up automatically. + + +#### Real time support + +Every view in the CMS has real time data support. This makes it suitable for +displaying data that needs to be always updated. + +**Forms** also support this feature, any modified value in the database will be +updated in any currently open form view, as long as it has not been touched by +the user. This makes it suitable for advanced cases where you trigger a Cloud +Function after saving an entity that modifies some values, and you want to get +real time updates. diff --git a/website/docs/mdx.md b/website/docs/mdx.md new file mode 100644 index 000000000..f0210fb70 --- /dev/null +++ b/website/docs/mdx.md @@ -0,0 +1,17 @@ +--- +id: mdx +title: Powered by MDX +--- + +You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). + +export const Highlight = ({children, color}) => ( {children} ); + +Docusaurus green and Facebook blue are my favorite colors. + +I can write **Markdown** alongside my _JSX_! diff --git a/website/docs/properties/array.md b/website/docs/properties/array.md new file mode 100644 index 000000000..b40bf8132 --- /dev/null +++ b/website/docs/properties/array.md @@ -0,0 +1,19 @@ +--- +id: array +title: Array +sidebar_label: Array +--- + +## `of` + +The property of this array. You can specify any property. You can also +specify an array or properties if you need the array to have a specific +limited shape such as [string, number, string]. + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. +* `min` Set the minimum length allowed. +* `max` Set the maximum length allowed. + diff --git a/website/docs/properties/boolean.md b/website/docs/properties/boolean.md new file mode 100644 index 000000000..ef64ade8b --- /dev/null +++ b/website/docs/properties/boolean.md @@ -0,0 +1,10 @@ +--- +id: boolean +title: Boolean +sidebar_label: Boolean +--- + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. diff --git a/website/docs/properties/geopoint.md b/website/docs/properties/geopoint.md new file mode 100644 index 000000000..b7dd36113 --- /dev/null +++ b/website/docs/properties/geopoint.md @@ -0,0 +1,8 @@ +--- +id: geopoint +title: Geopoint +sidebar_label: Geopoint +--- + +*THIS PROPERTY IS CURRENTLY NOT SUPPORTED* + diff --git a/website/docs/properties/map.md b/website/docs/properties/map.md new file mode 100644 index 000000000..1be13b114 --- /dev/null +++ b/website/docs/properties/map.md @@ -0,0 +1,17 @@ +--- +id: map +title: Map +sidebar_label: Map +--- + +## `properties` +Record of properties included in this map. + +## `previewProperties` +List of properties rendered as this map preview. Defaults to first 3. + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. + diff --git a/website/docs/properties/number.md b/website/docs/properties/number.md new file mode 100644 index 000000000..4ba8cc9ee --- /dev/null +++ b/website/docs/properties/number.md @@ -0,0 +1,23 @@ +--- +id: number +title: Number +sidebar_label: Number +--- + +## `config` + +* `enumValues` You can use the enum values providing a map of possible + exclusive values the property can take, mapped to the label that it is + displayed in the dropdown. + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. +* `min` Set the minimum value allowed. +* `max` Set the maximum value allowed. +* `lessThan` Value must be less than. +* `moreThan` Value must be more than. +* `positive` Value must be a positive number. +* `negative` Value must be a negative number. +* `integer` Value must be an integer. diff --git a/website/docs/properties/reference.md b/website/docs/properties/reference.md new file mode 100644 index 000000000..fed21abf7 --- /dev/null +++ b/website/docs/properties/reference.md @@ -0,0 +1,21 @@ +--- +id: reference +title: Reference +sidebar_label: Reference +--- +## `collectionPath` + +Absolute collection path of the collection this reference + points to. The schema of the entity is inferred based on the root navigation, + so the filters and search delegate existing there are applied to this view as + well. + +## `previewProperties` +List of properties rendered as this reference preview. + Defaults to first 3. + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. + diff --git a/website/docs/properties/string.md b/website/docs/properties/string.md new file mode 100644 index 000000000..abc0350b1 --- /dev/null +++ b/website/docs/properties/string.md @@ -0,0 +1,64 @@ +--- +id: string +title: String +sidebar_label: String +--- + +## `config` + +* `storageMeta` You can specify a `StorageMeta` configuration. It is used to + indicate that this string refers to a path in Google Cloud Storage. + * `mediaType` Media type of this reference, used for displaying the + preview. + * `storagePath` Absolute path in your bucket. You can specify it + directly or use a callback + * `acceptedFiles` File MIME types that can be uploaded to this + reference. + * `metadata` Specific metadata set in your uploaded file. + * `fileName` You can specify a fileName callback if you need to + customize the name of the file + * `storeUrl` When set to `true`, this flag indicates that the download + URL of the file will be saved in Firestore instead of the Cloud + storage path. Note that the generated URL may use a token that, if + disabled, may make the URL unusable and lose the original reference to + Cloud Storage, so it is not encouraged to use this flag. Defaults to + false. +* `url` If the value of this property is a URL, you can set this flag + to `true` + to add a link, or one of the supported media types to render a preview. +* `enumValues` You can use the enum values providing a map of possible + exclusive values the property can take, mapped to the label that it is + displayed in the dropdown. You can use a simple object with the format + `value` => `label`, or with the format `value` => `EnumValueConfig` if you + need extra customization, (like disabling specific options or assigning + colors). If you need to ensure the order of the elements, you can pass + a `Map` instead of a plain object. +* `multiline` Is this string property long enough, so it should be displayed + in a multiple line field. Defaults to false. If set to `true`, the number + of lines adapts to the content. +* `markdown` Should this string property be displayed as a markdown field. + If `true`, the field is rendered as a text editors that supports markdown + highlight syntax. It also includes a preview of the result. +* `previewAsTag` Should this string be rendered as a tag instead of just + text. + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. +* `unique` The value of this field must be unique in this collection. +* `uniqueInArray` If you set it to `true`, the user will only be allowed to + have the value of that property once in the parent + `ArrayProperty`. It works on direct children properties or on first level + children of a `MapProperty` (if set as the `.of` property of + the `ArrayProperty`). +* `length` Set a required length for the string value. +* `min` Set a minimum length limit for the string value. +* `max` Set a maximum length limit for the string value. +* `matches` Provide an arbitrary regex to match the value against. +* `email` Validates the value as an email address via a regex. +* `url` Validates the value as a valid URL via a regex. +* `trim` Transforms string values by removing leading and trailing + whitespace. +* `lowercase` Transforms the string value to lowercase. +* `uppercase` Transforms the string value to uppercase. diff --git a/website/docs/properties/timestamp.md b/website/docs/properties/timestamp.md new file mode 100644 index 000000000..f57cf0c8d --- /dev/null +++ b/website/docs/properties/timestamp.md @@ -0,0 +1,12 @@ +--- +id: timestamp +title: Timestamp +sidebar_label: Timestamp +--- + +## `validation` + +* `required` Should this field be compulsory. +* `requiredMessage` Message to be displayed as a validation error. +* `min` Set the minimum date allowed. +* `max` Set the maximum date allowed. diff --git a/website/docs/quickstart.md b/website/docs/quickstart.md new file mode 100644 index 000000000..a40d2e662 --- /dev/null +++ b/website/docs/quickstart.md @@ -0,0 +1,246 @@ +--- +id: quickstart +title: Quickstart +sidebar_label: Quickstart +--- + +- Create a new React app including Typescript: + +``` +npx create-react-app my-cms --template typescript +``` + +- Go into the new directory: + +``` +cd my-cms +``` + +- Install FireCMS and it's peer dependencies: + +``` +yarn add @camberi/firecms firebase @material-ui/core @material-ui/icons @material-ui/pickers @material-ui/lab +``` + +You can replace the content of the file App.tsx with the following sample code. +Remember to **replace** the Firebase config with the one you get after creating a +webapp in the Firebase console. + +```tsx +import React from "react"; + +import { + Authenticator, + buildCollection, + buildProperty, + buildSchema, + CMSApp, + NavigationBuilder, + NavigationBuilderProps +} from "@camberi/firecms"; + +import firebase from "firebase/app"; + +import "typeface-rubik"; +import "typeface-space-mono"; + +// TODO: Replace with your config +const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" +}; + +const locales = { + "de-DE": "German", + "en-US": "English (United States)", + "es-ES": "Spanish (Spain)", + "es-419": "Spanish (South America)" +}; + +const productSchema = buildSchema({ + name: "Product", + properties: { + name: { + title: "Name", + validation: { required: true }, + dataType: "string" + }, + price: { + title: "Price", + validation: { + required: true, + requiredMessage: "You must set a price between 0 and 1000", + min: 0, + max: 1000 + }, + description: "Price with range validation", + dataType: "number" + }, + status: { + title: "Status", + validation: { required: true }, + dataType: "string", + description: "Should this product be visible in the website", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + config: { + enumValues: { + private: "Private", + public: "Public" + } + } + }, + published: ({ values }) => buildProperty({ + title: "Published", + dataType: "boolean", + columnWidth: 100, + disabled: ( + values.status === "public" + ? false + : { + clearOnDisabled: true, + disabledMessage: "Status must be public in order to enable this the published flag" + } + ) + }), + main_image: buildProperty({ + title: "Image", + dataType: "string", + config: { + storageMeta: { + mediaType: "image", + storagePath: "images", + acceptedFiles: ["image/*"] + } + } + }), + tags: { + title: "Tags", + description: "Example of generic array", + validation: { required: true }, + dataType: "array", + of: { + dataType: "string" + } + }, + description: { + title: "Description", + description: "Not mandatory but it'd be awesome if you filled this up", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + dataType: "string", + columnWidth: 300 + }, + categories: { + title: "Categories", + validation: { required: true }, + dataType: "array", + of: { + dataType: "string", + config: { + enumValues: { + electronics: "Electronics", + books: "Books", + furniture: "Furniture", + clothing: "Clothing", + food: "Food" + } + } + } + }, + publisher: { + title: "Publisher", + description: "This is an example of a map property", + dataType: "map", + properties: { + name: { + title: "Name", + dataType: "string" + }, + external_id: { + title: "External id", + dataType: "string" + } + } + }, + expires_on: { + title: "Expires on", + dataType: "timestamp" + } + } +}); + +const localeSchema = buildSchema({ + customId: locales, + name: "Locale", + properties: { + title: { + title: "Title", + validation: { required: true }, + dataType: "string" + }, + selectable: { + title: "Selectable", + description: "Is this locale selectable", + dataType: "boolean" + }, + video: { + title: "Video", + dataType: "string", + validation: { required: false }, + config: { + storageMeta: { + mediaType: "video", + storagePath: "videos", + acceptedFiles: ["video/*"] + } + } + } + } +}); + +export function App() { + + const navigation: NavigationBuilder = ({ user }: NavigationBuilderProps) => ({ + collections: [ + buildCollection({ + relativePath: "products", + schema: productSchema, + name: "Products", + permissions: ({ user }) => ({ + edit: true, + create: true, + delete: true + }), + subcollections: [ + buildCollection({ + name: "Locales", + relativePath: "locales", + schema: localeSchema + }) + ] + }) + ] + }); + + const myAuthenticator: Authenticator = (user?: firebase.User) => { + console.log("Allowing access to", user?.email); + return true; + }; + + return ; +} +``` + +Then simply run +``` +yarn start +``` + diff --git a/website/docs/ref.md b/website/docs/ref.md new file mode 100644 index 000000000..5e5287a7e --- /dev/null +++ b/website/docs/ref.md @@ -0,0 +1,203 @@ +--- +id: ref +title: Ref +sidebar_label: Ref +slug: /ref +--- + +You can write content using [GitHub-flavored Markdown syntax](https://github.github.com/gfm/). + +## Markdown Syntax + +To serve as an example page when styling markdown based Docusaurus sites. + +## Headers + +# H1 - Create the best documentation + +## H2 - Create the best documentation + +### H3 - Create the best documentation + +#### H4 - Create the best documentation + +##### H5 - Create the best documentation + +###### H6 - Create the best documentation + +--- + +## Emphasis + +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ + +--- + +## Lists + +1. First ordered list item +1. Another item + - Unordered sub-list. +1. Actual numbers don't matter, just that it's a number + 1. Ordered sub-list +1. And another item. + +* Unordered list can use asterisks + +- Or minuses + ++ Or pluses + +--- + +## Links + +[I'm an inline-style link](https://www.google.com/) + +[I'm an inline-style link with title](https://www.google.com/ "Google's Homepage") + +[I'm a reference-style link][arbitrary case-insensitive reference text] + +[You can use numbers for reference-style link definitions][1] + +Or leave it empty and use the [link text itself]. + +URLs and URLs in angle brackets will automatically get turned into links. http://www.example.com/ or and sometimes example.com (but not on GitHub, for example). + +Some text to show that the reference links can follow later. + +[arbitrary case-insensitive reference text]: https://www.mozilla.org/ +[1]: http://slashdot.org/ +[link text itself]: http://www.reddit.com/ + +--- + +## Images + +Here's our logo (hover to see the title text): + +Inline-style: ![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png 'Logo Title Text 1') + +Reference-style: ![alt text][logo] + +[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png 'Logo Title Text 2' + +Images from any folder can be used by providing path to file. Path should be relative to markdown file. + +![img](../static/img/logo_small.png) + +--- + +## Code + +```javascript +var s = 'JavaScript syntax highlighting'; +alert(s); +``` + +```python +s = "Python syntax highlighting" +print(s) +``` + +``` +No language indicated, so no syntax highlighting. +But let's throw in a tag. +``` + +```js {2} +function highlightMe() { + console.log('This line can be highlighted!'); +} +``` + +--- + +## Tables + +Colons can be used to align columns. + +| Tables | Are | Cool | +| ------------- | :-----------: | -----: | +| col 3 is | right-aligned | \$1600 | +| col 2 is | centered | \$12 | +| zebra stripes | are neat | \$1 | + +There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown. + +| Markdown | Less | Pretty | +| -------- | --------- | ---------- | +| _Still_ | `renders` | **nicely** | +| 1 | 2 | 3 | + +--- + +## Blockquotes + +> Blockquotes are very handy in email to emulate reply text. This line is part of the same quote. + +Quote break. + +> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can _put_ **Markdown** into a blockquote. + +--- + +## Inline HTML + +
+
Definition list
+
Is something people use sometimes.
+ +
Markdown in HTML
+
Does *not* work **very** well. Use HTML tags.
+
+ +--- + +## Line Breaks + +Here's a line for us to start with. + +This line is separated from the one above by two newlines, so it will be a _separate paragraph_. + +This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the _same paragraph_. + +--- + +## Admonitions + +:::note + +This is a note + +::: + +:::tip + +This is a tip + +::: + +:::important + +This is important + +::: + +:::caution + +This is a caution + +::: + +:::warning + +This is a warning + +::: diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 000000000..39c5cacba --- /dev/null +++ b/website/docusaurus.config.js @@ -0,0 +1,110 @@ +module.exports = { + title: 'FireCMS', + tagline: 'Awesome Firestore based headless CMS, developed by Camberi', + url: 'https://firecms.co', + baseUrl: '/', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + favicon: 'img/favicon.ico', + organizationName: 'Camberi', + projectName: 'FireCMS', + plugins: [ + 'docusaurus-plugin-sass', + 'docusaurus-tailwindcss-loader', + [ + '@docusaurus/plugin-sitemap', + { + changefreq: 'weekly', + priority: 0.5, + trailingSlash: false + } + ] + ], + themeConfig: { + navbar: { + title: 'FireCMS', + logo: { + alt: 'FireCMS Logo', + src: 'img/logo_small.png' + }, + items: [ + { + to: 'docs/', + activeBasePath: 'docs', + label: 'Docs', + position: 'left' + }, + // { to: 'blog', label: 'Blog', position: 'left' }, + { + href: 'https://github.com/Camberi/firecms', + // label: 'GitHub', + className: 'header-github-link', + 'aria-label': 'GitHub repository', + position: 'right' + } + ] + }, + footer: { + links: [ + { + // Label of the section of these links + title: 'Docs', + items: [ + { + label: 'Intro', + to: 'docs/', + }, + ], + }, + { + title: 'Community', + items: [ + { + label: 'Reddit', + href: 'https://www.reddit.com/r/firecms/', + }, + // { + // label: 'Discord', + // href: 'https://discordapp.com/invite/docusaurus', + // }, + // { + // label: 'Twitter', + // href: 'https://twitter.com/docusaurus', + // }, + // { + // //Renders the html pass-through instead of a simple link + // html: ` + // + // Deploys by Netlify + // + // `, + // }, + ], + }, + ], + }, + prism: { + theme: require('prism-react-renderer/themes/vsDark'), + }, + }, + presets: [ + [ + '@docusaurus/preset-classic', + { + docs: { + sidebarPath: require.resolve('./sidebars.js') + }, + // blog: { + // showReadingTime: true, + // editUrl: + // 'https://github.com/facebook/docusaurus/edit/master/website/blog/' + // }, + theme: { + customCss: [ + require.resolve('./src/css/custom.css'), + ] + } + } + ] + ] +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 000000000..47eb550ec --- /dev/null +++ b/website/package.json @@ -0,0 +1,61 @@ +{ + "name": "website", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle --danger", + "deploy": "docusaurus deploy", + "serve": "docusaurus serve", + "clear": "docusaurus clear" + }, + "dependencies": { + "@docusaurus/core": "^2.0.0-beta.0", + "@docusaurus/plugin-sitemap": "^2.0.0-beta.0", + "@docusaurus/preset-classic": "^2.0.0-beta.0", + "@docusaurus/theme-live-codeblock": "^2.0.0-beta.0", + "@mdx-js/react": "^1.6.21", + "aos": "^2.3.4", + "clsx": "^1.1.1", + "docusaurus-plugin-sass": "^0.1.11", + "docusaurus-tailwindcss-loader": "file:plugins/docusaurus-tailwindcss-loader", + "gpu.js": "^2.11.0", + "gsap": "^3.5.1", + "node-sass": "^4.14.1", + "perlin": "^1.0.0", + "postcss": "^7.0.0", + "postcss-import": "^12.0.0", + "postprocessing": "^6.19.1", + "react": "^16.8.4", + "react-code-blocks": "^0.0.8", + "react-dom": "^16.8.4", + "tailwindcss": "npm:@tailwindcss/postcss7-compat", + "three": "^0.126.1", + "three-bas": "^2.12.0", + "three-buffer-geometry-utils": "^1.0.0", + "ts-easing": "^0.2.0" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^2.0.0-alpha.70", + "@tailwindcss/custom-forms": "^0.2.1", + "@tsconfig/docusaurus": "^1.0.2", + "@types/react": "^17.0.2", + "@types/react-helmet": "^6.1.0", + "@types/react-router-dom": "^5.1.7", + "typescript": "^4.2.2" + } +} diff --git a/website/plugins/docusaurus-tailwindcss-loader/index.js b/website/plugins/docusaurus-tailwindcss-loader/index.js new file mode 100644 index 000000000..88a13c397 --- /dev/null +++ b/website/plugins/docusaurus-tailwindcss-loader/index.js @@ -0,0 +1,34 @@ +module.exports = function (context, options) { + return { + name: 'postcss-tailwindcss-loader', + configureWebpack(config, isServer, utils) { + return { + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: require.resolve('postcss-loader'), + options: { + ident: 'postcss', + plugins: () => [ + require('postcss-import'), + require('tailwindcss'), + require('postcss-preset-env')({ + autoprefixer: { + flexbox: 'no-2009', + }, + stage: 4, + }), + ], + }, + }, + ], + }, + ], + }, + } + }, + } +} diff --git a/website/sidebars.js b/website/sidebars.js new file mode 100644 index 000000000..5c32e5a01 --- /dev/null +++ b/website/sidebars.js @@ -0,0 +1,47 @@ +module.exports = { + mySidebar: [ + { + type: 'doc', + label: 'Introduction', + id: 'intro' + }, + { + type: 'doc', + label: 'Quickstart', + id: 'quickstart' + }, + { + type: 'category', + label: 'Configuration', + items: [ + 'cms_config', + 'entity_schemas', + { + type: 'category', + label: 'Properties', + items: [ + 'properties/string', + 'properties/number', + 'properties/boolean', + 'properties/reference', + 'properties/timestamp', + 'properties/array', + 'properties/map', + 'properties/geopoint' + ] + }, + 'custom_fields', + 'collections', + { + type: 'category', + label: 'Contexts and hooks', + items: [ + 'contexts/auth_context', + 'contexts/side_entity_controller', + 'contexts/snackbar', + ] + }, + ] + } + ] +} diff --git a/website/src/css/additional-styles/utility-patterns.css b/website/src/css/additional-styles/utility-patterns.css new file mode 100644 index 000000000..a0ac1073e --- /dev/null +++ b/website/src/css/additional-styles/utility-patterns.css @@ -0,0 +1,38 @@ +.h1 { + @apply text-4xl font-extrabold leading-tight tracking-tighter; +} + +.h2 { + @apply text-3xl font-extrabold leading-tight tracking-tighter; +} + +.h3 { + @apply text-3xl font-bold leading-tight; +} + +.h4 { + @apply text-2xl font-bold leading-snug tracking-tight; +} + +@screen md { + .h1 { + @apply text-5xl; + } + + .h2 { + @apply text-4xl; + } +} + +.btn, +.btn-sm { + @apply font-medium inline-flex items-center justify-center border border-transparent rounded leading-snug transition duration-150 ease-in-out; +} + +.btn { + @apply px-8 py-3 shadow-lg; +} + +.btn-sm { + @apply px-4 py-2 shadow; +} diff --git a/website/src/css/custom.css b/website/src/css/custom.css new file mode 100644 index 000000000..a0e7562c2 --- /dev/null +++ b/website/src/css/custom.css @@ -0,0 +1,60 @@ +/* stylelint-disable docusaurus/copyright-header */ +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #0070f4; + --ifm-color-primary-dark: #0065dc; + --ifm-color-primary-darker: #005fcf; + --ifm-color-primary-darkest: #004eab; + --ifm-color-primary-light: #0d7cff; + --ifm-color-primary-lighter: #1a83ff; + --ifm-color-primary-lightest: #3e97ff; + --ifm-code-font-size: 95%; + /*--ifm-code-background: 95%;*/ +} + +.font-mono code { + width: 100%; + padding: 12px; +} + +.prism-code .token.keyword{ + font-weight: 600; +} + +.docusaurus-highlight-code-line { + /*background-color: rgb(72, 77, 91);*/ + /*display: block;*/ + /*margin: 0 calc(-1 * var(--ifm-pre-padding));*/ + /*padding: 0 var(--ifm-pre-padding);*/ +} + +button a { + text-decoration: none; +} + +a.btn { + text-decoration: none; +} + + +.header-github-link:hover { + opacity: 0.6; +} + +.header-github-link:before { + content: ''; + width: 24px; + height: 24px; + display: flex; + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; +} + +html[data-theme=dark] .header-github-link:before { + background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; +} diff --git a/website/src/css/tailwind.css b/website/src/css/tailwind.css new file mode 100644 index 000000000..d59961d23 --- /dev/null +++ b/website/src/css/tailwind.css @@ -0,0 +1,5 @@ +@import 'additional-styles/utility-patterns.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx new file mode 100644 index 000000000..7412e16a0 --- /dev/null +++ b/website/src/pages/index.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from "react"; +import Layout from "@theme/Layout"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import HeroHome from "../partials/HeroHome"; +import FeaturesHome from "../partials/Features"; +import FeaturesBlocks from "../partials/FeaturesBlocks"; +import Testimonials from "../partials/Testimonials"; + +import AOS from "aos"; +import "aos/dist/aos.css"; +import "../css/tailwind.css"; +import Separator from "../partials/Separator"; +import FirebaseIntro from "../partials/FirebaseIntro"; +import { ThreeJSAnimationShader } from "../shape/ThreeJSAnimationShader"; + +function Home() { + const context = useDocusaurusContext(); + const { siteConfig = {} } = context; + + const [scroll, setScroll] = useState(window.pageYOffset); + + useEffect(() => { + const listener = () => { + setScroll(window.pageYOffset); + }; + window.addEventListener("scroll", listener); + return () => { + window.removeEventListener("scroll", listener); + }; + }, []); + + useEffect(() => { + AOS.init(); + }, []); + + return ( + +
+ +
+ + + + + + + + + + + + + + +
+ +
+
+ ); +} + +export default Home; diff --git a/website/src/pages/styles.module.css b/website/src/pages/styles.module.css new file mode 100644 index 000000000..c1aa85121 --- /dev/null +++ b/website/src/pages/styles.module.css @@ -0,0 +1,37 @@ +/* stylelint-disable docusaurus/copyright-header */ + +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +@media screen and (max-width: 966px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} + +.features { + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; +} + +.featureImage { + height: 200px; + width: 200px; +} diff --git a/website/src/partials/Features.tsx b/website/src/partials/Features.tsx new file mode 100644 index 000000000..a7ea6209a --- /dev/null +++ b/website/src/partials/Features.tsx @@ -0,0 +1,285 @@ +import React, { useEffect, useRef, useState } from "react"; +import Transition from "./utils/Transition"; + +// @ts-ignore +import featuresBg from "@site/static/img/features-bg.png"; +// @ts-ignore +import featuresElement from "@site/static/img/features-element.png"; + +// @ts-ignore +import ReactLogo from "@site/static/img/reactjs-icon.svg"; +// @ts-ignore +import FireCMSLogo from "@site/static/img/firecms_logo.svg"; +// @ts-ignore +import FirebaseLogo from "@site/static/img/firebase.svg"; +// @ts-ignore +import pricePreview from "@site/static/img/price.png"; +// @ts-ignore +import cmsPreview from "@site/static/img/editing.mp4"; + +import { vs2015, atomOneLight, CodeBlock } from "react-code-blocks"; +import useThemeContext from "@theme/hooks/useThemeContext"; + +function Features() { + + const { isDarkTheme } = useThemeContext(); + const [tab, setTab] = useState(0); + + const tabsRef = useRef(); + + const heightFix = () => { + if (tabsRef.current && tabsRef.current.children[tab]) { + // @ts-ignore + tabsRef.current.style.height = tabsRef.current.children[tab].offsetHeight + "px"; + } + }; + useEffect(() => { + heightFix(); + }, [tab]); + + function buildFeature(title: string, text: string, icon: React.ReactNode, thisTab: number) { + return
{ + e.preventDefault(); + setTab(thisTab); + }} + > +
+
+ {title} +
+
+ {text} +
+
+
+ {icon} +
+
; + } + + + const tab0 = ( +
+ +
+ ); + + const tab1 = ( +
+ + + +
+ Element +
+
+ ); + + const tab2 = ( +
+ { + values.uppercase = values.name.toUpperCase(); + return values; + }, + properties: { + name: { + dataType: "string", + title: "Name" + }, + uppercase: { + dataType: "string", + title: "Uppercase Name", + readOnly: true + } + }, + defaultValues: { + name: "Default name", + } +}); +` + } + language={"typescript"} + showLineNumbers={false} + theme={isDarkTheme ? vs2015 : atomOneLight} + /> +
+ ); + + return ( +
+ +
+
+ +
+
+
+

Ready out of the + box

+

+ Map your collections and document schemas to + beautiful tables and forms +

+
+ +
+ + + {buildFeature("Powerful editing", + ` + Edit your collections and entities using both a spreadsheet + view and powerful forms. + `, + arrowIcon, 0)} + + {buildFeature("Easy schema definition", + ` + Define your entity schemas and choose from multiple + form widgets and validation options. + `, + lightningIcon, 1)} + + {buildFeature("Customization", + ` + Integrate your own custom form fields as React components, + value previews or save callbacks. + `, + settingsIcon, 2)} + +
+
+ +
+
+ + + {tab0} + + + + {tab1} + + + + {tab2} + + +
+
+
+
+
+
+ ); +} + + +const lightningIcon = + +; + +const arrowIcon = + +; + +const settingsIcon = + + + + +; + +export default Features; diff --git a/website/src/partials/FeaturesBlocks.tsx b/website/src/partials/FeaturesBlocks.tsx new file mode 100644 index 000000000..227deafb9 --- /dev/null +++ b/website/src/partials/FeaturesBlocks.tsx @@ -0,0 +1,289 @@ +import React from "react"; + +function FeaturesBlocks() { + return ( +
+
+
+
+

Full set of features

+

+ Everything you need to kickstart your Firebase based project +

+
+ +
+
+ + + + + + + + + + + +

+ Schema validation +

+

+ Define your data types and validation to ensure consistency +

+
+ +
+ + + + + + + + + +

+ Reference support +

+

+ Link entities in different collections in a seamless way +

+
+ +
+ + + + + + + + + + + +

+ Role system +

+

+ Define different app configurations based + on the logged user +

+
+ +
+ + + + + + + + + +

+ Customization +

+

+ Custom form fields, hooks and full views based + on React +

+
+ +
+ + + + + + + + + + + +

+ Subcollection support +

+

+ Complete navigation for collections under other + entities +

+
+
+ + + + + + + + + + + + +

+ Real time support +

+

+ In every view of your CMS, ideal for background + updates +

+
+
+ +
+
+
+ ); +} + +export default FeaturesBlocks; diff --git a/website/src/partials/FirebaseIntro.tsx b/website/src/partials/FirebaseIntro.tsx new file mode 100644 index 000000000..3f63646f3 --- /dev/null +++ b/website/src/partials/FirebaseIntro.tsx @@ -0,0 +1,62 @@ +import React from "react"; +// @ts-ignore +import featuresBg from "@site/static/img/features-bg.png"; +// @ts-ignore +import featuresElement from "@site/static/img/features-element.png"; + +// @ts-ignore +import ReactLogo from "@site/static/img/reactjs-icon.svg"; +// @ts-ignore +import FireCMSLogo from "@site/static/img/firecms_logo.svg"; +// @ts-ignore +import FirebaseLogo from "@site/static/img/firebase.svg"; + +function FirebaseIntro() { + return ( +
+ +
+ {/*
*/} +
+
+ + + +
+
+ +
+

+ Don't build another admin tool +

+

+ FireCMS is an open source CMS built by developers + for developers. +
+ Get a back office app for your Firebase project in + no time. +

+
+