├── 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 |
2 | {% component "map-widget:map" with { widget: data.widget } %} 3 |
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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 |
12 |

404 Error.

13 |

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

14 |
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 |
14 |
15 |
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 | 2 | 3 | -------------------------------------------------------------------------------- /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 |
2 | {% for member in data.widget._teamMembers %} 3 |
4 | {% set profileImage = apos.image.first(member.profileImage) %} 5 | {{ memeber.title }} 6 |

{{ member.title }}

7 |

{{ member.workTitle }}

8 |
9 | {% endfor %} 10 |
11 | -------------------------------------------------------------------------------- /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 |
2 | {% for item in data.widget.accordions %} 3 |
4 |

5 | 8 |

9 | 12 |
13 | {% endfor %} 14 |
-------------------------------------------------------------------------------- /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 |
8 | {% render image.render( 9 | widget = data.widget, 10 | options = data.options, 11 | manager = data.manager, 12 | contextOptions = data.contextOptions 13 | )%} 14 |
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 | 2 | 3 | -------------------------------------------------------------------------------- /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 |
2 |
3 | {% area data.widget, 'intro' %} 4 |
5 | {% for card in data.widget.cards %} 6 |
7 |
8 |
9 | {{ card.label }} 10 |
11 | {% area card, 'content' %} 12 |
    13 | {% for feature in card.features %} 14 |
  • {{ feature.title }}
  • 15 | {% endfor %} 16 |
17 |
18 | {% area card, 'buttons' %} 19 |
20 | {% endfor %} 21 |
22 |
23 |
-------------------------------------------------------------------------------- /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 |
11 | 12 | 13 | 14 |
15 | {% endfragment %} 16 | 17 | {# 18 | {% import 'button-arrows.html' as buttonArrows %} 19 | 20 | {% render buttonArrows.swiper() %} 21 | #} 22 | 23 | {% fragment swiper() %} 24 |
25 | {% render button('prev') %} 26 | {% render button('next') %} 27 |
28 | {% endfragment %} -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 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 | 2 | 3 | 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 |
21 |
22 | {% area data.widget, 'form' %} 23 |
24 | {% if layout === 'column' and data.widget._backgroundImage.length %} 25 |
26 | {% set image = apos.image.first(data.widget._backgroundImage) %} 27 | 28 | {{ image.title }} 29 |
30 | {% endif %} 31 |
-------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | {{ attachment._alt or '' }} 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 | 3 | Bitmap 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sites/modules/content-widget-modules/custom-form-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 4 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 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 | 3 | Artboard Copy 9 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sites/modules/content-widget-modules/accordion-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sites/modules/pieces-modules/product-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard Copy 8 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | $$ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | $$$ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /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 | 3 | Artboard Copy 6 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /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 | ``` --------------------------------------------------------------------------------