├── backend
├── modules
│ ├── asset
│ │ ├── ui
│ │ │ └── src
│ │ │ │ ├── scss
│ │ │ │ ├── _widgets.scss
│ │ │ │ ├── _settings.scss
│ │ │ │ ├── _layout.scss
│ │ │ │ ├── _type.scss
│ │ │ │ └── _welcome.scss
│ │ │ │ ├── index.js
│ │ │ │ └── index.scss
│ │ └── index.js
│ ├── @apostrophecms
│ │ ├── asset
│ │ │ └── index.js
│ │ ├── express
│ │ │ └── index.js
│ │ ├── widget-type
│ │ │ ├── views
│ │ │ │ └── box.html
│ │ │ ├── ui
│ │ │ │ └── src
│ │ │ │ │ └── index.scss
│ │ │ └── index.js
│ │ ├── page
│ │ │ ├── views
│ │ │ │ └── notFound.html
│ │ │ └── index.js
│ │ ├── settings
│ │ │ └── index.js
│ │ └── home-page
│ │ │ ├── index.js
│ │ │ └── views
│ │ │ └── page.html
│ ├── snippet-widget
│ │ ├── views
│ │ │ └── widget.html
│ │ └── index.js
│ ├── grid-layout-widget
│ │ ├── lib
│ │ │ ├── get-layout-classes.js
│ │ │ ├── get-layout-config.js
│ │ │ └── get-layout-areas.js
│ │ ├── views
│ │ │ └── widget.html
│ │ ├── public
│ │ │ └── preview.svg
│ │ ├── ui
│ │ │ └── src
│ │ │ │ └── index.scss
│ │ ├── index.js
│ │ └── layoutPreviews.html
│ ├── default-page
│ │ ├── views
│ │ │ └── page.html
│ │ └── index.js
│ ├── company-widget
│ │ ├── views
│ │ │ └── widget.html
│ │ └── index.js
│ └── snippet
│ │ └── index.js
├── public
│ └── images
│ │ └── logo.png
├── deployment
│ ├── README
│ ├── settings.staging
│ ├── migrate
│ ├── rsync_exclude.txt
│ ├── stop
│ ├── settings
│ ├── dependencies
│ └── start
├── .gitignore
├── LICENSE
├── lib
│ └── content-area-widgets.js
├── scripts
│ ├── sync-down
│ └── sync-up
├── package.json
├── views
│ └── layout.html
├── app.js
└── README.md
├── .eslintignore
├── frontend
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── index.html
├── src
│ ├── setupTests.js
│ ├── App.test.js
│ ├── index.css
│ ├── reportWebVitals.js
│ ├── index.js
│ ├── App.css
│ ├── App.js
│ └── logo.svg
├── config
│ ├── webpack
│ │ └── persistentCache
│ │ │ └── createEnvironmentHash.js
│ ├── jest
│ │ ├── cssTransform.js
│ │ ├── babelTransform.js
│ │ └── fileTransform.js
│ ├── getHttpsConfig.js
│ ├── paths.js
│ ├── modules.js
│ ├── env.js
│ └── webpack.config.js
├── .gitignore
├── scripts
│ ├── test.js
│ └── build.js
├── node
│ └── server.js
├── README.md
└── package.json
└── .eslintrc
/backend/modules/asset/ui/src/scss/_widgets.scss:
--------------------------------------------------------------------------------
1 | .bp-video-widget {
2 | width: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /public/apos-frontend
2 | /data/temp
3 | /apos-build
4 | /modules/asset/ui/public
5 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/gt-samples/main/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/gt-samples/main/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/gt-samples/main/frontend/public/logo512.png
--------------------------------------------------------------------------------
/backend/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/gt-samples/main/backend/public/images/logo.png
--------------------------------------------------------------------------------
/backend/modules/asset/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | // Your own project level JS may go here
3 | console.log('Hello World');
4 | };
5 |
--------------------------------------------------------------------------------
/backend/modules/asset/ui/src/scss/_settings.scss:
--------------------------------------------------------------------------------
1 | $color-purple: #6236ff;
2 | $color-purple-light: #8264f1;
3 | $color-pink: #fe5599;
4 | $color-green: #0c8;
5 | $color-gold: #f7b500;
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/asset/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // When not in production, refresh the page on restart
3 | options: {
4 | refreshOnRestart: true
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "apostrophe"
4 | ],
5 | "globals": {
6 | "apos": true
7 | },
8 | "rules": {
9 | "no-var": "error",
10 | "no-console": 0
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/express/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | session: {
4 | // If this still says `undefined`, set a real secret!
5 | secret: undefined
6 | }
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/widget-type/views/box.html:
--------------------------------------------------------------------------------
1 |
5 | {{ data.content | safe }}
6 |
7 |
--------------------------------------------------------------------------------
/backend/modules/snippet-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {# in sites/snippet-widget/views/widget.html #}
2 |
3 | {% for snippet in data.widget._snippets %}
4 | {% area snippet, 'content' with { edit: false } %}
5 | {% endfor %}
6 |
--------------------------------------------------------------------------------
/backend/deployment/README:
--------------------------------------------------------------------------------
1 | This is a deployment folder for use with Stagecoach.
2 |
3 | You don't have to use Stagecoach.
4 |
5 | It's just a neat solution for deploying node apps.
6 |
7 | See:
8 |
9 | http://github.com/apostrophecms/stagecoach
10 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/frontend/config/webpack/persistentCache/createEnvironmentHash.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { createHash } = require('crypto');
3 |
4 | module.exports = env => {
5 | const hash = createHash('md5');
6 | hash.update(JSON.stringify(env));
7 |
8 | return hash.digest('hex');
9 | };
10 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/backend/deployment/settings.staging:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Settings specific to the 'master' deployment target.
4 | # USER is the ssh user, SERVER is the ssh host. USER should
5 | # match the USER setting in /opt/stagecoach/settings on
6 | # the server
7 |
8 | USER=nodeapps
9 | SERVER=staging.apos.dev
10 |
--------------------------------------------------------------------------------
/backend/modules/asset/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | @import url('~normalize.css');
2 | // NOTE: We're using a `.bp-` namespace to indicate these are the boilerplate
3 | // styles.
4 | @import './scss/_settings';
5 | @import './scss/_type';
6 | @import './scss/_layout';
7 | @import './scss/_welcome';
8 | @import './scss/_widgets';
9 |
--------------------------------------------------------------------------------
/backend/modules/grid-layout-widget/lib/get-layout-classes.js:
--------------------------------------------------------------------------------
1 | // Build classes for the layout
2 |
3 | module.exports = widget => [
4 | 'custom-layout-widget',
5 | `custom-layout-widget-${widget._id}`,
6 | `layout-type-${widget.layoutType}`,
7 | widget.maxWidth
8 | ].filter(Boolean).join(' ');
9 |
10 |
--------------------------------------------------------------------------------
/backend/deployment/migrate:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export NODE_ENV=production
4 |
5 | # Run any necessary database migration tasks that should happen while the
6 | # site is paused here.
7 | #
8 | # We don't have any, 3.x policy is safe migrations only. -Tom
9 |
10 | # node app @apostrophecms/migration:migrate
11 | #
12 | #echo "Site migrated"
13 |
--------------------------------------------------------------------------------
/backend/modules/snippet-widget/index.js:
--------------------------------------------------------------------------------
1 | // in sites/snippet-widget/index.js
2 |
3 | module.exports = {
4 | extend: '@apostrophecms/widget-type',
5 | fields: {
6 | add: {
7 | _snippets: {
8 | type: 'relationship',
9 | withType: 'snippet',
10 | required: true,
11 | max: 1
12 | }
13 | }
14 | }
15 | };
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/page/views/notFound.html:
--------------------------------------------------------------------------------
1 | {#
2 | Use this template to build out your 404 error pages. Like page templates,
3 | it inherits a global layout.
4 | #}
5 |
6 | {% extends "layout.html" %}
7 |
8 | {% block title %}404 - Page not found{% endblock %}
9 |
10 | {% block main %}
11 | We're sorry. We couldn't find the page you're looking for.
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/frontend/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/backend/modules/default-page/views/page.html:
--------------------------------------------------------------------------------
1 | {#
2 | This is an example home page template. It inherits and extends a layout template
3 | that lives in the top-level views/ folder for convenience
4 | #}
5 |
6 | {% extends "layout.html" %}
7 |
8 | {% block main %}
9 |
10 | {{ data.page.title }}
11 | {% area data.page, 'main' %}
12 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/page/index.js:
--------------------------------------------------------------------------------
1 | // This configures the @apostrophecms/page module to add a "home" page type to the
2 | // pages menu
3 |
4 | module.exports = {
5 | options: {
6 | types: [
7 | {
8 | name: 'default-page',
9 | label: 'Default'
10 | },
11 | {
12 | name: '@apostrophecms/home-page',
13 | label: 'Home'
14 | }
15 | ]
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/settings/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | subforms: {
4 | title: {
5 | fields: [ 'title' ],
6 | protection: true,
7 | reload: true
8 | },
9 | changePassword: {
10 | fields: [ 'password' ]
11 | }
12 | },
13 |
14 | groups: {
15 | account: {
16 | label: 'Account',
17 | subforms: [ 'title', 'changePassword' ]
18 | }
19 | }
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/home-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | options: {
3 | label: 'Home Page'
4 | },
5 | fields: {
6 | add: {
7 | main: {
8 | type: 'area',
9 | options: {
10 | widgets: {
11 | 'grid-layout': {}
12 | }
13 | }
14 | }
15 | },
16 | group: {
17 | basics: {
18 | label: 'Basics',
19 | fields: [
20 | 'title',
21 | 'main'
22 | ]
23 | }
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/widget-type/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | .widget-percent {
2 | display: block;
3 | margin: 16px auto;
4 | }
5 |
6 | .widget-pixels {
7 | display: block;
8 | margin: 16px auto;
9 | max-width: 100%;
10 | }
11 |
12 | .widget-one-half {
13 | display: block;
14 | margin: 16px auto;
15 | width: 50%;
16 | }
17 |
18 | .widget-full {
19 | display: block;
20 | margin: 16px auto;
21 | width: 100%;
22 | }
23 |
24 | .bp-image-widget {
25 | // Otherwise it doesn't heed the box
26 | width: 100%;
27 | }
28 |
--------------------------------------------------------------------------------
/backend/modules/@apostrophecms/home-page/views/page.html:
--------------------------------------------------------------------------------
1 | {#
2 | This is an example home page template. It inherits and extends a layout template
3 | that lives in the top-level views/ folder for convenience
4 | #}
5 |
6 | {% extends "layout.html" %}
7 |
8 | {% block main %}
9 |
10 |
11 | CMS page: {{ data.page.title }}
12 |
13 |
14 | {% area data.page, 'main' %}
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/backend/modules/default-page/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@apostrophecms/page-type',
3 | options: {
4 | label: 'Default Page'
5 | },
6 | fields: {
7 | add: {
8 | main: {
9 | type: 'area',
10 | options: {
11 | widgets: {
12 | 'grid-layout': {}
13 | }
14 | }
15 | }
16 | },
17 | group: {
18 | basics: {
19 | label: 'Basics',
20 | fields: [
21 | 'title',
22 | 'main'
23 | ]
24 | }
25 | }
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | package-lock.json
2 | /locales
3 | npm-debug.log
4 | /data
5 | /public/uploads
6 | /public/apos-minified
7 | /data/temp/uploadfs
8 | node_modules
9 | # This folder is created on the fly and contains symlinks updated at startup (we'll come up with a Windows solution that actually copies things)
10 | /public/modules
11 | # Don't commit build files
12 | /apos-build
13 | /public/apos-frontend
14 | /modules/asset/ui/public/site.js
15 | # Don't commit masters generated on the fly at startup, these import all the rest
16 | /public/css/master-*.less
17 | .jshintrc
18 | .DS_Store
19 |
20 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(document.getElementById('root'));
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/backend/deployment/rsync_exclude.txt:
--------------------------------------------------------------------------------
1 | # List files and folders that shouldn't be deployed (such as data folders and runtime status files) here.
2 | # In our projects .git and .gitignore are good candidates, also 'data' which contains persistent files
3 | # that are *not* part of deployment. A good place for things like data/port, data/pid, and any
4 | # sqlite databases or static web content you may need
5 | data
6 | temp
7 | public/uploads
8 | public/apos-frontend
9 | .git
10 | .gitignore
11 | # We don't deploy these anymore, instead we always 'npm install' to ensure
12 | # that any compiled C++ modules are built for the right architecture
13 | node_modules
14 |
--------------------------------------------------------------------------------
/backend/modules/company-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {#
2 | Just an example of a more complex widget with array fields, to show how the
3 | live preview feature behaves in this context
4 | #}
5 |
6 | Company: {{ data.widget.name }}
7 |
8 | Locations
9 |
10 | {% for location in data.widget.locations %}
11 |
12 | {{ location.street }}
13 | {{ location.city }}, {{ location.state }} {{ location.zip }}
14 |
15 | {% endfor %}
16 |
17 | Products
18 |
19 | {% for product in data.widget.products %}
20 | {{ product.name }}
21 | Price: ${{ product.price }}
22 | {% area product, 'body' %}
23 | {% endfor %}
24 |
--------------------------------------------------------------------------------
/backend/modules/snippet/index.js:
--------------------------------------------------------------------------------
1 | // Don't allow snippets in snippets
2 | const { snippet, ...widgets } = require('../../lib/content-area-widgets.js');
3 |
4 | module.exports = {
5 | extend: '@apostrophecms/piece-type',
6 | options: {
7 | label: 'Snippet',
8 | pluralLabel: 'Snippets'
9 | },
10 | fields: {
11 | add: {
12 | content: {
13 | type: 'area',
14 | label: 'Content',
15 | options: {
16 | widgets
17 | }
18 | }
19 | },
20 | group: {
21 | basics: {
22 | label: 'Basics',
23 | fields: [
24 | 'title',
25 | 'content'
26 | ]
27 | }
28 | }
29 | }
30 | };
--------------------------------------------------------------------------------
/frontend/config/jest/babelTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const babelJest = require('babel-jest').default;
4 |
5 | const hasJsxRuntime = (() => {
6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
7 | return false;
8 | }
9 |
10 | try {
11 | require.resolve('react/jsx-runtime');
12 | return true;
13 | } catch (e) {
14 | return false;
15 | }
16 | })();
17 |
18 | module.exports = babelJest.createTransformer({
19 | presets: [
20 | [
21 | require.resolve('babel-preset-react-app'),
22 | {
23 | runtime: hasJsxRuntime ? 'automatic' : 'classic',
24 | },
25 | ],
26 | ],
27 | babelrc: false,
28 | configFile: false,
29 | });
30 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/deployment/stop:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export NODE_ENV=production
4 |
5 | # Shut the site down, for instance by tweaking a .htaccess file to display
6 | # a 'please wait' notice, or stopping a node server
7 |
8 | if [ ! -f "app.js" ]; then
9 | echo "I don't see app.js in the current directory."
10 | exit 1
11 | fi
12 |
13 | # Stop the node app via 'forever'. You'll get a harmless warning if the app
14 | # was not already running. Use `pwd` to make sure we have a full path,
15 | # forever is otherwise easily confused and will stop every server with
16 | # the same filename
17 | forever stop `pwd`/app.js && echo "Site stopped"
18 |
19 | # Stop the app without 'forever'. We recommend using 'forever' for node apps,
20 | # but this may be your best bet for non-node apps
21 | #
22 | # if [ -f "data/pid" ]; then
23 | # kill `cat data/pid`
24 | # rm data/pid
25 | # echo "Site stopped"
26 | # else
27 | # echo "Site was not running"
28 | # fi
29 |
30 |
--------------------------------------------------------------------------------
/backend/deployment/settings:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Settings shared by all targets (staging, production, etc). Usually the
4 | # shortname of the project (which is also the hostname for the frontend
5 | # proxy server used for staging sites) and the directory name. For our
6 | # web apps that use sc-proxy we make sure each is a subdirectory
7 | # of /opt/stagecoach/apps
8 |
9 | # Should match the repo name = short name = everything name!
10 | PROJECT=gt-samples
11 |
12 | DIR=/opt/stagecoach/apps/$PROJECT
13 |
14 | # Adjust the PATH environment variable on the remote host. Here's an example
15 | # for deploying to MacPorts
16 | #ADJUST_PATH='export PATH=/opt/local/bin:$PATH'
17 |
18 | # ... But you probably won't need to on real servers. I just find it handy for
19 | # testing parts of stagecoach locally on a Mac. : is an acceptable "no-op" (do-nothing) statement
20 | ADJUST_PATH=':'
21 |
22 | # ssh port. Sensible people leave this set to 22 but it's common to do the
23 | # "security by obscurity" thing alas
24 | SSH_PORT=22
25 |
--------------------------------------------------------------------------------
/backend/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Apostrophe Technologies
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/backend/lib/content-area-widgets.js:
--------------------------------------------------------------------------------
1 | // Reused by various areas so we can maintain the list in one place.
2 | // These are content widgets, not the high level grid layout widget,
3 | // which is currently offered at the page level in the home-page
4 | // and default-page modules to encourage users to think about layout
5 | // first, then about typing in content
6 |
7 | module.exports = {
8 | '@apostrophecms/rich-text': {
9 | toolbar: [
10 | 'styles',
11 | '|',
12 | 'bold',
13 | 'italic',
14 | 'strike',
15 | 'link',
16 | '|',
17 | 'bulletList',
18 | 'orderedList'
19 | ],
20 | styles: [
21 | {
22 | tag: 'p',
23 | label: 'Paragraph (P)'
24 | },
25 | {
26 | tag: 'h3',
27 | label: 'Heading 3 (H3)'
28 | },
29 | {
30 | tag: 'h4',
31 | label: 'Heading 4 (H4)'
32 | }
33 | ],
34 | insert: [
35 | 'table',
36 | 'image'
37 | ]
38 | },
39 | '@apostrophecms/image': {},
40 | '@apostrophecms/video': {},
41 | 'snippet': {},
42 | '@apostrophecms/form': {},
43 | 'company': {}
44 | };
45 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import { useState, useEffect } from 'react';
3 |
4 | function App() {
5 |
6 | const [ body, setBody ] = useState(null);
7 |
8 | useEffect(() => {
9 | fetchPage();
10 | async function fetchPage() {
11 | // You may also want the query string, just a basic example
12 | const url = `/cms${window.location.pathname}?aposRefresh=1`;
13 | const response = await fetch(url);
14 | setBody(await response.text());
15 | }
16 | }, []);
17 |
18 | // Has no dependencies (intended to run after each render)
19 | useEffect(() => {
20 | if (body) {
21 | // Run ApostropheCMS widget players
22 | window.apos.util.runPlayers();
23 | // Update the title tag after the new content is in the DOM
24 | const title = document.querySelector('[data-page-title]')?.getAttribute('data-page-title');
25 | if (title != null) {
26 | document.querySelector('title').innerText = title;
27 | }
28 | }
29 | });
30 |
31 | // Bare bones example, error handling is a good idea
32 | return (
33 |
34 | { body
35 | ?
36 |
37 | :
38 |
Loading...
39 | }
40 |
41 | );
42 | }
43 |
44 | export default App;
45 |
--------------------------------------------------------------------------------
/backend/modules/asset/ui/src/scss/_layout.scss:
--------------------------------------------------------------------------------
1 | // Use flex to keep the footer at the bottom.
2 | body,
3 | [data-apos-refreshable],
4 | .bp-wrapper {
5 | display: flex;
6 | flex-direction: column;
7 | }
8 |
9 | [data-apos-refreshable],
10 | .bp-wrapper,
11 | main {
12 | flex-grow: 1;
13 | }
14 |
15 | body {
16 | min-height: 100vh;
17 | // Nice system fonts.
18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
19 | }
20 |
21 | .bp-header,
22 | .bp-footer {
23 | width: 100%;
24 | max-width: 800px;
25 | margin-left: auto;
26 | margin-right: auto;
27 | }
28 |
29 | .bp-header {
30 | display: flex;
31 | align-items: center;
32 | justify-content: space-between;
33 | flex-wrap: wrap;
34 | margin-bottom: 10px;
35 | padding: 40px 0;
36 | }
37 |
38 | .bp-footer {
39 | padding: 40px 0;
40 | }
41 |
42 | .bp-footer__links {
43 | padding-left: 0px;
44 | list-style: none;
45 | text-align: center;
46 |
47 | li {
48 | display: inline-block;
49 | margin-right: 20px;
50 | }
51 | li:last-child { margin-right: 0; }
52 | }
53 |
54 | .bp-header__logo {
55 | display: block;
56 | width: 190px;
57 | max-width: 100%;
58 | object-fit: contain;
59 | }
60 |
61 | .bp-header__login {
62 |
63 | &:visited {
64 | color: white;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/backend/scripts/sync-down:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | TARGET="$1"
4 | if [ -z "$TARGET" ]; then
5 | echo "Usage: ./scripts/sync-down production"
6 | echo "(or as appropriate)"
7 | exit 1
8 | fi
9 |
10 | source deployment/settings || exit 1
11 | source "deployment/settings.$TARGET" || exit 1
12 |
13 | #Enter the Mongo DB name (should be same locally and remotely).
14 | dbName=$PROJECT
15 |
16 | #Enter the Project name (should be what you called it for stagecoach).
17 | projectName=$PROJECT
18 |
19 | #Enter the SSH username/url for the remote server.
20 | remoteSSH="-p $SSH_PORT $USER@$SERVER"
21 | rsyncTransport="ssh -p $SSH_PORT"
22 | rsyncDestination="$USER@$SERVER"
23 |
24 | echo "Syncing MongoDB"
25 | ssh $remoteSSH mongodump -d $dbName -o /tmp/mongodump.$dbName &&
26 | rsync -av -e "$rsyncTransport" $rsyncDestination:/tmp/mongodump.$dbName/ /tmp/mongodump.$dbName &&
27 | ssh $remoteSSH rm -rf /tmp/mongodump.$dbName &&
28 | # noIndexRestore increases compatibility between 3.x and 2.x,
29 | # and Apostrophe will recreate the indexes correctly at startup
30 | mongorestore --noIndexRestore --drop -d $dbName /tmp/mongodump.$dbName/$dbName &&
31 | echo "Syncing Files" &&
32 | rsync -av --delete -e "$rsyncTransport" $rsyncDestination:/opt/stagecoach/apps/$projectName/uploads/ ./public/uploads &&
33 | echo "Synced down from $TARGET"
34 | echo "YOU MUST RESTART THE SITE LOCALLY TO REBUILD THE MONGODB INDEXES."
35 |
--------------------------------------------------------------------------------
/frontend/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gt-samples",
3 | "version": "1.0.0",
4 | "description": "ApostropheCMS Essential Starter Kit Site",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "PORT=3001 node app",
8 | "dev": "PORT=3001 nodemon",
9 | "build": "NODE_ENV=production node app @apostrophecms/asset:build",
10 | "serve": "PORT=3001 NODE_ENV=production node app",
11 | "release": "npm install && npm run build && node app @apostrophecms/migration:migrate"
12 | },
13 | "nodemonConfig": {
14 | "delay": 1000,
15 | "verbose": true,
16 | "watch": [
17 | "./app.js",
18 | "./modules/**/*",
19 | "./lib/**/*.js",
20 | "./views/**/*.html"
21 | ],
22 | "ignoreRoot": [
23 | ".git"
24 | ],
25 | "ignore": [
26 | "**/ui/apos/",
27 | "**/ui/src/",
28 | "**/ui/public/",
29 | "locales/*.json",
30 | "public/uploads/",
31 | "public/apos-frontend/*.js",
32 | "data/"
33 | ],
34 | "ext": "json, js, html, scss, vue"
35 | },
36 | "repository": {
37 | "type": "git",
38 | "url": "https://github.com/apostrophecms/gt-samples"
39 | },
40 | "author": "Apostrophe Technologies, Inc.",
41 | "license": "MIT",
42 | "dependencies": {
43 | "@apostrophecms/form": "^1.4.2",
44 | "apostrophe": "^4.0.0",
45 | "normalize.css": "^8.0.1"
46 | },
47 | "devDependencies": {
48 | "eslint": "^8.0.0",
49 | "eslint-config-apostrophe": "^4.0.0",
50 | "nodemon": "^3.0.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/backend/modules/asset/ui/src/scss/_type.scss:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3 {
4 | text-align: center;
5 | }
6 |
7 | h1 {
8 | font-size: 4em;
9 | font-weight: 200;
10 | }
11 |
12 | a {
13 | color: $color-purple;
14 | text-decoration: none;
15 |
16 | &:hover,
17 | &:focus {
18 | text-decoration: underline;
19 | }
20 |
21 | &:visited {
22 | color: $color-purple-light;
23 | }
24 | }
25 |
26 | ::selection {
27 | background-color: $color-gold;
28 | }
29 |
30 | code {
31 | white-space: normal;
32 | }
33 |
34 | pre {
35 | display: flex;
36 | position: relative;
37 | font-family: Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
38 | color: white;
39 | font-size: 14px;
40 | text-align: left;
41 | white-space: pre;
42 | word-spacing: normal;
43 | word-break: normal;
44 | word-wrap: normal;
45 | line-height: 1.8;
46 | tab-size: 4;
47 | hyphens: none;
48 | border-radius: 6px;
49 | margin-bottom: 1.5rem;
50 | padding: 1.5rem;
51 | overflow: auto;
52 | background: #2b2b2b;
53 | max-width: 600px;
54 | margin: 0 auto;
55 | }
56 |
57 | .bp-button {
58 | display: inline-block;
59 | padding: 12px 24px 10px 24px;
60 | text-decoration: none;
61 | font-size: 14px;
62 | border-radius: 30px;
63 | line-height: 1;
64 | font-weight: 500;
65 | background: $color-purple;
66 | color: white;
67 | &:visited, &:hover, &:active {
68 | color: white;
69 | }
70 | }
71 |
72 | .bp-button--cta {
73 | font-size: 16px;
74 | font-weight: 400;
75 | padding: 20px 30px;
76 | }
--------------------------------------------------------------------------------
/backend/modules/company-widget/index.js:
--------------------------------------------------------------------------------
1 | const contentAreaWidgets = Object.fromEntries(
2 | Object.entries(require('../../lib/content-area-widgets.js')).filter(([ name, value ]) => name !== 'company')
3 | );
4 |
5 | module.exports = {
6 | extend: '@apostrophecms/widget-type',
7 | fields: {
8 | add: {
9 | name: {
10 | type: 'string',
11 | required: true
12 | },
13 | locations: {
14 | type: 'array',
15 | inline: true,
16 | fields: {
17 | add: {
18 | street: {
19 | type: 'string',
20 | required: true
21 | },
22 | city: {
23 | type: 'string',
24 | required: true
25 | },
26 | state: {
27 | type: 'string',
28 | required: true
29 | },
30 | zip: {
31 | type: 'string',
32 | required: true
33 | }
34 | }
35 | }
36 | },
37 | products: {
38 | type: 'array',
39 | fields: {
40 | add: {
41 | name: {
42 | type: 'string',
43 | required: true
44 | },
45 | price: {
46 | type: 'float'
47 | },
48 | body: {
49 | type: 'area',
50 | options: {
51 | widgets: contentAreaWidgets
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/frontend/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 | const jest = require('jest');
19 | const execSync = require('child_process').execSync;
20 | let argv = process.argv.slice(2);
21 |
22 | function isInGitRepository() {
23 | try {
24 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
25 | return true;
26 | } catch (e) {
27 | return false;
28 | }
29 | }
30 |
31 | function isInMercurialRepository() {
32 | try {
33 | execSync('hg --cwd . root', { stdio: 'ignore' });
34 | return true;
35 | } catch (e) {
36 | return false;
37 | }
38 | }
39 |
40 | // Watch unless on CI or explicitly running all tests
41 | if (
42 | !process.env.CI &&
43 | argv.indexOf('--watchAll') === -1 &&
44 | argv.indexOf('--watchAll=false') === -1
45 | ) {
46 | // https://github.com/facebook/create-react-app/issues/5210
47 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
48 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
49 | }
50 |
51 |
52 | jest.run(argv);
53 |
--------------------------------------------------------------------------------
/backend/modules/grid-layout-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% set widget = data.widget %}
2 | {% set areas = apos.gridLayoutWidget.getLayoutAreas(widget) %}
3 | {% set config = apos.gridLayoutWidget.getLayoutConfig(widget) %}
4 |
5 | {% set layoutClasses = apos.gridLayoutWidget.getLayoutClasses(widget) %}
6 |
7 |
--------------------------------------------------------------------------------
/backend/modules/grid-layout-widget/lib/get-layout-config.js:
--------------------------------------------------------------------------------
1 |
2 | // Constants for grid defaults
3 | const GRID_DEFAULTS = {
4 | COLUMNS: 12,
5 | GAP: '1rem',
6 | PADDING: '1rem',
7 | MARGIN: 'auto',
8 | MOBILE_GAP: '0.5rem',
9 | MOBILE_PADDING: '0.5rem'
10 | };
11 |
12 | // Basic preset configurations for different layout types
13 | const presetConfigs = {
14 | fullWidth: { rows: 2 },
15 | asideMain: { rows: 2 },
16 | mainAside: { rows: 2 },
17 | asideTwoMain: { rows: 2 },
18 | twoMainAside: { rows: 2 },
19 | headerTwoColFooter: { rows: 3 },
20 | featuredThreeGrid: { rows: 2 },
21 | magazineLayout: { rows: 2, gap: '1.5rem' },
22 | contentHub: { rows: 3, gap: '2rem' },
23 | galleryMasonry: { rows: 3 },
24 | dashboardLayout: { rows: 3, gap: '1.5rem' },
25 | productShowcase: { rows: 3, gap: '2rem' }
26 | };
27 |
28 | // Helper function to get layout configuration
29 | module.exports = widget => {
30 | // Our base configuration that applies to all layouts
31 | const defaultConfig = {
32 | columns: GRID_DEFAULTS.COLUMNS,
33 | gap: GRID_DEFAULTS.GAP,
34 | padding: GRID_DEFAULTS.PADDING,
35 | margin: GRID_DEFAULTS.MARGIN,
36 | mobileGap: GRID_DEFAULTS.MOBILE_GAP,
37 | mobilePadding: GRID_DEFAULTS.MOBILE_PADDING
38 | };
39 |
40 | // If we don't have a layout type, return default config
41 | if (!widget?.layoutType) {
42 | console.error('Layout type is required for custom layout widget');
43 | return defaultConfig;
44 | }
45 |
46 | // For preset layouts, merge with preset config
47 | return {
48 | ...defaultConfig,
49 | ...(presetConfigs[widget.layoutType] || { rows: 1 })
50 | };
51 | };
--------------------------------------------------------------------------------
/backend/scripts/sync-up:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | TARGET="$1"
4 | if [ -z "$TARGET" ]; then
5 | echo "Usage: ./scripts/sync-up production"
6 | echo "(or as appropriate)"
7 | echo
8 | echo "THIS WILL CLOBBER EVERYTHING ON THE"
9 | echo "TARGET SITE. MAKE SURE THAT IS WHAT"
10 | echo "YOU WANT!"
11 | exit 1
12 | fi
13 |
14 | read -p "THIS WILL CRUSH THE SITE'S CONTENT ON $TARGET. Are you sure? " -n 1 -r
15 | echo
16 | if [[ ! $REPLY =~ ^[Yy]$ ]]
17 | then
18 | exit 1
19 | fi
20 |
21 | source deployment/settings || exit 1
22 | source "deployment/settings.$TARGET" || exit 1
23 |
24 | #Enter the Mongo DB name (should be same locally and remotely).
25 | dbName=$PROJECT
26 |
27 | #Enter the Project name (should be what you called it for stagecoach).
28 | projectName=$PROJECT
29 |
30 | #Enter the SSH username/url for the remote server.
31 | remoteSSH="-p $SSH_PORT $USER@$SERVER"
32 | rsyncTransport="ssh -p $SSH_PORT"
33 | rsyncDestination="$USER@$SERVER"
34 |
35 | echo "Syncing MongoDB"
36 | mongodump -d $dbName -o /tmp/mongodump.$dbName &&
37 | echo rsync -av -e "$rsyncTransport" /tmp/mongodump.$dbName/ $rsyncDestination:/tmp/mongodump.$dbName &&
38 | rsync -av -e "$rsyncTransport" /tmp/mongodump.$dbName/ $rsyncDestination:/tmp/mongodump.$dbName &&
39 | rm -rf /tmp/mongodump.$dbName &&
40 | # noIndexRestore increases compatibility between 3.x and 2.x,
41 | # and Apostrophe will recreate the indexes correctly at startup
42 | ssh $remoteSSH mongorestore --noIndexRestore --drop -d $dbName /tmp/mongodump.$dbName/$dbName &&
43 | echo "Syncing Files" &&
44 | rsync -av --delete -e "$rsyncTransport" ./public/uploads/ $rsyncDestination:/opt/stagecoach/apps/$projectName/uploads &&
45 | echo "Synced up to $TARGET"
46 | echo "YOU MUST RESTART THE SITE ON $TARGET TO REBUILD THE MONGODB INDEXES."
47 |
--------------------------------------------------------------------------------
/backend/views/layout.html:
--------------------------------------------------------------------------------
1 | {# Automatically extends the right outer layout and also handles AJAX siutations #}
2 | {% extends data.outerLayout %}
3 |
4 | {% set title = data.piece.title or data.page.title %}
5 | {% block title %}
6 | {{ title }}
7 | {% if not title %}
8 | {{ apos.log('Looks like you forgot to override the title block in a template that does not have access to an Apostrophe page or piece.') }}
9 | {% endif %}
10 | {% endblock %}
11 |
12 | {% block beforeMain %}
13 |
14 |
24 |
25 | {% endblock %}
26 |
27 | {% block main %}
28 | {#
29 | Usually, your page templates in the @apostrophecms/pages module will override
30 | this block. It is safe to assume this is where your page-specific content
31 | should go.
32 | #}
33 | {% endblock %}
34 |
35 | {% block afterMain %}
36 |
37 |
46 |
{# Close .bp-wrapper #}
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/backend/modules/asset/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | routes(self) {
3 | return {
4 | get: {
5 | // /api/v1/asset/public-bundle-css returns the styles
6 | publicBundleCss(req) {
7 | return self.fetchBundle(req, 'css', 'public-bundle', 'text/css');
8 | },
9 | // /api/v1/asset/public-bundle-js returns the javascript
10 | publicBundleJs(req) {
11 | return self.fetchBundle(req, 'js', 'public-module-bundle', 'text/javascript');
12 | }
13 | }
14 | };
15 | },
16 | methods(self) {
17 | return {
18 | async fetchBundle(req, extension, bundleName, contentType) {
19 | try {
20 | let content = 'scripts\nstylesheets';
21 | content = self.apos.template.insertBundlesMarkup({
22 | scene: 'public',
23 | content,
24 | scriptsPlaceholder: 'scripts',
25 | stylesheetsPlaceholder: 'stylesheets',
26 | widgetsBundles: {}
27 | });
28 | const [ , url ] = content.match(new RegExp(`"(.*?${bundleName}.${extension})"`));
29 | if (url) {
30 | req.res.set('content-type', contentType);
31 | await self.retrieve(req, url, contentType);
32 | } else {
33 | throw self.apos.error('notfound');
34 | }
35 | } catch (e) {
36 | console.error('error:', e);
37 | return req.res.status(e.status || 500).send('error');
38 | }
39 | },
40 | async retrieve(req, url, contentType) {
41 | const result = await fetch(new URL(url, req.baseUrl));
42 | if (result.status >= 400) {
43 | throw self.apos.error('notfound');
44 | }
45 | req.res.set('content-type', contentType);
46 | return req.res.send(await result.text());
47 | }
48 | }
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/backend/modules/grid-layout-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bitmap
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/backend/deployment/dependencies:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export NODE_ENV=production
4 |
5 | # Also a good place to ensure any data folders
6 | # that are *not* supposed to be replaced on every deployment exist
7 | # and create a symlink to them from the latest deployment directory.
8 |
9 | # The real 'data' folder is shared. It lives two levels up and one over
10 | # (we're in a deployment dir, which is a subdir of 'deployments', which
11 | # is a subdir of the project's main dir)
12 |
13 | HERE=`pwd`
14 | mkdir -p ../../data
15 | ln -s ../../data $HERE/data
16 |
17 | # We also have a shared uploads folder which is convenient to keep
18 | # in a separate place so we don't have to have two express.static calls
19 |
20 | mkdir -p ../../uploads
21 | ln -s ../../../uploads $HERE/public/uploads
22 |
23 | # Install any dependencies that can't just be rsynced over with
24 | # the deployment. Example: node apps have npm modules in a
25 | # node_modules folder. These may contain compiled C++ code that
26 | # won't work portably from one server to another.
27 |
28 | # This script runs after the rsync, but before the 'stop' script,
29 | # so your app is not down during the npm installation.
30 |
31 | # Make sure node_modules exists so npm doesn't go searching
32 | # up the filesystem tree
33 | mkdir -p node_modules
34 |
35 | # If there is no package.json file then we don't need npm install
36 | if [ -f './package.json' ]; then
37 | # Install npm modules
38 | # Use a suitable version of Python
39 | # export PYTHON=/usr/bin/python26
40 | npm install
41 | if [ $? -ne 0 ]; then
42 | echo "Error during npm install!"
43 | exit 1
44 | fi
45 | fi
46 |
47 | node app @apostrophecms/migration:migrate
48 | # Generate new static asset files for this
49 | # deployment of the app without shutting down
50 | # (TODO: for 3.0 this is actually disruptive because
51 | # we don't have a generation identifier yet)
52 | npm run build
53 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
28 | React App
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/backend/modules/grid-layout-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | .custom-layout-widget {
2 | display: grid;
3 | grid-template-columns: repeat(var(--columns), 1fr);
4 | grid-template-rows: repeat(var(--rows), auto);
5 | gap: var(--gap);
6 | padding: var(--padding);
7 | margin: var(--margin);
8 | height: auto;
9 | }
10 |
11 | .custom-layout-area {
12 | grid-column: var(--col-start) / span var(--col-span);
13 | grid-row: var(--row-start) / span var(--row-span);
14 | min-height: var(--min-height, auto);
15 | align-self: var(--align-self, stretch);
16 | justify-self: var(--justify-self, stretch);
17 | }
18 |
19 | .max-width-768 {
20 | max-width: 768px;
21 | }
22 |
23 | .max-width-960 {
24 | max-width: 960px;
25 | }
26 |
27 | .max-width-1152 {
28 | max-width: 1152px;
29 | }
30 |
31 | .max-width-1344 {
32 | max-width: 1344px;
33 | }
34 |
35 | // Tablet-ish
36 |
37 | @media (max-width: 1023px) {
38 | .custom-layout-widget {
39 | grid-template-columns: repeat(12, 1fr);
40 | gap: calc(var(--gap) * 0.75);
41 | }
42 |
43 | .custom-layout-area {
44 | grid-column: auto / span var(--tablet-col-span);
45 | grid-row: auto;
46 | }
47 | }
48 |
49 | // Smartphone-ish
50 |
51 | @media (max-width: 480px) {
52 | .custom-layout-widget {
53 | grid-template-columns: repeat(12, 1fr);
54 | gap: calc(var(--gap) * 0.5);
55 | }
56 |
57 | .custom-layout-area {
58 | grid-column: auto / span var(--mobile-col-span);
59 | grid-row: auto;
60 | }
61 | }
62 |
63 | /* Layout Preview Styles - Used in widget editor UI */
64 | .layout-preview-container {
65 | margin: 0.5rem 0;
66 | }
67 |
68 | .layout-grid {
69 | display: grid;
70 | grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
71 | gap: 0.2rem;
72 | }
73 |
74 | .layout-card {
75 | border: 1px solid #ddd;
76 | border-radius: 4px;
77 | padding: 0.75rem;
78 | background: white;
79 | }
80 |
81 | .layout-title {
82 | font-weight: 600;
83 | margin-bottom: 0.5rem;
84 | color: #333;
85 | font-size: 0.9rem;
86 | }
87 |
88 | .preview-svg {
89 | width: 100%;
90 | height: auto;
91 | background: white;
92 | }
93 |
--------------------------------------------------------------------------------
/backend/modules/asset/ui/src/scss/_welcome.scss:
--------------------------------------------------------------------------------
1 | .bp-welcome {
2 | margin-left: auto;
3 | margin-right: auto;
4 | }
5 |
6 | .bp-welcome__area {
7 | margin-top: 40px;
8 | }
9 |
10 | .bp-welcome__headline {
11 | position: relative;
12 | margin: 0 0 50px;
13 | }
14 |
15 | .bp-welcome__verson-wrapper {
16 | display: flex;
17 | justify-content: center;
18 | }
19 |
20 | .bp-welcome__version {
21 | position: relative;
22 | background-color: #a992ff;
23 | padding: 4px 5px;
24 | border-radius: 2px;
25 | display: inline-block;
26 | color: #300ab7;
27 | font-size: 12px;
28 | box-shadow: 0 25px 50px rgba(64,70,104,.1);
29 | font-weight: 500;
30 | letter-spacing: 2px;
31 | text-transform: uppercase;
32 | }
33 |
34 | .bp-welcome__help {
35 | font-weight: 600;
36 | font-size: 22px;
37 | margin-bottom: 24px;
38 | }
39 |
40 | .bp-welcome p {
41 | max-width: 500px;
42 | margin: 20px auto;
43 | line-height: 1.5;
44 | font-size: 1.1rem;
45 | }
46 |
47 | .bp-welcome__code__context {
48 | position: absolute;
49 | display: inline-block;
50 | bottom: auto;
51 | left: auto;
52 | right: 0;
53 | top: 0;
54 | width: auto;
55 | height: auto;
56 | text-transform: uppercase;
57 | font-size: 11px;
58 | font-family: code-saver,sans-serif;
59 | letter-spacing: .025em;
60 | padding: 5px 10px;
61 | -webkit-user-select: none;
62 | -moz-user-select: none;
63 | -ms-user-select: none;
64 | user-select: none;
65 | border-top-right-radius: 2px;
66 | background: #ffffff20;
67 | color: #ffffff96;
68 | letter-spacing: 1px;
69 | }
70 |
71 | .bp-welcome__code {
72 | margin: 40px auto;
73 | }
74 |
75 | p.bp-welcome__cta {
76 | text-align: center;
77 | margin-bottom: 40px;
78 | }
79 |
80 | .bp-mode {
81 | display: inline-block;
82 | padding: 5px 5px;
83 | font-size: 12px;
84 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
85 | line-height: 10px;
86 | color: black;
87 | vertical-align: middle;
88 | background-color: white;
89 | border-bottom-color: black;
90 | border: 1px solid black;
91 | border-radius: 6px;
92 | box-shadow: inset 0 -1px 0 black;
93 | }
--------------------------------------------------------------------------------
/backend/app.js:
--------------------------------------------------------------------------------
1 | require('apostrophe')({
2 | shortName: 'gt-samples',
3 | modules: {
4 | // Apostrophe module configuration
5 | // *******************************
6 | //
7 | // NOTE: most configuration occurs in the respective modules' directories.
8 | // See modules/@apostrophecms/page/index.js for an example.
9 | //
10 | // Any modules that are not present by default in Apostrophe must at least
11 | // have a minimal configuration here to turn them on: `moduleName: {}`
12 | // ***********************************************************************
13 | // `className` options set custom CSS classes for Apostrophe core widgets.
14 | '@apostrophecms/rich-text-widget': {
15 | options: {
16 | className: 'bp-rich-text'
17 | }
18 | },
19 | '@apostrophecms/image-widget': {
20 | options: {
21 | className: 'bp-image-widget'
22 | }
23 | },
24 | '@apostrophecms/video-widget': {
25 | options: {
26 | className: 'bp-video-widget'
27 | }
28 | },
29 | // `asset` supports the project's webpack build for client-side assets.
30 | asset: {},
31 | // The project's first custom page type.
32 | 'default-page': {},
33 | 'snippet': {},
34 | 'snippet-widget': {},
35 | // The main form module
36 | '@apostrophecms/form': {},
37 | // The form widget module, allowing editors to add forms to content areas
38 | '@apostrophecms/form-widget': {},
39 | // Form field widgets, used by the main form module to build forms.
40 | '@apostrophecms/form-text-field-widget': {},
41 | '@apostrophecms/form-textarea-field-widget': {},
42 | '@apostrophecms/form-select-field-widget': {},
43 | '@apostrophecms/form-radio-field-widget': {},
44 | '@apostrophecms/form-file-field-widget': {},
45 | '@apostrophecms/form-checkboxes-field-widget': {},
46 | '@apostrophecms/form-boolean-field-widget': {},
47 | '@apostrophecms/form-conditional-widget': {},
48 | '@apostrophecms/form-divider-widget': {},
49 | '@apostrophecms/form-group-widget': {},
50 | 'grid-layout-widget': {},
51 | 'company-widget': {}
52 | }
53 | });
54 |
--------------------------------------------------------------------------------
/frontend/config/getHttpsConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const crypto = require('crypto');
6 | const chalk = require('react-dev-utils/chalk');
7 | const paths = require('./paths');
8 |
9 | // Ensure the certificate and key provided are valid and if not
10 | // throw an easy to debug error
11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
12 | let encrypted;
13 | try {
14 | // publicEncrypt will throw an error with an invalid cert
15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
16 | } catch (err) {
17 | throw new Error(
18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
19 | );
20 | }
21 |
22 | try {
23 | // privateDecrypt will throw an error with an invalid key
24 | crypto.privateDecrypt(key, encrypted);
25 | } catch (err) {
26 | throw new Error(
27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
28 | err.message
29 | }`
30 | );
31 | }
32 | }
33 |
34 | // Read file and throw an error if it doesn't exist
35 | function readEnvFile(file, type) {
36 | if (!fs.existsSync(file)) {
37 | throw new Error(
38 | `You specified ${chalk.cyan(
39 | type
40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
41 | );
42 | }
43 | return fs.readFileSync(file);
44 | }
45 |
46 | // Get the https config
47 | // Return cert files if provided in env, otherwise just true or false
48 | function getHttpsConfig() {
49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
50 | const isHttps = HTTPS === 'true';
51 |
52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
55 | const config = {
56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
58 | };
59 |
60 | validateKeyAndCerts({ ...config, keyFile, crtFile });
61 | return config;
62 | }
63 | return isHttps;
64 | }
65 |
66 | module.exports = getHttpsConfig;
67 |
--------------------------------------------------------------------------------
/backend/deployment/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Make the site live again, for instance by tweaking a .htaccess file
4 | # or starting a node server. In this example we also set up a
5 | # data/port file so that sc-proxy.js can figure out what port
6 | # to forward traffic to for this site. The idea is that every
7 | # folder in /var/webapps represents a separate project with a separate
8 | # node process, each listening on a specific port, and they all
9 | # need traffic forwarded from a reverse proxy server on port 80
10 |
11 | # Useful for debugging
12 | #set -x verbose
13 |
14 | # Express should not reveal information on errors,
15 | # also optimizes Express performance
16 | export NODE_ENV=production
17 |
18 | if [ ! -f "app.js" ]; then
19 | echo "I don't see app.js in the current directory."
20 | exit 1
21 | fi
22 |
23 | # Assign a port number if we don't yet have one
24 |
25 | if [ -f "data/port" ]; then
26 | PORT=`cat data/port`
27 | else
28 | # No port set yet for this site. Scan and sort the existing port numbers if any,
29 | # grab the highest existing one
30 | PORT=`cat ../../../*/data/port 2>/dev/null | sort -n | tail -1`
31 | if [ "$PORT" == "" ]; then
32 | echo "First app ever, assigning port 3000"
33 | PORT=3000
34 | else
35 | # Bash is much nicer than sh! We can do math without tears!
36 | let PORT+=1
37 | fi
38 | echo $PORT > data/port
39 | echo "First startup, chose port $PORT for this site"
40 | fi
41 |
42 | # Run the app via 'forever' so that it restarts automatically if it fails
43 | # Use `pwd` to make sure we have a full path, forever is otherwise easily confused
44 | # and will stop every server with the same filename
45 |
46 | # Use a "for" loop. A classic single-port file will do the
47 | # right thing, but so will a file with multiple port numbers
48 | # for load balancing across multiple cores
49 | for port in $PORT
50 | do
51 | export PORT=$port
52 | forever --minUptime=1000 --spinSleepTime=10000 -o data/console.log -e data/error.log start `pwd`/app.js && echo "Site started"
53 | done
54 |
55 | # Run the app without 'forever'. Record the process id so 'stop' can kill it later.
56 | # We recommend installing 'forever' instead for node apps. For non-node apps this code
57 | # may be helpful
58 | #
59 | # node app.js >> data/console.log 2>&1 &
60 | # PID=$!
61 | # echo $PID > data/pid
62 | #
63 | #echo "Site started"
64 |
--------------------------------------------------------------------------------
/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # Sample code for common asks
2 |
3 | This project is intended as sample code to demonstrate certain features. You **can** fork this repo to start a project, but it is usually a better idea to apply the techniques seen here to your **own** project.
4 |
5 | This project is based on the [starter-kit-essentials](https://github.com/apostrophecms/starter-kit-essentials) project, which is a good starting point if you don't want any extra code pre-built in the application. Whereas if you want a richer starting point, consider any of our other starter kits.
6 |
7 | ## Note on ESM versus commonjs
8 |
9 | This code is in commonjs format (with `require` and `module.exports`) to accommodate ease of use in a client project that was built with that syntax. commonjs and ESM cannot be mixed and matched in a single project. So if you are incorporating these samples into a newer ESM-based project, just modify to use `import` and `export default { ... }` as needed.
10 |
11 | ## Running the project directly
12 |
13 | To experiment with this project, `git clone` this repository and install its dependencies using `npm install`. Add an initial admin user with `node app @apostrophecms/user:add admin admin`.
14 |
15 | ## Running the project
16 |
17 | Run `npm run dev` to build the Apostrophe UI and start the site up. The `dev` script will watch for saves in client-side CSS and Javascript and trigger a build and page refresh if they are detected. It will also restart the app when server-side code is saved.
18 |
19 | ## Features demonstrated
20 |
21 | ### "Snippets"
22 |
23 | A common request: *"we'd like reusable chunks of content that can be inserted in more than one place around the site."*
24 |
25 | This project achieves that by:
26 |
27 | * Implementing a piece type module called `snippet` that contains an `area` field in which editors can create content by selecting "Snippets" from the admin bar, then clicking "New Snippet."
28 |
29 | * Implementing a widget type module called `snippet-widget-type` which contains a `relationship` field, allowing the editor to choose an existing or new snippet to be displayed at any point in an area that includes `snippet` widgets in its configuration.
30 |
31 | * Configuring the `snippet` widget as one of the choices in the `default-page` and `@apostrophecms/home-page` modules.
32 |
33 | Note that like any widget, you can also reuse the snippet widget in other piece types, not just page types.
34 |
35 | ## You really want the docs
36 |
37 | Right now, [all the juicy info is in the ApostropheCMS docs](https://docs.apostrophecms.org), so head over there and start reading! This boilerplate project is a fun introduction to the UI, but you'll want to know more to really try it out.
38 |
39 |
--------------------------------------------------------------------------------
/frontend/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
13 | // "public path" at which the app is served.
14 | // webpack needs to know it to put the right