├── .env.sample ├── modules ├── asset │ ├── ui │ │ └── src │ │ │ ├── scss │ │ │ ├── _functions.scss │ │ │ ├── _variables.scss │ │ │ ├── _mixins.scss │ │ │ ├── _theme.scss │ │ │ ├── _typography.scss │ │ │ ├── _containers.scss │ │ │ └── _default-variables.scss │ │ │ ├── index.js │ │ │ ├── js │ │ │ └── nav-buttons.js │ │ │ ├── scss-elements │ │ │ ├── _button-arrow.scss │ │ │ ├── _buttons.scss │ │ │ ├── _footer.scss │ │ │ ├── _navigation.scss │ │ │ └── _forms.scss │ │ │ └── index.scss │ └── index.js ├── content-widget-modules │ ├── map-widget │ │ ├── ui │ │ │ └── src │ │ │ │ ├── index.scss │ │ │ │ └── index.js │ │ ├── public │ │ │ ├── map-icon.png │ │ │ └── preview.jpg │ │ ├── views │ │ │ ├── widget.html │ │ │ └── map.html │ │ └── index.js │ ├── button-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ └── index.js │ ├── image-widget │ │ ├── public │ │ │ └── preview.jpg │ │ ├── views │ │ │ └── widget.html │ │ └── index.js │ ├── rich-text-widget │ │ ├── public │ │ │ └── preview.png │ │ ├── views │ │ │ └── widget.html │ │ └── index.js │ ├── call-to-action-widget │ │ ├── public │ │ │ └── preview.jpg │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ └── index.js │ ├── image-gallery-widget │ │ ├── public │ │ │ └── preview.jpg │ │ ├── ui │ │ │ └── src │ │ │ │ ├── index.scss │ │ │ │ └── index.js │ │ ├── index.js │ │ └── views │ │ │ └── widget.html │ ├── side-by-side-widget │ │ ├── public │ │ │ └── preview.jpg │ │ └── index.js │ ├── button-strip-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── index.js │ │ └── ui │ │ │ └── src │ │ │ └── index.scss │ ├── side-by-side-content-widget │ │ └── index.js │ ├── accordion-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── custom-form-widget │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ ├── views │ │ │ └── widget.html │ │ ├── public │ │ │ └── preview.svg │ │ └── index.js │ ├── pricing-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── modules.js │ └── columns-widget │ │ ├── views │ │ └── widget.html │ │ ├── ui │ │ └── src │ │ │ └── index.scss │ │ ├── public │ │ └── preview.svg │ │ └── index.js ├── @apostrophecms │ ├── layout-widget │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── asset │ │ └── index.js │ ├── video-widget │ │ ├── public │ │ │ └── preview.jpg │ │ └── index.js │ ├── express │ │ └── index.js │ ├── home-page │ │ ├── views │ │ │ └── page.html │ │ └── index.js │ ├── page │ │ ├── index.js │ │ └── views │ │ │ └── notFound.html │ ├── settings │ │ └── index.js │ ├── layout-column-widget │ │ └── index.js │ ├── admin-bar │ │ └── index.js │ ├── form │ │ └── index.js │ └── global │ │ └── index.js ├── settings │ └── index.js ├── pieces-modules │ ├── modules.js │ ├── product-widget │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ ├── views │ │ │ └── widget.html │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── team-member-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── product │ │ └── index.js │ └── team-member │ │ └── index.js ├── default-page │ ├── views │ │ └── page.html │ └── index.js └── helper │ └── index.js ├── postcss.config.js ├── public └── images │ ├── logo.png │ ├── menu.svg │ ├── social-icons │ ├── facebook.svg │ ├── twitter.svg │ ├── linkedin.svg │ ├── meta.svg │ └── instagram.svg │ └── checked-icon.svg ├── eslint.config.js ├── deployment ├── README ├── settings.staging ├── migrate ├── rsync_exclude.txt ├── stop ├── settings ├── dependencies └── start ├── views ├── link.html ├── button-arrows.html ├── button.html ├── fragments │ ├── header.html │ └── footer.html ├── layout.html └── ui.html ├── .gitignore ├── lib ├── buttonSchema.js ├── linkSchema.js ├── area.js └── aosSchema.js ├── LICENSE ├── scripts ├── sync-down └── sync-up ├── package.json ├── app.js └── README.md /.env.sample: -------------------------------------------------------------------------------- 1 | GEOCODER_API_KEY= -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_functions.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | @import "./theme"; 2 | @import "./default-variables"; 3 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/public/images/logo.png -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "ol/ol.css"; 2 | 3 | .map { 4 | height: 500px; 5 | } 6 | -------------------------------------------------------------------------------- /modules/@apostrophecms/layout-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | defaultSpan: 4, 4 | previewImage: 'svg', 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // When not in production, refresh the page on restart 3 | options: { 4 | refreshOnRestart: true 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /modules/@apostrophecms/video-widget/public/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/@apostrophecms/video-widget/public/preview.jpg -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'eslint-config-apostrophe'; 2 | import { defineConfig } from 'eslint/config'; 3 | 4 | export default defineConfig([ 5 | apostrophe 6 | ]); 7 | -------------------------------------------------------------------------------- /modules/content-widget-modules/button-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import "button.html" as buttonFrag %} 2 | 3 | {% set button = data.widget %} 4 | 5 | {% render buttonFrag.render(button) %} -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/public/map-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/map-widget/public/map-icon.png -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/public/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/map-widget/public/preview.jpg -------------------------------------------------------------------------------- /modules/content-widget-modules/image-widget/public/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/image-widget/public/preview.jpg -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 | {% component "map-widget:map" with { widget: data.widget } %} 3 |
4 | -------------------------------------------------------------------------------- /modules/content-widget-modules/rich-text-widget/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/rich-text-widget/public/preview.png -------------------------------------------------------------------------------- /modules/settings/index.js: -------------------------------------------------------------------------------- 1 | const d = new Date(); 2 | const year = d.getFullYear(); 3 | 4 | export default { 5 | options: { 6 | ignoreNoCodeWarning: true, 7 | year 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /modules/@apostrophecms/express/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | session: { 4 | // If this still says `undefined`, set a real secret! 5 | secret: undefined 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /modules/content-widget-modules/call-to-action-widget/public/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/call-to-action-widget/public/preview.jpg -------------------------------------------------------------------------------- /modules/content-widget-modules/image-gallery-widget/public/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/image-gallery-widget/public/preview.jpg -------------------------------------------------------------------------------- /modules/content-widget-modules/rich-text-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 | {% area data.widget, 'richText' %} 3 |
-------------------------------------------------------------------------------- /modules/content-widget-modules/side-by-side-widget/public/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-marketing/main/modules/content-widget-modules/side-by-side-widget/public/preview.jpg -------------------------------------------------------------------------------- /modules/@apostrophecms/video-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | label: 'Video', 4 | description: 'Add a video player from services like YouTube', 5 | previewImage: 'jpg' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /modules/pieces-modules/modules.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Pieces 3 | product: {}, 4 | 'team-member': {}, 5 | 6 | // Related Pieces Widgets 7 | 'product-widget': {}, 8 | 'team-member-widget': {} 9 | }; 10 | -------------------------------------------------------------------------------- /modules/asset/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import { navButton } from './js/nav-buttons'; 2 | import AOS from 'aos'; 3 | 4 | export default () => { 5 | AOS.init(); 6 | navButton(); 7 | // Your own project level JS may go here 8 | }; 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/content-widget-modules/button-strip-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import "button.html" as buttonFrag %} 2 | 3 |
4 | {% for button in data.widget.buttons %} 5 | {% render buttonFrag.render(button) %} 6 | {% endfor %} 7 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/content-widget-modules/button-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | .btn-widget { 2 | display: flex; 3 | 4 | &--alignment-left { 5 | justify-content: flex-start; 6 | } 7 | &--alignment-center { 8 | justify-content: center; 9 | } 10 | &--alignment-right { 11 | justify-content: flex-end; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | {% area data.page, 'main' %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /public/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /modules/content-widget-modules/side-by-side-content-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/layout-column-widget', 5 | fields: { 6 | add: { 7 | content: { 8 | type: 'area', 9 | options: { 10 | widgets: areaConfig.all 11 | } 12 | } 13 | 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /modules/content-widget-modules/image-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import "@apostrophecms/image-widget:fragment.html" as image -%} 2 |
3 | {% render image.render( 4 | widget = data.widget, 5 | options = data.options, 6 | manager = data.manager, 7 | contextOptions = data.contextOptions 8 | )%} 9 |
10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/index.js: -------------------------------------------------------------------------------- 1 | // This configures the @apostrophecms/pages module to add a "home" page type to the 2 | // pages menu 3 | 4 | export default { 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 | -------------------------------------------------------------------------------- /modules/asset/ui/src/js/nav-buttons.js: -------------------------------------------------------------------------------- 1 | export function navButton() { 2 | if (document.querySelector('.navigation__menu-btn')) { 3 | const navBtn = document.querySelector('.navigation__menu-btn'); 4 | const navItems = document.querySelector('.navigation__nav-items'); 5 | 6 | navBtn.addEventListener('click', function () { 7 | navItems.classList.toggle('navigation__show'); 8 | }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /modules/content-widget-modules/button-widget/index.js: -------------------------------------------------------------------------------- 1 | import buttonSchema from '../../../lib/buttonSchema.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Button', 7 | icon: 'button-icon' 8 | }, 9 | icons: { 10 | 'button-icon': 'ShapeRectanglePlus' 11 | }, 12 | fields: { 13 | add: { 14 | ...buttonSchema.button 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /modules/content-widget-modules/image-widget/index.js: -------------------------------------------------------------------------------- 1 | import aosSchema from '../../../lib/aosSchema.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/image-widget', 5 | options: { 6 | icon: 'image-icon', 7 | label: 'Image', 8 | description: 'Display an image on your page', 9 | previewImage: 'jpg', 10 | className: 'img-fluid' 11 | }, 12 | fields: { 13 | add: { 14 | ...aosSchema 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /modules/pieces-modules/product-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .menu { 4 | &__list { 5 | display: grid; 6 | grid-gap: 2rem; 7 | @media only screen and (min-width: 600px) { 8 | &.split { 9 | grid-template-columns: 1fr 1fr; 10 | } 11 | } 12 | } 13 | &__item { 14 | &__title { 15 | display: flex; 16 | justify-content: space-between; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/images/social-icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 |
12 |

404 Error.

13 |

We're sorry. We couldn't find the page you're looking for.

14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /modules/@apostrophecms/settings/index.js: -------------------------------------------------------------------------------- 1 | export default { 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 | -------------------------------------------------------------------------------- /views/link.html: -------------------------------------------------------------------------------- 1 | {# 2 | {% import "link.html" as link %} 3 | {% render link.render(options) %} 4 | 5 | * options - options object 6 | ~ class 7 | ~ path 8 | ~ target 9 | ~ label 10 | #} 11 | 12 | {% fragment render(options) %} 13 | {{ options.label }} 18 | {% endfragment %} -------------------------------------------------------------------------------- /modules/@apostrophecms/layout-column-widget/index.js: -------------------------------------------------------------------------------- 1 | // This is a custom layout column widget, 2 | // to redefine the available widgets within a layout columns. 3 | 4 | import areaConfig from '../../../lib/area.js'; 5 | 6 | export default { 7 | fields: { 8 | add: { 9 | content: { 10 | type: 'area', 11 | options: { 12 | expanded: true, 13 | groups: { 14 | ...areaConfig.columnExpandedGroup 15 | } 16 | } 17 | } 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/views/map.html: -------------------------------------------------------------------------------- 1 | {% if not data.response %} 2 |

Something went wrong.

3 | {% elif data.response.message %} 4 |

{{ data.response.message }}

5 | {% else %} 6 |
14 |
15 |
16 | {% endif %} -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin link-style { 2 | color: rgba($link-color, 0.7); 3 | text-decoration: $link-decoration; 4 | transition: color $animate-easing; 5 | 6 | &:hover { 7 | color: rgba($link-color, 1); 8 | text-decoration: $link-hover-decoration; 9 | } 10 | } 11 | 12 | @mixin inverse-link-style { 13 | color: rgba($link-color, 1); 14 | text-decoration: $link-decoration; 15 | transition: color $animate-easing; 16 | 17 | &:hover { 18 | color: rgba($link-color, 0.7); 19 | text-decoration: $link-hover-decoration; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.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 | .env 19 | .DS_Store -------------------------------------------------------------------------------- /modules/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | build: { 3 | vite: { 4 | extensions: { 5 | buildSizeWarning: { 6 | // https://vite.dev/config/build-options.html#build-chunksizewarninglimit 7 | build: { 8 | chunkSizeWarningLimit: 650 9 | } 10 | } 11 | } 12 | } 13 | }, 14 | handlers(self) { 15 | return { 16 | '@apostrophecms/page:beforeSend': { 17 | webpack(req) { 18 | req.data.isDev = (process.env.NODE_ENV !== 'production'); 19 | } 20 | } 21 | }; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | 3 | export default { 4 | options: { 5 | label: 'Home Page' 6 | }, 7 | fields: { 8 | add: { 9 | main: { 10 | type: 'area', 11 | options: { 12 | expanded: true, 13 | groups: { 14 | ...areaConfig.fullExpandedGroup 15 | } 16 | } 17 | } 18 | }, 19 | group: { 20 | basics: { 21 | label: 'Basics', 22 | fields: [ 23 | 'title', 24 | 'main' 25 | ] 26 | } 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /modules/default-page/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../lib/area.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/page-type', 5 | options: { 6 | label: 'Default Page' 7 | }, 8 | fields: { 9 | add: { 10 | main: { 11 | type: 'area', 12 | options: { 13 | expanded: true, 14 | groups: { 15 | ...areaConfig.fullExpandedGroup 16 | } 17 | } 18 | } 19 | }, 20 | group: { 21 | basics: { 22 | label: 'Basics', 23 | fields: [ 24 | 'title', 25 | 'main' 26 | ] 27 | } 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/images/social-icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /modules/pieces-modules/team-member-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 | {% for member in data.widget._teamMembers %} 3 |
4 | {% set profileImage = apos.image.first(member.profileImage) %} 5 | {{ memeber.title }} 6 |

{{ member.title }}

7 |

{{ member.workTitle }}

8 |
9 | {% endfor %} 10 |
11 | -------------------------------------------------------------------------------- /modules/helper/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | alias: 'helper' 4 | }, 5 | init(self) { 6 | self.addHelpers({ 7 | linkPath: (link) => { 8 | if (!link) { 9 | return; 10 | } 11 | let path; 12 | if (link.linkType === 'page' && link._linkPage && link._linkPage[0]) { 13 | path = link._linkPage[0]._url; 14 | } else if (link.linkType === 'file' && link._linkFile && link._linkFile[0]) { 15 | path = link._linkFile[0]._url; 16 | } else if (link.linkType === 'custom') { 17 | path = link.linkUrl; 18 | } 19 | return path; 20 | } 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /modules/pieces-modules/product-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% set splitClass = 'split' if data.widget.style === 'split'%} 2 | -------------------------------------------------------------------------------- /modules/content-widget-modules/accordion-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 | {% for item in data.widget.accordions %} 3 |
4 |

5 | 8 |

9 | 12 |
13 | {% endfor %} 14 |
-------------------------------------------------------------------------------- /modules/content-widget-modules/side-by-side-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/layout-widget', 3 | options: { 4 | label: 'Side by side', 5 | icon: 'layout-side-icon', 6 | description: 'Display two sections of content side by side', 7 | previewImage: 'jpg', 8 | columns: 2, 9 | minSpan: 1, 10 | defaultSpan: 1, 11 | gap: '1rem' 12 | }, 13 | icons: { 14 | 'layout-side-icon': 'PageLayoutSidebarRight' 15 | }, 16 | fields: { 17 | add: { 18 | columns: { 19 | type: 'area', 20 | options: { 21 | widgets: { 22 | 'side-by-side-content': {} 23 | } 24 | } 25 | } 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /modules/content-widget-modules/button-strip-widget/index.js: -------------------------------------------------------------------------------- 1 | import buttonSchema from '../../../lib/buttonSchema.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Buttons', 8 | icon: 'button-icon' 9 | }, 10 | icons: { 11 | 'button-icon': 'ShapeRectanglePlus' 12 | }, 13 | fields: { 14 | add: { 15 | buttons: { 16 | type: 'array', 17 | label: 'Button strip', 18 | titleField: 'linkText', 19 | fields: { 20 | add: { 21 | ...buttonSchema.button 22 | } 23 | } 24 | }, 25 | ...aosSchema 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /public/images/social-icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /modules/pieces-modules/product/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/piece-type', 3 | options: { 4 | label: 'Product', 5 | openGraph: false, 6 | seoFields: false 7 | }, 8 | fields: { 9 | add: { 10 | title: { 11 | type: 'string', 12 | label: 'Title' 13 | }, 14 | description: { 15 | type: 'string', 16 | label: 'Description' 17 | }, 18 | price: { 19 | type: 'float', 20 | label: 'Item price', 21 | min: 0.01, 22 | def: 0.00, 23 | required: true 24 | } 25 | }, 26 | group: { 27 | basics: { 28 | fields: [ 'title', 'description', 'price' ] 29 | } 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /modules/content-widget-modules/rich-text-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Rich Text', 8 | description: 'Add styled text to your page', 9 | icon: 'format-text-icon', 10 | previewImage: 'png', 11 | defaultData: { content: '

wassuppe

' } 12 | }, 13 | fields: { 14 | add: { 15 | richText: { 16 | type: 'area', 17 | label: 'Rich Text', 18 | options: { 19 | widgets: { 20 | ...areaConfig.richText 21 | } 22 | } 23 | }, 24 | ...aosSchema 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /modules/content-widget-modules/button-strip-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | .button-strip { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin: 1.5rem 0; 5 | .btn-widget { 6 | display: flex; 7 | align-items: center; 8 | width: 100%; 9 | 10 | &--alignment-left { 11 | justify-content: flex-start; 12 | } 13 | &--alignment-center { 14 | justify-content: center; 15 | } 16 | &--alignment-right { 17 | justify-content: flex-end; 18 | } 19 | 20 | & + .btn-widget { 21 | margin-top: 20px; 22 | } 23 | 24 | @media only screen and (min-width: 600px) { 25 | width: auto; 26 | & + .btn-widget { 27 | margin-top: 0; 28 | margin-left: 20px; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /modules/content-widget-modules/custom-form-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .custom-form { 4 | display: flex; 5 | flex-direction: column; 6 | gap: 2rem; 7 | 8 | &--primary { 9 | color: $primary; 10 | } 11 | &--secondary { 12 | color: $secondary; 13 | } 14 | &--tertiary { 15 | color: $tertiary; 16 | } 17 | &--black { 18 | color: $black; 19 | } 20 | &--white { 21 | color: $white; 22 | } 23 | 24 | @media only screen and (min-width: 900px) { 25 | flex-direction: row; 26 | } 27 | 28 | &--background { 29 | background-position: 50% 50%; 30 | background-size: cover; 31 | 32 | align-items: center; 33 | justify-content: center; 34 | } 35 | 36 | &__column-item { 37 | flex-basis: 50%; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/content-widget-modules/call-to-action-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
5 | {% if data.widget.featureImage.items[0].aposPlaceholder %} 6 |
7 | {% else %} 8 | {% set backgroundImage = apos.image.first(data.widget.featureImage) %} 9 |
10 | {% endif %} 11 |
12 | {% area data.widget, 'content' %} 13 |
14 |
-------------------------------------------------------------------------------- /lib/buttonSchema.js: -------------------------------------------------------------------------------- 1 | import linkSchema from './linkSchema.js'; 2 | 3 | const button = { 4 | ...linkSchema, 5 | style: { 6 | type: 'select', 7 | label: 'Color Style', 8 | required: true, 9 | choices: [ 10 | { 11 | label: 'Primary', 12 | value: 'primary' 13 | }, 14 | { 15 | label: 'Secondary', 16 | value: 'secondary' 17 | } 18 | ] 19 | }, 20 | size: { 21 | type: 'select', 22 | label: 'Size', 23 | required: true, 24 | choices: [ 25 | { 26 | label: 'Regular', 27 | value: '' 28 | }, 29 | { 30 | label: 'Small', 31 | value: 'sm' 32 | }, 33 | { 34 | label: 'Large', 35 | value: 'lg' 36 | } 37 | ] 38 | } 39 | }; 40 | 41 | export default { button }; 42 | -------------------------------------------------------------------------------- /modules/content-widget-modules/pricing-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% area data.widget, 'intro' %} 4 |
5 | {% for card in data.widget.cards %} 6 |
7 |
8 |
9 | {{ card.label }} 10 |
11 | {% area card, 'content' %} 12 |
    13 | {% for feature in card.features %} 14 |
  • {{ feature.title }}
  • 15 | {% endfor %} 16 |
17 |
18 | {% area card, 'buttons' %} 19 |
20 | {% endfor %} 21 |
22 |
23 |
-------------------------------------------------------------------------------- /modules/pieces-modules/team-member-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .team-widget { 4 | display: grid; 5 | grid-template-columns: repeat(auto-fit, minmax(220px, max-content)); 6 | grid-gap: 2rem; 7 | justify-content: space-around; 8 | @media only screen and (min-width: 900px) { 9 | justify-content: center; 10 | &--three-col { 11 | grid-template-columns: 1fr 1fr 1fr; 12 | } 13 | &--four-col { 14 | grid-template-columns: 1fr 1fr 1fr 1fr; 15 | } 16 | } 17 | &__profile { 18 | margin-bottom: 20px; 19 | text-align: center; 20 | } 21 | &__profile-image { 22 | margin: 0 auto; 23 | border-radius: 50%; 24 | width: 200px; 25 | height: 200px; 26 | object-fit: cover; 27 | } 28 | &__work-title { 29 | text-transform: uppercase; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/pieces-modules/team-member/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/piece-type', 3 | options: { 4 | label: 'Team Member', 5 | openGraph: false, 6 | seoFields: false 7 | }, 8 | fields: { 9 | add: { 10 | title: { 11 | type: 'string', 12 | label: 'Name', 13 | required: true 14 | }, 15 | profileImage: { 16 | label: 'Profile image', 17 | type: 'area', 18 | options: { 19 | max: 1, 20 | widgets: { 21 | '@apostrophecms/image': {} 22 | } 23 | } 24 | }, 25 | workTitle: { 26 | type: 'string', 27 | label: 'Work title' 28 | } 29 | }, 30 | group: { 31 | basics: { 32 | fields: [ 'title', 'profileImage', 'workTitle' ] 33 | } 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /views/button-arrows.html: -------------------------------------------------------------------------------- 1 | {# 2 | {% import 'button-arrows.html' as buttonArrows %} 3 | 4 | {% render buttonArrows.button(class) %} 5 | 6 | * Class name - class name for the button arrow icon 7 | #} 8 | 9 | {% fragment button(class) %} 10 |
11 | 12 | 13 | 14 |
15 | {% endfragment %} 16 | 17 | {# 18 | {% import 'button-arrows.html' as buttonArrows %} 19 | 20 | {% render buttonArrows.swiper() %} 21 | #} 22 | 23 | {% fragment swiper() %} 24 |
25 | {% render button('prev') %} 26 | {% render button('next') %} 27 |
28 | {% endfragment %} -------------------------------------------------------------------------------- /public/images/social-icons/meta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss-elements/_button-arrow.scss: -------------------------------------------------------------------------------- 1 | .button-arrow { 2 | background: $secondary; 3 | border-radius: 50%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 56px; 8 | height: 56px; 9 | color: $primary; 10 | cursor: pointer; 11 | transition: background-color $animate-easing; 12 | 13 | &:hover { 14 | background: lighten($secondary, 10%); 15 | } 16 | } 17 | 18 | .swiper-button-arrows { 19 | .button-arrow { 20 | position: absolute; 21 | top: 50%; 22 | transform: translateY(-50%); 23 | z-index: 3; 24 | &--prev { 25 | left: 0.5rem; 26 | } 27 | &--next { 28 | right: 0.5rem; 29 | .button-arrow__icon { 30 | transform: rotate(180deg); 31 | } 32 | } 33 | &.swiper-button-disabled { 34 | opacity: 0.4; 35 | cursor: auto; 36 | &:hover { 37 | background: $secondary; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /views/button.html: -------------------------------------------------------------------------------- 1 | {# 2 | {% import "button.html" as buttonFrag %} 3 | {% render buttonFrag.render(button) %} 4 | 5 | * button - button object 6 | ~ block 7 | ~ alignment 8 | ~ _id 9 | ~ linkText 10 | ~ linkTarget 11 | ~ style 12 | ~ size 13 | #} 14 | 15 | {% import "link.html" as link %} 16 | 17 | {% fragment render(button) %} 18 | {% set path = apos.helper.linkPath(button) %} 19 | {% set class = '' %} 20 | {% if button.block %} 21 | {% set class = class + ' btn-widget--block-' + button.block %} 22 | {% endif %} 23 | {% if button.alignment %} 24 | {% set class = class + ' btn-widget--alignment-' + button.alignment %} 25 | {% endif %} 26 | 27 |
28 | {% render link.render({ 29 | label: button.linkText, 30 | path: path, 31 | target: button.linkTarget, 32 | class: 'btn btn--' + button.style + ' btn--' + button.size 33 | }) %} 34 |
35 | {% endfragment %} -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_theme.scss: -------------------------------------------------------------------------------- 1 | $amaranth: #ef2d56; 2 | $black-pearl: #0d1b2a; 3 | $midnight-express: #1b263b; 4 | $chambray: #415a77; 5 | $ship-cove: #778da9; 6 | $black-squeeze: #e0e1dd; 7 | $valencia: #d64545; 8 | $white: #ffffff; 9 | 10 | // Theme colour variables 11 | $primary: $black-pearl; 12 | $secondary: $amaranth; 13 | $tertiary: $midnight-express; 14 | $success: $midnight-express; 15 | $danger: $valencia; 16 | 17 | $border-radius: 0; 18 | 19 | $btn-font-weight: bold; 20 | $btn-secondary-text: $white; 21 | 22 | $font-family-sans-serif: "Lato", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", 23 | "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 24 | $font-family-monospace: "Merriweather", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 25 | monospace; 26 | 27 | $body-font-family: $font-family-monospace; 28 | $headings-font-family: $font-family-sans-serif; 29 | -------------------------------------------------------------------------------- /modules/content-widget-modules/modules.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Widgets 3 | 'accordion-widget': {}, 4 | 'button-widget': {}, 5 | 'button-strip-widget': {}, 6 | 'call-to-action-widget': {}, 7 | 'custom-form-widget': {}, 8 | 'image-gallery-widget': {}, 9 | 'map-widget': { 10 | options: { 11 | geocoderSettings: { 12 | // For a full list of the node-geocoder npm package options please view the modules documentation - https://www.npmjs.com/package/node-geocoder 13 | // Requred 14 | provider: 'mapbox', 15 | 16 | // Optional depending on the providers 17 | apiKey: process.env.GEOCODER_API_KEY, // for Mapquest, OpenCage, Google Premier 18 | formatter: null, // 'gpx', 'string', ... 19 | minConfidence: 0.5, 20 | limit: 1 21 | } 22 | } 23 | }, 24 | 'pricing-widget': {}, 25 | 'side-by-side-widget': {}, 26 | 'side-by-side-content-widget': {}, 27 | 'image-widget': {}, 28 | 'rich-text-widget': {} 29 | }; 30 | -------------------------------------------------------------------------------- /modules/content-widget-modules/accordion-widget/ui/src/index.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | apos.util.widgetPlayers.accordion = { 3 | selector: '[data-accordion]', 4 | player: function (el) { 5 | // Find our accordion buttons 6 | const buttons = el.querySelectorAll('[data-accordion-item]'); 7 | 8 | // For each accordion button set up the trigger 9 | buttons.forEach((button) => { 10 | const btnEl = button.querySelector('[data-accordion-button]'); 11 | // Find our hidden text 12 | const target = button.querySelector('[data-accordion-detail]'); 13 | 14 | btnEl.addEventListener('click', () => { 15 | const isExpanded = btnEl.getAttribute('aria-expanded') === 'true'; 16 | // Update the btn's aria attribute 17 | btnEl.setAttribute('aria-expanded', !isExpanded); 18 | // Update the `hidden` attribute on the detail 19 | target.hidden = isExpanded; 20 | }); 21 | }); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /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=a3-boilerplate 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 | -------------------------------------------------------------------------------- /views/fragments/header.html: -------------------------------------------------------------------------------- 1 | {% import "ui.html" as ui %} 2 | {% import "button.html" as buttonFrag %} 3 | 4 | {% fragment navigationBar(data) %} 5 | 33 | {% endfragment %} -------------------------------------------------------------------------------- /modules/content-widget-modules/accordion-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .accordion { 4 | &__item { 5 | border-top: 1px solid $primary; 6 | &:last-child { 7 | border-bottom: 1px solid $primary; 8 | } 9 | } 10 | &__heading { 11 | margin: 0; 12 | } 13 | &__button { 14 | background-color: transparent; 15 | border: none; 16 | display: flex; 17 | justify-content: space-between; 18 | width: 100%; 19 | color: $primary; 20 | text-align: left; 21 | cursor: pointer; 22 | -webkit-user-select: none; 23 | -moz-user-select: none; 24 | -ms-user-select: none; 25 | user-select: none; 26 | &:focus-visible { 27 | outline: none; 28 | } 29 | &[aria-expanded="false"] .accordion__button-icon:after { 30 | content: "\002B"; 31 | } 32 | &[aria-expanded="true"] .accordion__button-icon:after { 33 | content: "\2212"; 34 | } 35 | } 36 | &__button, 37 | &__content { 38 | padding: 1rem 1.5rem; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/content-widget-modules/columns-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% area data.widget, 'one' with data.contextOptions %} 4 |
5 | {% if data.widget.cols === 'double' or data.widget.cols === 'triple' or data.widget.cols === 'quadruple' %} 6 |
7 | {% area data.widget, 'two' with data.contextOptions %} 8 |
9 | {% endif %} 10 | {% if data.widget.cols === 'triple' or data.widget.cols === 'quadruple' %} 11 |
12 | {% area data.widget, 'three' with data.contextOptions %} 13 |
14 | {% endif %} 15 | {% if data.widget.cols === 'quadruple' %} 16 |
17 | {% area data.widget, 'four' with data.contextOptions %} 18 |
19 | {% endif %} 20 |
21 | -------------------------------------------------------------------------------- /modules/content-widget-modules/columns-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import "Modules/asset/scss/variables"; 4 | 5 | .widget-columns { 6 | color: $primary; 7 | display: flex; 8 | flex-direction: column; 9 | &__column { 10 | width: 100%; 11 | &:not(:last-child) { 12 | margin-bottom: 10px; 13 | } 14 | } 15 | 16 | @media only screen and (min-width: 900px) { 17 | flex-direction: row; 18 | justify-content: space-between; 19 | gap: 5%; 20 | 21 | &__column { 22 | &:not(:last-child) { 23 | margin: 0; 24 | } 25 | display: flex; 26 | flex-direction: column; 27 | flex-basis: 100%; 28 | flex: 1; 29 | } 30 | 31 | &--single .widget-columns__column { 32 | width: 100%; 33 | } 34 | 35 | &--double .widget-columns__column { 36 | width: 50%; 37 | } 38 | 39 | &--triple .widget-columns__column { 40 | width: 33.3333%; 41 | } 42 | &--quadruple .widget-columns__column { 43 | width: 25%; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/content-widget-modules/accordion-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Accordion', 8 | icon: 'menu-open-icon', 9 | description: 'Add expandable content to your page', 10 | previewImage: 'svg' 11 | }, 12 | icons: { 13 | 'menu-open-icon': 'MenuOpen' 14 | }, 15 | fields: { 16 | add: { 17 | accordions: { 18 | type: 'array', 19 | label: 'Accordions', 20 | titleField: 'title', 21 | inline: true, 22 | fields: { 23 | add: { 24 | title: { 25 | type: 'string', 26 | label: 'Title' 27 | }, 28 | content: { 29 | type: 'area', 30 | label: 'Content', 31 | options: { 32 | widgets: areaConfig.apos 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | ...aosSchema 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /modules/content-widget-modules/image-gallery-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import 'Modules/asset/scss/variables'; 2 | @import 'swiper/swiper-bundle.css'; 3 | @import 'photoswipe/photoswipe.css'; 4 | 5 | // Main Swiper Gallery loaded on the page 6 | .image-gallery { 7 | &__swiper { 8 | padding: 0 2rem; 9 | .swiper-wrapper { 10 | align-items: center; 11 | } 12 | 13 | .image-gallery__svg { 14 | min-width: 30%; 15 | width: auto; 16 | } 17 | 18 | .swiper-slide { 19 | display: flex; 20 | justify-content: center; 21 | } 22 | } 23 | } 24 | 25 | // Photoswipe lightbox gallery target - when opened in the modal 26 | .imageGallery--pswp { 27 | .pswp__zoom-wrap { 28 | width: 100%; 29 | height: 100%; 30 | } 31 | .pswp__img--placeholder { 32 | display: none; 33 | } 34 | .pswp__img { 35 | width: auto !important; 36 | height: auto !important; 37 | max-width: 100% !important; 38 | /* height: auto; */ 39 | top: 50%; 40 | left: 50%; 41 | transform: translate(-50%, -50%); 42 | object-fit: contain; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/images/checked-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/content-widget-modules/call-to-action-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .call-to-action { 4 | position: relative; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | width: 100%; 9 | color: $white; 10 | 11 | &__background { 12 | background-position: 50% 50%; 13 | background-size: cover; 14 | position: absolute; 15 | filter: brightness(0.7); 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | z-index: -1; 21 | } 22 | 23 | &--basic { 24 | height: 50vh; 25 | height: 50svh; 26 | } 27 | &--large-marquee { 28 | height: 100vh; 29 | height: 100svh; 30 | } 31 | 32 | &--left { 33 | text-align: left; 34 | .button-strip { 35 | justify-content: flex-start; 36 | } 37 | } 38 | &--centered { 39 | text-align: center; 40 | .button-strip { 41 | justify-content: center; 42 | } 43 | } 44 | &--right { 45 | text-align: right; 46 | .button-strip { 47 | justify-content: flex-end; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss-elements/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn, 2 | .my-form__submit { 3 | padding: 1rem 2rem; 4 | width: 100%; 5 | display: inline-block; 6 | font-weight: $btn-font-weight; 7 | color: white; 8 | text-decoration: none; 9 | text-align: center; 10 | transition: background-color $animate-easing; 11 | text-transform: uppercase; 12 | border-radius: $border-radius; 13 | &:hover { 14 | color: $white; 15 | background-color: bisque; 16 | text-decoration: none; 17 | } 18 | } 19 | 20 | .btn--sm { 21 | padding: 0.75rem 1.5rem; 22 | font-size: 0.75rem; 23 | } 24 | 25 | .btn--lg { 26 | padding: 1.25rem 2.5rem; 27 | font-size: 1.25rem; 28 | } 29 | 30 | .btn--primary, 31 | .my-form__submit { 32 | background: $primary; 33 | border: none; 34 | color: $white; 35 | &:hover { 36 | color: $white; 37 | background: lighten($primary, 5%); 38 | } 39 | } 40 | 41 | .btn--secondary { 42 | background: $secondary; 43 | border: none; 44 | color: $btn-secondary-text; 45 | &:hover { 46 | color: $primary; 47 | background: lighten($secondary, 5%); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modules/@apostrophecms/admin-bar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | groups: [ 4 | { 5 | name: 'pages', 6 | label: 'Pages', 7 | items: [ 8 | '@apostrophecms/page' 9 | ] 10 | }, 11 | { 12 | name: 'forms', 13 | label: 'Forms', 14 | items: [ 15 | '@apostrophecms/form' 16 | ] 17 | }, 18 | { 19 | name: 'products', 20 | label: 'Products', 21 | items: [ 22 | 'product' 23 | ] 24 | }, 25 | { 26 | name: 'teams', 27 | label: 'Teams', 28 | items: [ 29 | 'team-member' 30 | ] 31 | }, 32 | { 33 | name: 'media', 34 | label: 'Media', 35 | items: [ 36 | '@apostrophecms/image', 37 | '@apostrophecms/file', 38 | '@apostrophecms/image-tag', 39 | '@apostrophecms/file-tag' 40 | ] 41 | }, 42 | { 43 | name: 'admin', 44 | label: 'Admin', 45 | items: [ 46 | '@apostrophecms/user' 47 | ] 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /modules/content-widget-modules/image-gallery-widget/index.js: -------------------------------------------------------------------------------- 1 | import aosSchema from '../../../lib/aosSchema.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Image Gallery', 7 | description: 'Add a gallery of images to your page', 8 | previewImage: 'jpg', 9 | icon: 'image-gallery-icon' 10 | }, 11 | icons: { 12 | 'image-gallery-icon': 'ImageAlbum' 13 | }, 14 | fields: { 15 | add: { 16 | displayType: { 17 | type: 'select', 18 | label: 'Slide display type', 19 | required: true, 20 | choices: [ 21 | { 22 | label: 'Large, single slide', 23 | value: 1, 24 | def: true 25 | }, 26 | { 27 | label: 'Three slides', 28 | value: 3 29 | }, 30 | { 31 | label: 'Four slides', 32 | value: 4 33 | } 34 | ] 35 | }, 36 | _images: { 37 | type: 'relationship', 38 | withType: '@apostrophecms/image', 39 | label: 'Images', 40 | required: true, 41 | max: 10 42 | }, 43 | ...aosSchema 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_typography.scss: -------------------------------------------------------------------------------- 1 | %heading { 2 | margin-top: 0; // 1 3 | margin-bottom: $headings-margin-bottom; 4 | font-family: $headings-font-family; 5 | font-style: $headings-font-style; 6 | font-weight: $headings-font-weight; 7 | line-height: $headings-line-height; 8 | color: $headings-color; 9 | } 10 | 11 | h1 { 12 | @extend %heading; 13 | @include font-size($h1-font-size); 14 | } 15 | 16 | h2 { 17 | @extend %heading; 18 | @include font-size($h2-font-size); 19 | } 20 | 21 | h3 { 22 | @extend %heading; 23 | @include font-size($h3-font-size); 24 | } 25 | 26 | h4 { 27 | @extend %heading; 28 | @include font-size($h4-font-size); 29 | } 30 | 31 | h5 { 32 | @extend %heading; 33 | @include font-size($h5-font-size); 34 | } 35 | 36 | h6 { 37 | @extend %heading; 38 | @include font-size($h6-font-size); 39 | } 40 | 41 | .h1 { 42 | @extend h1; 43 | } 44 | 45 | .h2 { 46 | @extend h2; 47 | } 48 | 49 | .h3 { 50 | @extend h3; 51 | } 52 | 53 | .h4 { 54 | @extend h4; 55 | } 56 | 57 | .h5 { 58 | @extend h5; 59 | } 60 | 61 | .h6 { 62 | @extend h6; 63 | } 64 | 65 | p:not([class]) { 66 | margin-top: 0; 67 | margin-bottom: 1rem; 68 | line-height: $line-height-base; 69 | } 70 | -------------------------------------------------------------------------------- /modules/pieces-modules/team-member-widget/index.js: -------------------------------------------------------------------------------- 1 | import aosSchema from '../../../lib/aosSchema.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Team Members', 7 | icon: 'teams-icon', 8 | description: 'Display team members on your page', 9 | previewImage: 'svg' 10 | }, 11 | icons: { 12 | 'teams-icon': 'AccountMultiplePlus' 13 | }, 14 | fields: { 15 | add: { 16 | style: { 17 | type: 'select', 18 | label: 'Layout style', 19 | required: true, 20 | choices: [ 21 | { 22 | label: 'Three column', 23 | value: 'three-col' 24 | }, 25 | { 26 | label: 'Four column', 27 | value: 'four-col' 28 | } 29 | ] 30 | }, 31 | _teamMembers: { 32 | type: 'relationship', 33 | withType: 'team-member', 34 | label: 'Select team member(s)...', 35 | required: true, 36 | builders: { 37 | project: { 38 | type: 'team-member', 39 | title: 1, 40 | profileImage: 1, 41 | workTitle: 1 42 | } 43 | } 44 | }, 45 | ...aosSchema 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /views/fragments/footer.html: -------------------------------------------------------------------------------- 1 | {% import "ui.html" as ui %} 2 | 3 | {% fragment footer(data) %} 4 | 39 | {% endfragment %} -------------------------------------------------------------------------------- /modules/asset/ui/src/scss-elements/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | background-color: $secondary; 3 | 4 | &__main { 5 | background-color: $white; 6 | border-radius: $border-radius; 7 | padding: 2rem; 8 | 9 | @media (min-width: 1024px) { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | } 14 | } 15 | 16 | &__logo { 17 | img { 18 | width: 200px; 19 | height: auto; 20 | } 21 | } 22 | 23 | ul { 24 | display: flex; 25 | padding-left: 0; 26 | list-style: none; 27 | } 28 | 29 | &__nav-items { 30 | ul { 31 | display: block; 32 | 33 | @media (min-width: 1024px) { 34 | display: flex; 35 | } 36 | } 37 | 38 | li { 39 | margin: 1.25rem 0px; 40 | 41 | @media (min-width: 1024px) { 42 | margin: 0 1.25rem; 43 | } 44 | a { 45 | @include inverse-link-style; 46 | } 47 | } 48 | } 49 | 50 | &__social-items { 51 | li { 52 | margin: 0 1rem; 53 | 54 | &:first-child { 55 | margin-left: 0; 56 | } 57 | } 58 | } 59 | 60 | &__copyright { 61 | text-align: center; 62 | margin: 1rem 0; 63 | font-weight: bold; 64 | p { 65 | margin: 0; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/content-widget-modules/image-gallery-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import 'button-arrows.html' as buttonArrows %} 2 | 3 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | {# Automatically extends the right outer layout and also handles AJAX siutations #} 2 | 3 | {% import 'fragments/header.html' as header %} 4 | {% import 'fragments/footer.html' as footer %} 5 | 6 | {% extends data.outerLayout %} 7 | {% set title = data.piece.title or data.page.title %} 8 | {% block title %} 9 | {{ data.global.title }} 10 | {% if not title %} 11 | {{ 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.') }} 12 | {% endif %} 13 | {% endblock %} 14 | 15 | {% block beforeMain %} 16 | 17 | 18 | {# Marketing fonts #} 19 | 20 | {% render header.navigationBar(data) %} 21 | {% endblock %} 22 | 23 | {% block main %} 24 | {# 25 | Usually, your page templates in the @apostrophecms/pages module will override 26 | this block. It is safe to assume this is where your page-specific content 27 | should go. 28 | #} 29 | {% endblock %} 30 | 31 | {% block afterMain %} 32 | {% render footer.footer(data) %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /modules/content-widget-modules/pricing-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .pricing { 4 | background: $secondary; 5 | &__title { 6 | text-align: center; 7 | } 8 | .cards { 9 | margin-top: 2rem; 10 | display: grid; 11 | grid-template-columns: repeat(auto-fit, minmax(350px, max-content)); 12 | grid-gap: 2rem; 13 | justify-content: center; 14 | padding: initial; 15 | } 16 | .card { 17 | border-radius: $border-radius; 18 | padding: 2rem; 19 | width: 350px; 20 | background: $white; 21 | color: $primary; 22 | &:nth-child(even) { 23 | background: none; 24 | border: 1px solid $tertiary; 25 | color: $primary; 26 | } 27 | &--pricing { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-between; 31 | } 32 | 33 | &__label { 34 | margin-bottom: 0.5rem; 35 | font-weight: 700; 36 | color: $tertiary; 37 | text-transform: uppercase; 38 | } 39 | &__feature-list { 40 | padding-left: 0; 41 | list-style: none; 42 | li { 43 | margin-bottom: 10px; 44 | display: flex; 45 | } 46 | li:before { 47 | margin-right: 10px; 48 | font-weight: 700; 49 | color: $tertiary; 50 | content: "\2713"; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/content-widget-modules/custom-form-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% set layout = data.widget.layout %} 2 | {% set style = data.widget.backgroundStyle %} 3 | 4 | {% if style === 'image' %} 5 | {% set backgroundImage = apos.image.first(data.widget._backgroundImage) %} 6 | {% else %} 7 | {% set backgroundColor = data.widget.backgroundColor %} 8 | {% endif %} 9 | 10 |
21 |
22 | {% area data.widget, 'form' %} 23 |
24 | {% if layout === 'column' and data.widget._backgroundImage.length %} 25 |
26 | {% set image = apos.image.first(data.widget._backgroundImage) %} 27 | 28 | {{ image.title }} 29 |
30 | {% endif %} 31 |
-------------------------------------------------------------------------------- /modules/pieces-modules/product-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Product Menu', 8 | icon: 'list-icon', 9 | description: 'Display a product menu on your page', 10 | previewImage: 'svg' 11 | }, 12 | icons: { 13 | 'list-icon': 'FormatListChecks' 14 | }, 15 | fields: { 16 | add: { 17 | headingIntro: { 18 | type: 'area', 19 | label: 'Heading intro', 20 | options: { 21 | widgets: areaConfig.richText 22 | } 23 | }, 24 | style: { 25 | type: 'select', 26 | label: 'Layout style', 27 | required: true, 28 | choices: [ 29 | { 30 | label: 'Full', 31 | value: 'full', 32 | def: true 33 | }, 34 | { 35 | label: 'Split', 36 | value: 'split' 37 | } 38 | ] 39 | }, 40 | currencySybmol: { 41 | type: 'string', 42 | label: 'Currency symbol', 43 | max: 1 44 | }, 45 | _menuItems: { 46 | label: 'Menu', 47 | type: 'relationship', 48 | withType: 'product', 49 | builders: { 50 | project: { 51 | type: 'product', 52 | title: 1, 53 | description: 1, 54 | price: 1 55 | } 56 | } 57 | }, 58 | ...aosSchema 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/linkSchema.js: -------------------------------------------------------------------------------- 1 | const link = { 2 | linkText: { 3 | label: 'Link Text', 4 | type: 'string' 5 | }, 6 | linkType: { 7 | label: 'Link Type', 8 | type: 'select', 9 | choices: [ 10 | { 11 | label: 'Page', 12 | value: 'page' 13 | }, 14 | { 15 | label: 'File', 16 | value: 'file' 17 | }, 18 | { 19 | label: 'Custom URL', 20 | value: 'custom' 21 | } 22 | ] 23 | }, 24 | _linkPage: { 25 | label: 'Page to link', 26 | type: 'relationship', 27 | withType: '@apostrophecms/page', 28 | max: 1, 29 | builders: { 30 | project: { 31 | type: '@apostrophecms/page', 32 | title: 1, 33 | _url: 1 34 | } 35 | }, 36 | if: { 37 | linkType: 'page' 38 | } 39 | }, 40 | _linkFile: { 41 | label: 'File to link', 42 | type: 'relationship', 43 | withType: '@apostrophecms/file', 44 | max: 1, 45 | if: { 46 | linkType: 'file' 47 | } 48 | }, 49 | linkUrl: { 50 | label: 'URL for custom link', 51 | type: 'url', 52 | if: { 53 | linkType: 'custom' 54 | } 55 | }, 56 | linkTarget: { 57 | label: 'Will the link open a new browser tab?', 58 | type: 'checkboxes', 59 | choices: [ 60 | { 61 | label: 'Open in new tab', 62 | value: '_blank' 63 | } 64 | ] 65 | }, 66 | ariaLabel: { 67 | label: 'Aria label', 68 | type: 'string', 69 | help: 'This is used for screen readers and SEO' 70 | } 71 | }; 72 | 73 | export default link; 74 | -------------------------------------------------------------------------------- /public/images/social-icons/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /modules/asset/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import url("normalize.css"); 2 | // Responsive font size npm package (https://www.npmjs.com/package/rfs) 3 | @import "rfs/scss"; 4 | 5 | @import "./scss/_functions"; 6 | @import "./scss/_variables"; 7 | @import "./scss/_mixins"; 8 | 9 | @import "./scss/_containers"; 10 | @import "./scss/_typography"; 11 | 12 | // Theme 13 | @import "./scss/_theme"; 14 | 15 | // Navigation styles 16 | @import "./scss-elements/navigation"; 17 | // Footer styles 18 | @import "./scss-elements/footer"; 19 | 20 | @import "./scss-elements/_buttons"; 21 | @import "./scss-elements/_button-arrow"; 22 | @import "./scss-elements/_forms"; 23 | @import "aos/dist/aos.css"; 24 | 25 | // apos line-height interferance fix 26 | .apos-area, 27 | .apos-area-widget-inner, 28 | .apos-area-widget-wrapper, 29 | .apos-areas-widgets-list { 30 | line-height: unset !important; 31 | } 32 | 33 | *, 34 | ::after, 35 | ::before { 36 | box-sizing: border-box; 37 | } 38 | 39 | body { 40 | background: $body-bg; 41 | width: 100%; 42 | margin: 0; // 1 43 | font-family: $body-font-family; 44 | @include font-size(1rem); 45 | font-weight: 400; 46 | line-height: $line-height-base; 47 | color: $body-color; 48 | -webkit-text-size-adjust: 100%; // 3 49 | -webkit-tap-highlight-color: rgba($black, 0); // 4 50 | } 51 | 52 | a { 53 | @include link-style; 54 | } 55 | 56 | .img-fluid { 57 | max-width: 100%; 58 | height: auto; 59 | border-radius: $border-radius; 60 | box-shadow: $box-shadow; 61 | } 62 | 63 | .widget-my-spacing { 64 | margin-top: 3rem; 65 | margin-bottom: 3rem; 66 | } 67 | 68 | .widget-py-spacing { 69 | padding-top: 3rem; 70 | padding-bottom: 3rem; 71 | } 72 | -------------------------------------------------------------------------------- /views/ui.html: -------------------------------------------------------------------------------- 1 | {# Button #} 2 | {% macro button(button, className = "", iconName = "", iconPosition = "l") %} 3 | 19 | {% if iconName %} 20 | {% if iconPosition === 'l' %} 21 | 22 | {% endif %} 23 | {% endif %} 24 | {{ button.linkLabel }} 25 | {% if iconName %} 26 | {% if iconPosition === 'r' %} 27 | 28 | {% endif %} 29 | {% endif %} 30 | 31 | {% endmacro %} 32 | 33 | {# Image #} 34 | {% macro image (image, className = '') %} 35 | {% set attachment = apos.image.first(image) %} 36 | 37 | {% if attachment %} 38 | {{ attachment._alt or '' }} 51 | {% endif %} 52 | {% endmacro %} -------------------------------------------------------------------------------- /modules/asset/ui/src/scss-elements/_navigation.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | position: relative; 3 | margin-top: 1.875rem; 4 | margin-bottom: 1.875rem; 5 | 6 | @media (min-width: 1024px) { 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | } 11 | 12 | &__logo-container { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | } 17 | 18 | &__logo { 19 | img { 20 | width: 200px; 21 | height: auto; 22 | vertical-align: middle; 23 | } 24 | } 25 | 26 | &__nav-items { 27 | display: none; 28 | 29 | @media (min-width: 1024px) { 30 | display: flex; 31 | justify-content: space-between; 32 | width: 100%; 33 | } 34 | } 35 | &__nav-links { 36 | padding-left: 0; 37 | list-style: none; 38 | 39 | @media (min-width: 1024px) { 40 | display: flex; 41 | justify-content: center; 42 | align-items: center; 43 | width: 100%; 44 | margin: 0; 45 | } 46 | li { 47 | margin: 1.25rem 0; 48 | 49 | @media (min-width: 1024px) { 50 | margin: 0 1rem; 51 | } 52 | a { 53 | @include inverse-link-style; 54 | } 55 | } 56 | } 57 | 58 | &__show { 59 | display: block; 60 | } 61 | 62 | &__user-items { 63 | display: flex; 64 | flex-shrink: 0; 65 | } 66 | 67 | &__menu-btn { 68 | display: block; 69 | width: 40px; 70 | height: 40px; 71 | background-image: url("/images/menu.svg"); 72 | background-repeat: no-repeat; 73 | background-position: center; 74 | background-size: cover; 75 | 76 | @media (min-width: 1024px) { 77 | display: none; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/@apostrophecms/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 | -------------------------------------------------------------------------------- /modules/content-widget-modules/columns-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_containers.scss: -------------------------------------------------------------------------------- 1 | @mixin min($bp, $max: "null", $device: "screen") { 2 | @if $max == "null" { 3 | @media only #{$device} and (min-width: #{$bp}) { 4 | @content; 5 | } 6 | } @else { 7 | @media only #{$device} and (min-width: #{$bp}) and (max-width: #{$max}) { 8 | @content; 9 | } 10 | } 11 | } 12 | 13 | @function bp($bp) { 14 | @return map-get($breakpoints, $bp); 15 | } 16 | 17 | $breakpoints: ( 18 | na: 0px, 19 | // For BS grid 20 | xs: 320px, 21 | // Smartphone 22 | sm: 600px, 23 | // Tablets 24 | md: 900px, 25 | // Tablets Landscape and small desktops 26 | lg: 1200px, 27 | // Desktops 28 | xl: 1800px, 29 | // Large Desktop 30 | ); 31 | 32 | @function container($container-size, $true-val: false) { 33 | @return map-get($container-sizes, $container-size); 34 | } 35 | 36 | $container-sizes: ( 37 | sm: map-get($breakpoints, sm) - 30px, 38 | md: map-get($breakpoints, md) - 40px, 39 | lg: map-get($breakpoints, lg) - 50px, 40 | xl: map-get($breakpoints, xl) - 400px, 41 | ); 42 | 43 | .container { 44 | padding-right: 1rem; 45 | padding-left: 1rem; 46 | 47 | &:not(.is-fluid) { 48 | margin-left: auto; 49 | margin-right: auto; 50 | 51 | @each $bp, $container-size in $container-sizes { 52 | @include min(#{bp(#{$bp})}) { 53 | width: 100%; 54 | max-width: container(#{$bp}); 55 | } 56 | } 57 | } 58 | } 59 | 60 | @each $bp, $container-size in $container-sizes { 61 | .container-#{$bp} { 62 | margin: 0 auto; 63 | padding-right: 1rem; 64 | padding-left: 1rem; 65 | width: 100%; 66 | 67 | $i: index($container-sizes, $bp $container-size); 68 | 69 | @for $j from $i through length($container-sizes) { 70 | @include min(#{bp(nth(nth($container-sizes, $j), 1))}) { 71 | max-width: container(#{nth(nth($container-sizes, $j), 1)}); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /modules/content-widget-modules/custom-form-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 4 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /modules/content-widget-modules/pricing-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Pricing', 8 | icon: 'cards-icon', 9 | description: 'Display pricing cards on your page', 10 | previewImage: 'svg' 11 | }, 12 | icons: { 13 | 'cards-icon': 'Cards' 14 | }, 15 | fields: { 16 | add: { 17 | intro: { 18 | type: 'area', 19 | label: 'Intro', 20 | options: { 21 | widgets: areaConfig.richText 22 | } 23 | }, 24 | cards: { 25 | type: 'array', 26 | label: 'Cards', 27 | titleField: 'label', 28 | inline: true, 29 | max: 4, 30 | fields: { 31 | add: { 32 | label: { 33 | type: 'string', 34 | label: 'Label' 35 | }, 36 | content: { 37 | type: 'area', 38 | label: 'Content', 39 | options: { 40 | widgets: areaConfig.richText 41 | } 42 | }, 43 | features: { 44 | type: 'array', 45 | label: 'Features list', 46 | titleField: 'title', 47 | fields: { 48 | add: { 49 | title: { 50 | type: 'string', 51 | label: 'Title' 52 | } 53 | } 54 | } 55 | }, 56 | buttons: { 57 | type: 'area', 58 | label: 'Buttons', 59 | options: { 60 | max: 2, 61 | widgets: { 62 | button: {} 63 | } 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | ...aosSchema 70 | } 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a3-starter-kit-marketing", 3 | "version": "1.0.0", 4 | "description": "Marketing starter kit for ApostropheCMS", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node app", 8 | "dev": "nodemon", 9 | "build": "cross-env NODE_ENV=production node app @apostrophecms/asset:build", 10 | "serve": "cross-env 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 | "./apos.vite.config.js", 19 | "./modules/**/*", 20 | "./lib/**/*.js", 21 | "./views/**/*.html" 22 | ], 23 | "ignoreRoot": [ 24 | ".git" 25 | ], 26 | "ignore": [ 27 | "**/ui/apos/", 28 | "**/ui/src/", 29 | "**ui/public/", 30 | "locales/*.json", 31 | "public/uploads/", 32 | "public/apos-frontend/*.js", 33 | "data/" 34 | ], 35 | "ext": "json, js, html, scss, vue" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git@github.com:apostrophecms/starter-kit-marketing.git" 40 | }, 41 | "author": "Apostrophe Technologies", 42 | "license": "MIT", 43 | "dependencies": { 44 | "@apostrophecms/form": "^1.1.1", 45 | "@apostrophecms/open-graph": "^1.2.1", 46 | "@apostrophecms/seo": "^1.2.0", 47 | "@apostrophecms/sitemap": "^1.0.2", 48 | "@apostrophecms/vite": "^1.0.0", 49 | "aos": "^2.3.4", 50 | "apostrophe": "^4.18.0", 51 | "cross-env": "^10.1.0", 52 | "dotenv": "^16.0.3", 53 | "node-geocoder": "^4.2.0", 54 | "normalize.css": "^8.0.1", 55 | "ol": "^7.3.0", 56 | "photoswipe": "^5.3.7", 57 | "rfs": "^10.0.0", 58 | "swiper": "^9.2.3" 59 | }, 60 | "devDependencies": { 61 | "autoprefixer": "^10.4.20", 62 | "eslint-config-apostrophe": "^6.0.1", 63 | "nodemon": "^2.0.7" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/index.js: -------------------------------------------------------------------------------- 1 | import aosSchema from '../../../lib/aosSchema.js'; 2 | import NodeGeocoder from 'node-geocoder'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | // geocoder options can be include inside the main map-widget reference 8 | // located in the /modules/content-widget-modules/modules.js file 9 | label: 'Map', 10 | icon: 'map-icon', 11 | description: 'Add a map to your page', 12 | previewImage: 'jpg' 13 | }, 14 | icons: { 15 | 'map-icon': 'Map' 16 | }, 17 | fields: { 18 | add: { 19 | address: { 20 | type: 'string', 21 | label: 'Address', 22 | required: true 23 | }, 24 | mapZoomLevel: { 25 | type: 'integer', 26 | label: 'Map zoom level', 27 | min: 1, 28 | max: 14, 29 | def: 14 30 | }, 31 | ...aosSchema 32 | } 33 | }, 34 | components(self) { 35 | return { 36 | async map(req, data) { 37 | 38 | const body = {}; 39 | try { 40 | if (!self.options.geocoderSettings.apiKey) { 41 | body.message = 'No geocoder api key found, please set in the widget options'; 42 | } 43 | // View node-geocoder npm package for full list of options and providers - https://www.npmjs.com/package/node-geocoder 44 | const options = { 45 | ...self.options.geocoderSettings 46 | }; 47 | const geocoder = NodeGeocoder(options); 48 | const geocoderAddress = await geocoder.geocode(data.widget.address); 49 | 50 | if (!geocoderAddress.length) { 51 | throw new Error('No results found for entered street address, please check address is valid and update the field'); 52 | } 53 | 54 | data.widget.latitude = geocoderAddress[0].latitude; 55 | data.widget.longitude = geocoderAddress[0].longitude; 56 | } catch (error) { 57 | body.message = error.message; 58 | } 59 | 60 | return { 61 | response: body, 62 | widget: data.widget 63 | }; 64 | } 65 | }; 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /modules/content-widget-modules/call-to-action-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Call to action', 8 | icon: 'gesture-tap-button-icon', 9 | placeholderUrl: '/modules/@apostrophecms/image-widget/placeholder.jpg', 10 | description: 'Add a large hero image with a call to action to your page', 11 | previewImage: 'jpg' 12 | }, 13 | init(self) { 14 | self.determineBestAssetUrl('placeholder'); 15 | }, 16 | icons: { 17 | 'gesture-tap-button-icon': 'GestureTapButton' 18 | }, 19 | fields: { 20 | add: { 21 | style: { 22 | type: 'select', 23 | label: 'Layout style', 24 | required: true, 25 | choices: [ 26 | { 27 | label: 'Basic', 28 | value: 'basic', 29 | def: true 30 | }, 31 | { 32 | label: 'Large Marquee', 33 | value: 'large-marquee' 34 | } 35 | ] 36 | }, 37 | contentAlignment: { 38 | type: 'select', 39 | label: 'Content alignment', 40 | required: true, 41 | choices: [ 42 | { 43 | label: 'Left', 44 | value: 'left', 45 | def: true 46 | }, 47 | { 48 | label: 'Centered', 49 | value: 'centered' 50 | }, 51 | { 52 | label: 'Right', 53 | value: 'right' 54 | } 55 | ] 56 | }, 57 | featureImage: { 58 | type: 'area', 59 | label: 'Feature image', 60 | required: true, 61 | options: { 62 | max: 1, 63 | widgets: { 64 | '@apostrophecms/image': {} 65 | } 66 | } 67 | }, 68 | content: { 69 | type: 'area', 70 | label: 'Content', 71 | required: true, 72 | options: { 73 | widgets: { 74 | ...areaConfig.richText, 75 | 'button-strip': {} 76 | } 77 | } 78 | }, 79 | ...aosSchema 80 | } 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /lib/area.js: -------------------------------------------------------------------------------- 1 | const apostropheWidgets = { 2 | '@apostrophecms/image': { 3 | className: 'img-fluid' 4 | }, 5 | '@apostrophecms/video': {}, 6 | '@apostrophecms/rich-text': {} 7 | }; 8 | 9 | const area = { 10 | all: { 11 | '@apostrophecms/layout': {}, 12 | 'call-to-action': {}, 13 | 'custom-form': {}, 14 | 'image-gallery': {}, 15 | product: {}, 16 | 'side-by-side': {}, 17 | 'rich-text': {}, 18 | image: {}, 19 | map: {}, 20 | 21 | // Marketing widgets 22 | accordion: {}, 23 | pricing: {}, 24 | 'team-member': {} 25 | }, 26 | columnExpandedGroup: { 27 | basic: { 28 | label: 'Basic Tools', 29 | widgets: { 30 | image: {}, 31 | 'rich-text': {} 32 | }, 33 | columns: 2 34 | }, 35 | layout: { 36 | label: 'Layout Tools', 37 | widgets: { 38 | accordion: {}, 39 | 'call-to-action': {}, 40 | 'side-by-side': {} 41 | }, 42 | columns: 2 43 | }, 44 | general: { 45 | label: 'Themed Widgets', 46 | widgets: { 47 | 'custom-form': {}, 48 | 'image-gallery': {}, 49 | map: {}, 50 | pricing: {}, 51 | product: {}, 52 | 'team-member': {} 53 | }, 54 | columns: 3 55 | } 56 | }, 57 | apos: { 58 | ...apostropheWidgets 59 | }, 60 | richText: { 61 | '@apostrophecms/rich-text': {} 62 | }, 63 | fullExpandedGroup: { 64 | layout: { 65 | label: 'Layout Tools', 66 | widgets: { 67 | '@apostrophecms/layout': {}, 68 | 'side-by-side': {} 69 | }, 70 | columns: 2 71 | }, 72 | media: { 73 | label: 'Media Widgets', 74 | widgets: { 75 | image: {}, 76 | '@apostrophecms/video': {}, 77 | 'image-gallery': {} 78 | }, 79 | columns: 2 80 | }, 81 | general: { 82 | label: 'Content Widgets', 83 | widgets: { 84 | 'rich-text': {}, 85 | accordion: {}, 86 | 'call-to-action': {}, 87 | 'custom-form': {}, 88 | map: {}, 89 | pricing: {}, 90 | product: {}, 91 | 'team-member': {} 92 | }, 93 | columns: 3 94 | } 95 | } 96 | }; 97 | 98 | export default area; 99 | -------------------------------------------------------------------------------- /modules/content-widget-modules/image-gallery-widget/ui/src/index.js: -------------------------------------------------------------------------------- 1 | // import Swiper JS 2 | import Swiper from 'swiper/bundle'; 3 | import PhotoSwipeLightbox from 'photoswipe/lightbox'; 4 | import PhotoSwipe from 'photoswipe'; 5 | 6 | export default () => { 7 | apos.util.widgetPlayers['image-gallery'] = { 8 | selector: '[data-image-gallery]', 9 | player: function (el) { 10 | const slides = el.dataset.slides || 1; 11 | 12 | // Swiper.js slideshow 13 | const swiper = new Swiper(el, { 14 | slidesPerView: slides, 15 | spaceBetween: 30, 16 | navigation: { 17 | nextEl: '.button-arrow--next', 18 | prevEl: '.button-arrow--prev' 19 | } 20 | }); 21 | 22 | // Photoswiper lightbox and gallery 23 | const photoSwipeOptions = { 24 | mainClass: 'imageGallery--pswp', 25 | gallery: '#imageGallery', 26 | pswpModule: PhotoSwipe, 27 | // set background opacity 28 | bgOpacity: 1, 29 | showHideOpacity: true, 30 | children: 'a', 31 | loop: true, 32 | showHideAnimationType: 'fade' /* options: fade, zoom, none */, 33 | 34 | /* Click on image moves to the next slide */ 35 | imageClickAction: 'next', 36 | tapAction: 'next', 37 | 38 | /* ## Hiding a specific UI element ## */ 39 | zoom: false, 40 | close: true, 41 | counter: true, 42 | arrowKeys: true 43 | }; 44 | 45 | const lightbox = new PhotoSwipeLightbox(photoSwipeOptions); 46 | 47 | lightbox.init(); 48 | 49 | lightbox.on('change', () => { 50 | const { pswp } = lightbox; 51 | swiper.slideTo(pswp.currIndex, 0, false); 52 | }); 53 | 54 | lightbox.on('afterInit', () => { 55 | if (swiper.params.autoplay.enabled) { 56 | swiper.autoplay.stop(); 57 | } 58 | }); 59 | 60 | lightbox.on('closingAnimationStart', () => { 61 | const { pswp } = lightbox; 62 | swiper.slideTo(pswp.currIndex, 0, false); 63 | /* if autoplay enabled == true -> autoplay.start() when close lightbox */ 64 | if (swiper.params.autoplay.enabled) { 65 | swiper.autoplay.start(); 66 | } 67 | }); 68 | } 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/aosSchema.js: -------------------------------------------------------------------------------- 1 | const aos = { 2 | animationEffects: { 3 | label: 'Animation Effect Style', 4 | type: 'select', 5 | def: 'no-animation', 6 | choices: [ 7 | { 8 | label: 'No Animation', 9 | value: 'no-animation' 10 | }, 11 | { 12 | label: 'Fade Up', 13 | value: 'fade-up' 14 | }, 15 | { 16 | label: 'Fade Down', 17 | value: 'fade-down' 18 | }, 19 | { 20 | label: 'Fade right', 21 | value: 'fade-right' 22 | }, 23 | { 24 | label: 'Fade Left', 25 | value: 'fade-left' 26 | }, 27 | { 28 | label: 'Fade Up Right', 29 | value: 'fade-up-right' 30 | }, 31 | { 32 | label: 'Fade Up Left', 33 | value: 'fade-up-left' 34 | }, 35 | { 36 | label: 'Fade Down Right', 37 | value: 'fade-down-right' 38 | }, 39 | { 40 | label: 'Fade Down Left', 41 | value: 'fade-down-left' 42 | }, 43 | { 44 | label: 'Flip Left', 45 | value: 'flip-left' 46 | }, 47 | { 48 | label: 'Flip Right', 49 | value: 'flip-right' 50 | }, 51 | { 52 | label: 'Flip Up', 53 | value: 'flip-up' 54 | }, 55 | { 56 | label: 'Flip Down', 57 | value: 'Flip-down' 58 | }, 59 | { 60 | label: 'Zoom in', 61 | value: 'Zoom-in' 62 | }, 63 | { 64 | label: 'Zoom In Up', 65 | value: 'zoom-in-up' 66 | }, 67 | { 68 | label: 'Zoom In down', 69 | value: 'zoom-in-down' 70 | }, 71 | { 72 | label: 'Zoom In Left', 73 | value: 'zoom-in-left' 74 | }, 75 | { 76 | label: 'Zoom In Right', 77 | value: 'zoom-in-right' 78 | }, 79 | { 80 | label: 'Zoom Out', 81 | value: 'zoom-out' 82 | }, 83 | { 84 | label: 'Zoom Out Up', 85 | value: 'zoom-out-up' 86 | }, 87 | { 88 | label: 'Zoom Out Down', 89 | value: 'zoom-out-down' 90 | }, 91 | { 92 | label: 'Zoom Out Right', 93 | value: 'zoom-out-right' 94 | }, 95 | { 96 | label: 'Zoom Out Left', 97 | value: 'zoom-out-left' 98 | } 99 | ] 100 | } 101 | }; 102 | 103 | export default aos; 104 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss-elements/_forms.scss: -------------------------------------------------------------------------------- 1 | @import "Modules/asset/scss/variables"; 2 | 3 | .my-form { 4 | // Form resets 5 | 6 | &__fieldset, 7 | &__input, 8 | &__legend { 9 | appearance: none; 10 | } 11 | 12 | &__fieldset, 13 | &__input, 14 | &__legend { 15 | -webkit-appearance: none; 16 | -moz-appearance: none; 17 | appearance: none; 18 | } 19 | 20 | &__fieldset, 21 | &__input, 22 | &__legend { 23 | -webkit-appearance: none; 24 | -moz-appearance: none; 25 | appearance: none; 26 | background-color: transparent; 27 | border: none; 28 | padding: 0; 29 | margin: 0; 30 | box-sizing: border-box; 31 | } 32 | 33 | // Form element styling 34 | &__input { 35 | border: 1px solid $primary; 36 | background-color: white; 37 | border-radius: 0.25rem; 38 | color: $primary; 39 | &:focus { 40 | outline: none; 41 | box-shadow: 0 0 0 0.1rem rgba($primary, 50%); 42 | } 43 | } 44 | 45 | &__input:not([type="checkbox"]):not([type="radio"]) { 46 | display: block; 47 | box-sizing: border-box; 48 | width: 100%; 49 | padding: 1rem; 50 | } 51 | 52 | textarea#{&}__input { 53 | min-height: 10rem; 54 | resize: none; 55 | } 56 | 57 | &__input::placeholder { 58 | opacity: 1; /* Firefox */ 59 | color: $primary; 60 | } 61 | 62 | select#{&}__input { 63 | background: url("images/down-arrow.svg") no-repeat center right 0.75rem; 64 | } 65 | 66 | &__input[type="radio"], 67 | &__input[type="checkbox"] { 68 | height: 1.5em; 69 | width: 1.5em; 70 | vertical-align: middle; 71 | } 72 | 73 | &__input[type="radio"] { 74 | border-radius: 50%; 75 | &:checked { 76 | background-image: radial-gradient($primary 40%, transparent calc(40% + 1px)); 77 | } 78 | } 79 | 80 | &__input[type="checkbox"]:checked { 81 | background: $primary url("/images/checked-icon.svg") no-repeat center / 75% auto; 82 | } 83 | 84 | // Class assigned to button styling in modules/asset/ui/src/_buttons.scss 85 | &__submit { 86 | width: auto !important; 87 | } 88 | 89 | &__label, 90 | &_label, 91 | &__legend, 92 | &__fieldset .apos-form-field-optional { 93 | vertical-align: middle; 94 | display: inline-block; 95 | margin: 0 0 0.5rem; 96 | } 97 | &__fieldset { 98 | .my-form__label { 99 | margin: 0; 100 | } 101 | } 102 | .apos-form-input, 103 | .apos-form-fieldset { 104 | margin-bottom: 1rem; 105 | } 106 | .my-form__check-wrapper + .my-form__check-wrapper { 107 | margin-top: 0.5rem; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'apostrophe'; 2 | import dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | apostrophe({ 7 | root: import.meta, 8 | shortName: 'starter-kit-marketing', 9 | baseUrl: process.env.APOS_BASE_URL || 'http://localhost:3000', 10 | nestedModuleSubdirs: true, 11 | modules: { 12 | // Apostrophe module configuration 13 | // ******************************* 14 | // 15 | // NOTE: most configuration occurs in the respective modules' directories. 16 | // See modules/@apostrophecms/page/index.js for an example. 17 | // 18 | // Any modules that are not present by default in Apostrophe must at least 19 | // have a minimal configuration here to turn them on: `moduleName: {}` 20 | // *********************************************************************** 21 | // `className` options set custom CSS classes for Apostrophe core widgets. 22 | '@apostrophecms/rich-text-widget': { 23 | options: {} 24 | }, 25 | '@apostrophecms/image-widget': { 26 | options: { 27 | className: 'img-fluid' 28 | } 29 | }, 30 | '@apostrophecms/video-widget': { 31 | options: {} 32 | }, 33 | 34 | // The main form module 35 | '@apostrophecms/form': { 36 | options: { 37 | shortcut: 'a,f' 38 | } 39 | }, 40 | // The form widget module, allowing editors to add forms to content areas 41 | '@apostrophecms/form-widget': {}, 42 | // Form field widgets, used by the main form module to build forms. 43 | '@apostrophecms/form-text-field-widget': {}, 44 | '@apostrophecms/form-textarea-field-widget': {}, 45 | '@apostrophecms/form-select-field-widget': {}, 46 | '@apostrophecms/form-radio-field-widget': {}, 47 | '@apostrophecms/form-file-field-widget': {}, 48 | '@apostrophecms/form-checkboxes-field-widget': {}, 49 | '@apostrophecms/form-boolean-field-widget': {}, 50 | '@apostrophecms/form-conditional-widget': {}, 51 | 52 | '@apostrophecms/sitemap': { 53 | options: { 54 | excludeTypes: [ 'team-member', 'product' ] 55 | } 56 | }, 57 | '@apostrophecms/seo': {}, 58 | '@apostrophecms/open-graph': {}, 59 | 60 | // `asset` supports the project's webpack build for client-side assets. 61 | helper: {}, 62 | asset: {}, 63 | settings: {}, 64 | 65 | // The project's first custom page type. 66 | 'default-page': {}, 67 | 'content-widget-modules': { 68 | options: { 69 | ignoreNoCodeWarning: true 70 | } 71 | }, 72 | 'pieces-modules': { 73 | options: { 74 | ignoreNoCodeWarning: true 75 | } 76 | }, 77 | // Use Vite bundling for the project 78 | '@apostrophecms/vite': {} 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /modules/content-widget-modules/custom-form-widget/index.js: -------------------------------------------------------------------------------- 1 | import aosSchema from '../../../lib/aosSchema.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Custom Form', 7 | icon: 'form-icon', 8 | description: 'Display a an interactive form on your page', 9 | previewImage: 'svg' 10 | }, 11 | icons: { 12 | 'form-icon': 'FormDropdown' 13 | }, 14 | fields: { 15 | add: { 16 | layout: { 17 | type: 'select', 18 | def: 'background', 19 | choices: [ 20 | { 21 | label: 'Background', 22 | value: 'background' 23 | }, 24 | { 25 | label: 'Two column', 26 | value: 'column' 27 | } 28 | ] 29 | }, 30 | backgroundStyle: { 31 | type: 'select', 32 | label: 'Background style', 33 | def: 'image', 34 | choices: [ 35 | { 36 | label: 'Image', 37 | value: 'image' 38 | }, 39 | { 40 | label: 'Color', 41 | value: 'color' 42 | } 43 | ], 44 | if: { 45 | layout: 'background' 46 | } 47 | }, 48 | _backgroundImage: { 49 | type: 'relationship', 50 | withType: '@apostrophecms/image', 51 | label: 'Select an image', 52 | max: 1, 53 | if: { 54 | $or: [ 55 | { backgroundStyle: 'image' }, 56 | { layout: 'column' } 57 | ] 58 | 59 | } 60 | }, 61 | backgroundColor: { 62 | type: 'color', 63 | label: 'Pick a background color', 64 | if: { 65 | backgroundStyle: 'color' 66 | } 67 | }, 68 | fontColor: { 69 | type: 'select', 70 | label: 'Change font color', 71 | choices: [ 72 | { 73 | label: 'Primary', 74 | value: 'primary' 75 | }, 76 | { 77 | label: 'Secondary', 78 | value: 'secondary' 79 | }, 80 | { 81 | label: 'Tertiary', 82 | value: 'tertiary' 83 | }, 84 | { 85 | label: 'Black', 86 | value: 'black' 87 | }, 88 | { 89 | label: 'White', 90 | value: 'white' 91 | } 92 | ] 93 | }, 94 | form: { 95 | type: 'area', 96 | options: { 97 | max: 1, 98 | widgets: { 99 | '@apostrophecms/form': {} 100 | } 101 | } 102 | }, 103 | ...aosSchema 104 | } 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /modules/@apostrophecms/form/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | 3 | export default { 4 | options: { 5 | classPrefix: 'my-form', 6 | formWidgets: { 7 | '@apostrophecms/form-text-field': {}, 8 | '@apostrophecms/form-textarea-field': {}, 9 | '@apostrophecms/form-boolean-field': {}, 10 | '@apostrophecms/form-select-field': {}, 11 | '@apostrophecms/form-radio-field': {}, 12 | '@apostrophecms/form-checkboxes-field': {}, 13 | '@apostrophecms/form-conditional': {}, 14 | ...areaConfig.richText 15 | } 16 | }, 17 | fields: { 18 | add: { 19 | subscription: { 20 | type: 'boolean', 21 | label: 'Set as a subscription form', 22 | def: false 23 | }, 24 | emailSubscriptionField: { 25 | label: 'Which is your subscription email field?', 26 | help: 'aposForm:confEmailFieldHelp', 27 | type: 'string', 28 | required: true, 29 | if: { 30 | subscription: true 31 | } 32 | } 33 | }, 34 | group: { 35 | subscription: { 36 | label: 'Enable Subscriptions', 37 | fields: [ 'subscription', 'emailSubscriptionField' ] 38 | } 39 | } 40 | }, 41 | handlers(self) { 42 | return { 43 | submission: { 44 | async subscription(req, form, data) { 45 | if (form.subscription === false) { 46 | return; 47 | } 48 | // Test email field has valid email 49 | // Email validation (Regex reference: https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript) 50 | const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 51 | 52 | if ( 53 | data[form.emailSubscriptionField] && 54 | (typeof data[form.emailSubscriptionField] !== 'string' || 55 | !re.test(data[form.emailSubscriptionField])) 56 | ) { 57 | await self.apos.notify(req, 'aposForm:errorEmailConfirm', { 58 | type: 'warning', 59 | icon: 'alert-circle-icon', 60 | interpolate: { 61 | field: form.emailSubscriptionField 62 | } 63 | }); 64 | return null; 65 | } 66 | 67 | // Include subscription set up below 68 | try { 69 | self.apos.util.log('⚠️ You need to set up a custom subscription service here'); 70 | return null; 71 | } catch (err) { 72 | self.apos.util.error('⚠️ @apostrophecms/form submission email subscription error: ', err); 73 | 74 | return null; 75 | } 76 | } 77 | } 78 | }; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /modules/@apostrophecms/global/index.js: -------------------------------------------------------------------------------- 1 | import linkSchema from '../../../lib/linkSchema.js'; 2 | import buttonSchema from '../../../lib/buttonSchema.js'; 3 | 4 | export default { 5 | fields: { 6 | add: { 7 | logo: { 8 | label: 'Logo', 9 | type: 'area', 10 | options: { 11 | max: 1, 12 | widgets: { 13 | '@apostrophecms/image': {} 14 | } 15 | } 16 | }, 17 | title: { 18 | type: 'string', 19 | label: 'Website Title', 20 | required: true 21 | }, 22 | headerBtns: { 23 | label: 'Header Button/s', 24 | type: 'array', 25 | titleField: 'linkText', 26 | limit: 1, 27 | fields: { 28 | add: { 29 | ...buttonSchema.button 30 | } 31 | } 32 | }, 33 | headerNav: { 34 | label: 'Header Navigation Items', 35 | type: 'array', 36 | titleField: 'linkText', 37 | limit: 5, 38 | fields: { 39 | add: { 40 | ...linkSchema 41 | } 42 | } 43 | }, 44 | footerNav: { 45 | label: 'Footer Navigation Items', 46 | type: 'array', 47 | titleField: 'linkText', 48 | limit: 5, 49 | fields: { 50 | add: { 51 | ...linkSchema 52 | } 53 | } 54 | }, 55 | social: { 56 | label: 'Social Media Accounts', 57 | type: 'array', 58 | limit: 5, 59 | inline: true, 60 | fields: { 61 | add: { 62 | link: { 63 | type: 'url', 64 | label: 'Social link', 65 | required: true 66 | }, 67 | icon: { 68 | label: 'Icon', 69 | type: 'select', 70 | required: true, 71 | choices: [ 72 | { 73 | label: 'Instagram', 74 | value: 'instagram' 75 | }, 76 | { 77 | label: 'Facebook', 78 | value: 'facebook' 79 | }, 80 | { 81 | label: 'Twitter', 82 | value: 'twitter' 83 | }, 84 | { 85 | label: 'LinkedIn', 86 | value: 'linkedin' 87 | } 88 | ] 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | group: { 95 | brand: { 96 | label: 'Brand', 97 | fields: [ 'title', 'logo', 'social' ] 98 | }, 99 | navigations: { 100 | label: 'Navigations', 101 | fields: [ 'headerNav', 'footerNav', 'headerBtns' ] 102 | } 103 | } 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /modules/content-widget-modules/columns-widget/index.js: -------------------------------------------------------------------------------- 1 | import areaConfig from '../../../lib/area.js'; 2 | import aosSchema from '../../../lib/aosSchema.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Columns', 8 | icon: 'pillar', 9 | description: 'Create flexible layouts for your content.', 10 | previewImage: 'svg' 11 | }, 12 | icons: { 13 | 'view-column-icon': 'ViewColumn' 14 | }, 15 | fields: { 16 | add: { 17 | style: { 18 | type: 'select', 19 | label: 'Layout style', 20 | required: true, 21 | choices: [ 22 | { 23 | label: 'Full width', 24 | value: 'full', 25 | def: true 26 | }, 27 | { 28 | label: 'Contained width', 29 | value: 'contained' 30 | } 31 | ] 32 | }, 33 | cols: { 34 | type: 'select', 35 | label: 'Column configuration', 36 | required: true, 37 | choices: [ 38 | { 39 | label: 'Single column', 40 | value: 'single', 41 | def: true 42 | }, 43 | { 44 | label: '50% / 50%', 45 | value: 'double' 46 | }, 47 | { 48 | label: '33% / 33% / 33%', 49 | value: 'triple' 50 | }, 51 | { 52 | label: '25% / 25% / 25% / 25%', 53 | value: 'quadruple' 54 | } 55 | ] 56 | }, 57 | one: { 58 | type: 'area', 59 | contextual: true, 60 | options: { 61 | expanded: true, 62 | groups: { 63 | ...areaConfig.columnExpandedGroup 64 | } 65 | } 66 | }, 67 | two: { 68 | type: 'area', 69 | contextual: true, 70 | options: { 71 | expanded: true, 72 | groups: { 73 | ...areaConfig.columnExpandedGroup 74 | } 75 | }, 76 | if: { 77 | $or: [ 78 | { cols: 'double' }, 79 | { cols: 'triple' }, 80 | { cols: 'quadruple' } 81 | ] 82 | } 83 | }, 84 | three: { 85 | type: 'area', 86 | contextual: true, 87 | if: { 88 | $or: [ 89 | { cols: 'quadruple' }, 90 | { cols: 'triple' } 91 | ] 92 | }, 93 | options: { 94 | expanded: true, 95 | groups: { 96 | ...areaConfig.columnExpandedGroup 97 | } 98 | } 99 | }, 100 | four: { 101 | type: 'area', 102 | contextual: true, 103 | if: { 104 | cols: 'quadruple' 105 | }, 106 | options: { 107 | expanded: true, 108 | groups: { 109 | ...areaConfig.columnExpandedGroup 110 | } 111 | } 112 | }, 113 | ...aosSchema 114 | } 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /modules/content-widget-modules/map-widget/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import Map from 'ol/Map'; 2 | import View from 'ol/View'; 3 | import TileLayer from 'ol/layer/Tile'; 4 | import VectorLayer from 'ol/layer/Vector.js'; 5 | import XYZ from 'ol/source/XYZ'; 6 | import Feature from 'ol/Feature.js'; 7 | import Point from 'ol/geom/Point.js'; 8 | import VectorSource from 'ol/source/Vector.js'; 9 | import { fromLonLat } from 'ol/proj'; 10 | import { Icon, Style } from 'ol/style'; 11 | 12 | export default () => { 13 | apos.util.widgetPlayers.map = { 14 | selector: '[data-map]', 15 | player: function (el) { 16 | apos.util.onReady(() => { 17 | if (!el.querySelector('[data-map-target')) { 18 | return; 19 | } 20 | 21 | const mapEl = el.querySelector('[data-map-target'); 22 | // Define the latitude and longitude variables 23 | const latitude = mapEl.dataset.latitude; 24 | const longitude = mapEl.dataset.longitude; 25 | 26 | // Convert the latitude and longitude to the map's projection 27 | const coords = fromLonLat([ longitude, latitude ]); 28 | 29 | const map = new Map({ 30 | layers: [ 31 | new TileLayer({ 32 | source: new XYZ({ 33 | url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' 34 | }) 35 | }) 36 | ], 37 | view: new View({ 38 | center: coords, 39 | zoom: mapEl.dataset.mapzoomlevel 40 | }), 41 | target: mapEl.querySelector('.map') 42 | }); 43 | 44 | // Define the dynamic position variable 45 | let position = coords; 46 | 47 | // Update the position variable with new latitude and longitude values 48 | function updatePosition(lat, lng) { 49 | const newCoords = fromLonLat([ lng, lat ]); 50 | position = newCoords; 51 | } 52 | 53 | // Define the icon style 54 | const iconStyle = new Style({ 55 | image: new Icon({ 56 | src: apos.util.assetUrl('/modules/map-widget/map-icon.png'), 57 | scale: 0.05 58 | }) 59 | }); 60 | 61 | // Create a marker with the dynamic position variable and the icon style 62 | const marker = new Feature({ 63 | geometry: new Point(position) 64 | }); 65 | marker.setStyle(iconStyle); 66 | 67 | // Add the marker to a vector layer 68 | const vectorLayer = new VectorLayer({ 69 | source: new VectorSource({ 70 | features: [ marker ] 71 | }) 72 | }); 73 | 74 | // Add the vector layer to the map 75 | map.addLayer(vectorLayer); 76 | 77 | // Update the marker position whenever the position variable changes 78 | setInterval(function () { 79 | marker.getGeometry().setCoordinates(position); 80 | }, 1000); 81 | 82 | // Call the updatePosition function with new latitude and longitude values 83 | updatePosition(latitude, longitude); 84 | }); 85 | } 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /modules/pieces-modules/team-member-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 9 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 | 30 | -------------------------------------------------------------------------------- /modules/content-widget-modules/accordion-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /modules/pieces-modules/product-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 8 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 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_default-variables.scss: -------------------------------------------------------------------------------- 1 | // Base colours 2 | $black: #000 !default; 3 | $gray-100: #f8f9fa !default; 4 | $gray-200: #e9ecef !default; 5 | $gray-300: #dee2e6 !default; 6 | $gray-400: #ced4da !default; 7 | $gray-500: #adb5bd !default; 8 | $gray-600: #6c757d !default; 9 | $gray-700: #495057 !default; 10 | $gray-800: #343a40 !default; 11 | $gray-900: #212529 !default; 12 | $white: #fff !default; 13 | $red: #dc3545 !default; 14 | $yellow: #ffc107 !default; 15 | $cyan: #0dcaf0 !default; 16 | $green: #198754 !default; 17 | 18 | // Theme colour variables 19 | $primary: $black !default; 20 | $secondary: $white !default; 21 | $tertiary: $gray-600 !default; 22 | $success: $green !default; 23 | $info: $cyan !default; 24 | $warning: $yellow !default; 25 | $danger: $red !default; 26 | 27 | // Themes color map 28 | $theme-colors: ( 29 | "primary": $primary, 30 | "secondary": $secondary, 31 | "tertiary": $tertiary, 32 | "success": $success, 33 | "info": $info, 34 | "warning": $warning, 35 | "danger": $danger, 36 | ) !default; 37 | 38 | // Gradients variables 39 | $gradient-dark: linear-gradient(180deg, $primary 0%, $secondary 100%); 40 | $gradient-light: linear-gradient(180deg, $white 0%, $primary 100%); 41 | 42 | // Box shadows 43 | $box-shadow: 0px 12px 16px -4px rgba(0, 0, 0, 0.08), 0px 4px 6px -2px rgba(0, 0, 0, 0.03) !default; 44 | $box-shadow-sm: 0px 4px 8px -2px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.06) !default; 45 | $box-shadow-lg: 0px 20px 24px -4px rgba(0, 0, 0, 0.08), 0px 8px 8px -4px rgba(0, 0, 0, 0.03) !default; 46 | 47 | // Spacer variables map 48 | $spacer: 1rem !default; 49 | $spacers: ( 50 | 0: 0, 51 | 1: $spacer * 0.25, 52 | 2: $spacer * 0.5, 53 | 3: $spacer, 54 | 4: $spacer * 1.5, 55 | 5: $spacer * 3, 56 | ) !default; 57 | 58 | // Body 59 | $body-color: $primary !default; 60 | $body-bg: $white !default; 61 | 62 | // Link 63 | $link-color: $primary !default; 64 | $link-decoration: none !default; 65 | $link-hover-decoration: none !default; 66 | 67 | // Font settings 68 | $font-family-sans-serif: "Lato", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", 69 | "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; 70 | $font-family-monospace: "Abril Fatface", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", 71 | monospace !default; 72 | 73 | $body-font-family: $font-family-sans-serif !default; 74 | 75 | // $font-size-base affects the font size of text 76 | $font-size-base: 1rem !default; // Assumes the browser default, typically `16px` 77 | $font-size-sm: $font-size-base * 0.875 !default; 78 | $font-size-lg: $font-size-base * 1.25 !default; 79 | 80 | $line-height-base: 1.5 !default; 81 | $line-height-sm: 1.25 !default; 82 | $line-height-lg: 2 !default; 83 | 84 | $h1-font-size: $font-size-base * 3 !default; 85 | $h2-font-size: $font-size-base * 2.5 !default; 86 | $h3-font-size: $font-size-base * 2 !default; 87 | $h4-font-size: $font-size-base * 1.5 !default; 88 | $h5-font-size: $font-size-base * 1.25 !default; 89 | $h6-font-size: $font-size-base !default; 90 | 91 | $headings-margin-bottom: $spacer * 0.5 !default; 92 | $headings-font-family: $font-family-monospace !default; 93 | $headings-font-style: null !default; 94 | $headings-font-weight: null !default; 95 | $headings-line-height: 1.2 !default; 96 | $headings-color: inherit !default; 97 | 98 | $lead-font-size: $font-size-base * 1.25 !default; 99 | $lead-font-weight: 300 !default; 100 | 101 | $small-font-size: 0.875em !default; 102 | 103 | $border-radius: 5px !default; 104 | 105 | // Easing 106 | $animate-easing: 250ms ease 0s !default; 107 | -------------------------------------------------------------------------------- /modules/content-widget-modules/pricing-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 6 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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Marketing Starter Kit 2 | A starter kit for building marketing-themed websites. Add hero blocks, feature lists, multi-column layouts, pricing, team members, and more with this rich starter kit for Apostrophe. 3 | 4 | ## Demo 5 | 7 | 8 | ## Get started 9 | ### Method 1 10 | 1. Install the project using the [Apostrophe CLI tool]() using: 11 | ``` sh 12 | apos create my-project --starter=marketing 13 | ``` 14 | 2. Start the site using: 15 | ``` sh 16 | npm run dev 17 | ``` 18 | 3. Login using the credentials created during project creation - note that the username is `admin` 19 | 20 | ### Method 2 21 | 1. Clone the template locally. You can use the GitHub desktop app or the CLI 22 | 2. Install dependencies using: `npm install` 23 | 3. Create Admin user 24 | ```bash 25 | node app @apostrophecms/user:add admin admin 26 | ``` 27 | 4. Start the site using: 28 | ``` sh 29 | npm run dev 30 | ``` 31 | 5. Login using the credentials just created - note that the username is `admin` 32 | 33 | ## Theming 34 | 35 | Colors, fonts, and other aesthetic variables are set in `modules/asset/ui/src/scss/_theme.scss`. These can be overridden by making changes directly in the `_theme.scss` file. 36 | 37 | The marketing starter kit uses fonts provided by Google. These are imported into the primary layout file in `views/layout.html:19` 38 | 39 | ## Maps and Geocoding 40 | This project comes with a map widget that geocodes addresses to points on the map. Geocoding is provided by [`node-geocoder`](https://www.npmjs.com/package/node-geocoder). You must configure a geocoding service provider and API key in order to use it. [See a list of geocoding service providers](https://github.com/nchaulet/node-geocoder#geocoder-providers-in-alphabetical-order). 41 | 42 | To configure your project's geocoding service provider see `modules/content-widget-modules/modules.js` 43 | 44 | ```js 45 | 'map-widget': { 46 | options: { 47 | geocoderSettings: { 48 | // For a full list of the node-geocoder npm package options please view the modules documentation - https://www.npmjs.com/package/node-geocoder 49 | // Requred 50 | provider: 'mapbox', 51 | 52 | // Optional depending on the providers 53 | apiKey: process.env.GEOCODER_API_KEY, // for Mapquest, OpenCage, Google Premier 54 | formatter: null, // 'gpx', 'string', ... 55 | minConfidence: 0.5, 56 | limit: 1 57 | } 58 | } 59 | }, 60 | 61 | ``` 62 | 63 | More configuration details for `node-geocoder` [noted here](#node-geocoder-config). 64 | 65 | ## Making use of the .env set up 66 | 67 | There is a `.env.sample` file located within the top-level directory. To set up the `.env` file: 68 | 69 | - Either rename this file to `.env` or, 70 | - Add a new file named `.env` and copy the contents from `.env.sample` 71 | 72 | Next, these variables need to have values set: 73 | 74 | GEOCODER_API_KEY= must be the value of the geocoder provider 75 | 76 | ## Running the project 77 | 78 | Run `npm run dev` to build the Apostrophe UI and start the site up. Remember, this is during alpha development, so we're all in "dev mode." 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. 79 | 80 | ## Making it your own 81 | 82 | This starter kit is designed so you can install and start running it right away. If you are starting a project that will go into production one day, there are a few things you should be sure to check: 83 | 84 | - [ ] **Update the shortname.** This step can be skipped if the kit was installed using the CLI tool as it does this for you. The `shortname` option in `app.js` is used for the database name (unless another is given in the `@apostrophecms/db` module). You should change this to an appropriate project name before you start adding any users or content you would like to keep. 85 | - [ ] **Update the Express.js session secret.** The secret is set to `undefined` initially in the `modules/@apostrophecms/express/index.js` file. You should update this to a unique string. 86 | - [ ] **Decide if you want hot reloading on.** This starter kit uses nodemon to restart the app when files are changed. In `modules/@apostrophecms/asset/index.js` there is an option enabled to refresh the browser on restart. If you like this, do nothing. If you don't, remove the option or set it to `false`. The option has no effect when the app is in production. 87 | 88 | ## ApostropheCMS module extensions 89 | The below ApostropheCMS extensions have been included within this theme's main ApostropheCMS `app.js` file. 90 | 91 | - [ ] **@apostrophecms/form.** Allows editors to create their own forms for gathering and delivering user input. 92 | (View extension - https://apostrophecms.com/extensions/form-builder-2) 93 | - [ ] **@apostrophecms/open-graph.** Provides a way to edit metadata for Facebook's Open Graph standard. 94 | (View extension - https://apostrophecms.com/extensions/open-graph-tools-3) 95 | - [ ] **@apostrophecms/seo.** SEO configuration for ApostropheCMS. Adds useful meta fields to all pages and pieces. 96 | (View extension - https://apostrophecms.com/extensions/seo-tools-3) 97 | - [ ] **@apostrophecms/sitemap.** Generates XML and plaintext sitemaps for sites in Apostrophe. 98 | (View extension - https://apostrophecms.com/extensions/site-maps) 99 | 100 | ## Themes imported NPM Packages 101 | - [ ] **dotenv.** "dotenv" is a npm package that loads environment variables from a .env file. It's useful for securely storing sensitive information like API keys, passwords, and other configuration settings. 102 | 103 | The import statement should only be set within the main ApostropheCMS `app.js` file: 104 | ```js 105 | import dotenv from 'dotenv'; 106 | 107 | dotenv.config(); 108 | ``` 109 | You can then reference environment variables within the server js files for Apostrophe, 110 | 111 | ```js 112 | const port = process.env.PORT || 3000; 113 | const dbHost = process.env.DB_HOST || 'localhost'; 114 | const dbUser = process.env.DB_USER; 115 | const dbPassword = process.env.DB_PASSWORD; 116 | ``` 117 | - [ ] **rfs (Responsive Font Sizing).** The "rfs" package is a node.js module that provides a simple way to create responsive and fluid typography in CSS. It allows you to define font sizes using a unitless value (usually "rem") and automatically adjust them based on the viewport or container width. 118 | 119 | Mainly used within `modules/asset/ui/src/_typography.scss` file for consistent rem based font sizing. Usage applied to headings and paragraphs. 120 | 121 | E.g ``` @include font-size($h1-font-size)``` 122 | where $h1-font-size is a rem value determined by a base value. 123 | 124 | in this theme a default of 16px: ```$h1-font-size: $font-size-base * 3; // 48px``` 125 | 126 | - [ ] **swiper (Slideshows).** The "swiper" package is a JavaScript library for creating responsive and touch-enabled sliders, carousels, and other interactive content on the web. Its usage within this theme is located at modules/content-widget-modules/image-gallery-widget 127 | 128 | You can import it in your JavaScript file using the import statement: 129 | ```js 130 | import Swiper from 'swiper/bundle'; 131 | ``` 132 | Then, you can create a new instance of the "swiper" class and pass in a configuration object with your desired options. For example: 133 | ```js 134 | const mySwiper = new Swiper('.swiper-container', { 135 | // Optional parameters 136 | direction: 'horizontal', 137 | loop: true, 138 | 139 | // Navigation arrows 140 | navigation: { 141 | nextEl: '.swiper-button-next', 142 | prevEl: '.swiper-button-prev', 143 | }, 144 | 145 | // And more... 146 | }); 147 | ``` 148 | - [ ] **photoswipe (Lightbox)** "PhotoSwipe Lightbox" is a plugin for the popular JavaScript library "PhotoSwipe" that simplifies the process of creating responsive image galleries with lightbox functionality. It offers an easy-to-use API for adding lightboxes to your images and is highly customizable and flexible. 149 | 150 | Its usage within this theme is located at `modules/content-widget-modules/image-gallery-widget`, and is applied in conjunction with the swiper npm package. 151 | 152 | You can import it in your JavaScript file using the import statement: 153 | 154 | ```js 155 | import PhotoSwipeLightbox from 'photoswipe/lightbox'; 156 | import PhotoSwipe from 'photoswipe'; 157 | ``` 158 | Then, you can create a new instance of the "Photoswiper lightbox" class and pass in a configuration object with your desired options. For example: 159 | ```js 160 | // Photoswiper lightbox and gallery 161 | const photoSwipeOptions = { 162 | gallery: '#imageGallery', 163 | pswpModule: PhotoSwipe, 164 | // set background opacity 165 | bgOpacity: 1, 166 | showHideOpacity: true, 167 | children: 'a', 168 | loop: true, 169 | showHideAnimationType: 'fade', /* options: fade, zoom, none */ 170 | 171 | /* Click on image moves to the next slide */ 172 | imageClickAction: 'next', 173 | tapAction: 'next', 174 | 175 | /* ## Hiding a specific UI element ## */ 176 | zoom: false, 177 | close: true, 178 | counter: true, 179 | arrowKeys: true 180 | }; 181 | 182 | const lightbox = new PhotoSwipeLightbox(photoSwipeOptions); 183 | 184 | lightbox.init(); 185 | ``` 186 | 187 | - [ ] [**Node-geocoder**](#node-geocoder-config) is an npm package that simplifies geocoding and reverse-geocoding in Node.js. It supports various geocoding services, allowing developers to convert addresses to coordinates and vice versa with ease. 188 | 189 | See a basic example of this package below: 190 | ```js 191 | import NodeGeocoder from 'node-geocoder'; 192 | ``` 193 | Then, you can create a new instance of the "Map" class and add one or more layers to it. For example: 194 | ```js 195 | const options = { 196 | // For a full list of the node-geocoder npm package options please view the modules documentation - https://www.npmjs.com/package/node-geocoder 197 | // Requred 198 | provider: 'mapbox', 199 | 200 | // Optional depending on the providers 201 | apiKey: 'include provider apikey', // for Mapquest, OpenCage, Google Premier 202 | formatter: null, // 'gpx', 'string', ... 203 | minConfidence: 0.5, 204 | limit: 1 205 | }; 206 | const geocoder = NodeGeocoder(options); 207 | const geocoderAddress = await geocoder.geocode(data.widget.address); 208 | ``` 209 | 210 | - [ ] **ol (Maps)** The "ol" package is a JavaScript library for creating interactive maps on the web. Its usage within this theme is located at `modules/content-widget-modules/map-widget` 211 | 212 | See a basic example of this package below: 213 | ```js 214 | import { Map, View } from 'ol'; 215 | import TileLayer from 'ol/layer/Tile'; 216 | import OSM from 'ol/source/OSM'; 217 | ``` 218 | Then, you can create a new instance of the "Map" class and add one or more layers to it. For example: 219 | ```js 220 | const map = new Map({ 221 | target: 'map', 222 | layers: [ 223 | new TileLayer({ 224 | source: new OSM(), 225 | }), 226 | ], 227 | view: new View({ 228 | center: [0, 0], 229 | zoom: 2, 230 | }), 231 | }); 232 | ``` 233 | 234 | - [ ] **AOS (Animations)** Animate On Scroll (AOS) library allows you to animate elements as you scroll down and up. If you scroll back to the top, elements will animate to their previous state and are ready to animate again if you scroll down. 235 | 236 | You can import it in your JavaScript file (`modules/asset/ui/src/index.js`) using the import statement: 237 | 238 | ```js 239 | import AOS from 'aos'; 240 | ``` 241 | Then, initialize AOS. For example: 242 | 243 | ```js 244 | AOS.init(); 245 | ``` 246 | 247 | For usage within this theme, Create an instance of 'aosSchema' in your widget and add it as field. For example: 248 | 249 | ```js 250 | import aosSchema from '../../../lib/aosSchema.js'; 251 | 252 | add: { 253 | ...aosSchema 254 | } 255 | ``` 256 | 257 | ## Advanced Apostrophe configuration 258 | 259 | Right now, [all the juicy info is in the ApostropheCMS docs](https://docs.apostrophecms.org), so head over there and start reading! This starter kit project is a fun introduction to the UI, but you'll want to know more to really try it out. 260 | 261 | ## Thanks 262 | 263 | ![Thanks to our partner Hellocomputer](https://static.apostrophecms.com/apostrophecms/starter-kit-hellocomputer/images/thanks.png) 264 | 265 | ### About Apostrophe 266 | ApostropheCMS is a powerful content management system designed for developers who want to build dynamic and robust websites and applications. The intuitive admin interface provides a user-friendly editing experience, empowering content editors to create and update content effortlessly. As a developer, you have complete control over the content structure, defining custom schemas and creating relationships between different content types. 267 | 268 | With its developer-friendly architecture, ApostropheCMS provides the flexibility and extensibility you need to build a simple blog, an enterprise-level website, or a complex web application. 269 | 270 | 271 | ## Apostrophe starter kits 272 | This Starter Kit was originally crafted by Hellocomputer in partnership with Apostrophe. To learn more about Hellocomputer visit [hellocomputer.com](https://www.hellocomputer.com/). As with any open-source project, this too is a community collaboration. We welcome feedback, tickets, bug fixes, and improvements. 273 | 274 | Want to upgrade your Starter Kit to Apostrophe Pro? [Get started here](https://apostrophecms.com/pro). 275 | 276 | Interested in publishing an Apostrophe Starter Kit and becoming a featured Apostrophe Partner? [Submit a Starter Kit](https://apostrophecms.com/starter-kits). 277 | --------------------------------------------------------------------------------