├── test-deploy
├── public
├── svgs
│ └── .gitkeep
└── robots.txt
├── .stylelintignore
├── sites
├── 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
│ │ │ ├── views
│ │ │ │ ├── widget.html
│ │ │ │ └── map.html
│ │ │ ├── public
│ │ │ │ ├── map-icon.png
│ │ │ │ └── preview.jpg
│ │ │ └── index.js
│ │ ├── button-widget
│ │ │ ├── views
│ │ │ │ └── widget.html
│ │ │ ├── ui
│ │ │ │ └── src
│ │ │ │ │ └── index.scss
│ │ │ └── index.js
│ │ ├── image-widget
│ │ │ ├── public
│ │ │ │ └── preview.jpg
│ │ │ ├── index.js
│ │ │ └── views
│ │ │ │ └── widget.html
│ │ ├── side-by-side-widget
│ │ │ ├── public
│ │ │ │ └── preview.jpg
│ │ │ └── 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
│ │ ├── 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.scss
│ │ │ │ │ └── index.js
│ │ │ ├── index.js
│ │ │ └── public
│ │ │ │ └── preview.svg
│ │ ├── pricing-widget
│ │ │ ├── views
│ │ │ │ └── widget.html
│ │ │ ├── ui
│ │ │ │ └── src
│ │ │ │ │ └── index.scss
│ │ │ ├── index.js
│ │ │ └── public
│ │ │ │ └── preview.svg
│ │ ├── modules.js
│ │ └── custom-form-widget
│ │ │ ├── ui
│ │ │ └── src
│ │ │ │ └── index.scss
│ │ │ ├── views
│ │ │ └── widget.html
│ │ │ ├── public
│ │ │ └── preview.svg
│ │ │ └── index.js
│ ├── @apostrophecms
│ │ ├── layout-widget
│ │ │ ├── index.js
│ │ │ └── public
│ │ │ │ └── preview.svg
│ │ ├── asset
│ │ │ └── index.js
│ │ ├── home-page
│ │ │ ├── views
│ │ │ │ └── page.html
│ │ │ └── index.js
│ │ ├── page
│ │ │ ├── views
│ │ │ │ └── notFound.html
│ │ │ └── index.js
│ │ ├── layout-column-widget
│ │ │ └── index.js
│ │ ├── admin-bar
│ │ │ └── index.js
│ │ ├── global
│ │ │ ├── ui
│ │ │ │ └── apos
│ │ │ │ │ └── components
│ │ │ │ │ └── AssemblyInputFontFamily.vue
│ │ │ └── index.js
│ │ └── form
│ │ │ └── index.js
│ ├── theme-default
│ │ ├── ui
│ │ │ └── src
│ │ │ │ ├── index.scss
│ │ │ │ └── index.js
│ │ └── 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
│ ├── websocket
│ │ └── index.js
│ ├── helper
│ │ └── index.js
│ └── @apostrophecms-pro
│ │ └── palette
│ │ ├── index.js
│ │ └── lib
│ │ ├── choices.js
│ │ └── configs
│ │ ├── 5footer.js
│ │ ├── 4header.js
│ │ ├── 2type.js
│ │ ├── 1base.js
│ │ └── 3buttons.js
├── public
│ └── images
│ │ ├── logo.png
│ │ ├── menu.svg
│ │ ├── social-icons
│ │ ├── facebook.svg
│ │ ├── twitter.svg
│ │ ├── linkedin.svg
│ │ ├── meta.svg
│ │ └── instagram.svg
│ │ └── checked-icon.svg
├── lib
│ ├── theme-default.js
│ ├── buttonSchema.js
│ ├── linkSchema.js
│ ├── area.js
│ └── aosSchema.js
├── views
│ ├── link.html
│ ├── button-arrows.html
│ ├── button.html
│ ├── fragments
│ │ ├── header.html
│ │ └── footer.html
│ ├── ui.html
│ └── layout.html
└── index.js
├── dashboard
├── modules
│ ├── asset
│ │ ├── ui
│ │ │ └── src
│ │ │ │ ├── scss
│ │ │ │ ├── settings
│ │ │ │ │ ├── _borders.scss
│ │ │ │ │ ├── _icons.scss
│ │ │ │ │ ├── _animation.scss
│ │ │ │ │ ├── _layout.scss
│ │ │ │ │ ├── _zindex.scss
│ │ │ │ │ ├── _colors.scss
│ │ │ │ │ └── _fonts.scss
│ │ │ │ ├── components
│ │ │ │ │ ├── _site-list.scss
│ │ │ │ │ ├── _admin.scss
│ │ │ │ │ ├── _site-menu.scss
│ │ │ │ │ ├── _navigation.scss
│ │ │ │ │ └── _card.scss
│ │ │ │ ├── objects
│ │ │ │ │ ├── _icon.scss
│ │ │ │ │ └── _containers.scss
│ │ │ │ ├── utilities
│ │ │ │ │ ├── _media.scss
│ │ │ │ │ └── _display.scss
│ │ │ │ └── tools
│ │ │ │ │ ├── _transitions.scss
│ │ │ │ │ ├── _clearfix.scss
│ │ │ │ │ ├── _icons.scss
│ │ │ │ │ ├── _functions.scss
│ │ │ │ │ ├── _fonts.scss
│ │ │ │ │ └── _layout.scss
│ │ │ │ ├── index.js
│ │ │ │ └── index.scss
│ │ └── index.js
│ ├── @apostrophecms
│ │ ├── admin-bar
│ │ │ └── index.js
│ │ ├── express
│ │ │ └── index.js
│ │ ├── asset
│ │ │ └── index.js
│ │ ├── page
│ │ │ ├── views
│ │ │ │ └── notFound.html
│ │ │ └── index.js
│ │ ├── file
│ │ │ └── index.js
│ │ ├── file-tag
│ │ │ └── index.js
│ │ ├── image-tag
│ │ │ └── index.js
│ │ └── template
│ │ │ └── views
│ │ │ └── outerLayout.html
│ ├── helper
│ │ └── index.js
│ └── site
│ │ └── index.js
├── locales
│ └── en.json
├── index.js
└── views
│ └── layout.html
├── .dockerignore
├── postcss.config.js
├── eslint.config.js
├── deployment
├── before-deploying
└── rsync_exclude.txt
├── .editorconfig
├── themes.js
├── .stylelintrc
├── LICENSE.md
├── domains.js
├── scripts
├── for-each-theme
└── wait-for-port
├── nodemon.json
├── .gitignore
├── telemetry.js
├── app.js
├── package.json
├── Dockerfile
└── self-hosting.md
/test-deploy:
--------------------------------------------------------------------------------
1 | 1
2 |
--------------------------------------------------------------------------------
/public/svgs/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | apos-build
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss/_functions.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_borders.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/components/_site-list.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /sites/public/uploads
2 | /dashboard/public/uploads
3 | /node_modules
4 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_icons.scss:
--------------------------------------------------------------------------------
1 | $icons: (
2 | icon: (2, 2)
3 | );
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/dashboard/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Server error, please try again.": "Server error, please try again."
3 | }
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | @import "./theme";
2 | @import "./default-variables";
3 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | suppressWarning: true
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_animation.scss:
--------------------------------------------------------------------------------
1 | $duration-short: 0.25s;
2 | $duration-long: 0.6s;
3 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/objects/_icon.scss:
--------------------------------------------------------------------------------
1 | .o-icon { display: block; }
2 |
3 | @include generate-icons ();
4 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/admin-bar/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | pageTree: false
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/map-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | @import "ol/ol.css";
2 |
3 | .map {
4 | height: 500px;
5 | }
6 |
--------------------------------------------------------------------------------
/sites/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/public/images/logo.png
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms/layout-widget/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | defaultSpan: 4,
4 | previewImage: 'svg',
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/express/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | session: {
4 | secret: 'CHANGEME'
5 | }
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/sites/lib/theme-default.js:
--------------------------------------------------------------------------------
1 | export default function(site, config) {
2 | config.modules = {
3 | ...config.modules,
4 | 'theme-default': {}
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/utilities/_media.scss:
--------------------------------------------------------------------------------
1 | .u-img-fluid {
2 | max-width: 100%;
3 | height: auto;
4 | }
5 |
6 | .u-img-full {
7 | width: 100%;
8 | }
9 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | // eslint-disable-next-line no-console
3 | console.log('Dashboard project level javascript');
4 | };
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | # Sitemap: http://EXAMPLE.com/sitemap.xml
3 | # TODO: Remove following line once the site goes live.
4 | Disallow: /
5 | Disallow: /modules/
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/asset/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | // When not in production, refresh the page on restart
4 | refreshOnRestart: true
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/sites/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) %}
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/page/views/notFound.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 |
3 | {% block title %}404 - Page Not Found{% endblock %}
4 | {% block main %}404 - Page Not Found{% endblock %}
5 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/map-widget/views/widget.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/tools/_transitions.scss:
--------------------------------------------------------------------------------
1 | @mixin transition($property, $duration: $duration-short, $easing: ease-out, $delay: 0s) {
2 | transition: $property $duration $easing $delay;
3 | }
4 |
--------------------------------------------------------------------------------
/sites/modules/theme-default/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | // Styles for this theme
2 |
3 | // If you need to share styles across themes, just @import them here
4 |
5 | .o-widget--image {
6 | max-width: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/deployment/before-deploying:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This project requires an asset build. Assembly will
4 | # run this script at the appropriate step in the deployment
5 | # process.
6 |
7 | npm run build || exit 1
8 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/image-widget/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/modules/content-widget-modules/image-widget/public/preview.jpg
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/map-widget/public/map-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/modules/content-widget-modules/map-widget/public/map-icon.png
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/map-widget/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/modules/content-widget-modules/map-widget/public/preview.jpg
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/file/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | quickCreate: false
4 | },
5 | methods(self) {
6 | return {
7 | addToAdminBar() {}
8 | };
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/file-tag/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | quickCreate: false
4 | },
5 | methods(self) {
6 | return {
7 | addToAdminBar() {}
8 | };
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/image-tag/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | quickCreate: false
4 | },
5 | methods(self) {
6 | return {
7 | addToAdminBar() {}
8 | };
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/side-by-side-widget/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/modules/content-widget-modules/side-by-side-widget/public/preview.jpg
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/tools/_clearfix.scss:
--------------------------------------------------------------------------------
1 | .t-clearfix::after, .t-clearfix::before {
2 | visibility: hidden;
3 | display: block;
4 | height: 0;
5 | font-size: $font-0;
6 | content: ' ';
7 | clear: both;
8 | }
9 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/index.js:
--------------------------------------------------------------------------------
1 | import { navButton } from './js/nav-buttons.js';
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 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/call-to-action-widget/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/modules/content-widget-modules/call-to-action-widget/public/preview.jpg
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/image-gallery-widget/public/preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apostrophecms/starter-kit-assembly-hospitality/main/sites/modules/content-widget-modules/image-gallery-widget/public/preview.jpg
--------------------------------------------------------------------------------
/dashboard/modules/helper/index.js:
--------------------------------------------------------------------------------
1 | const widgets = {
2 | gallery: {}
3 | };
4 |
5 | export default {
6 | options: {
7 | alias: 'helpers'
8 | },
9 | helpers(self, options) {
10 | return {
11 | widgets
12 | };
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/themes.js:
--------------------------------------------------------------------------------
1 | // Maintained in one place so we don't forget to add them to the
2 | // list-themes task, which apostrophe cloud needs to minify assets
3 | // for each theme
4 |
5 | export default [
6 | {
7 | value: 'default',
8 | label: 'Default'
9 | }
10 | ];
11 |
--------------------------------------------------------------------------------
/sites/modules/asset/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | handlers(self) {
3 | return {
4 | '@apostrophecms/page:beforeSend': {
5 | webpack(req) {
6 | req.data.isDev = (process.env.NODE_ENV !== 'production');
7 | }
8 | }
9 | };
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/components/_admin.scss:
--------------------------------------------------------------------------------
1 | .apos-ui .apos-button--global {
2 | background-image: linear-gradient(36deg, #4950f6 12%, #578beb 94%);
3 | }
4 |
5 | .apos-ui .apos-button:active, .apos-ui .apos-button:focus {
6 | box-shadow: 0 0 13px 4px rgb(85 130 236 / 40%);
7 | }
8 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_layout.scss:
--------------------------------------------------------------------------------
1 | // Spacing:
2 | $spacing-unit: 1rem;
3 |
4 | $spacing-gutter: ($spacing-unit * 2);
5 |
6 | $blocks: (
7 | tiny: ($spacing-unit / 2)
8 | small: ($spacing-unit * 2),
9 | medium: ($spacing-unit * 4),
10 | large: ($spacing-unit * 6)
11 | );
12 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/utilities/_display.scss:
--------------------------------------------------------------------------------
1 | .u-sr-only {
2 | position: absolute;
3 | left: -999rem;
4 |
5 | &--focusable:focus,
6 | &--focusable:active {
7 | z-index: zindex(modal);
8 | left: 0;
9 | }
10 | }
11 |
12 | .u-no-scroll {
13 | overflow: hidden;
14 | }
15 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/tools/_icons.scss:
--------------------------------------------------------------------------------
1 | @mixin generate-icons () {
2 | @each $key, $value in $icons {
3 | .o-icon--#{$key} {
4 | $width: nth($value, 1);
5 | $height: nth($value, 2);
6 |
7 | width: #{$width}rem;
8 | height: #{$height}rem;
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-apostrophe",
3 | "rules": {
4 | "scss/selector-nest-combinators": null,
5 | "at-rule-disallowed-list": ["extend"]
6 | },
7 | "overrides": [
8 | {
9 | "files": [ "**/*.scss" ],
10 | "customSyntax": "postcss-scss"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms/asset/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | // When not in production, refresh the page on restart
4 | refreshOnRestart: true
5 | },
6 | methods(self) {
7 | return {
8 | getNamespace() {
9 | return self.apos.options.theme;
10 | }
11 | };
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/sites/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 |
8 | &--alignment-center {
9 | justify-content: center;
10 | }
11 |
12 | &--alignment-right {
13 | justify-content: flex-end;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_zindex.scss:
--------------------------------------------------------------------------------
1 | $z-index: (
2 | under: -1,
3 | default: 0,
4 | float: 1,
5 | nav: 2,
6 | modal: 3
7 | );
8 |
9 | @function zindex($key) {
10 | @if map-has-key($z-index, $key) {
11 | @return map-get($z-index, $key);
12 | }
13 |
14 | @warn 'Unknown `#{$key}` in $z-index.';
15 |
16 | @return null;
17 | }
18 |
--------------------------------------------------------------------------------
/sites/public/images/menu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020, 2021 Apostrophe Technologies. Licensed to enterprise customers of Apostrophe Technologies according to the terms of their individual agreements. This code is intended as a starting point for your own work and license holders may modify it freely for their own purposes related to operating a project based on the Apostrophe Assembly and Apostrophe multisite technologies.
2 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | @import 'scss/settings/_fonts';
2 | @import 'scss/settings/_colors';
3 | @import 'scss/objects/_containers';
4 | @import 'scss/tools/_clearfix';
5 | @import 'scss/components/_admin';
6 | @import 'scss/components/_site-list';
7 | @import 'scss/components/_site-menu';
8 | @import 'scss/components/_navigation';
9 | @import 'scss/components/_card';
10 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/objects/_containers.scss:
--------------------------------------------------------------------------------
1 | .o-container {
2 | margin-right: auto;
3 | margin-left: auto;
4 | padding: 2rem 0;
5 | max-width: 1180px;
6 | }
7 |
8 | .c-site-index__background {
9 | position: fixed;
10 | inset: 0;
11 | display: flex;
12 | width: 100%;
13 | height: 100%;
14 | user-select: none;
15 | background-color: $white;
16 | }
17 |
--------------------------------------------------------------------------------
/domains.js:
--------------------------------------------------------------------------------
1 | export default {
2 | local: 'localhost:3000',
3 | // Should be a real registered domain
4 | // or subdomain with a DNS wildcard pointing to the cloud
5 | staging: 'a3-assembly-staging.apostrophecms.com',
6 | // Should be a real registered domain
7 | // or subdomain with a DNS wildcard pointing to the cloud
8 | prod: 'a3-assembly-demo.apostrophecms.com'
9 | };
10 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/page/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods(self, options) {
3 | return {
4 | async serveNotFound(req) {
5 | if (!req.user) {
6 | req.redirect = '/login';
7 | return;
8 | }
9 | if (self.isFound(req)) {
10 | return;
11 | }
12 | req.redirect = '/login';
13 | }
14 | };
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/public/images/social-icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sites/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 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/sites/modules/websocket/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods(self) {
3 | return {
4 | connected(ws, req) {
5 | ws.on('message', m => {
6 | console.log(`message received: ${m}`);
7 | ws.send('I am the websocket server and now I will close the connection');
8 | ws.close();
9 | });
10 | ws.on('close', () => {
11 | console.log('websocket closed');
12 | });
13 | }
14 | };
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/deployment/rsync_exclude.txt:
--------------------------------------------------------------------------------
1 | # List files and folders that shouldn't be deployed (such as data folders
2 | # and runtime status files) here. Prefix things properly with / so we don't
3 | # have any effect on unrelated subdirectories of node_modules
4 |
5 | /data
6 | /public/uploads
7 | /public/apos-minified
8 | .git
9 | .gitignore
10 | # We DO deploy node_modules to the apostrophe cloud
11 | # We DO deploy release-id to the apostrophe cloud (from the worker
12 | # that generates it)
13 |
--------------------------------------------------------------------------------
/dashboard/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | root: import.meta,
3 | privateDashboards: true,
4 | modules: {
5 | '@apostrophecms/uploadfs': {
6 | options: {
7 | uploadfs: {
8 | disabledFileKey: 'CHANGEME'
9 | }
10 | }
11 | },
12 | '@apostrophecms-pro/multisite-dashboard': {},
13 | helper: {},
14 | site: {},
15 | 'site-page': {},
16 | asset: {},
17 | // Use Vite bundler
18 | '@apostrophecms/vite': {}
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/sites/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 %}
--------------------------------------------------------------------------------
/dashboard/views/layout.html:
--------------------------------------------------------------------------------
1 | {% extends data.outerLayout %}
2 |
3 | {% block startHead %}
4 |
5 | {% endblock %}
6 |
7 | {% block title %}
8 | {% if data.piece %}
9 | {{ data.piece.title }}
10 | {% elseif data.page %}
11 | {{ data.page.title }}
12 | {% else %}
13 | {{ 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.') }}
14 | {% endif %}
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/scripts/for-each-theme:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { spawnSync as spawn } from 'child_process';
4 | import themes from '../themes.js';
5 |
6 | for (const theme of themes) {
7 | const args = [ 'app', ...process.argv.slice(2), '--temporary-site', `--theme=${theme.value}` ];
8 | const result = spawn('node', args, {
9 | encoding: 'utf8',
10 | stdio: 'inherit'
11 | });
12 | if (result.status !== 0) {
13 | throw new Error(result.status || ('exited on signal ' + result.signal));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms/page/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | options: {
3 | types: [
4 | {
5 | name: 'default-page',
6 | label: 'Default'
7 | },
8 | {
9 | name: '@apostrophecms/home-page',
10 | label: 'Home'
11 | }
12 | ]
13 | },
14 | handlers(self, options) {
15 | return {
16 | '@apostrophecms/page:beforeSend': {
17 | setTheme(req) {
18 | req.data.theme = self.apos.options.theme;
19 | }
20 | }
21 | };
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/sites/modules/pieces-modules/product-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable media-feature-name-allowed-list */
2 |
3 | @import "Modules/asset/scss/variables";
4 |
5 | .menu {
6 | &__list {
7 | display: grid;
8 | grid-gap: 2rem;
9 |
10 | @media only screen and (width >= 600px) {
11 | &.split {
12 | grid-template-columns: 1fr 1fr;
13 | }
14 | }
15 | }
16 |
17 | &__item {
18 | &__title {
19 | display: flex;
20 | justify-content: space-between;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sites/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 |
16 | {% endif %}
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/theme-default/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // This is a good place to add theme specific
3 | // variations on helpers; since the active theme
4 | // is aliased as `apos.theme` these can be called easily
5 | options: {
6 | alias: 'theme',
7 | // Silence startup warning about the lack of code since this
8 | // is just an empty starting point for your own work
9 | ignoreNoCodeWarning: true,
10 | // Silence startup warning displayed if this module is
11 | // not activated at all, since only one theme module
12 | // will be activated per site
13 | ignoreUnusedFolderWarning: true
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/public/images/social-icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dashboard/modules/@apostrophecms/template/views/outerLayout.html:
--------------------------------------------------------------------------------
1 | {% extends "outerLayoutBase.html" %}
2 |
3 | {% if data.piece %}
4 | {% if data.piece.seoTitle %}
5 | {% set title = data.piece.seoTitle %}
6 | {% else %}
7 | {% set title = data.piece.title %}
8 | {% endif %}
9 | {% else %}
10 | {% if data.page.seoTitle %}
11 | {% set title = data.page.seoTitle %}
12 | {% else %}
13 | {% set title = data.page.title %}
14 | {% endif %}
15 | {% endif %}
16 |
17 | {% block title %}{{ title }} ?????{% endblock %}
18 | {% block extraHead %}
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/sites/modules/pieces-modules/team-member-widget/views/widget.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/pieces-modules/product-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% set splitClass = 'split' if data.widget.style === 'split'%}
2 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/accordion-widget/views/widget.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/image-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% import "@apostrophecms/image-widget:fragment.html" as image %}
2 | {#- Ensure we have animations only if the widget is not loaded via the
3 | REST API. Basically an "edit mode" detection. If not done here,
4 | the AOS module breaks our "apos-area-widget-guard" overlay and we
5 | can't select an image in edit mode. -#}
6 | {%- set dataAos = data.widget.animationEffects if not data.widget._virtual else '' -%}
7 |
15 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/public/images/social-icons/linkedin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "delay": 1000,
3 | "verbose": true,
4 | "watch": [
5 | "./app.js",
6 | "./dashboard/index.js",
7 | "./dashboard/modules/**/*",
8 | "./dashboard/lib/**/*.js",
9 | "./dashboard/views/**/*.html",
10 | "./sites/index.js",
11 | "./sites/modules/**/*",
12 | "./sites/lib/**/*.js",
13 | "./sites/views/**/*.html"
14 | ],
15 | "ignoreRoot": [".git"],
16 | "ignore": [
17 | "**/ui/",
18 | "sites/apos-build/**",
19 | "sites/public/uploads/**",
20 | "sites/public/apos-frontend/**",
21 | "dashboard/apos-build/**",
22 | "dashboard/public/uploads/**",
23 | "dashboard/public/apos-frontend/**",
24 | "locales/*.json",
25 | "data"
26 | ],
27 | "ext": "json, js, cjs, html"
28 | }
29 |
--------------------------------------------------------------------------------
/sites/modules/theme-default/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | // eslint-disable-next-line no-console
3 | console.log('Default theme project level js file');
4 | // Uncomment to demonstrate a websocket connection
5 | // // Simple test that works locally and in the cloud: http -> ws, https -> wss
6 | // const url = window.location.href.replace(/^http/, 'ws');
7 | // const ws = new WebSocket(url);
8 | // ws.onopen = () => {
9 | // ws.send('message from websocket client');
10 | // };
11 | // ws.onmessage = m => {
12 | // console.log(`websocket server said: ${m.data}`);
13 | // };
14 | // ws.onerror = e => {
15 | // console.error(e);
16 | // };
17 | // ws.onclose = e => {
18 | // console.error('websocket closed');
19 | // };
20 | };
21 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/button-strip-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable media-feature-name-allowed-list */
2 |
3 | .button-strip {
4 | display: flex;
5 | flex-wrap: wrap;
6 | margin: 1.5rem 0;
7 |
8 | .btn-widget {
9 | display: flex;
10 | align-items: center;
11 | width: 100%;
12 |
13 | &--alignment-left {
14 | justify-content: flex-start;
15 | }
16 |
17 | &--alignment-center {
18 | justify-content: center;
19 | }
20 |
21 | &--alignment-right {
22 | justify-content: flex-end;
23 | }
24 |
25 | & + .btn-widget {
26 | margin-top: 20px;
27 | }
28 |
29 | @media only screen and (width >= 600px) {
30 | width: auto;
31 |
32 | & + .btn-widget {
33 | margin-top: 0;
34 | margin-left: 20px;
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/pricing-widget/views/widget.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
15 |
--------------------------------------------------------------------------------
/sites/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 |
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 %}
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_colors.scss:
--------------------------------------------------------------------------------
1 | $white: #fff;
2 |
3 | $blue: #66f;
4 | $green: #00bf9a;
5 | $red: #ea433a;
6 | $purple: #b327bf;
7 | $black: #1d232b;
8 | $light: #f2f2f2;
9 | $background: #11151f;
10 | $input: #2c354d;
11 | $input-border: #8895a7;
12 |
13 | $colors: (
14 | text-primary: $black,
15 | text-inverse: $white,
16 | background: $white,
17 | shadow-base: $black
18 | );
19 |
20 | .o-light { color: $light; }
21 |
22 | @function color($key) {
23 | @if map-has-key($colors, $key) {
24 | @return map-get($colors, $key);
25 | }
26 |
27 | @warn 'Unknown `#{$key}` in $colors.';
28 |
29 | @return null;
30 | }
31 |
32 | @function color-alpha($color, $opacity) {
33 | @if map-has-key($colors, $color) {
34 | @return rgba(map-get($colors, $color), $opacity);
35 | }
36 |
37 | @warn 'Unknown `#{$color}` in $colors.';
38 |
39 | @return null;
40 | }
41 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss/_theme.scss:
--------------------------------------------------------------------------------
1 | // Hospitality colours
2 | // Primary
3 | $jungle-green: #84a98c;
4 | $navy: #2f3e46;
5 |
6 | // Secondary
7 | $light-green-tea: #f6fff8;
8 | $stone: #cad2c5;
9 | $sea-green: #52796f;
10 | $pine-green: #354f52;
11 | $error-red: #d64545;
12 |
13 | // Theme colour variables
14 | $primary: $navy;
15 | $secondary: $stone;
16 | $tertiary: $sea-green;
17 | $success: $jungle-green;
18 | $danger: $error-red;
19 |
20 | $btn-font-weight: initial;
21 | $btn-secondary-text: $primary;
22 |
23 | $font-family-sans-serif: "Lato", system-ui, -apple-system, "Segoe UI", roboto, "Helvetica Neue", "Noto Sans",
24 | "Liberation Sans", arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
25 | $font-family-monospace: "Abril Fatface", sfmono-regular, menlo, monaco, consolas, "Liberation Mono", "Courier New",
26 | monospace !default;
27 |
--------------------------------------------------------------------------------
/sites/public/images/social-icons/meta.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/tools/_functions.scss:
--------------------------------------------------------------------------------
1 | @function str-replace($string, $search, $replace: '') {
2 | $index: str-index($string, $search);
3 |
4 | @if $index {
5 | @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
6 | }
7 |
8 | @return $string;
9 | }
10 |
11 | // Remove the unit of a length
12 | // https://css-tricks.com/snippets/sass/strip-unit-function/
13 | // @param {Number} $number - Number to remove unit from
14 | // @return {Number} - Unitless number
15 | @function strip-unit($number) {
16 | @if type-of($number) == 'number' and not unitless($number) {
17 | @return $number / ($number * 0 + 1);
18 | }
19 |
20 | @return $number;
21 | }
22 |
23 | @function subtractUnitMatch($value, $subtract) {
24 | $unit: $value / $value; // Gets one of the unit.
25 | $subtrahend: strip-unit($subtract) * $unit;
26 |
27 | @return $value - $subtrahend;
28 | }
29 |
--------------------------------------------------------------------------------
/sites/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 | };
29 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/custom-form-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable media-feature-name-allowed-list */
2 |
3 | @import "Modules/asset/scss/variables";
4 |
5 | .custom-form {
6 | display: flex;
7 | flex-direction: column;
8 | gap: 2rem;
9 |
10 | &--primary {
11 | color: $primary;
12 | }
13 |
14 | &--secondary {
15 | color: $secondary;
16 | }
17 |
18 | &--tertiary {
19 | color: $tertiary;
20 | }
21 |
22 | &--black {
23 | color: $black;
24 | }
25 |
26 | &--white {
27 | color: $white;
28 | }
29 |
30 | &--background {
31 | background-position: 50% 50%;
32 | background-size: cover;
33 | align-items: center;
34 | justify-content: center;
35 | }
36 |
37 | &__column-item {
38 | flex-basis: 50%;
39 | }
40 |
41 | @media only screen and (width >= 900px) {
42 | flex-direction: row;
43 | }
44 | }
45 |
46 | .widget-columns__column .custom-form__column-item {
47 | flex-basis: 90%;
48 | }
49 |
--------------------------------------------------------------------------------
/sites/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 %}
--------------------------------------------------------------------------------
/sites/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 |
7 | &:last-child {
8 | border-bottom: 1px solid $primary;
9 | }
10 | }
11 |
12 | &__heading {
13 | margin: 0;
14 | }
15 |
16 | &__button {
17 | display: flex;
18 | justify-content: space-between;
19 | width: 100%;
20 | border: none;
21 | color: $primary;
22 | text-align: left;
23 | background-color: transparent;
24 | cursor: pointer;
25 | user-select: none;
26 |
27 | &:focus-visible {
28 | outline: none;
29 | }
30 |
31 | &[aria-expanded="false"] .accordion__button-icon::after {
32 | content: "\002B";
33 | }
34 |
35 | &[aria-expanded="true"] .accordion__button-icon::after {
36 | content: "\2212";
37 | }
38 | }
39 |
40 | &__button,
41 | &__content {
42 | padding: 1rem 1.5rem;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sites/modules/pieces-modules/team-member-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable declaration-property-unit-allowed-list, media-feature-name-allowed-list */
2 |
3 | @import "Modules/asset/scss/variables";
4 |
5 | .team-widget {
6 | display: grid;
7 | grid-template-columns: repeat(auto-fit, minmax(220px, max-content));
8 | grid-gap: 2rem;
9 | justify-content: space-around;
10 |
11 | &__profile {
12 | margin-bottom: 20px;
13 | text-align: center;
14 | }
15 |
16 | &__profile-image {
17 | width: 200px;
18 | height: 200px;
19 | margin: 0 auto;
20 | border-radius: 50%;
21 | object-fit: cover;
22 | }
23 |
24 | &__work-title {
25 | text-transform: uppercase;
26 | }
27 |
28 | @media only screen and (width >= 900px) {
29 | justify-content: center;
30 |
31 | &--three-col {
32 | grid-template-columns: 1fr 1fr 1fr;
33 | }
34 |
35 | &--four-col {
36 | grid-template-columns: 1fr 1fr 1fr 1fr;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/scripts/wait-for-port:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import net from 'node:net';
4 |
5 | const port = parseInt(process.argv[process.argv.length - 1]);
6 |
7 | go();
8 |
9 | async function go() {
10 | let connected = false;
11 | while (!connected) {
12 | try {
13 | await attempt('127.0.0.1', port);
14 | connected = true;
15 | } catch (e) {
16 | console.error(e);
17 | console.log(`Port ${port} not available yet, retrying in 1 second...`);
18 | await delay(1000);
19 | }
20 | }
21 | console.log(`Port ${port} connected`);
22 | }
23 |
24 | async function attempt(host, port) {
25 | return new Promise((resolve, reject) => {
26 | const client = net.createConnection(port, host);
27 | client.on('connect', () => {
28 | client.destroy();
29 | resolve();
30 | });
31 | client.on('error', e => {
32 | reject(e);
33 | });
34 | });
35 | }
36 |
37 | function delay(ms) {
38 | return new Promise(resolve => setTimeout(resolve, ms));
39 | }
40 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/views/fragments/header.html:
--------------------------------------------------------------------------------
1 | {% import "ui.html" as ui %}
2 | {% import "button.html" as buttonFrag %}
3 |
4 | {% fragment navigationBar(data) %}
5 |
6 |
13 |
14 |
15 |
16 | {% for nav in data.global.headerNav %}
17 | {% set path = apos
18 | .helper
19 | .linkPath(nav) %}
20 | -
21 | {{ nav.linkText }}
22 |
23 | {% endfor %}
24 |
25 |
26 |
27 | {% for btn in data.global.headerBtns %}
28 | {% render buttonFrag.render(btn) %}
29 | {% endfor %}
30 |
31 |
32 |
33 | {% endfragment %}
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/public/images/checked-icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vim swapfiles
2 | *.swp
3 | .DS_Store
4 | /dashboard/locales
5 | /locales
6 | npm-debug.log
7 | /data
8 | /dashboard/data
9 | /sites/data
10 | */apos-build
11 | */public/apos-frontend
12 | /dashboard/modules/asset/ui/public
13 | /sites/modules/theme-*/ui/public
14 | */public/modules
15 | */public/uploads
16 | */public/svgs/*.svg
17 | */public/apos-minified
18 | node_modules
19 | # 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)
20 | /public/modules
21 | # We don't commit CSS, only LESS
22 | */public/css/*.css
23 | */public/css/*.less
24 | # Don't commit CSS sourcemap files
25 | */public/css/*.map
26 | # Don't commit masters generated on the fly at startup, these import all the rest
27 | /public/css/master-*.less
28 | .jshintrc
29 | /public/js/_site-compiled.js
30 | /public/sitemap.xml
31 | dashboard/modules/assets/public/css/site.css
32 | dashboard/modules/assets/public/js/site.js
33 | sites/modules/theme-*/public/js/site.js
34 | sites/modules/theme-*/public/css/site.css
35 | # Deployed, but not committed
36 | /release-id
37 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss-elements/_button-arrow.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable declaration-property-unit-allowed-list, scale-unlimited/declaration-strict-value, max-nesting-depth */
2 |
3 | .button-arrow {
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | width: 56px;
8 | height: 56px;
9 | color: $primary;
10 | background: $secondary;
11 | transition: background-color $animate-easing;
12 | border-radius: 50%;
13 | cursor: pointer;
14 |
15 | &:hover {
16 | background: lighten($secondary, 10%);
17 | }
18 | }
19 |
20 | .swiper-button-arrows {
21 | .button-arrow {
22 | z-index: 3;
23 | position: absolute;
24 | top: 50%;
25 | transform: translateY(-50%);
26 |
27 | &--prev {
28 | left: 0.5rem;
29 | }
30 |
31 | &--next {
32 | right: 0.5rem;
33 |
34 | .button-arrow__icon {
35 | transform: rotate(180deg);
36 | }
37 | }
38 |
39 | &.swiper-button-disabled {
40 | opacity: 0.4;
41 | cursor: auto;
42 |
43 | &:hover {
44 | background: $secondary;
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss/_typography.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable at-rule-disallowed-list */
2 |
3 | %heading {
4 | margin-top: 0; // 1
5 | margin-bottom: $headings-margin-bottom;
6 | color: $headings-color;
7 | font-family: $headings-font-family;
8 | font-style: $headings-font-style;
9 | font-weight: $headings-font-weight;
10 | line-height: $headings-line-height;
11 | }
12 |
13 | // font size removed to allow palette to set font size, media queries are difficult to override
14 |
15 | h1 {
16 | @extend %heading;
17 | }
18 |
19 | h2 {
20 | @extend %heading;
21 | }
22 |
23 | h3 {
24 | @extend %heading;
25 | }
26 |
27 | h4 {
28 | @extend %heading;
29 | }
30 |
31 | h5 {
32 | @extend %heading;
33 | }
34 |
35 | h6 {
36 | @extend %heading;
37 | }
38 |
39 | .h1 {
40 | @extend h1;
41 | }
42 |
43 | .h2 {
44 | @extend h2;
45 | }
46 |
47 | .h3 {
48 | @extend h3;
49 | }
50 |
51 | .h4 {
52 | @extend h4;
53 | }
54 |
55 | .h5 {
56 | @extend h5;
57 | }
58 |
59 | .h6 {
60 | @extend h6;
61 | }
62 |
63 | p:not([class]) {
64 | margin-top: 0;
65 | margin-bottom: 1rem;
66 | line-height: $line-height-base;
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss-elements/_buttons.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable scale-unlimited/declaration-strict-value, color-named */
2 |
3 | .btn,
4 | .my-form__submit {
5 | display: inline-block;
6 | width: 100%;
7 | padding: 1rem 2rem;
8 | color: white;
9 | text-align: center;
10 | transition: background-color $animate-easing;
11 | font-weight: $btn-font-weight;
12 | text-decoration: none;
13 | text-transform: uppercase;
14 | border-radius: $border-radius;
15 |
16 | &:hover {
17 | // color: $white;
18 | background-color: bisque;
19 | text-decoration: none;
20 | }
21 | }
22 |
23 | .btn--sm {
24 | padding: 0.75rem 1.5rem;
25 | font-size: 0.75rem;
26 | }
27 |
28 | .btn--lg {
29 | padding: 1.25rem 2.5rem;
30 | font-size: 1.25rem;
31 | }
32 |
33 | .btn--primary,
34 | .my-form__submit {
35 | border: none;
36 | color: $white;
37 | background: $primary;
38 |
39 | &:hover {
40 | background: lighten($primary, 5%);
41 | }
42 | }
43 |
44 | .btn--secondary {
45 | border: none;
46 | color: $btn-secondary-text;
47 | background: $secondary;
48 |
49 | &:hover {
50 | background: lighten($secondary, 5%);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/image-gallery-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable selector-class-pattern, declaration-no-important */
2 |
3 | @import 'Modules/asset/scss/variables';
4 | @import 'swiper/swiper-bundle.css';
5 | @import 'photoswipe/photoswipe.css';
6 |
7 | // Main Swiper Gallery loaded on the page
8 | .image-gallery {
9 | &__swiper {
10 | padding: 0 2rem;
11 |
12 | .swiper-wrapper {
13 | align-items: center;
14 | }
15 |
16 | .image-gallery__svg {
17 | min-width: 30%;
18 | width: auto;
19 | }
20 |
21 | .swiper-slide {
22 | display: flex;
23 | justify-content: center;
24 | }
25 | }
26 | }
27 |
28 | // Photoswipe lightbox gallery target - when opened in the modal
29 | .imageGallery--pswp {
30 | .pswp__zoom-wrap {
31 | width: 100%;
32 | height: 100%;
33 | }
34 |
35 | .pswp__img--placeholder {
36 | display: none;
37 | }
38 |
39 | .pswp__img {
40 | /* height: auto; */
41 | top: 50%;
42 | left: 50%;
43 | width: auto !important;
44 | height: auto !important;
45 | max-width: 100% !important;
46 | transform: translate(-50%, -50%);
47 | object-fit: contain;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/tools/_fonts.scss:
--------------------------------------------------------------------------------
1 | // Font generation mixin a variation of:
2 | // https://gist.github.com/jonathantneal/d0460e5c2d5d7f9bc5e6
3 | @mixin generate-font($name, $url, $file, $style: normal, $weight: normal, $exts: eot woff2 woff ttf svg) {
4 | $src: null;
5 |
6 | $extmods: (
7 | eot: '?#iefix',
8 | svg: '#' + str-replace($name, ' ', '_')
9 | );
10 |
11 | $formats: (
12 | eot: 'embedded-opentype',
13 | otf: 'opentype',
14 | ttf: 'truetype'
15 | );
16 |
17 | $eot: if(index($exts, eot), url('#{$url}#{$file}.eot'), null);
18 |
19 | @each $ext in $exts {
20 | $extmod: if(
21 | map-has-key($extmods, $ext),
22 | $ext + map-get($extmods, $ext),
23 | $ext
24 | );
25 | $format: if(
26 | map-has-key($formats, $ext),
27 | map-get($formats, $ext),
28 | $ext
29 | );
30 | $src: append(
31 | $src,
32 | url(quote($url + $file + '.' + $extmod)
33 | ) format(quote($format)),
34 | comma);
35 | }
36 |
37 | @font-face {
38 | font-family: quote($name);
39 | font-weight: $weight;
40 | font-style: $style;
41 | src: $eot;
42 | src: $src;
43 | font-display: fallback;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/index.js:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import url from 'node:url';
3 | import { glob } from 'glob';
4 |
5 | const getConfigs = async (folder) => {
6 | const dirname = path.dirname(url.fileURLToPath(import.meta.url));
7 | const files = await glob(path.join(dirname, folder, '**/*.js'));
8 |
9 | const configs = [];
10 | for (const file of files) {
11 | const { default: config } = await import(file);
12 | configs.push(config);
13 | }
14 |
15 | return configs;
16 | };
17 |
18 | const configs = await getConfigs('lib/configs');
19 |
20 | export default {
21 | fields: {
22 | add: generateFields(configs),
23 | group: generateGroups(configs)
24 | }
25 | };
26 |
27 | function generateFields(configurations) {
28 | let fields = {};
29 | for (const config of Object.keys(configurations)) {
30 | fields = {
31 | ...fields,
32 | ...configurations[config].add
33 | };
34 | };
35 |
36 | return fields;
37 | }
38 |
39 | function generateGroups(configurations) {
40 | let groups = {};
41 |
42 | for (const config of Object.keys(configurations)) {
43 | groups = {
44 | ...groups,
45 | ...configurations[config].group
46 | };
47 | };
48 |
49 | return groups;
50 | }
51 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/lib/choices.js:
--------------------------------------------------------------------------------
1 | export default {
2 | BASE_SIZES: [
3 | {
4 | label: '14px',
5 | value: '14'
6 | },
7 | {
8 | label: '16px',
9 | value: '16'
10 | },
11 | {
12 | label: '18px',
13 | value: '18'
14 | }
15 | ],
16 | BASE_WEIGHTS: [
17 | {
18 | label: '400',
19 | value: '400'
20 | },
21 | {
22 | label: '500',
23 | value: '500'
24 | },
25 | {
26 | label: '600',
27 | value: '600'
28 | }
29 | ],
30 | BASE_FONTS: [
31 | {
32 | label: 'Abril Fatface',
33 | value: '"Abril Fatface", serif'
34 | },
35 | {
36 | label: 'Roboto',
37 | value: '"Roboto", sans-serif'
38 | },
39 | {
40 | label: 'Poppins',
41 | value: '"Poppins", sans-serif'
42 | },
43 | {
44 | label: 'Cormorant Garamond',
45 | value: '"Cormorant Garamond", serif'
46 | },
47 | {
48 | label: 'Kaushan Script',
49 | value: '"Kaushan Script", cursive'
50 | },
51 | {
52 | label: 'Josefin Slab',
53 | value: '"Josefin Slab", serif'
54 | },
55 | {
56 | label: 'Alfa Slab One',
57 | value: '"Alfa Slab One", serif'
58 | }
59 | ]
60 | };
61 |
--------------------------------------------------------------------------------
/sites/views/fragments/footer.html:
--------------------------------------------------------------------------------
1 | {% import "ui.html" as ui %}
2 |
3 | {% fragment footer(data) %}
4 |
39 | {% endfragment %}
--------------------------------------------------------------------------------
/telemetry.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 |
3 | import { NodeSDK, resources } from '@opentelemetry/sdk-node';
4 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
5 | import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
6 | import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
7 |
8 | // 1. Add the application meta data (resource)
9 | const { name, version } = JSON.parse(await fs.readFile('./package.json'));
10 | const resource = new resources.Resource({
11 | [SemanticResourceAttributes.SERVICE_NAME]: name,
12 | [SemanticResourceAttributes.SERVICE_VERSION]: version
13 | });
14 |
15 | // 2. Initialize the exporter
16 | const traceExporter = new JaegerExporter({
17 | tags: [],
18 | endpoint: 'http://localhost:14268/api/traces'
19 | });
20 |
21 | // 3. Initialize the SDK
22 | const sdk = new NodeSDK({
23 | resource,
24 | traceExporter,
25 | instrumentations: [ getNodeAutoInstrumentations() ]
26 | });
27 |
28 | // 4. The shutdown handler
29 | const shutdown = async () => {
30 | await sdk
31 | .shutdown()
32 | .then(
33 | () => console.log('OpenTelemetry stopped'),
34 | (err) => console.log('Error shutting down OpenTelemetry', err)
35 | );
36 | };
37 |
38 | export {
39 | sdk,
40 | shutdown
41 | };
42 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/image-gallery-widget/views/widget.html:
--------------------------------------------------------------------------------
1 | {% import 'button-arrows.html' as buttonArrows %}
2 |
3 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/lib/configs/5footer.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | add: {
3 | footerBgColor: {
4 | label: 'Background color',
5 | type: 'color',
6 | selector: 'body .footer',
7 | property: 'background-color'
8 | },
9 | footerTextColor: {
10 | label: 'Text color',
11 | type: 'color',
12 | selector: '.footer',
13 | property: 'color'
14 | },
15 | footerLinkColor: {
16 | label: 'Link color',
17 | type: 'color',
18 | help: 'Text link color',
19 | // TODO: Update rich text data attribute to a class when RTE className
20 | // bug is fixed.
21 | selector: [ '.footer [data-rich-text] a', '.footer a' ],
22 | property: 'color',
23 | def: 'royalblue'
24 | },
25 | footerPadding: {
26 | label: 'Vertical padding',
27 | type: 'range',
28 | selector: 'body .footer',
29 | unit: 'px',
30 | property: [
31 | 'padding-top',
32 | 'padding-bottom'
33 | ],
34 | min: 0,
35 | max: 64
36 | }
37 | },
38 | group: {
39 | footer: {
40 | label: 'Footer Settings',
41 | fields: [
42 | 'footerBgColor',
43 | 'footerTextColor',
44 | 'footerLinkColor',
45 | 'footerPadding'
46 | ]
47 | }
48 | }
49 | };
50 |
51 | export default config;
52 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss-elements/_footer.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable media-feature-name-allowed-list, max-nesting-depth */
2 |
3 | .footer {
4 | background-color: $secondary;
5 |
6 | &__main {
7 | border-radius: $border-radius;
8 | padding: 2rem;
9 |
10 | @media (width >= 1024px) {
11 | display: flex;
12 | align-items: center;
13 | justify-content: space-between;
14 | }
15 | }
16 |
17 | &__logo {
18 | img {
19 | width: 200px;
20 | height: auto;
21 | }
22 | }
23 |
24 | ul {
25 | display: flex;
26 | padding-left: 0;
27 | list-style: none;
28 | }
29 |
30 | &__nav-items {
31 | ul {
32 | display: block;
33 |
34 | @media (width >= 1024px) {
35 | display: flex;
36 | }
37 | }
38 |
39 | li {
40 | margin: 1.25rem 0;
41 |
42 | // a {
43 | // @include inverse-link-style;
44 | // }
45 |
46 | @media (width >= 1024px) {
47 | margin: 0 1.25rem;
48 | }
49 | }
50 | }
51 |
52 | &__social-items {
53 | li {
54 | margin: 0 1rem;
55 |
56 | &:first-child {
57 | margin-left: 0;
58 | }
59 | }
60 | }
61 |
62 | &__copyright {
63 | margin: 1rem 0;
64 | text-align: center;
65 | font-weight: 700;
66 |
67 | p {
68 | margin: 0;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/components/_site-menu.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable selector-no-qualifying-type */
2 |
3 | .c-site-menu {
4 | display: flex;
5 | align-items: flex-end;
6 | justify-content: space-between;
7 | margin-top: 50px;
8 | margin-bottom: 50px;
9 | padding-bottom: 20px;
10 | border-bottom: 2px solid fade($white, 30%);
11 | }
12 |
13 | .c-site__tabs {
14 | display: flex;
15 | align-items: baseline;
16 | }
17 |
18 | .c-site__tab {
19 | margin: 0 1rem;
20 |
21 | a {
22 | display: flex;
23 |
24 | svg {
25 | align-self: flex-end;
26 | width: 11px;
27 | margin-left: 10px;
28 | }
29 |
30 | svg g, svg path {
31 | fill: currentcolor;
32 | }
33 | }
34 |
35 | button {
36 | padding: 10px 10px 0;
37 | font-family: $font;
38 | font-weight: 500;
39 | font-size: $font-12;
40 | letter-spacing: 1px;
41 |
42 | &:hover, &:focus, &.c-site__tab-button--active {
43 | color: $blue;
44 | }
45 | }
46 | }
47 |
48 | .c-site__tabs-container {
49 | position: relative;
50 |
51 | .apos-area-widget-wrapper {
52 | margin-bottom: 50px;
53 | }
54 | }
55 |
56 | .c-site__tab-content {
57 | display: none;
58 | }
59 |
60 | .c-site__tab-content--active {
61 | display: block;
62 | }
63 |
64 | .c-site-menu__location { margin-bottom: 10px; }
65 | .c-site-menu__title { margin-bottom: 10px; }
66 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/call-to-action-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable scale-unlimited/declaration-strict-value */
2 |
3 | @import "Modules/asset/scss/variables";
4 |
5 | .call-to-action {
6 | position: relative;
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: center;
10 | width: 100%;
11 | color: $white;
12 |
13 | &__screen {
14 | z-index: 1;
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | width: 100%;
19 | height: 100%;
20 | background-color: rgba($black, 0.35);
21 | }
22 |
23 | &__content {
24 | z-index: 2;
25 | position: relative;
26 | h2 { color: white; }
27 | }
28 |
29 | &__background {
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | width: 100%;
34 | height: 100%;
35 | background-position: 50% 50%;
36 | background-size: cover;
37 | filter: brightness(0.7);
38 | }
39 |
40 | &--basic {
41 | height: 50svh;
42 | }
43 |
44 | &--large-marquee {
45 | height: 100svh;
46 | }
47 |
48 | &--left {
49 | text-align: left;
50 |
51 | .button-strip {
52 | justify-content: flex-start;
53 | }
54 | }
55 |
56 | &--centered {
57 | text-align: center;
58 |
59 | .button-strip {
60 | justify-content: center;
61 | }
62 | }
63 |
64 | &--right {
65 | text-align: right;
66 |
67 | .button-strip {
68 | justify-content: flex-end;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/pricing-widget/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable max-nesting-depth */
2 |
3 | @import "Modules/asset/scss/variables";
4 |
5 | .pricing {
6 | background: $secondary;
7 |
8 | &__title {
9 | text-align: center;
10 | }
11 |
12 | .cards {
13 | display: grid;
14 | justify-content: center;
15 | margin-top: 2rem;
16 | padding: initial;
17 | grid-template-columns: repeat(auto-fit, minmax(350px, max-content));
18 | grid-gap: 2rem;
19 | }
20 |
21 | .card {
22 | width: 350px;
23 | padding: 2rem;
24 | color: $primary;
25 | background: $white;
26 | border-radius: $border-radius;
27 |
28 | &:nth-child(even) {
29 | border: 1px solid $tertiary;
30 | color: $primary;
31 | background: none;
32 | }
33 |
34 | &--pricing {
35 | display: flex;
36 | flex-direction: column;
37 | justify-content: space-between;
38 | }
39 |
40 | &__label {
41 | margin-bottom: 0.5rem;
42 | font-weight: 700;
43 | color: $tertiary;
44 | text-transform: uppercase;
45 | }
46 |
47 | &__feature-list {
48 | padding-left: 0;
49 | list-style: none;
50 |
51 | li {
52 | display: flex;
53 | margin-bottom: 10px;
54 | }
55 |
56 | li::before {
57 | margin-right: 10px;
58 | font-weight: 700;
59 | color: $tertiary;
60 | content: "\2713";
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/sites/lib/linkSchema.js:
--------------------------------------------------------------------------------
1 | export default {
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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/public/images/social-icons/instagram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/lib/configs/4header.js:
--------------------------------------------------------------------------------
1 | import choices from '../choices.js';
2 |
3 | const config = {
4 | add: {
5 | headerBgColor: {
6 | label: 'Background color',
7 | type: 'color',
8 | selector: '.navigation',
9 | property: 'background-color',
10 | def: null
11 | },
12 | headerLinkColor: {
13 | label: 'Link color',
14 | type: 'color',
15 | selector: '.navigation__nav-links li a',
16 | property: 'color',
17 | def: null
18 | },
19 | headerFontSize: {
20 | label: 'Text Size',
21 | type: 'range',
22 | selector: [ '.navigation__nav-links li a', '.navigation__nav-links .link.btn' ],
23 | property: 'font-size',
24 | unit: 'px',
25 | min: 9,
26 | max: 52,
27 | def: 12
28 | },
29 | headerFont: {
30 | label: 'Font',
31 | type: 'select',
32 | selector: [ '.navigation__nav-links li a', '.navigation__nav-links .link.btn' ],
33 | property: 'font-family',
34 | choices: choices.BASE_FONTS
35 | },
36 | headerVertPadding: {
37 | label: 'Vertical Padding',
38 | type: 'range',
39 | selector: '.navigation',
40 | property: [ 'padding-top', 'padding-bottom' ],
41 | unit: 'rem',
42 | min: 0.2,
43 | max: 5,
44 | def: 1.875,
45 | step: 0.125
46 | }
47 | },
48 | group: {
49 | header: {
50 | label: 'Header Settings',
51 | fields: [
52 | 'headerBgColor',
53 | 'headerFontSize',
54 | 'headerLinkColor',
55 | 'headerFont',
56 | 'headerVertPadding'
57 | ]
58 | }
59 | }
60 | };
61 |
62 | export default config;
63 |
--------------------------------------------------------------------------------
/sites/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 |
51 | {% endif %}
52 | {% endmacro %}
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/settings/_fonts.scss:
--------------------------------------------------------------------------------
1 | $font-size-base: 62.5%;
2 |
3 | $font-0: 0;
4 | $font-10: 10px;
5 | $font-12: 12px;
6 | $font-18: 1.5rem;
7 | $font-36: 3rem;
8 |
9 | $font: -apple-system, system-ui, 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
10 |
11 | $font-family: (
12 | serif: #{'FontFamilyName', serif},
13 | sans: #{'FontFamilyName', sans-serif},
14 | mono: #{'FontFamilyName', monospace}
15 | );
16 |
17 | $font-size: (
18 | title: (
19 | marquee: 8.2rem,
20 | large: 5.4rem,
21 | medium: 3.6rem,
22 | small: 2rem
23 | ),
24 | paragraph: (
25 | large: 2rem,
26 | base: 1.6rem,
27 | small: 1.4rem,
28 | meta: 1.2rem,
29 | micro: 1rem
30 | )
31 | );
32 |
33 | $font-weight: (
34 | light: 300,
35 | normal: 400,
36 | semibold: 600,
37 | bold: 700,
38 | xbold: 900
39 | );
40 |
41 | @function family($key) {
42 | @if map-has-key($font-family, $key) {
43 | @return map-get($font-family, $key);
44 | }
45 |
46 | @warn 'Unknown `#{$key}` in $font-family.';
47 |
48 | @return null;
49 | }
50 |
51 | @function weight($key) {
52 | @if map-has-key($font-weight, $key) {
53 | @return map-get($font-weight, $key);
54 | }
55 |
56 | @warn 'Unknown `#{$key}` in $font-weight.';
57 |
58 | @return null;
59 | }
60 |
61 | @mixin font($family: null, $type: null, $size: null, $weight: null) {
62 | @if $family {
63 | font-family: family($family);
64 | }
65 |
66 | @if $type and $size {
67 | font-size: map-get(map-get($font-size, $type), $size);
68 | }
69 |
70 | @if $weight {
71 | font-weight: weight($weight); /* stylelint-disable-line */
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/components/_navigation.scss:
--------------------------------------------------------------------------------
1 | .c-navigation {
2 | margin-top: 60px;
3 | margin-bottom: 60px;
4 |
5 | // undoing a lot of apostrophe chrome here
6 | .apos-ui { font-size: $font-10; }
7 |
8 | .apos-admin-bar {
9 | z-index: zindex(default);
10 | position: relative;
11 | top: auto;
12 | left: auto;
13 | overflow: visible;
14 | height: auto;
15 | border: none;
16 | border-radius: 0;
17 | color: $white;
18 | background-color: transparent;
19 | font-family: $font;
20 | font-weight: 500;
21 | letter-spacing: 1px;
22 | box-shadow: none;
23 |
24 | * {
25 | transition: none;
26 | }
27 | }
28 |
29 | [data-apos-admin-bar-logo] {
30 | display: none;
31 | }
32 |
33 | .apos-admin-bar-inner {
34 | display: flex;
35 | align-items: center;
36 | justify-content: space-between;
37 | font-size: $font-12;
38 | }
39 |
40 | .apos-admin-bar-item {
41 | height: auto;
42 |
43 | &:hover {
44 | background-color: transparent;
45 | }
46 |
47 | &:hover a, &:hover .apos-admin-bar-item-inner {
48 | text-decoration: underline;
49 | }
50 | }
51 |
52 | .apos-admin-bar-item-inner {
53 | line-height: 1.2;
54 | height: auto;
55 | padding: 1.75rem;
56 | }
57 | }
58 |
59 | .apos-ui .c-navigation__logo {
60 | margin-right: 20px;
61 | }
62 |
63 | .apos-ui .apos-admin-bar-item .apos-active {
64 | background-color: transparent;
65 | }
66 |
67 | .apos-ui .apos-admin-bar .apos-admin-bar-item .apos-dropdown-items {
68 | top: 102%;
69 | }
70 |
71 | .apos-ui .apos-admin-bar .apos-admin-bar-item .apos-dropdown.apos-active .apos-admin-bar-item-inner {
72 | background-color: $white;
73 | box-shadow: 0 2px 7px 2px rgb(21 22 22 / 23%);
74 | }
75 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss-elements/_navigation.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable media-feature-name-allowed-list, max-nesting-depth */
2 |
3 | .navigation {
4 | position: relative;
5 | padding-top: 1.875rem;
6 | padding-bottom: 1.875rem;
7 |
8 | &__logo-container {
9 | display: flex;
10 | align-items: center;
11 | justify-content: space-between;
12 | }
13 |
14 | &__logo {
15 | img {
16 | width: 200px;
17 | height: auto;
18 | vertical-align: middle;
19 | }
20 | }
21 |
22 | &__nav-items {
23 | display: none;
24 |
25 | @media (width >= 1024px) {
26 | display: flex;
27 | justify-content: space-between;
28 | width: 100%;
29 | }
30 | }
31 |
32 | &__nav-links {
33 | padding-left: 0;
34 | list-style: none;
35 |
36 | li {
37 | margin: 1.25rem 0;
38 |
39 | a {
40 | @include inverse-link-style;
41 | }
42 |
43 | @media (width >= 1024px) {
44 | margin: 0 1rem;
45 | }
46 | }
47 |
48 | @media (width >= 1024px) {
49 | display: flex;
50 | align-items: center;
51 | justify-content: center;
52 | width: 100%;
53 | margin: 0;
54 | }
55 | }
56 |
57 | &__show {
58 | display: block;
59 | }
60 |
61 | &__user-items {
62 | display: flex;
63 | flex-shrink: 0;
64 | }
65 |
66 | &__menu-btn {
67 | display: block;
68 | width: 40px;
69 | height: 40px;
70 | background-image: url("/images/menu.svg");
71 | background-repeat: no-repeat;
72 | background-position: center;
73 | background-size: cover;
74 |
75 | @media (width >= 1024px) {
76 | display: none;
77 | }
78 | }
79 |
80 | @media (width >= 1024px) {
81 | display: flex;
82 | align-items: center;
83 | justify-content: space-between;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms/layout-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/custom-form-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/index.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable declaration-no-important */
2 |
3 | @import "normalize.css";
4 | // Responsive font size npm package (https://www.npmjs.com/package/rfs)
5 | @import "rfs/scss";
6 |
7 | @import "./scss/_functions";
8 | @import "./scss/_variables";
9 | @import "./scss/_mixins";
10 |
11 | @import "./scss/_containers";
12 | @import "./scss/_typography";
13 |
14 | // Theme
15 | @import "./scss/_theme";
16 |
17 | // Navigation styles
18 | @import "./scss-elements/navigation";
19 | // Footer styles
20 | @import "./scss-elements/footer";
21 |
22 | @import "./scss-elements/_buttons";
23 | @import "./scss-elements/_button-arrow";
24 | @import "./scss-elements/_forms";
25 | @import "aos/dist/aos.css";
26 |
27 | // apos line-height interferance fix
28 | .apos-area,
29 | .apos-area-widget-inner,
30 | .apos-area-widget-wrapper,
31 | .apos-areas-widgets-list {
32 | line-height: unset !important;
33 | }
34 |
35 | *,
36 | ::after,
37 | ::before {
38 | box-sizing: border-box;
39 | }
40 |
41 | body {
42 | @include font-size(1rem);
43 |
44 | width: 100%;
45 | margin: 0; // 1
46 | color: $body-color;
47 | background: $body-bg;
48 | font-family: $body-font-family;
49 | font-weight: 400;
50 | line-height: $line-height-base;
51 | text-size-adjust: 100%; // 3
52 | -webkit-tap-highlight-color: rgba($black, 0); // 4
53 | }
54 |
55 | a {
56 | @include link-style;
57 | }
58 |
59 | body {
60 | background-color: #ebebeb;
61 | }
62 |
63 | body:not(.apos-login-page) [data-apos-refreshable] {
64 | border-style: solid;
65 | margin: 0 auto;
66 | }
67 |
68 | .apos-login-page [data-apos-refreshable] {
69 | max-width: unset;
70 | }
71 |
72 |
73 | .img-fluid {
74 | max-width: 100%;
75 | height: auto;
76 | border-radius: $border-radius;
77 | box-shadow: $box-shadow;
78 | }
79 |
80 | .widget-my-spacing {
81 | margin-top: 3rem;
82 | margin-bottom: 3rem;
83 | }
84 |
85 | .widget-py-spacing {
86 | padding-top: 3rem;
87 | padding-bottom: 3rem;
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/sites/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 extraHead %}
16 |
17 |
18 |
19 | {% endblock %}
20 |
21 | {% block beforeMain %}
22 |
23 |
24 | {# Hospitality fonts #}
25 |
26 | {% render header.navigationBar(data) %}
27 |
28 | {% endblock %}
29 |
30 | {% block main %}
31 | {#
32 | Usually, your page templates in the @apostrophecms/pages module will override
33 | this block. It is safe to assume this is where your page-specific content
34 | should go.
35 | #}
36 | {% endblock %}
37 |
38 | {% block afterMain %}
39 | {% render footer.footer(data) %}
40 | {% endblock %}
41 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss/_containers.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable media-feature-name-allowed-list, scss/dollar-variable-first-in-block, length-zero-no-unit, scss/at-mixin-named-arguments */
2 |
3 | @mixin min($bp, $max: "null", $device: "screen") {
4 | @if $max == "null" {
5 | @media only #{$device} and (min-width: #{$bp}) {
6 | @content;
7 | }
8 | }
9 |
10 | @else {
11 | @media only #{$device} and (min-width: #{$bp}) and (max-width: #{$max}) {
12 | @content;
13 | }
14 | }
15 | }
16 |
17 | @function bp($bp) {
18 | @return map-get($breakpoints, $bp);
19 | }
20 |
21 | $breakpoints: (
22 | na: 0px,
23 | // For BS grid
24 | xs: 320px,
25 | // Smartphone
26 | sm: 600px,
27 | // Tablets
28 | md: 900px,
29 | // Tablets Landscape and small desktops
30 | lg: 1200px,
31 | // Desktops
32 | xl: 1800px,
33 | // Large Desktop
34 | );
35 |
36 | @function container($container-size, $true-val: false) {
37 | @return map-get($container-sizes, $container-size);
38 | }
39 |
40 | $container-sizes: (
41 | sm: map-get($breakpoints, sm) - 30px,
42 | md: map-get($breakpoints, md) - 40px,
43 | lg: map-get($breakpoints, lg) - 50px,
44 | xl: map-get($breakpoints, xl) - 400px,
45 | );
46 |
47 | .container {
48 | padding-right: 1rem;
49 | padding-left: 1rem;
50 |
51 | &:not(.is-fluid) {
52 | margin-right: auto;
53 | margin-left: auto;
54 |
55 | @each $bp, $container-size in $container-sizes {
56 | @include min(#{bp(#{$bp})}) {
57 | width: 100%;
58 | max-width: container(#{$bp});
59 | }
60 | }
61 | }
62 | }
63 |
64 | @each $bp, $container-size in $container-sizes {
65 | .container-#{$bp} {
66 | width: 100%;
67 | margin: 0 auto;
68 | padding-right: 1rem;
69 | padding-left: 1rem;
70 |
71 | $i: index($container-sizes, $bp $container-size);
72 |
73 | @for $j from $i through length($container-sizes) {
74 | @include min(#{bp(nth(nth($container-sizes, $j), 1))}) {
75 | max-width: container(#{nth(nth($container-sizes, $j), 1)});
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/components/_card.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable declaration-property-unit-allowed-list, scale-unlimited/declaration-strict-value */
2 |
3 | .c-cards {
4 | display: grid;
5 | align-items: center;
6 | grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
7 | column-gap: 32px;
8 | row-gap: 32px;
9 | }
10 |
11 | .c-card {
12 | position: relative;
13 | padding: 8px;
14 | border-radius: 6px;
15 | background: $white;
16 | box-shadow: 0 10px 20px 0 rgb(0 0 0 / 20%);
17 | }
18 |
19 | .c-card__media {
20 | height: 200px;
21 | }
22 |
23 | .c-card__media-image {
24 | max-height: 80%;
25 | object-fit: contain;
26 | max-width: 80%;
27 | }
28 |
29 | .c-card__dashboard-link {
30 | display: flex;
31 | align-items: center;
32 | justify-content: center;
33 | height: 100%;
34 | background-color: $light;
35 | }
36 |
37 | .c-card__media-placeholder {
38 | width: 100%;
39 | height: 100%;
40 | background-image: linear-gradient(36deg, #4950f6 12%, #578beb 94%);
41 | }
42 |
43 | .c-card__content {
44 | padding: 16px 0 8px;
45 | }
46 |
47 | .c-card__title {
48 | overflow: hidden;
49 | margin: 0;
50 | margin-bottom: 8px;
51 | font-family: $font;
52 | font-size: 16px;
53 | line-height: 1.333;
54 | white-space: nowrap;
55 | text-overflow: ellipsis;
56 | }
57 |
58 | .c-card__button {
59 | display: inline-flex;
60 | overflow: visible;
61 | align-items: center;
62 | justify-content: center;
63 | width: 32px;
64 | height: 32px;
65 | margin: 0;
66 | padding: 0;
67 | border: none;
68 | color: inherit;
69 | background: transparent;
70 | text-align: center;
71 | font: inherit;
72 | line-height: normal;
73 | text-decoration: none;
74 | cursor: pointer;
75 | border-radius: 4px;
76 | background-color: $white;
77 | -webkit-font-smoothing: inherit;
78 | -moz-osx-font-smoothing: inherit;
79 | appearance: none;
80 |
81 | &:hover {
82 | background-color: #f4f4f4;
83 | }
84 | }
85 |
86 | .c-card__button--icon {
87 | width: 16px;
88 | height: 16px;
89 | fill: $black;
90 | user-select: none;
91 | pointer-events: none;
92 | }
93 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/map-widget/index.js:
--------------------------------------------------------------------------------
1 | import NodeGeocoder from 'node-geocoder';
2 | import aosSchema from '../../../lib/aosSchema.js';
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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/lib/area.js:
--------------------------------------------------------------------------------
1 | const apostropheWidgets = {
2 | '@apostrophecms/image': {
3 | className: 'img-fluid'
4 | },
5 | '@apostrophecms/video': {},
6 | '@apostrophecms/rich-text': {}
7 | };
8 |
9 | export default {
10 | all: {
11 | '@apostrophecms/layout': {},
12 | 'call-to-action': {},
13 | 'custom-form': {},
14 | 'image-gallery': {},
15 | product: {},
16 | 'side-by-side': {},
17 | '@apostrophecms/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 | '@apostrophecms/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 | '@apostrophecms/rich-text': {},
61 | fullExpandedGroup: {
62 | layout: {
63 | label: 'Layout Tools',
64 | widgets: {
65 | '@apostrophecms/layout': {},
66 | 'side-by-side': {}
67 | },
68 | columns: 2
69 | },
70 | media: {
71 | label: 'Media Widgets',
72 | widgets: {
73 | image: {},
74 | '@apostrophecms/video': {},
75 | 'image-gallery': {}
76 | },
77 | columns: 2
78 | },
79 | general: {
80 | label: 'Content Widgets',
81 | widgets: {
82 | '@apostrophecms/rich-text': {},
83 | accordion: {},
84 | 'call-to-action': {},
85 | 'custom-form': {},
86 | map: {},
87 | pricing: {},
88 | product: {},
89 | 'team-member': {}
90 | },
91 | columns: 3
92 | }
93 | }
94 | };
95 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | import multisite from '@apostrophecms-pro/multisite';
2 | import { sdk } from './telemetry.js';
3 | import sites from './sites/index.js';
4 | import dashboard from './dashboard/index.js';
5 |
6 | go();
7 |
8 | async function go() {
9 | try {
10 | if (process.env.APOS_OPENTELEMETRY) {
11 | await sdk.start();
12 | }
13 | await multisite({
14 | root: import.meta,
15 | // Default port, for dev
16 | port: 3000,
17 | websocket: true,
18 | // Change this to a hardcoded string when forking to make a new project.
19 | // Just set it to a string which should never change. Ideally should match
20 | // your repo name followed by a -, however if you plan to use a
21 | // cheap Atlas cluster (below M10), you must use a unique prefix less
22 | // than 12 characters (before the -).
23 | shortNamePrefix: process.env.APOS_PREFIX || 'a3hpab-',
24 | // Suffix, used only for building hostnames and not affecting
25 | // e.g. database names. For example, if you set this to `-assembly`,
26 | // and your short name is `site`, the hostname for that site would be
27 | // `site-assembly.your-domain.com`, and your dashboard would become available
28 | // at `dashboard-assembly.your-domain.com`.
29 | shortNameSuffix: '',
30 | // Used to separate the locale name from the short name in hostnames.
31 | // For example, if you set this to `-` and your short name is `site`,
32 | // the hostname for the `fr` locale with "Separate Host" enabled,
33 | // would be `fr-site.your-domain.com`.
34 | localeSeparator: '.',
35 | // You may set the dashboard short name to a different value than the default
36 | // 'dashboard'. For example if set to `admin`, the dashboard would be
37 | // available at `https://admin.yourdomain.com`.
38 | dashboardShortName: process.env.APOS_DASHBOARD_SHORTNAME || 'dashboard',
39 | // For development. An environment variable overrides this in staging/production
40 | mongodbUrl: 'mongodb://localhost:27017',
41 | sessionSecret: 'CHANGEME',
42 | sites,
43 | dashboard
44 | });
45 | } catch (e) {
46 |
47 | console.error(e);
48 | process.exit(1);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "starter-kit-assembly-hospitality",
3 | "version": "1.0.0",
4 | "description": "Hospitality multisite starter kit for ApostropheCMS",
5 | "type": "module",
6 | "private": true,
7 | "scripts": {
8 | "build": "APOS_UPLOADFS_ASSETS=1 NODE_ENV=production bash -c 'node app @apostrophecms/asset:build --site=dashboard && ./scripts/for-each-theme @apostrophecms/asset:build'",
9 | "//": "because nodemon insists on executing 'start' if it exists, we must distinguish production",
10 | "production-start": "APOS_UPLOADFS_ASSETS=1 NODE_ENV=production npm run start",
11 | "start": "node app",
12 | "dev": "nodemon",
13 | "test": "eslint . && stylelint 'dashboard/**/*.scss' && stylelint 'sites/**/*.scss'"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/apostrophecms/starter-kit-assembly-hospitality"
18 | },
19 | "engines": {
20 | "node": ">=10.0.0"
21 | },
22 | "author": "Apostrophe Technologies",
23 | "license": "MIT",
24 | "dependencies": {
25 | "@apostrophecms-pro/document-versions": "^2.3.1",
26 | "@apostrophecms-pro/multisite": "^4.3.0",
27 | "@apostrophecms-pro/multisite-dashboard": "^1.4.0",
28 | "@apostrophecms-pro/palette": "^4.3.2",
29 | "@apostrophecms/form": "^1.1.1",
30 | "@apostrophecms/open-graph": "^1.2.1",
31 | "@apostrophecms/seo": "^1.2.0",
32 | "@apostrophecms/sitemap": "^1.0.2",
33 | "@apostrophecms/vite": "^1.0.0",
34 | "@opentelemetry/auto-instrumentations-node": "^0.37.1",
35 | "@opentelemetry/exporter-jaeger": "^1.26.0",
36 | "@opentelemetry/sdk-node": "^0.41.0",
37 | "@opentelemetry/semantic-conventions": "^1.15.0",
38 | "aos": "^2.3.4",
39 | "apostrophe": "^4.18.0",
40 | "glob": "^10.4.5",
41 | "node-fetch": "^2.6.5",
42 | "node-geocoder": "^4.2.0",
43 | "normalize.css": "^8.0.1",
44 | "ol": "^7.3.0",
45 | "photoswipe": "^5.3.7",
46 | "qs": "^6.9.6",
47 | "rfs": "^10.0.0",
48 | "swiper": "^9.2.3"
49 | },
50 | "devDependencies": {
51 | "autoprefixer": "^10.4.20",
52 | "eslint-config-apostrophe": "^6.0.1",
53 | "nodemon": "^3.0.1",
54 | "stylelint": "^15.0.0",
55 | "stylelint-config-apostrophe": "^3.0.0"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/dashboard/modules/site/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import fetch from 'node-fetch';
3 |
4 | import themes from '../../../themes.js';
5 | import baseUrlDomains from '../../../domains.js';
6 |
7 | export default {
8 | options: {
9 | baseUrlDomains,
10 | localizedSites: true
11 | },
12 | tasks(self, options) {
13 | return {
14 | 'list-themes': {
15 | usage: 'List the theme shortnames. Used by the cloud asset generation system.',
16 | async task(argv) {
17 | console.log(themes.map(theme => theme.value).join('\n'));
18 | }
19 | }
20 | };
21 | },
22 | handlers(self, options) {
23 | return {
24 | afterSave: {
25 | async ensureCertificate(req, piece, options) {
26 | // Use the platform balancer API to immediately get a certificate for
27 | // the new site, so it can be accessed right away after creation.
28 | //
29 | // Sites created on a worker will be temporary and the worker won't
30 | // have the PB API key, so just make sure we have it first.
31 |
32 | if ((self.apos.options.multisite.activeEnv !== self.apos.options.multisite.debugEnv) && self.apos.baseUrl && fs.existsSync('/opt/cloud/platform-balancer-api-key')) {
33 | const key = fs.readFileSync('/opt/cloud/platform-balancer-api-key', 'utf8').trim();
34 | if (key.length) {
35 | const refreshUrl = self.apos.baseUrl + '/platform-balancer/refresh';
36 | const response = await fetch(refreshUrl, {
37 | method: 'POST',
38 | headers: {
39 | 'Content-Type': 'application/json'
40 | },
41 | body: JSON.stringify({
42 | // Since this key is visible
43 | // to the Apostrophe application code in production,
44 | // it is only capable of one thing: asking nicely that certificates be
45 | // generated, if it's time and they are needed, for sites
46 | // that are already in the system. Thus not a security risk
47 | key
48 | })
49 | });
50 | if (response.status !== 200) {
51 | throw await response.text();
52 | }
53 | }
54 | }
55 | }
56 | }
57 | };
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/sites/lib/aosSchema.js:
--------------------------------------------------------------------------------
1 | export default {
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 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms/global/ui/apos/components/AssemblyInputFontFamily.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
31 |
32 |
33 |
34 |
35 |
89 |
90 |
95 |
--------------------------------------------------------------------------------
/dashboard/modules/asset/ui/src/scss/tools/_layout.scss:
--------------------------------------------------------------------------------
1 | // Import Bootstrap grid system mixins
2 | // See https://getbootstrap.com/docs/4.1/layout/grid/ for docs.
3 | // Commented snippets from v4.1.1
4 | //
5 | // Generate semantic grid columns with these mixins.
6 | //
7 | // @mixin make-container() {
8 | // width: 100%;
9 | // padding-right: ($grid-gutter-width / 2);
10 | // padding-left: ($grid-gutter-width / 2);
11 | // margin-right: auto;
12 | // margin-left: auto;
13 | // }
14 | //
15 | //
16 | // For each breakpoint, define the maximum width of the container in a media query
17 | // @mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {
18 | // @each $breakpoint, $container-max-width in $max-widths {
19 | // @include media-breakpoint-up($breakpoint, $breakpoints) {
20 | // max-width: $container-max-width;
21 | // }
22 | // }
23 | // }
24 | //
25 | // @mixin make-row() {
26 | // display: flex;
27 | // flex-wrap: wrap;
28 | // margin-right: ($grid-gutter-width / -2);
29 | // margin-left: ($grid-gutter-width / -2);
30 | // }
31 | //
32 | // @mixin make-col-ready() {
33 | // position: relative;
34 | // // Prevent columns from becoming too narrow when at smaller grid tiers by
35 | // // always setting `width: 100%;`. This works because we use `flex` values
36 | // // later on to override this initial width.
37 | // width: 100%;
38 | // min-height: 1px; // Prevent collapsing
39 | // padding-right: ($grid-gutter-width / 2);
40 | // padding-left: ($grid-gutter-width / 2);
41 | // }
42 | //
43 | // @mixin make-col($size, $columns: $grid-columns) {
44 | // flex: 0 0 percentage($size / $columns);
45 | // // Add a `max-width` to ensure content within each column does not blow out
46 | // // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
47 | // // do not appear to require this.
48 | // max-width: percentage($size / $columns);
49 | // }
50 | //
51 | // @mixin make-col-offset($size, $columns: $grid-columns) {
52 | // $num: $size / $columns;
53 | // margin-left: if($num == 0, 0, percentage($num));
54 | // }
55 |
56 | // Grid Variables:
57 | // NOTE: Vars declared here are used in Bootstrap mixins. Some names repeated
58 | // to indicate this relationship.
59 | $grid-gutter-width: $spacing-gutter;
60 | $container-max-widths: $max-widths;
61 | $grid-breakpoints: $breakpoints;
62 | $grid-columns: $grid-columns;
63 |
64 | @mixin block ($size, $display: block) {
65 | display: $display;
66 | margin-bottom: map-get($blocks, $size);
67 | }
68 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/lib/configs/2type.js:
--------------------------------------------------------------------------------
1 | import choices from '../choices.js';
2 |
3 | const h1 = baseProperties('heading1', 'Heading 1', '[data-rich-text] h1, [contenteditable] h1', 48);
4 | const h2 = baseProperties('heading2', 'Heading 2', '[data-rich-text] h2, [contenteditable] h2', 40);
5 | const h3 = baseProperties('heading3', 'Heading 3', '[data-rich-text] h3, [contenteditable] h3', 32);
6 | const p = baseProperties('body', 'Body Copy', '[data-rich-text] p, [contenteditable] p', 16);
7 |
8 | export default {
9 | add: {
10 | ...h1.fields,
11 | ...h2.fields,
12 | ...h3.fields,
13 | ...p.fields
14 | },
15 | group: {
16 | typography: {
17 | label: 'Typography',
18 | group: {
19 | [h1.groupName]: {
20 | label: h1.label,
21 | fields: Object.keys(h1.fields)
22 | },
23 | [h2.groupName]: {
24 | label: h2.label,
25 | fields: Object.keys(h2.fields)
26 | },
27 | [h3.groupName]: {
28 | label: h3.label,
29 | fields: Object.keys(h3.fields)
30 | },
31 | [p.groupName]: {
32 | label: p.label,
33 | fields: Object.keys(p.fields)
34 | }
35 | }
36 | }
37 | }
38 | };
39 |
40 | function baseProperties(name, label, selector, fontSize) {
41 | return {
42 | name,
43 | label,
44 | selector,
45 | groupName: `${name}Group`,
46 | fields: {
47 | [`${name}Font`]: {
48 | label: 'Font',
49 | type: 'select',
50 | selector,
51 | property: 'font-family',
52 | choices: choices.BASE_FONTS,
53 | def: '"Abril Fatface", serif'
54 | },
55 | [`${name}Size`]: {
56 | label: 'Size',
57 | type: 'range',
58 | selector,
59 | property: 'font-size',
60 | unit: 'px',
61 | min: 10,
62 | max: 120,
63 | def: fontSize
64 | },
65 | [`${name}LetterSpacing`]: {
66 | label: 'Letter Spacing',
67 | type: 'range',
68 | selector,
69 | property: 'letter-spacing',
70 | unit: 'px',
71 | min: 0,
72 | max: 5,
73 | def: 0.5,
74 | step: 0.25
75 | },
76 | [`${name}Color`]: {
77 | label: 'Color',
78 | type: 'color',
79 | selector,
80 | property: 'color',
81 | def: '#000000'
82 | },
83 | [`${name}LineHeight`]: {
84 | label: 'Line Height',
85 | type: 'range',
86 | selector,
87 | property: 'line-height',
88 | min: 0.5,
89 | max: 3,
90 | def: 1.2,
91 | step: 0.1
92 | }
93 | }
94 | };
95 | };
96 |
--------------------------------------------------------------------------------
/sites/modules/asset/ui/src/scss-elements/_forms.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable scale-unlimited/declaration-strict-value, color-named, declaration-property-unit-allowed-list, declaration-no-important */
2 |
3 | @import "Modules/asset/scss/variables";
4 |
5 | .my-form {
6 | // Form resets
7 |
8 | &__fieldset,
9 | &__input,
10 | &__legend {
11 | appearance: none;
12 | }
13 |
14 | &__fieldset,
15 | &__input,
16 | &__legend {
17 | appearance: none;
18 | }
19 |
20 | &__fieldset,
21 | &__input,
22 | &__legend {
23 | box-sizing: border-box;
24 | margin: 0;
25 | padding: 0;
26 | border: none;
27 | appearance: none;
28 | background-color: transparent;
29 | }
30 |
31 | // Form element styling
32 | &__input {
33 | border: 1px solid $primary;
34 | background-color: white;
35 | border-radius: 0.25rem;
36 | color: $primary;
37 |
38 | &:focus {
39 | outline: none;
40 | box-shadow: 0 0 0 0.1rem rgba($primary, 50%);
41 | }
42 | }
43 |
44 | &__input:not([type="checkbox"], [type="radio"]) {
45 | display: block;
46 | box-sizing: border-box;
47 | width: 100%;
48 | padding: 1rem;
49 | }
50 |
51 | textarea#{&}__input {
52 | min-height: 10rem;
53 | resize: none;
54 | }
55 |
56 | &__input::placeholder {
57 | opacity: 1; /* Firefox */
58 | color: $primary;
59 | }
60 |
61 | select#{&}__input {
62 | background: url("images/down-arrow.svg") no-repeat center right 0.75rem;
63 | }
64 |
65 | &__input[type="radio"],
66 | &__input[type="checkbox"] {
67 | width: 1.5em;
68 | height: 1.5em;
69 | vertical-align: middle;
70 | }
71 |
72 | &__input[type="radio"] {
73 | border-radius: 50%;
74 |
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 |
98 | &__fieldset {
99 | .my-form__label {
100 | margin: 0;
101 | }
102 | }
103 |
104 | .apos-form-input,
105 | .apos-form-fieldset {
106 | margin-bottom: 1rem;
107 | }
108 |
109 | .my-form__check-wrapper + .my-form__check-wrapper {
110 | margin-top: 0.5rem;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/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 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/map-widget/ui/src/index.js:
--------------------------------------------------------------------------------
1 | import Map from 'ol/Map.js';
2 | import View from 'ol/View.js';
3 | import TileLayer from 'ol/layer/Tile.js';
4 | import VectorLayer from 'ol/layer/Vector.js';
5 | import XYZ from 'ol/source/XYZ.js';
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.js';
10 | import { Icon, Style } from 'ol/style.js';
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 |
--------------------------------------------------------------------------------
/sites/modules/pieces-modules/team-member-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/accordion-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sites/modules/pieces-modules/product-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/lib/configs/1base.js:
--------------------------------------------------------------------------------
1 | export default {
2 | add: {
3 | backgroundColor: {
4 | label: 'Site Background Color',
5 | type: 'color',
6 | selector: '[data-apos-refreshable]',
7 | property: 'background-color',
8 | def: '#fefefe'
9 | },
10 | borderWidth: {
11 | label: 'Site Border',
12 | type: 'range',
13 | selector: '[data-apos-refreshable]',
14 | property: 'border-width',
15 | def: '2',
16 | unit: 'px',
17 | min: 0,
18 | max: 10
19 | },
20 | borderColor: {
21 | label: 'Site Border Color',
22 | type: 'color',
23 | selector: '[data-apos-refreshable]',
24 | property: 'border-color',
25 | def: '#1e1e1e'
26 | },
27 | siteWidth: {
28 | label: 'Site Width',
29 | type: 'range',
30 | selector: '[data-apos-refreshable]',
31 | property: 'max-width',
32 | def: '90',
33 | unit: 'vw',
34 | min: 50,
35 | max: 100
36 | },
37 | contentSpacing: {
38 | label: 'Widget Spacing',
39 | type: 'range',
40 | selector: '.widget-my-spacing',
41 | property: 'margin-bottom',
42 | def: '20',
43 | unit: 'px',
44 | min: 5,
45 | max: 80
46 | }
47 | // accentColor: {
48 | // label: 'Accent Color',
49 | // type: 'color',
50 | // help: 'The accent color of butons around the site',
51 | // selector: ':root',
52 | // property: '--accent-color',
53 | // def: '#76fac1'
54 | // },
55 | // accentColorContrast: {
56 | // label: 'Accent Color Contrast',
57 | // type: 'color',
58 | // help: 'This color is used to style text inside accented buttons',
59 | // selector: ':root',
60 | // property: '--accent-color-contrast',
61 | // def: '#0b1f9c'
62 | // },
63 | // secondaryAccentColor: {
64 | // label: 'Secondary Accent Color',
65 | // type: 'color',
66 | // help: 'The accent color of butons around the site',
67 | // selector: ':root',
68 | // property: '--secondary-accent-color',
69 | // def: '#76fac1'
70 | // },
71 | // secondaryAccentColorContrast: {
72 | // label: 'Secondary Accent Color Contrast',
73 | // type: 'color',
74 | // help: 'This color is used to style text inside accented buttons',
75 | // selector: ':root',
76 | // property: '--secondary-accent-color-contrast',
77 | // def: '#0b1f9c'
78 | // }
79 | },
80 | group: {
81 | site: {
82 | label: 'Site Settings',
83 | fields: [
84 | 'backgroundColor',
85 | 'borderWidth',
86 | 'borderColor',
87 | 'siteWidth',
88 | 'contentSpacing'
89 | // 'accentColor',
90 | // 'accentColorContrast',
91 | // 'secondaryAccentColor',
92 | // 'secondaryAccentColorContrast'
93 | ]
94 | }
95 | // typography: {
96 | // label: 'Typography',
97 | // group: {
98 | // default: {
99 | // label: 'Default',
100 | // fields: [
101 | // 'baseFont',
102 | // 'baseFontSize',
103 | // 'baseFontColor'
104 | // ]
105 | // },
106 | // title: {
107 | // label: 'Title',
108 | // fields: [
109 | // 'titleFont',
110 | // 'titleFontColor'
111 | // ]
112 | // },
113 | // button: {
114 | // label: 'Buttons',
115 | // fields: [
116 | // 'buttonFont'
117 | // ]
118 | // }
119 | // }
120 | // }
121 | }
122 | };
123 |
--------------------------------------------------------------------------------
/sites/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: 0 12px 16px -4px rgb(0 0 0 / 8%), 0 4px 6px -2px rgb(0 0 0 / 3%) !default;
44 | $box-shadow-sm: 0 4px 8px -2px rgb(0 0 0 / 10%), 0 2px 4px -2px rgb(0 0 0 / 6%) !default;
45 | $box-shadow-lg: 0 20px 24px -4px rgb(0 0 0 / 8%), 0 8px 8px -4px rgb(0 0 0 / 3%) !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 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Dockerfile for self-hosted A3 assembly boilerplate. Assumes
2 | # S3 for media storage and an external mongodb server.
3 |
4 | FROM node:18-bullseye
5 | ENV APOS_MINIFY=1
6 | WORKDIR /app
7 |
8 | # Not required with latest uploadfs
9 | # RUN apt-get -y install imagemagick
10 |
11 | COPY package*.json ./
12 |
13 | ARG NPMRC
14 |
15 | # So npm install can succeed with private modules
16 | ENV NPMRC=${NPMRC}
17 |
18 | RUN echo $NPMRC > /root/.npmrc
19 |
20 | # Not npm ci because developers don't always agree that
21 | # build tools are not devDependencies
22 | RUN npm install --include=dev
23 |
24 | # Not until after npm install because developers don't
25 | # always agree that build tools are not devDependencies
26 | ENV NODE_ENV=production
27 |
28 | # For mongodb and utilities installed via "m"
29 | ENV PATH="/root/.local/bin:$PATH"
30 | RUN mkdir -p /root/.local/bin
31 |
32 | # Install mongodb command line tools
33 | RUN npm install -g m && echo y | m tools stable
34 | # Temporary: install mongodb itself purely to satisfy the multisite module,
35 | # this instance will not be used in production
36 | RUN echo y | m 5.0 && mkdir -p /root/tmp-mongodb-data
37 |
38 | # See below for comments on each
39 | ARG APOS_PREFIX
40 | ARG ENV
41 | ARG APOS_S3_REGION
42 | ARG APOS_S3_BUCKET
43 | ARG APOS_S3_KEY
44 | ARG APOS_S3_SECRET
45 | ARG APOS_DASHBOARD_HOSTNAME
46 | ARG PLATFORM_BALANCER_API_KEY
47 | ARG CDN
48 |
49 | # Can be removed if you remove the relevant code from
50 | # the boilerplate project, which is intended to pass a hint
51 | # to your load balancer that a site has been added or changed
52 | RUN mkdir -p /opt/cloud && echo $PLATFORM_BALANCER_API_KEY > /opt/cloud/platform-balancer-api-key
53 |
54 | # Prefix for site database names, e.g. "myproject-"
55 | # Dashboard will be "my-project-dashboard"
56 |
57 | ENV APOS_PREFIX=${APOS_PREFIX}
58 |
59 | # dev, staging or prod as appropriate
60 | # Must be "prod" for the "production hostname" field
61 | # in the website dashboard to take effect
62 | ENV ENV=${ENV}
63 |
64 | # e.g. dashboard.myplatformsdomainname.com
65 | ENV APOS_DASHBOARD_HOSTNAME=${APOS_DASHBOARD_HOSTNAME}
66 |
67 | # If not using S3, configure uploadfs for your preferred
68 | # storage using environment variables, or make sure
69 | # sites/uploads and dashboard/uploads are on a shared
70 | # filesystem across all load-balanced instances
71 | ENV APOS_S3_REGION=${APOS_S3_REGION}
72 | ENV APOS_S3_BUCKET=${APOS_S3_BUCKET}
73 | ENV APOS_S3_KEY=${APOS_S3_KEY}
74 | ENV APOS_S3_SECRET=${APOS_S3_SECRET}
75 |
76 | # You can remove this from the boilerplate code if you
77 | # don't plan to implement a similar API in your own
78 | # load balancer for notification that a site was changed
79 | ENV PLATFORM_BALANCER_API_KEY=${PLATFORM_BALANCER_API_KEY}
80 |
81 | # Optional, for cloudflare, cloudfront, etc.
82 | ENV CDN=${CDN}
83 |
84 | # Bring in most of the code late to benefit from caching
85 | COPY . ./
86 |
87 | # Generate a unique identifier for this particular build
88 | RUN APOS_RELEASE_ID=`cat /dev/random | tr -dc '0-9' | head -c 8` && echo ${APOS_RELEASE_ID} > ./sites/release-id && echo ${APOS_RELEASE_ID} > ./dashboard/release-id
89 |
90 | # Store shared static assets in uploadfs
91 | ENV APOS_UPLOADFS_ASSETS=1
92 |
93 | # Temporary mongod server is deliberately in the background during the build task
94 | RUN bash -c "export MONGODB_URL=mongodb://localhost:27017 && mongod --dbpath=/root/tmp-mongodb-data & ./scripts/wait-for-port 27017 && npm run build"
95 |
96 | # At runtime everything is already baked in
97 | EXPOSE 3000
98 |
99 | # Will fail unless --env is used to specify the true MONGODB_URL to "docker run"
100 | CMD bash -c "npm run production-start"
101 |
--------------------------------------------------------------------------------
/sites/index.js:
--------------------------------------------------------------------------------
1 | export default async function (site) {
2 | const config = {
3 | root: import.meta,
4 | // Theme name is globally available as apos.options.theme
5 | theme: site.theme,
6 | nestedModuleSubdirs: true,
7 | modules: {
8 | // Apostrophe module configuration
9 | // *******************************
10 | //
11 | // NOTE: most configuration occurs in the respective modules' directories.
12 | // See modules/@apostrophecms/page/index.js for an example.
13 | //
14 | // Any modules that are not present by default in Apostrophe must at least
15 | // have a minimal configuration here to turn them on: `moduleName: {}`
16 | // ***********************************************************************
17 | // `className` options set custom CSS classes for Apostrophe core widgets.
18 | '@apostrophecms/rich-text-widget': {
19 | options: {}
20 | },
21 | '@apostrophecms/image-widget': {
22 | options: {
23 | className: 'img-fluid'
24 | }
25 | },
26 | '@apostrophecms/video-widget': {
27 | options: {}
28 | },
29 |
30 | // The main form module
31 | '@apostrophecms/form': {
32 | options: {
33 | shortcut: 'a,f'
34 | }
35 | },
36 | // The form widget module, allowing editors to add forms to content areas
37 | '@apostrophecms/form-widget': {},
38 | // Form field widgets, used by the main form module to build forms.
39 | '@apostrophecms/form-text-field-widget': {},
40 | '@apostrophecms/form-textarea-field-widget': {},
41 | '@apostrophecms/form-select-field-widget': {},
42 | '@apostrophecms/form-radio-field-widget': {},
43 | '@apostrophecms/form-file-field-widget': {},
44 | '@apostrophecms/form-checkboxes-field-widget': {},
45 | '@apostrophecms/form-boolean-field-widget': {},
46 | '@apostrophecms/form-conditional-widget': {},
47 |
48 | '@apostrophecms/sitemap': {
49 | options: {
50 | excludeTypes: [ 'team-member', 'product' ]
51 | }
52 | },
53 | '@apostrophecms/seo': {},
54 | '@apostrophecms/open-graph': {},
55 |
56 | // `asset` supports the project's webpack build for client-side assets.
57 | helper: {},
58 | asset: {},
59 |
60 | // The project's first custom page type.
61 | 'default-page': {},
62 | 'content-widget-modules': {
63 | options: {
64 | ignoreNoCodeWarning: true
65 | }
66 | },
67 | 'pieces-modules': {
68 | options: {
69 | ignoreNoCodeWarning: true
70 | }
71 | },
72 | '@apostrophecms/uploadfs': {
73 | options: {
74 | uploadfs: {
75 | // Be sure to change
76 | disabledFileKey: 'CHANGEME'
77 | }
78 | }
79 | },
80 | '@apostrophecms/express': {
81 | options: {
82 | session: {
83 | secret: 'CHANGEME'
84 | }
85 | }
86 | },
87 | // Just a nice place to keep our helper functions and macros that are
88 | // used across all sites
89 |
90 | // The @apostrophecms/home-page module always exists, no need to activate it here
91 | '@apostrophecms-pro/palette': {},
92 | '@apostrophecms-pro/document-versions': {},
93 | // Use Vite bundler
94 | '@apostrophecms/vite': {},
95 | websocket: {}
96 | }
97 | };
98 | // Allow each theme to modify the configuration object,
99 | // enabling additional modules etc.
100 | const { default: theme } = await import(`./lib/theme-${site.theme}.js`);
101 | theme(site, config);
102 |
103 | return config;
104 | };
105 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms-pro/palette/lib/configs/3buttons.js:
--------------------------------------------------------------------------------
1 | import choices from '../choices.js';
2 |
3 | export default {
4 | add: {
5 | buttonVPadding: {
6 | label: 'Vertical Padding',
7 | type: 'range',
8 | selector: [ '.btn', '.my-form__submit' ],
9 | property: [ 'padding-top', 'padding-bottom' ],
10 | unit: 'px',
11 | min: 5,
12 | max: 20,
13 | def: 10,
14 | step: 1
15 | },
16 | buttonHPadding: {
17 | label: 'Horizontal Padding',
18 | type: 'range',
19 | selector: [ '.btn', '.my-form__submit' ],
20 | property: [ 'padding-left', 'padding-right' ],
21 | unit: 'px',
22 | min: 5,
23 | max: 40,
24 | def: 10,
25 | step: 1
26 | },
27 | buttonBorderRadius: {
28 | label: 'Border Radius',
29 | type: 'range',
30 | selector: [ '.btn', '.my-form__submit' ],
31 | property: 'border-radius',
32 | unit: 'px',
33 | min: 0,
34 | max: 100,
35 | def: 4,
36 | step: 1
37 | },
38 | buttonFont: {
39 | label: 'Font',
40 | type: 'select',
41 | selector: [ '.btn', '.my-form__submit' ],
42 | property: 'font-family',
43 | choices: choices.BASE_FONTS
44 | },
45 | buttonSize: {
46 | label: 'Text Size',
47 | type: 'range',
48 | selector: [ '.btn', '.my-form__submit' ],
49 | property: 'font-size',
50 | unit: 'px',
51 | min: 9,
52 | max: 24,
53 | def: 12
54 | },
55 | buttonBackground: {
56 | label: 'Button Background Color',
57 | type: 'color',
58 | selector: [ '.btn--primary', '.my-form__submit' ],
59 | property: 'background-color',
60 | def: '#76fac1'
61 | },
62 | buttonBackgroundHover: {
63 | label: 'Button Background Color (Hover)',
64 | type: 'color',
65 | selector: [ '.btn--primary:hover', '.my-form__submit:hover' ],
66 | property: 'background-color',
67 | def: '#76fac1'
68 | },
69 | buttonColor: {
70 | label: 'Button Text Color',
71 | type: 'color',
72 | selector: [ '.btn--primary', '.my-form__submit' ],
73 | property: 'color',
74 | def: '#0b1f9c'
75 | },
76 | secondaryButtonBackground: {
77 | label: 'Secondary Button Background Color',
78 | type: 'color',
79 | selector: '.btn--secondary',
80 | property: 'background-color',
81 | def: '#76fac1'
82 | },
83 | secondaryButtonBackgroundHover: {
84 | label: 'Secondary Button Background Color (Hover)',
85 | type: 'color',
86 | selector: '.btn--secondary:hover',
87 | property: 'background-color',
88 | def: '#76fac1'
89 | },
90 | secondaryButtonColor: {
91 | label: 'Secondary Button Text Color',
92 | type: 'color',
93 | selector: '.btn--secondary',
94 | property: 'color',
95 | def: '#0b1f9c'
96 | }
97 | },
98 | group: {
99 | buttons: {
100 | label: 'Buttons',
101 | group: {
102 | buttonStyle: {
103 | label: 'Button Style',
104 | fields: [
105 | 'buttonFont',
106 | 'buttonSize',
107 | 'buttonHPadding',
108 | 'buttonVPadding',
109 | 'buttonBorderRadius'
110 | ]
111 | },
112 | primary: {
113 | label: 'Primary Button',
114 | fields: [
115 | 'buttonBackground',
116 | 'buttonBackgroundHover',
117 | 'buttonColor'
118 | ]
119 | },
120 | secondary: {
121 | label: 'Secondary Button',
122 | fields: [
123 | 'secondaryButtonBackground',
124 | 'secondaryButtonBackgroundHover',
125 | 'secondaryButtonColor'
126 | ]
127 | }
128 | }
129 | }
130 | }
131 | };
132 |
--------------------------------------------------------------------------------
/sites/modules/content-widget-modules/pricing-widget/public/preview.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sites/modules/@apostrophecms/global/index.js:
--------------------------------------------------------------------------------
1 | import qs from 'qs';
2 | import linkSchema from '../../../lib/linkSchema.js';
3 | import buttonSchema from '../../../lib/buttonSchema.js';
4 |
5 | export default {
6 | fields: {
7 | add: {
8 | logo: {
9 | label: 'Logo',
10 | type: 'area',
11 | options: {
12 | max: 1,
13 | widgets: {
14 | '@apostrophecms/image': {}
15 | }
16 | }
17 | },
18 | title: {
19 | type: 'string',
20 | label: 'Website Title',
21 | required: true
22 | },
23 | headerBtns: {
24 | label: 'Header Button/s',
25 | type: 'array',
26 | titleField: 'linkText',
27 | limit: 1,
28 | fields: {
29 | add: {
30 | ...buttonSchema.button
31 | }
32 | }
33 | },
34 | headerNav: {
35 | label: 'Header Navigation Items',
36 | type: 'array',
37 | titleField: 'linkText',
38 | limit: 5,
39 | fields: {
40 | add: {
41 | ...linkSchema
42 | }
43 | }
44 | },
45 | footerNav: {
46 | label: 'Footer Navigation Items',
47 | type: 'array',
48 | titleField: 'linkText',
49 | limit: 5,
50 | fields: {
51 | add: {
52 | ...linkSchema
53 | }
54 | }
55 | },
56 | social: {
57 | label: 'Social Media Accounts',
58 | type: 'array',
59 | limit: 5,
60 | inline: true,
61 | fields: {
62 | add: {
63 | link: {
64 | type: 'url',
65 | label: 'Social link',
66 | required: true
67 | },
68 | icon: {
69 | label: 'Icon',
70 | type: 'select',
71 | required: true,
72 | choices: [
73 | {
74 | label: 'Instagram',
75 | value: 'instagram'
76 | },
77 | {
78 | label: 'Facebook',
79 | value: 'facebook'
80 | },
81 | {
82 | label: 'Twitter',
83 | value: 'twitter'
84 | },
85 | {
86 | label: 'LinkedIn',
87 | value: 'linkedin'
88 | }
89 | ]
90 | }
91 | }
92 | }
93 | },
94 | googleFontScript: {
95 | type: 'string',
96 | label: 'Google Font Script',
97 | textarea: true,
98 | htmlHelp: 'Google Fonts will provide several script tags for embedding your fonts, find the scripts here. Paste them here.'
99 | }
100 | },
101 | group: {
102 | brand: {
103 | label: 'Brand',
104 | fields: [ 'title', 'logo', 'social' ]
105 | },
106 | navigations: {
107 | label: 'Navigations',
108 | fields: [ 'headerNav', 'footerNav', 'headerBtns' ]
109 | }
110 | }
111 | },
112 | handlers(self, options) {
113 | return {
114 | beforeSave: {
115 | addFontFamilies(req, doc, options) {
116 | if (
117 | !doc.googleFontScript &&
118 | (!req.data.global || !req.data.global.googleFontScript)
119 | ) {
120 | return;
121 | }
122 | try {
123 | const choices = [];
124 | let parsedQuery = null;
125 | // parse tag for quoted strings (attribute values)
126 | const quotedStrings = doc.googleFontScript.match(/"(\\.|[^"\\])*"/g) || [];
127 | quotedStrings.forEach(str => {
128 | // get just querystring portion of url
129 | const test = str.split('"').join('').split('?');
130 | // is a query string and has a 'family' property
131 | if (test.length > 1 && qs.parse(test[1]).family) {
132 | parsedQuery = qs.parse(test[1]);
133 | }
134 | });
135 | if (parsedQuery) {
136 | if (!Array.isArray(parsedQuery.family)) {
137 | // If there is only one family you do not get an array back
138 | // from the parser since google doesn't use explicit [] syntax
139 | parsedQuery.family = [ parsedQuery.family ];
140 | }
141 | parsedQuery.family.forEach(family => {
142 | const fontFamily = family.split(':')[0];
143 | const variantChoices = [];
144 | const variants = family.split('@')[1] ? family.split('@')[1].split(';') : [ '400' ];
145 | variants.forEach(font => {
146 | const isItalic = font.split(',')[1] ? parseInt(font.split(',')[0]) === 1 : false;
147 | const weight = font.split(',')[1] ? font.split(',')[1] : (font.split(',')[0] || '400');
148 | variantChoices.push({
149 | label: `${fontFamily} / ${isItalic ? 'Italic' : 'Normal'} / ${weight};`,
150 | value: `${isItalic ? 'italic ' : ''}${weight} 14px ${fontFamily}`
151 | });
152 | });
153 | choices.push(...variantChoices);
154 | });
155 | // Google is picky about encoding so don't nitpick, do it their way.
156 | // Just block anything that would escape from the quoted attribute
157 | doc.fontFamilyParameters = parsedQuery.family.map(family => {
158 | family = family.replace(/[">]/g, '');
159 | return `family=${family}`;
160 | }).join('&');
161 | } else if (doc.googleFontScript) {
162 | // has an entry but didn't parse to anything we can use
163 | self.apos.notify(req, 'Unable to parse Google Font Script. Double check your input', {
164 | type: 'danger',
165 | icon: 'alert-circle-icon'
166 | });
167 | }
168 | doc.fontFamilies = choices;
169 | } catch (error) {
170 | throw self.apos.error('invalid', 'That is not a valid google fonts embed code');
171 | }
172 | }
173 | }
174 | };
175 | },
176 | extendMethods(self, options) {
177 | return {
178 | getBrowserData(_super, req) {
179 | const result = _super(req);
180 | result.fontFamilies = req.data.global.fontFamilies;
181 | return result;
182 | }
183 | };
184 | },
185 | init(self, options) {
186 | self.apos.schema.addFieldType({
187 | name: 'assemblyFontFamily',
188 | convert: async function (req, field, data, object) {
189 | const choices = req.data.global.fontFamilies || [];
190 | object[field.name] = self.apos.launder.select(
191 | data[field.name],
192 | choices, field.def
193 | );
194 | },
195 | vueComponent: 'AssemblyInputFontFamily'
196 | });
197 | }
198 | };
199 |
--------------------------------------------------------------------------------
/self-hosting.md:
--------------------------------------------------------------------------------
1 | # Self-hosting recommendations
2 |
3 | *This page is about self-hosting Apostrophe Assembly in staging and production environments.
4 | For a **development** environment, we recommend running Apostrophe Assembly directly on
5 | MacOS, Linux, or Windows Subsystem for Linux.*
6 |
7 | A sample `Dockerfile` is provided with this project and can be used for self-hosting.
8 | See also the provided `.dockerignore` file. The rest of this document will assume
9 | you are basing your deployment plan on these.
10 |
11 | ## Working with the provided `Dockerfile`
12 |
13 | The provided `Dockerfile` assumes you have obtained an Amazon S3 storage bucket and set up
14 | MongoDB hosting via MongoDB Atlas. The `Dockerfile` is designed to support running as many
15 | instances as you wish on separate servers and load-balancing them via a mechanism of your
16 | choice.
17 |
18 | Typical Docker `build` and `run` commands look like:
19 |
20 | ```bash
21 | # build command
22 | docker build -t a3-assembly-boilerplate . \
23 | --build-arg="NPMRC=//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN_GOES_HERE" \
24 | --build-arg="ENV=prod" --build-arg="APOS_PREFIX=YOUR-PREFIX-GOES-HERE-" \
25 | --build-arg="DASHBOARD_HOSTNAME=dashboard.YOUR-DOMAIN-NAME-GOES-HERE.com" \
26 | --build-arg="PLATFORM_BALANCER_API_KEY=YOUR-STRING-GOES-HERE" \
27 | --build-arg="APOS_S3_REGION=YOURS-GOES-HERE" \
28 | --build-arg="APOS_S3_BUCKET=YOURS-GOES-HERE" \
29 | --build-arg="APOS_S3_KEY=YOURS-GOES-HERE" \
30 | --build-arg="APOS_S3_SECRET=YOURS-GOES-HERE"
31 |
32 | # run command
33 | docker run -it --env MONGODB_URL=YOUR-MONGODB-ATLAS-URL-GOES-HERE a3-assembly-boilerplate
34 | ```
35 |
36 | To avoid passing the real MongoDB URL to the build task, currently the provided Dockerfile uses a
37 | temporary instance of `mongod` to satisfy a requirement that it be present for the build task.
38 |
39 | An npm token is required to successfully `npm install` the private packages inside the
40 | image during the build.
41 |
42 | S3 credentials are passed to the build so that the static assets can be mirrored to S3, however
43 | at a cost in performance this can be avoided by removing `APOS_UPLOADFS_ASSETS=1` from
44 | the `Dockerfile` and removing the references to these environment variables as well. Note
45 | that you will still need S3 credentials in the `run` command, unless you arrange for
46 | `dashboard/public/uploads` and `sites/public/uploads` to be persistent volumes on a
47 | filesystem shared by all instances. This is slow, so we recommend using S3 or configuring
48 | a different [uploadfs backend](https://github.com/apostrophecms/uploadfs) such as
49 | Azure Blob Storage or Google Cloud Storage.
50 |
51 | If you provide a `PLATFORM_BALANCER_API_KEY`, then your dashboard hostname must
52 | also accept a JSON-encoded `POST` request to `/platform-balancer/refresh` with a single `key`
53 | parameter. You can use that request as a trigger to refresh your list of sites when an admin adds
54 | or edits a site in the dashboard. If you don't want to do this, just don't set the variable.
55 |
56 | ## Understanding our typical deployment process
57 |
58 | In setting up your own self-hosted process, you may find it helpful to better understand
59 | our own process. Toward that end, here is an overview:
60 |
61 | * All of the content that users create while editing is stored in the external MongoDB,
62 | except media which is stored in the external S3 buckets. The application servers are 100%
63 | stateless. This is essential so that you can load-balance application servers without
64 | worrying about any differences. The only way they ever change is when a new image build
65 | is pushed (see below).
66 | * Accordingly, zero code changes are ever made within the running container.
67 | * Static assets, e.g. the js and css bundles loaded on the page and related files,
68 | are pushed to S3 during the asset build (if using APOS_UPLOADFS_ASSETS=1 and the S3
69 | variables) or served directly by Express from the container image (if you don't set
70 | that variable).
71 | * There is a randomly generated release-id, as you've seen in the `Dockerfile, that
72 | uniquely identifies this particular build so it can always find its static assets
73 | in S3 and including that id in `sites/release-id` and `dashboard/release-id` in the
74 | container itself guarantees the running application sees the id that the build task
75 | generated.
76 | * Our normal practice is to set up github webhooks to trigger a container build and
77 | redeployment whenever the staging or production github branches are updated.
78 | * We then use github deploy keys to allow our build worker to git clone the code.
79 | * We then use `docker save` and `docker load` to pipe the new container image from
80 | our build worker to our application servers, stop the old container and start the
81 | new one. You might prefer to use your own container registry.
82 | * If an admin user creates a site in the dashboard and gives it the "short name"
83 | `mysite`, then Apostrophe automatically expects to receive HTTP requests for
84 | `mysite.yourdomainhere.com`, based on your `DASHBOARD_HOSTNAME`, which should be
85 | `dashboard.yourdomainhere.com` unless this pattern has been adjusted (see the
86 | `@apostrophecms-pro/multisite` documentation).
87 | * If that pattern suffices for your needs, you can set up your reverse proxy server
88 | with a DNS wildcard "A" record and a wildcard HTTPS certificate and pass all
89 | traffic to Apostrophe.
90 | * Many of our customers need separate production domain names per site. For this
91 | use case, the reverse proxy server configuratino must be rebuilt dynamically when
92 | sites are added, updated or removed via the dashboard, paying special attention to the
93 | `prodHostname` property of each site piece in the dashboard database that
94 | is not marked `archived: true`.
95 | * Our reverse proxy update software also generates certificates on the fly
96 | using letsencrypt.
97 |
98 | ## A sample query to identify live sites
99 |
100 | With regard to updating reverse proxy server configuration on the fly, the specifics
101 | are up to you, but it is useful to have a way to query Apostrophe for a list of all
102 | live websites in the dashboard.
103 |
104 | First, identify the correct MongoDB database name. If you are setting `APOS_PREFIX` to
105 | `mycompany-`, then the database you need is `mycompany-dashboard`.
106 |
107 | Then, make a query as follows:
108 |
109 | ```javascript
110 | const sites = await docs.find({
111 | type: 'site',
112 | archived: {
113 | $ne: true
114 | }
115 | ).sort({ _id: 1 }).toArray();
116 | ```
117 |
118 | You can then examine the `shortName` and `prodHostname` properties of each site to
119 | set up appropriate routing to your application servers.
120 |
121 | ## Routing for large numbers of sites
122 |
123 | Because Apostrophe can serve many sites from a single process, a single EC2 t2.medium
124 | instance can typically handle about 25 sites before RAM or CPU becomes an issue, depending
125 | greatly of course on the amount of traffic involved. This is good news for lower costs.
126 |
127 | However, if you have 100 sites and use a simple round robin load balancing strategy, you
128 | will have a problem because all 100 sites will soon be consuming RAM on all of your
129 | application servers.
130 |
131 | So when operating at this scale, we recommend generating separate `nginx` configurations
132 | for each site, pointing to just two application server backends each (two per site to provide
133 | high availability).
134 |
135 | The following code snippet is useful in determining which servers to assign based on the
136 | number of application servers available and the `_id` property of an individual site:
137 |
138 | ```javascript
139 | function pickBackends(appServerIps, _id) {
140 | const random = _id.substring(_id.length - 8, _id.length);
141 | const n = parseInt(random, 36);
142 | return [ appServerIps[n % appServerIps.length], appServerIps[(n + 1) % appServerIps.length] ];
143 | }
144 | ```
--------------------------------------------------------------------------------