├── .env ├── .github └── workflows │ └── deployApp.yml ├── .gitignore ├── .husky └── pre-commit ├── README.md ├── build ├── _redirects ├── index.html ├── manifest.json └── static │ ├── component-definition.json │ ├── filter-definition.json │ ├── media │ ├── AktivGroteskCorp-Medium.26fa917a.woff │ ├── AktivGroteskCorp-Regular.ffbc9aa3.woff │ ├── Back.881c26e9.svg │ ├── Poppins-Medium.673ed423.ttf │ ├── Poppins-Regular.35d26b78.ttf │ ├── icon-loading.2c27a19f.svg │ ├── wknd-card.42e74d83.jpeg │ └── wknd-logo-dk.c361d33f.svg │ └── model-definition.json ├── deploy └── script.sh ├── gulpfile.js ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static │ ├── component-definition.json │ ├── filter-definition.json │ └── model-definition.json ├── src ├── App.js ├── App.scss ├── App.test.js ├── api │ └── useGraphQL.js ├── components │ ├── About.jsx │ ├── AdventureDetail.jsx │ ├── AdventureDetail.scss │ ├── Adventures.jsx │ ├── Adventures.scss │ ├── ArticleDetail.jsx │ ├── Articles.jsx │ ├── Articles.scss │ ├── Home.jsx │ ├── Home.scss │ ├── Teaser.jsx │ ├── Teaser.scss │ └── base │ │ ├── Container.jsx │ │ ├── Error.js │ │ ├── Image.jsx │ │ ├── Loading.js │ │ ├── Text.jsx │ │ └── Title.jsx ├── hooks │ ├── index.js │ └── useSparkleAppUrl.js ├── images │ ├── Back.svg │ ├── footer.jpeg │ ├── icon-close.svg │ ├── icon-loading.svg │ ├── wknd-card.jpeg │ └── wknd-logo-dk.svg ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── setupTests.js ├── styles │ ├── AktivGroteskCorp-Medium.woff │ ├── AktivGroteskCorp-Regular.woff │ ├── Poppins-Medium.ttf │ ├── Poppins-Regular.ttf │ ├── _fonts.scss │ └── _variables.scss └── utils │ ├── commons.js │ ├── fetchData.js │ └── renderRichText.js ├── webpack.config.js └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_DEFAULT_AUTHOR_HOST=https://author-p7452-e12433.adobeaemcloud.com 2 | REACT_APP_DEFAULT_PUBLISH_HOST=https://publish-p7452-e12433.adobeaemcloud.com 3 | REACT_APP_GRAPHQL_ENDPOINT=/content/graphql/global/endpoint.json 4 | # disable build source maps 5 | INLINE_RUNTIME_CHUNK=false 6 | GENERATE_SOURCEMAP=false 7 | SKIP_PREFLIGHT_CHECK=true 8 | REACT_APP_SERVICE_TOKEN= 9 | -------------------------------------------------------------------------------- /.github/workflows/deployApp.yml: -------------------------------------------------------------------------------- 1 | # Flow for deploying the app to https://ue-remote-app.adobe.net 2 | name: Deploy to https://ue-remote-app.adobe.net 3 | on: 4 | push: 5 | branches: 6 | - prod 7 | paths: 8 | - 'build/index.html' 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Deploy to https://ue-remote-app.adobe.net 15 | uses: nwtgck/actions-netlify@v1.2.3 16 | with: 17 | publish-dir: './build' 18 | production-branch: prod 19 | github-token: ${{ secrets.AUTH_TOKEN }} 20 | deploy-message: "Deploy from GitHub Actions" 21 | enable-pull-request-comment: false 22 | enable-commit-comment: true 23 | overwrites-pull-request-comment: true 24 | env: 25 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 26 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 27 | timeout-minutes: 1 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build/*.* 13 | /build/static/* 14 | !/build/manifest.json 15 | !/build/static/*.json 16 | !/build/fonts 17 | !/build/fonts.css 18 | !/build/static/media 19 | !/build/index.html 20 | 21 | #auth_credentials 22 | /src/auth 23 | 24 | # misc 25 | .DS_Store 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | .yalc 35 | 36 | # local Netlify folder 37 | .netlify 38 | 39 | # demo app 40 | auth/ 41 | bin/ 42 | lib/ 43 | yalc.lock 44 | .idea/ 45 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn install && npm run build && git add -A ./build/index.html 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Editor Sample App 2 | 3 | ## Using the Sample App 4 | This Sample App is hosted at https://ue-remote-app.adobe.net. 5 | Per Default the content is retrieved and written back to our Production Demo Environment: 6 | ``` 7 | authorHost=https://author-p7452-e12433.adobeaemcloud.com 8 | publishHost=https://publish-p7452-e12433.adobeaemcloud.com 9 | service= // using defualt built in Universal Editor 10 | protocol=aem // protocol to work on AEMCS 11 | ``` 12 | If you'd like to retrieve content from another environment add authorHost & publishHost as query parameters, e.g. 13 | 14 | [https://ue-remote-app.adobe.net?authorHost=https://author-p15902-e145656-cmstg.adobeaemcloud.com&publishHost=https://publish-p15902-e145656-cmstg.adobeaemcloud.com](https://ue-remote-app.adobe.net?authorHost=https://author-p15902-e145656-cmstg.adobeaemcloud.com&publishHost=https://publish-p15902-e145656-cmstg.adobeaemcloud.com) 15 | 16 | respectively if run on local dev environment: 17 | 18 | [https://localhost:3000?authorHost=https://author-p15902-e145656-cmstg.adobeaemcloud.com&publishHost=https://publish-p15902-e145656-cmstg.adobeaemcloud.com](https://localhost:3000?authorHost=https://author-p15902-e145656-cmstg.adobeaemcloud.com&publishHost=https://publish-p15902-e145656-cmstg.adobeaemcloud.com) 19 | 20 | ## Prerequisites 21 | 22 | - AEMCS instance is available 23 | - WKND project is installed on the instance 24 | - CORS enabled on AEM instance for the app 25 | - For local development with editor, ensure app is using *https* 26 | 27 | ## Available Scripts 28 | 29 | In the project directory, you can run: 30 | 31 | ### `yarn start` 32 | 33 | Runs the app in the development mode.\ 34 | Open [https://localhost:3000](https://localhost:3000) to view it in your browser. 35 | 36 | The page will reload when you make changes.\ 37 | You may also see any lint errors in the console. 38 | 39 | ### `yarn build` 40 | 41 | Builds the app for production to the `build` folder. 42 | Utilize a gulp task to bundle all the JS and CSS files in the static build folder into the single main `index.html` file. 43 | This is useful for having the `index.html` bundle file automatically deployed on `https://ue-remote-app.adobe.net` when pushing new changes on the `main` branch. 44 | 45 | This command is executed automatically before each commit by the `pre-commit` script. 46 | 47 | ## Automatic deployment flow 48 | 49 | The application uses the husky package (https://www.npmjs.com/package/husky), for adding a pre-commit script, located in the `.husky` folder. 50 | The `pre-commit` script will be run before each commit. It will build the project and will add the build bundle from `build/index.html` to the commit. 51 | We expose this bundle to GitHub. This is happening due to the usage of internal artifactory packages (we cannot build the project on a deployment environment). 52 | 53 | The flow is that we build the application locally and deploy the bundle through GitHub workflow to https://ue-remote-app.adobe.net, on each PR merged to the `main` branch. 54 | 55 | ## Manual deployments 56 | 57 | ### Prerequisites 58 | Install Netlify CLI 59 | 60 | `npm install netlify-cli -g` 61 | 62 | Set the following environment variables in your terminal settings (for https://ue-remote-app.adobe.net): 63 | 64 | `NETLIFY_AUTH_TOKEN = ` 65 | 66 | `NETLIFY_SITE_ID = ` 67 | 68 | ### Deploy commands 69 | Run in project root: 70 | 71 | `npm run deploy` - deploy the app at any point to a non-production link, e.g https://62ff59a019923a6f7aec439d--prismatic-panda-c194c0.netlify.app/. 72 | 73 | `npm run deploy prod` - deploy the app to the production link https://ue-remote-app.adobe.net (this is usually not needed, the application is automatically deployed on every PR merged to the `main` branch). 74 | 75 | If case of permission issues, run `chmod +x deploy/script.sh` at the root of the project. 76 | 77 | -------------------------------------------------------------------------------- /build/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /build/static/component-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "title": "General Components", 5 | "id": "general", 6 | "components": [ 7 | { 8 | "title": "Text", 9 | "id": "text", 10 | "plugins": { 11 | "aem": { 12 | "page": { 13 | "resourceType": "wknd/components/text", 14 | "template": { 15 | "text": "Default Text" 16 | } 17 | } 18 | }, 19 | "aem65": { 20 | "page": { 21 | "resourceType": "wknd/components/text", 22 | "template": { 23 | "text": "Default Text" 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | { 30 | "title": "Title", 31 | "id": "title", 32 | "plugins": { 33 | "aem": { 34 | "page": { 35 | "resourceType": "wknd/components/title", 36 | "template": { 37 | "jcr:title": "Default Title" 38 | } 39 | } 40 | }, 41 | "aem65": { 42 | "page": { 43 | "resourceType": "wknd/components/title", 44 | "template": { 45 | "jcr:title": "Default Title" 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | { 52 | "title": "Image", 53 | "id": "image", 54 | "plugins": { 55 | "aem": { 56 | "page": { 57 | "resourceType": "wknd/components/image", 58 | "template": { 59 | "fileReference": "/content/dam/wknd-shared/en/magazine/arctic-surfing/camp-tent.jpg" 60 | } 61 | } 62 | }, 63 | "aem65": { 64 | "page": { 65 | "resourceType": "wknd/components/image", 66 | "template": { 67 | "fileReference": "/content/dam/wknd-shared/en/magazine/arctic-surfing/camp-tent.jpg" 68 | } 69 | } 70 | } 71 | } 72 | }, 73 | { 74 | "title": "List", 75 | "id": "list", 76 | "plugins": {}, 77 | "model": { 78 | "uri": "", 79 | "fields": [] 80 | } 81 | }, 82 | { 83 | "title": "Container", 84 | "id": "container", 85 | "plugins": { 86 | "aem": { 87 | "page": { 88 | "resourceType": "wknd/components/container" 89 | } 90 | }, 91 | "aem65": { 92 | "page": { 93 | "resourceType": "wknd/components/container" 94 | } 95 | } 96 | } 97 | } 98 | ] 99 | }, 100 | { 101 | "title": "Advanced Components", 102 | "id": "advanced", 103 | "components": [ 104 | { 105 | "title": "Rich Text", 106 | "id": "richtext", 107 | "plugins": { 108 | "aem": { 109 | "page": { 110 | "resourceType": "wknd/components/text", 111 | "template": { 112 | "textIsRich": true, 113 | "text": "

Default Richtext

" 114 | } 115 | } 116 | }, 117 | "aem65": { 118 | "page": { 119 | "resourceType": "wknd/components/text", 120 | "template": { 121 | "textIsRich": true, 122 | "text": "

Default Richtext

" 123 | } 124 | } 125 | } 126 | } 127 | } 128 | ] 129 | } 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /build/static/filter-definition.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "container", 4 | "components":null 5 | } 6 | ] -------------------------------------------------------------------------------- /build/static/media/AktivGroteskCorp-Medium.26fa917a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/build/static/media/AktivGroteskCorp-Medium.26fa917a.woff -------------------------------------------------------------------------------- /build/static/media/AktivGroteskCorp-Regular.ffbc9aa3.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/build/static/media/AktivGroteskCorp-Regular.ffbc9aa3.woff -------------------------------------------------------------------------------- /build/static/media/Back.881c26e9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build/static/media/Poppins-Medium.673ed423.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/build/static/media/Poppins-Medium.673ed423.ttf -------------------------------------------------------------------------------- /build/static/media/Poppins-Regular.35d26b78.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/build/static/media/Poppins-Regular.35d26b78.ttf -------------------------------------------------------------------------------- /build/static/media/icon-loading.2c27a19f.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /build/static/media/wknd-card.42e74d83.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/build/static/media/wknd-card.42e74d83.jpeg -------------------------------------------------------------------------------- /build/static/media/wknd-logo-dk.c361d33f.svg: -------------------------------------------------------------------------------- 1 | Asset 2 -------------------------------------------------------------------------------- /build/static/model-definition.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "text", 4 | "fields": [ 5 | { 6 | "component": "text-input", 7 | "name": "text", 8 | "value": "Text", 9 | "label": "Custom Text", 10 | "valueType": "string" 11 | } 12 | ] 13 | }, 14 | { 15 | "id": "title", 16 | "fields": [ 17 | { 18 | "component": "select", 19 | "name": "type", 20 | "value": "h1", 21 | "label": "Type", 22 | "valueType": "string", 23 | "options": [ 24 | { "name": "h1", "value": "h1" }, 25 | { "name": "h2", "value": "h2" }, 26 | { "name": "h3", "value": "h3" }, 27 | { "name": "h4", "value": "h4" }, 28 | { "name": "h5", "value": "h5" }, 29 | { "name": "h6", "value": "h6" } 30 | ] 31 | } 32 | ] 33 | }, 34 | { 35 | "id": "richtext", 36 | "fields": [ 37 | { 38 | "component": "text-area", 39 | "name": "text", 40 | "value": "Text", 41 | "label": "Custom Richtext", 42 | "valueType": "string" 43 | } 44 | ] 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /deploy/script.sh: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | # Install Netlify CLI 4 | # npm install netlify-cli -g 5 | 6 | # Set the following environment variables in your terminal settings 7 | # NETLIFY_AUTH_TOKEN = 8 | # NETLIFY_SITE_ID = 9 | 10 | # Run in project root: 11 | # - If you want to deploy the app to a non-production link, e.g https://62ff59a019923a6f7aec439d--prismatic-panda-c194c0.netlify.app/. 12 | # - If you want to deploy the app to the production link https://prismatic-panda-c194c0.netlify.app/. 13 | 14 | # If case of permission issues, run at the root of the project. 15 | 16 | if npm run build; then 17 | echo "Build created successfully" 18 | netlify link 19 | if [ "$1" = "prod" ]; then 20 | netlify deploy --prod 21 | else 22 | netlify deploy 23 | fi 24 | else 25 | echo "Build failed, cannot be deployed" 26 | fi -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // bundles all the JS and CSS files in the static build folder into the main index.html file 2 | 3 | const gulp = require('gulp'); 4 | const inlinesource = require('gulp-inline-source'); 5 | const replace = require('gulp-replace'); 6 | 7 | gulp.task('default', () => { 8 | return gulp 9 | .src('./build/*.html') 10 | .pipe(replace('.js">', '.js" inline>')) 11 | .pipe(replace('rel="stylesheet">', 'rel="stylesheet" inline>')) 12 | .pipe( 13 | inlinesource({ 14 | compress: false, 15 | ignore: ['png'], 16 | }) 17 | ) 18 | .pipe(gulp.dest('./build')); 19 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "PORT=3000 HTTPS=true react-scripts start", 7 | "build": "react-scripts build && npx gulp", 8 | "test": "react-scripts test", 9 | "eject": "react-scripts eject", 10 | "server": "node server/index.js", 11 | "deploy": "./deploy/script.sh", 12 | "postinstall": "husky install" 13 | }, 14 | "dependencies": { 15 | "@adobe/aem-headless-client-js": "^3.1.0", 16 | "@adobe/aem-headless-client-nodejs": "^1.1.0", 17 | "@testing-library/jest-dom": "^5.16.4", 18 | "@testing-library/react": "^13.3.0", 19 | "@testing-library/user-event": "^13.5.0", 20 | "buffer": "^6.0.3", 21 | "express-session": "^1.17.3", 22 | "gulp": "^4.0.2", 23 | "gulp-inline-source": "^4.0.0", 24 | "gulp-replace": "^1.1.3", 25 | "http-proxy-middleware": "^2.0.6", 26 | "miragejs": "^0.1.45", 27 | "penpal": "^6.2.2", 28 | "process": "^0.11.10", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "react-helmet-async": "^1.3.0", 32 | "react-penpal": "^1.0.4", 33 | "react-router-dom": "^6.3.0", 34 | "react-scripts": "4.0.3", 35 | "sass": "^1.53.0", 36 | "stream-browserify": "^3.0.0", 37 | "util": "^0.12.4", 38 | "web-vitals": "^2.1.4", 39 | "zustand": "^4.1.1" 40 | }, 41 | "devDependencies": { 42 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 43 | "husky": "^8.0.1" 44 | }, 45 | "eslintConfig": { 46 | "extends": [ 47 | "react-app", 48 | "react-app/jest" 49 | ] 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version" 61 | ] 62 | }, 63 | "engines": { 64 | "node": ">=16.0.0 <=16.20.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 | 27 | 28 | 29 | React App 30 | 41 | 42 | 43 | 44 | 45 |
46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/static/component-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "title": "General Components", 5 | "id": "general", 6 | "components": [ 7 | { 8 | "title": "Text", 9 | "id": "text", 10 | "plugins": { 11 | "aem": { 12 | "page": { 13 | "resourceType": "wknd/components/text", 14 | "template": { 15 | "text": "Default Text" 16 | } 17 | } 18 | }, 19 | "aem65": { 20 | "page": { 21 | "resourceType": "wknd/components/text", 22 | "template": { 23 | "text": "Default Text" 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | { 30 | "title": "Title", 31 | "id": "title", 32 | "plugins": { 33 | "aem": { 34 | "page": { 35 | "resourceType": "wknd/components/title", 36 | "template": { 37 | "jcr:title": "Default Title" 38 | } 39 | } 40 | }, 41 | "aem65": { 42 | "page": { 43 | "resourceType": "wknd/components/title", 44 | "template": { 45 | "jcr:title": "Default Title" 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | { 52 | "title": "Image", 53 | "id": "image", 54 | "plugins": { 55 | "aem": { 56 | "page": { 57 | "resourceType": "wknd/components/image", 58 | "template": { 59 | "fileReference": "/content/dam/wknd-shared/en/magazine/arctic-surfing/camp-tent.jpg" 60 | } 61 | } 62 | }, 63 | "aem65": { 64 | "page": { 65 | "resourceType": "wknd/components/image", 66 | "template": { 67 | "fileReference": "/content/dam/wknd-shared/en/magazine/arctic-surfing/camp-tent.jpg" 68 | } 69 | } 70 | } 71 | } 72 | }, 73 | { 74 | "title": "List", 75 | "id": "list", 76 | "plugins": {}, 77 | "model": { 78 | "uri": "", 79 | "fields": [] 80 | } 81 | }, 82 | { 83 | "title": "Container", 84 | "id": "container", 85 | "plugins": { 86 | "aem": { 87 | "page": { 88 | "resourceType": "wknd/components/container" 89 | } 90 | }, 91 | "aem65": { 92 | "page": { 93 | "resourceType": "wknd/components/container" 94 | } 95 | } 96 | } 97 | } 98 | ] 99 | }, 100 | { 101 | "title": "Advanced Components", 102 | "id": "advanced", 103 | "components": [ 104 | { 105 | "title": "Rich Text", 106 | "id": "richtext", 107 | "plugins": { 108 | "aem": { 109 | "page": { 110 | "resourceType": "wknd/components/text", 111 | "template": { 112 | "textIsRich": true, 113 | "text": "

Default Richtext

" 114 | } 115 | } 116 | }, 117 | "aem65": { 118 | "page": { 119 | "resourceType": "wknd/components/text", 120 | "template": { 121 | "textIsRich": true, 122 | "text": "

Default Richtext

" 123 | } 124 | } 125 | } 126 | } 127 | } 128 | ] 129 | } 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /public/static/filter-definition.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "container", 4 | "components":null 5 | } 6 | ] -------------------------------------------------------------------------------- /public/static/model-definition.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "text", 4 | "fields": [ 5 | { 6 | "component": "text-input", 7 | "name": "text", 8 | "value": "Text", 9 | "label": "Custom Text", 10 | "valueType": "string" 11 | } 12 | ] 13 | }, 14 | { 15 | "id": "title", 16 | "fields": [ 17 | { 18 | "component": "select", 19 | "name": "type", 20 | "value": "h1", 21 | "label": "Type", 22 | "valueType": "string", 23 | "options": [ 24 | { "name": "h1", "value": "h1" }, 25 | { "name": "h2", "value": "h2" }, 26 | { "name": "h3", "value": "h3" }, 27 | { "name": "h4", "value": "h4" }, 28 | { "name": "h5", "value": "h5" }, 29 | { "name": "h6", "value": "h6" } 30 | ] 31 | } 32 | ] 33 | }, 34 | { 35 | "id": "richtext", 36 | "fields": [ 37 | { 38 | "component": "text-area", 39 | "name": "text", 40 | "value": "Text", 41 | "label": "Custom Richtext", 42 | "valueType": "string" 43 | } 44 | ] 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import {React} from "react"; 2 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 3 | import {BrowserRouter as Router, Route, Routes} from "react-router-dom"; 4 | import Home from "./components/Home"; 5 | import AdventureDetail from "./components/AdventureDetail"; 6 | import Articles from "./components/Articles"; 7 | import ArticleDetail from "./components/ArticleDetail"; 8 | import About from "./components/About"; 9 | import {getAuthorHost, getProtocol, getService} from "./utils/fetchData"; 10 | import logo from "./images/wknd-logo-dk.svg"; 11 | import "./App.scss"; 12 | // import { useSparkleAppUrl } from "./hooks"; 13 | 14 | const NavMenu = () => ( 15 | 22 | ); 23 | 24 | const Header = () => { 25 | // const sparkleAppUrl = useSparkleAppUrl(); 26 | return ( 27 |
28 | {/*WKND Logo*/} 29 | WKND Logo 30 | 31 | 32 |
33 | ); 34 | }; 35 | 36 | const Footer = () => ( 37 |
38 | WKND Logo 39 | 40 | Copyright © 2023 Adobe. All rights reserved 41 |
42 | ); 43 | 44 | function App() { 45 | 46 | return ( 47 | 48 |
49 | 50 | 51 | { getService() && } 52 | 53 | 54 |
55 |
56 |
57 | 58 | } /> 59 | } /> 60 | } /> 61 | } /> 62 | } /> 63 | 64 |
65 | 66 |
67 |
68 |
69 |
70 | ); 71 | } 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | /* Normalize */ 9 | @import './styles/variables'; 10 | @import './styles/fonts'; 11 | 12 | body { 13 | background-color: $black; 14 | font-family: $font-family-base; 15 | margin: 0; 16 | padding: 0; 17 | font-size: $font-size-base; 18 | text-align: left; 19 | color: $white; 20 | line-height: $line-height-base; 21 | } 22 | 23 | // Headings 24 | // ------------------------- 25 | 26 | h1, h2, h3, h4, h5, h6, 27 | .h1, .h2, .h3, .h4, .h5, .h6 { 28 | line-height: $line-height-base; 29 | font-weight: 500; 30 | } 31 | 32 | h1, .h1, 33 | h2, .h2, 34 | h3, .h3 { 35 | margin-top: $line-height-computed; 36 | margin-bottom: calc($line-height-computed / 2); 37 | } 38 | 39 | h1, .h1 { font-size: $font-size-h1; } 40 | h2, .h2 { font-size: $font-size-h2; } 41 | h3, .h3 { font-size: $font-size-h3; } 42 | h4, .h4 { font-size: $font-size-h4; } 43 | h5, .h5 { font-size: $font-size-h5; } 44 | h6, .h6 { font-size: $font-size-h6; } 45 | 46 | a { 47 | text-decoration: none; 48 | } 49 | 50 | h1 a, h2 a, h3 a { 51 | color: $text-color; 52 | } 53 | 54 | h1 u, h2 u, h3 u { 55 | text-decoration: none; 56 | border-bottom: 1px #ededed solid; 57 | } 58 | 59 | // Body text 60 | // ------------------------- 61 | 62 | p { 63 | margin: 0 0 calc($line-height-computed / 2); 64 | font-size: $font-size-base; 65 | line-height: $line-height-base + 0.75; 66 | text-align: justify; 67 | } 68 | 69 | a { 70 | cursor: pointer; 71 | } 72 | 73 | ul { 74 | list-style-position: inside; 75 | } 76 | 77 | ol, ul { 78 | padding-left: 0; 79 | margin-bottom: 0; 80 | list-style: none; 81 | } 82 | 83 | hr { 84 | border: none; 85 | border-bottom: 1px solid $gray; 86 | margin: 0; 87 | } 88 | 89 | button { 90 | background-color: $button-color; 91 | color: $black; 92 | padding: 12px 40px; 93 | font-size: $font-size-base; 94 | font-family: $font-family-base; 95 | border: 1px solid $black; 96 | border-radius: 4px; 97 | min-width: 4rem; 98 | margin-right: 1rem; 99 | cursor: pointer; 100 | 101 | &:hover { 102 | background-color: #C4E018; 103 | } 104 | 105 | &:focus, &:active{ 106 | background-color: #ABC123; 107 | } 108 | &.dark { 109 | border: 1px solid $white; 110 | background: inherit; 111 | color: $white; 112 | } 113 | } 114 | 115 | .pill { 116 | padding: 11px 16px 13px; 117 | border-radius: 20px 118 | } 119 | .pill.default { 120 | border: 1px solid #696969; 121 | } 122 | 123 | // Error 124 | .error { 125 | /*position: absolute; 126 | top: 50%; 127 | left: 0;*/ 128 | margin-top: 2em; 129 | width: 100%; 130 | text-align: left; 131 | 132 | &-message { 133 | color: $red; 134 | } 135 | 136 | } 137 | 138 | // Loading 139 | .loading { 140 | position: absolute; 141 | top: 40%; 142 | width: 100%; 143 | left: 0; 144 | text-align: center; 145 | } 146 | 147 | .customfont { 148 | font-family: "Crimson Pro", Arial, sans-serif; 149 | } 150 | 151 | .menu a, .article-item article a:first-child { 152 | color: $white; 153 | } 154 | 155 | .article { 156 | > div { 157 | display: grid; 158 | grid-template-columns: 1fr 2fr; 159 | grid-column-gap: 20px; 160 | } 161 | li { 162 | border: 1px solid #ccc; 163 | border-radius: 10px; 164 | padding: 10px; 165 | display: flex; 166 | flex-direction: column; 167 | margin-bottom: 20px; 168 | img { 169 | align-self: end; 170 | } 171 | } 172 | footer { 173 | width: 80%; 174 | margin-top: 50px; 175 | border-top: 1px solid #ccc; 176 | font-size: 0.75rem; 177 | 178 | > p { 179 | font-size: inherit; 180 | } 181 | } 182 | img { 183 | position:relative; 184 | } 185 | img:after { 186 | content: attr(alt); 187 | color: rgb(100, 100, 100); 188 | position: absolute; 189 | z-index: 1; 190 | left: 0; 191 | width: 100%; 192 | color: #fff; 193 | text-align: center; 194 | background-image: url("./images/wknd-card.jpeg") 195 | } 196 | } 197 | 198 | .header, .footer { 199 | background-color: $black; 200 | color: $white; 201 | display: grid; 202 | align-items: center; 203 | max-width: $max-width; 204 | margin: 0 auto; 205 | .logo { 206 | height: 24px; 207 | } 208 | .menu { 209 | margin: 0; 210 | display: flex; 211 | column-gap: 40px; 212 | } 213 | } 214 | .header { 215 | padding: 0 80px; 216 | justify-items: center; 217 | grid-template-columns: 1fr 4fr 1fr; 218 | a { 219 | justify-self: flex-start; 220 | } 221 | .logo { 222 | margin: 44px 0; 223 | } 224 | button { 225 | white-space: nowrap; 226 | justify-self: flex-end; 227 | margin: 0; 228 | } 229 | } 230 | .footer { 231 | padding: 40px 80px; 232 | grid-template-columns: 1fr 1fr; 233 | nav, small { 234 | justify-self: end; 235 | } 236 | small { 237 | padding: 40px 0; 238 | color: $gray; 239 | grid-column: span 2; 240 | } 241 | } 242 | 243 | .container img { 244 | max-width: 50%; 245 | } 246 | 247 | @media only screen and (max-width: $tablet-breakpoint) { 248 | .header, .footer { 249 | padding: $tablet-padding; 250 | } 251 | .header { 252 | .logo { 253 | margin: 0; 254 | } 255 | } 256 | } 257 | 258 | @media only screen and (max-width: $mobile-breakpoint) { 259 | .header { 260 | button { 261 | display:none; 262 | } 263 | } 264 | .header, .footer { 265 | grid-template-columns: 1fr 1.5fr; 266 | white-space: nowrap; 267 | column-gap: 10px; 268 | } 269 | } -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/api/useGraphQL.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | import {useState, useEffect} from 'react'; 9 | import {getAuthorHost} from "../utils/fetchData"; 10 | 11 | const {AEMHeadless} = require('@adobe/aem-headless-client-js') 12 | const {GRAPHQL_ENDPOINT} = process.env; 13 | 14 | /** 15 | * Custom React Hook to perform a GraphQL query 16 | * @param path - Persistent query path 17 | */ 18 | function useGraphQL(path) { 19 | let [data, setData] = useState(null); 20 | let [errorMessage, setErrors] = useState(null); 21 | useEffect(() => { 22 | function makeRequest() { 23 | const sdk = new AEMHeadless({ 24 | serviceURL: getAuthorHost(), 25 | endpoint: GRAPHQL_ENDPOINT, 26 | }); 27 | const request = sdk.runPersistedQuery.bind(sdk); 28 | 29 | request(path, {}, {credentials: "include"}) 30 | .then(({data, errors}) => { 31 | //If there are errors in the response set the error message 32 | if (errors) { 33 | setErrors(mapErrors(errors)); 34 | } 35 | //If data in the response set the data as the results 36 | if (data) { 37 | setData(data); 38 | } 39 | }) 40 | .catch((error) => { 41 | setErrors(error); 42 | sessionStorage.removeItem('accessToken'); 43 | }); 44 | } 45 | 46 | makeRequest(); 47 | }, [path]); 48 | 49 | 50 | return {data, errorMessage} 51 | } 52 | 53 | /** 54 | * concatenate error messages into a single string. 55 | * @param {*} errors 56 | */ 57 | function mapErrors(errors) { 58 | return errors.map((error) => error.message).join(","); 59 | } 60 | 61 | export default useGraphQL; 62 | -------------------------------------------------------------------------------- /src/components/About.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import image from '../images/wknd-card.jpeg'; 3 | 4 | const About = () => ( 5 |
6 |

About Us

7 | Sample 8 |

The WKND is a fictional online magazine and adventure company that focuses 9 | on outdoor activities and trips across the globe. The WKND site is designed 10 | to demonstrate functionality for Adobe Experience Manager. There is also a 11 | corresponding tutorial that walks a developer through the development. 12 | Special thanks to Lorenzo Buosi and Kilian Amendola who created the 13 | beautiful design for the WKND site. 14 |

15 |
16 | ); 17 | 18 | export default About; 19 | -------------------------------------------------------------------------------- /src/components/AdventureDetail.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | import React from 'react'; 9 | import {Link, useNavigate, useParams} from "react-router-dom"; 10 | import backIcon from '../images/Back.svg'; 11 | import Error from './base/Error'; 12 | import Loading from './base/Loading'; 13 | import {mapJsonRichText} from '../utils/renderRichText'; 14 | import './AdventureDetail.scss'; 15 | import useGraphQL from '../api/useGraphQL'; 16 | import {getImageURL} from "../utils/fetchData"; 17 | 18 | function AdventureDetail() { 19 | // params hook from React router 20 | const {slug} = useParams(); 21 | const navigate = useNavigate(); 22 | const persistentQuery = `wknd-shared/adventure-by-slug;slug=${slug}`; 23 | 24 | //Use a custom React Hook to execute the GraphQL query 25 | const {data, errorMessage} = useGraphQL(persistentQuery); 26 | 27 | //If there is an error with the GraphQL query 28 | if (errorMessage) return ; 29 | 30 | //If query response is null then return a loading icon... 31 | if (!data) return ; 32 | 33 | //Set adventure properties variable based on graphQL response 34 | const currentAdventure = getAdventure(data); 35 | 36 | // set references of current adventure 37 | const references = data.adventureList._references; 38 | 39 | //Must have title, path, and image 40 | if (!currentAdventure) { 41 | return ; 42 | } 43 | 44 | const editorProps = { 45 | "data-aue-resource": "urn:aemconnection:" + currentAdventure._path + "/jcr:content/data/master", 46 | "data-aue-type": "reference", 47 | itemfilter: "cf" 48 | }; 49 | 50 | return ( 51 |
52 |
53 | 56 |

{currentAdventure.title}

57 |
58 | {currentAdventure.activity} 61 | 62 |
63 |
64 | 65 |
66 | ); 67 | } 68 | 69 | function AdventureDetailRender({ 70 | title, 71 | primaryImage, 72 | adventureType, 73 | tripLength, 74 | groupSize, 75 | difficulty, 76 | description, 77 | itinerary, references 78 | }) { 79 | return (
80 | {title} 82 |
83 | 84 |
{mapJsonRichText(description.json, customRenderOptions(references))}
86 |
87 |
88 |
Adventure Type
89 | {adventureType} 92 |
93 |
94 |
Trip Length
95 | {tripLength} 98 |
99 |
100 |
Difficulty
101 | {difficulty} 104 |
105 |
106 |
Group Size
107 | {groupSize} 110 |
111 |
112 |
Itinerary
113 |
{mapJsonRichText(itinerary.json)}
115 |
116 | 117 |
118 | ); 119 | 120 | } 121 | 122 | function NoAdventureFound() { 123 | return ( 124 |
125 | 126 | Return 127 | 128 | 129 |
130 | ); 131 | } 132 | 133 | /** 134 | * Helper function to get the first adventure from the response 135 | * @param {*} response 136 | */ 137 | function getAdventure(data) { 138 | 139 | if (data && data.adventureList && data.adventureList.items) { 140 | return data.adventureList.items.find(item => { 141 | return item._path.startsWith("/content/dam/wknd-shared/en"); 142 | }); 143 | } 144 | return undefined; 145 | } 146 | 147 | /** 148 | * Example of using a custom render for in-line references in a multi line field 149 | */ 150 | function customRenderOptions(references) { 151 | 152 | const renderReference = { 153 | // node contains merged properties of the in-line reference and _references object 154 | 'ImageRef': (node) => { 155 | // when __typename === ImageRef 156 | return {'in-line 157 | }, 158 | 'AdventureModel': (node) => { 159 | // when __typename === AdventureModel 160 | return {`${node.title}: ${node.price}`}; 161 | } 162 | }; 163 | 164 | return { 165 | nodeMap: { 166 | 'reference': (node, children) => { 167 | 168 | // variable for reference in _references object 169 | let reference; 170 | 171 | // asset reference 172 | if (node.data.path) { 173 | // find reference based on path 174 | reference = references.find(ref => ref._path === node.data.path); 175 | } 176 | // Fragment Reference 177 | if (node.data.href) { 178 | // find in-line reference within _references array based on href and _path properties 179 | reference = references.find(ref => ref._path === node.data.href); 180 | } 181 | 182 | // if reference found return render method of it 183 | return reference ? renderReference[reference.__typename]({...reference, ...node}) : null; 184 | } 185 | }, 186 | }; 187 | } 188 | 189 | export default AdventureDetail; 190 | -------------------------------------------------------------------------------- /src/components/AdventureDetail.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | @use "sass:math"; 9 | 10 | @import '../styles/variables'; 11 | 12 | .adventure-detail { 13 | padding: 0 80px; 14 | background: $white; 15 | color: $black; 16 | button.dark { 17 | border: none; 18 | color: inherit; 19 | padding: 0; 20 | } 21 | 22 | .adventure-detail-header { 23 | display: flex; 24 | justify-content: space-between; 25 | flex-flow: wrap; 26 | align-items: center; 27 | row-gap: 40px; 28 | padding-bottom: 40px; 29 | .adventure-detail-back-nav { 30 | width: 24px; 31 | flex-basis: 100%; 32 | text-align: left; 33 | display: flex; 34 | align-items: center; 35 | column-gap: 15px; 36 | } 37 | } 38 | > div { 39 | max-width: $max-width; 40 | margin: 0 auto; 41 | } 42 | } 43 | 44 | 45 | 46 | .adventure-detail-content, .adventure-detail-header { 47 | padding: 40px 20%; 48 | } 49 | 50 | .adventure-detail-content { 51 | .adventure-detail-info { 52 | display: flex; 53 | background: $lime; 54 | border-radius: 16px; 55 | align-items: flex-start; 56 | padding: 24px 40px; 57 | column-gap: 80px; 58 | justify-content: space-evenly; 59 | margin: 40px 0; 60 | } 61 | h6 { 62 | color: rgba(0, 0, 0, 0.4); 63 | font-size: 12px; 64 | text-transform: inherit; 65 | margin: 0 0 8px 0; 66 | } 67 | span { 68 | line-height: 24px; 69 | } 70 | } 71 | .adventure-detail-title { 72 | margin: 0; 73 | } 74 | 75 | .adventure-detail-content h2 { 76 | margin-top: 32px; 77 | padding: 0px; 78 | } 79 | 80 | .adventure-detail-primaryimage { 81 | margin: 0 auto; 82 | border-radius: 16px; 83 | aspect-ratio: 16/8; 84 | width: 100%; 85 | } 86 | 87 | .adventure-detail-itinerary h2 { 88 | font-family: $font-family-base; 89 | font-weight: bold; 90 | font-size: $font-size-large; 91 | } 92 | 93 | @media only screen and (max-width: $mobile-breakpoint) { 94 | .adventure-detail-content { 95 | .adventure-detail-info { 96 | flex-direction: column; 97 | align-items: flex-start; 98 | row-gap: 10px; 99 | } 100 | } 101 | } 102 | 103 | @media only screen and (max-width: $tablet-breakpoint) { 104 | .adventure-detail-content, .adventure-detail-header { 105 | padding: 40px 0; 106 | } 107 | } 108 | 109 | /* Contributer Styles */ 110 | $contributor-image-size: 60px; 111 | 112 | .contributor { 113 | width: 100%; 114 | float: left; 115 | margin: 20px 0; 116 | &-image { 117 | width: $contributor-image-size; 118 | height: $contributor-image-size; 119 | border-radius: math.div($contributor-image-size, 2); 120 | object-fit: cover; 121 | float: left; 122 | } 123 | 124 | &-name { 125 | margin-left: $contributor-image-size + 20px; 126 | font-family: $font-family-serif; 127 | } 128 | 129 | &-occupation { 130 | margin-left: $contributor-image-size + 20px; 131 | margin-top: 0em; 132 | } 133 | 134 | &-separator { 135 | border-width: 1px solid $gray-light; 136 | margin-top: 2em; 137 | margin-bottom: 2em; 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /src/components/Adventures.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | import React from 'react'; 9 | import {Link} from 'react-router-dom'; 10 | import useGraphQL from '../api/useGraphQL'; 11 | import Loading from './base/Loading'; 12 | import "./Adventures.scss"; 13 | import Title from './base/Title'; 14 | import {getImageURL} from "../utils/fetchData"; 15 | 16 | function AdventureItem(props) { 17 | const editorProps = { 18 | "data-aue-resource": "urn:aemconnection:" + props?._path + "/jcr:content/data/master", 19 | "data-aue-type": "reference", 20 | "data-aue-filter": "cf", 21 | "data-aue-label": props.slug 22 | }; 23 | 24 | //Must have title, path, and image 25 | if(!props || !props._path || !props.title || !props.primaryImage ) { 26 | return null; 27 | } 28 | 29 | return ( 30 |
  • 31 |
    32 | 33 | {props.title} 35 | 36 |
    37 |

    {props.title}

    38 |
    39 |
    40 | {props.tripLength?.toLowerCase()} 43 | 44 |
    45 |
    $ 46 | {props.price} 49 | 50 |
    51 |
    52 |
  • 53 | ); 54 | } 55 | 56 | function Adventures() { 57 | const persistentQuery = 'wknd-shared/adventures-all'; 58 | //Use a custom React Hook to execute the GraphQL query 59 | const { data, errorMessage } = useGraphQL(persistentQuery); 60 | 61 | //If there is an error with the GraphQL query 62 | if(errorMessage) return; 63 | 64 | //If data is null then return a loading state... 65 | if(!data) return ; 66 | 67 | return ( 68 |
    69 | 70 | <ul className="adventure-items"> 71 | { 72 | //Iterate over the returned data items from the query 73 | data.adventureList.items.map((adventure, index) => { 74 | return ( 75 | <AdventureItem key={index} {...adventure} /> 76 | ); 77 | }) 78 | } 79 | </ul> 80 | </section> 81 | ); 82 | } 83 | 84 | export default Adventures; 85 | -------------------------------------------------------------------------------- /src/components/Adventures.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | @import '../styles/variables'; 9 | 10 | $adventureItemWidth: 420px; 11 | $adventureItemHeight: 360px; 12 | 13 | $adventureItemWidthMobile: 300px; 14 | $adventureItemHeightMobile: 250px; 15 | 16 | 17 | .adventures { 18 | padding: 70px; 19 | background-color: $white; 20 | color: $black; 21 | text-align: left; 22 | 23 | & > h1 { 24 | padding-bottom: 40px; 25 | } 26 | 27 | & > h1, &> ul { 28 | max-width: $max-width; 29 | margin: 0 auto; 30 | } 31 | } 32 | 33 | .adventure-items { 34 | display: grid; 35 | grid-template-columns: repeat(3, 1fr); 36 | list-style: none; 37 | grid-column-gap: 40px; 38 | } 39 | 40 | .adventure-item { 41 | margin: 0 0 2rem; 42 | } 43 | 44 | .adventure-image-card { 45 | .adventure-item-image { 46 | border-radius: 8px; 47 | width: 100%; 48 | aspect-ratio: 5/4; 49 | object-fit: cover; 50 | object-position: center; 51 | overflow: hidden; 52 | } 53 | } 54 | 55 | .adventure-item-title { 56 | margin-bottom: 5px; 57 | text-transform: capitalize; 58 | font-weight: 500; 59 | line-height: 41px; 60 | } 61 | 62 | .adventure-item-details { 63 | display: flex; 64 | justify-content: flex-start; 65 | column-gap: 8px; 66 | 67 | > div { 68 | display: flex; 69 | align-items: center; 70 | } 71 | 72 | > :not(.adventure-item-price) { 73 | text-transform: capitalize; 74 | } 75 | 76 | .adventure-item-price{ 77 | background: $black; 78 | color: $white; 79 | } 80 | } 81 | 82 | 83 | .card { 84 | padding: 20px 10px; 85 | border-radius: 4px; 86 | font-family: sans-serif; 87 | display: flex; 88 | line-height: 1.5em; 89 | img { 90 | width: 400px; 91 | margin: 0 20px; 92 | } 93 | button { 94 | margin-top: 10px; 95 | } 96 | } 97 | 98 | @media only screen and (max-width: $tablet-breakpoint) { 99 | .adventures { 100 | padding: $tablet-padding; 101 | } 102 | 103 | .adventure-items { 104 | grid-template-columns: repeat(2, 1fr); 105 | } 106 | } 107 | 108 | @media only screen and (max-width: $mobile-breakpoint) { 109 | .adventure-items { 110 | grid-template-columns: 1fr; 111 | } 112 | } -------------------------------------------------------------------------------- /src/components/ArticleDetail.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | import React from 'react'; 9 | import {Link, useNavigate, useParams} from "react-router-dom"; 10 | import backIcon from '../images/Back.svg'; 11 | import Error from './base/Error'; 12 | import Loading from './base/Loading'; 13 | import {mapJsonRichText} from '../utils/renderRichText'; 14 | import './AdventureDetail.scss'; 15 | import useGraphQL from '../api/useGraphQL'; 16 | import {getArticle} from '../utils/commons'; 17 | import {getImageURL} from "../utils/fetchData"; 18 | 19 | function ArticleDetail({article}) { 20 | 21 | // params hook from React router 22 | const {slug} = useParams(); 23 | const navigate = useNavigate(); 24 | const articleSlug = slug || article; 25 | 26 | const persistentQuery = `wknd-shared/article-by-slug;slug=${articleSlug}`; 27 | 28 | //Use a custom React Hook to execute the GraphQL query 29 | const {data, errorMessage} = useGraphQL(persistentQuery); 30 | 31 | //If there is an error with the GraphQL query 32 | if (errorMessage) return <Error errorMessage={errorMessage}/>; 33 | 34 | //If query response is null then return a loading icon... 35 | if (!data) return <Loading/>; 36 | 37 | //Set adventure properties variable based on graphQL response 38 | const currentArticle = getArticle(data); 39 | 40 | //Must have title, path, and image 41 | if (!currentArticle) { 42 | return <NoArticleFound/>; 43 | } 44 | 45 | const editorProps = { 46 | "data-aue-resource": "urn:aemconnection:" + currentArticle._path + "/jcr:content/data/master", 47 | "data-aue-type": "reference", 48 | "data-aue-filter": "cf" 49 | }; 50 | 51 | return (<div {...editorProps} className="adventure-detail"> 52 | <div class="adventure-detail-header"> 53 | <button className="adventure-detail-back-nav dark" onClick={() => navigate(-1)}> 54 | <img className="Backbutton-icon" src={backIcon} alt="Return"/> Back 55 | </button> 56 | <h1 className="adventure-detail-title" data-aue-prop="title" 57 | data-aue-type="text">{currentArticle.title}</h1> 58 | {/* <span className="pill default" itemProp="title" itemType="text">{currentAdventure.activity}</span> */} 59 | </div> 60 | <ArticleDetailRender {...currentArticle} slug={articleSlug}/> 61 | </div>); 62 | } 63 | 64 | function ArticleDetailRender({ 65 | _path, title, 66 | featuredImage, slug, 67 | main, 68 | authorFragment 69 | }) { 70 | 71 | 72 | return (<div> 73 | <img className="adventure-detail-primaryimage" data-aue-type="media" data-aue-prop="featuredImage" 74 | src={`${getImageURL(featuredImage)}`} alt={title}/> 75 | <div className="adventure-detail-content"> 76 | <div data-aue-prop="main" data-aue-type="richtext">{mapJsonRichText(main.json)}</div> 77 | </div> 78 | </div> 79 | ); 80 | } 81 | 82 | function NoArticleFound() { 83 | return ( 84 | <div className="adventure-detail"> 85 | <Link className="adventure-detail-close-button" to={`/${window.location.search}`}> 86 | <img className="Backbutton-icon" src={backIcon} alt="Return"/> 87 | </Link> 88 | <Error errorMessage="Missing data, article could not be rendered."/> 89 | </div> 90 | ); 91 | } 92 | 93 | export default ArticleDetail; 94 | -------------------------------------------------------------------------------- /src/components/Articles.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React from 'react'; 10 | import useGraphQL from '../api/useGraphQL'; 11 | import {Link} from 'react-router-dom'; 12 | import Error from './base/Error'; 13 | import Loading from './base/Loading'; 14 | import "./Articles.scss"; 15 | import { mapJsonRichText } from '../utils/renderRichText'; 16 | import {getImageURL} from "../utils/fetchData"; 17 | 18 | const Article = ({_path, title, synopsis, authorFragment, slug}) => { 19 | const editorProps = { 20 | "data-aue-resource": "urn:aemconnection:" + _path + "/jcr:content/data/master", 21 | "data-aue-type": "reference", 22 | "data-aue-filter": "cf" 23 | }; 24 | return ( 25 | <li className="article-item" {...editorProps}> 26 | <aside> 27 | <img className="article-item-image" 28 | src={`${getImageURL(authorFragment?.profilePicture)}`} 29 | alt={title} data-aue-prop="profilePicture" data-aue-type="media"/> 30 | </aside> 31 | <article> 32 | <Link to={`/articles/article/${slug}${window.location.search}`}> 33 | <h3 data-id="title" data-aue-prop="title" data-aue-type="text">{title}</h3> 34 | </Link> 35 | 36 | <p>{`By ${authorFragment.firstName} ${authorFragment.lastName}`}</p> 37 | { synopsis && 38 | <div className="article-content" data-aue-prop='synopsis' data-aue-type='richtext'> 39 | {mapJsonRichText(synopsis.json)} 40 | </div> 41 | } 42 | <Link to={`/articles/article/${slug}${window.location.search}`}> 43 | <button>Read more</button> 44 | </Link> 45 | </article> 46 | 47 | </li> 48 | ); 49 | }; 50 | 51 | const Articles = () => { 52 | const persistentQuery = 'wknd-shared/articles-all'; 53 | 54 | //Use a custom React Hook to execute the GraphQL query 55 | const { data, errorMessage } = useGraphQL(persistentQuery); 56 | 57 | //If there is an error with the GraphQL query 58 | if(errorMessage) return <Error errorMessage={errorMessage} />; 59 | 60 | //If data is null then return a loading state... 61 | if(!data) return <Loading />; 62 | 63 | return ( 64 | <section className="articles"> 65 | <h2>Articles</h2> 66 | <ul> 67 | { 68 | data.articleList.items.map((article, index) => { 69 | return ( 70 | <Article key={index} {...article} /> 71 | ); 72 | }) 73 | } 74 | </ul> 75 | </section> 76 | ); 77 | 78 | }; 79 | 80 | export default Articles; 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/components/Articles.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | @import '../styles/variables'; 9 | 10 | .articles { 11 | padding: 70px; 12 | text-align: left; 13 | max-width: $max-width; 14 | margin: 0 auto; 15 | } 16 | .article-item { 17 | list-style: none; 18 | column-gap: 30px; 19 | padding: 10px; 20 | display: flex; 21 | border-bottom: 1px solid #eee; 22 | 23 | &:nth-child(even) { 24 | flex-direction: row-reverse; 25 | } 26 | 27 | .article-item-image { 28 | max-width: 350px; 29 | max-height: 350px; 30 | } 31 | 32 | .article-content{ 33 | align-self: flex-end; 34 | } 35 | } 36 | @media only screen and (max-width: $mobile-breakpoint) { 37 | .article-item { 38 | display:inherit; 39 | } 40 | } -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | import React from 'react'; 9 | import { Link } from 'react-router-dom'; 10 | import Container from './base/Container'; 11 | import Title from './base/Title'; 12 | import Text from './base/Text'; 13 | import Teaser from './Teaser'; 14 | import Adventures from './Adventures'; 15 | import "./Home.scss"; 16 | 17 | /*** 18 | * Displays a grid of current adventures 19 | */ 20 | function Home() { 21 | return ( 22 | <div className="Home"> 23 | <Teaser /> 24 | <Adventures /> 25 | <section className="newsletter"> 26 | <div className="content"> 27 | <Title resource="urn:aemconnection:/content/wknd/us/en/newsletter/jcr:content/root/container/title" prop="jcr:title" type="text"/> 28 | <Text resource="urn:aemconnection:/content/wknd/us/en/newsletter/jcr:content/root/container/text" prop="text" type="richtext" /> 29 | </div> 30 | <button>Subscribe</button> 31 | </section> 32 | <section className="about-us"> 33 | <div className="content"> 34 | <Title resource="urn:aemconnection:/content/wknd/language-masters/en/universal-editor-container/jcr:content/root/title" prop="jcr:title" type="text"/> 35 | <Container resource="urn:aemconnection:/content/wknd/language-masters/en/universal-editor-container/jcr:content/root/container" type="container" /> 36 | </div> 37 | <Link to={`/aboutus${window.location.search}`}> 38 | <button className="dark">Read more</button> 39 | </Link> 40 | </section> 41 | </div> 42 | ); 43 | } 44 | 45 | export default Home; 46 | -------------------------------------------------------------------------------- /src/components/Home.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | @import '../styles/variables'; 9 | 10 | section { 11 | padding: 80px; 12 | text-align: center; 13 | 14 | .content { 15 | display: grid; 16 | align-items: start; 17 | padding-bottom: 20px; 18 | column-gap: 20px; 19 | max-width: $max-width; 20 | margin: 0 auto; 21 | 22 | h1 { 23 | margin-top: 0; 24 | text-align:left; 25 | } 26 | 27 | .container { 28 | text-align: left; 29 | } 30 | } 31 | 32 | &.about-us .content { 33 | grid-template-columns: 1fr 3fr; 34 | } 35 | 36 | &.newsletter { 37 | background-color: $lime; 38 | color: $black; 39 | .content { 40 | grid-template-columns: repeat(2,1fr); 41 | } 42 | } 43 | 44 | @media only screen and (max-width: $mobile-breakpoint) { 45 | .content { 46 | display: inherit; 47 | } 48 | } 49 | } 50 | 51 | @media only screen and (max-width: $tablet-breakpoint) { 52 | section { 53 | padding: $tablet-padding; 54 | } 55 | } -------------------------------------------------------------------------------- /src/components/Teaser.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React from 'react'; 10 | import { Link } from 'react-router-dom'; 11 | import useGraphQL from '../api/useGraphQL'; 12 | import { getArticle } from '../utils/commons'; 13 | import { mapJsonRichText } from '../utils/renderRichText'; 14 | import Loading from './base/Loading'; 15 | import "./Teaser.scss"; 16 | import {getImageURL} from "../utils/fetchData"; 17 | 18 | const Teaser = () => { 19 | const persistentQuery = `wknd-shared/article-by-slug;slug=aloha-spirits-in-northern-norway`; 20 | const {data, errorMessage} = useGraphQL(persistentQuery); 21 | //If there is an error with the GraphQL query 22 | if (errorMessage) return; 23 | 24 | //If query response is null then return a loading icon... 25 | if (!data) return <Loading/>; 26 | 27 | const article = getArticle(data); 28 | if(!article) return <></> 29 | const { title, _path, featuredImage, synopsis } = article; 30 | 31 | const editorProps = { 32 | "data-aue-resource": "urn:aemconnection:" + _path + "/jcr:content/data/master", 33 | "data-aue-type": "reference", 34 | "data-aue-filter": "cf" 35 | }; 36 | 37 | return ( 38 | 39 | <section {...editorProps} className="Teaser"> 40 | <article> 41 | <p>Latest article</p> 42 | <h1 data-aue-prop="title" data-aue-type="text">{title}</h1> 43 | {synopsis && <div data-aue-prop="synopsis" data-aue-type="richtext">{mapJsonRichText(synopsis.json)}</div>} 44 | <div> 45 | <span className='pill'>Magazine</span> 46 | <span className='pill'>Surfing</span> 47 | </div> 48 | <Link to={`/articles/article/aloha-spirits-in-northern-norway${window.location.search}`}> 49 | <button>Read more</button> 50 | </Link> 51 | </article> 52 | {featuredImage && <img src={`${getImageURL(featuredImage)}`} alt={title} data-aue-type="media" data-aue-prop="featuredImage" />} 53 | </section> 54 | 55 | ); 56 | } 57 | 58 | export default Teaser; 59 | 60 | -------------------------------------------------------------------------------- /src/components/Teaser.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | @import '../styles/variables'; 9 | 10 | .Teaser { 11 | background-color: #141414; 12 | color: #FFFFFF; 13 | padding: 40px 80px 80px; 14 | display: grid; 15 | grid-template-columns: repeat(5, 1fr); 16 | align-items: center; 17 | text-align: left; 18 | max-width: 1280px; 19 | margin: 0 auto; 20 | 21 | article { 22 | grid-column: 1/3; 23 | grid-row: 1; 24 | z-index: 2; 25 | color: $white; 26 | 27 | > div { 28 | margin: 50px 0; 29 | } 30 | 31 | .pill { 32 | border: 1px solid $white; 33 | } 34 | 35 | .pill:first-child { 36 | margin-right: 10px; 37 | } 38 | } 39 | img { 40 | border-radius: 16px; 41 | max-width: 100%; 42 | grid-column: span 3; 43 | grid-column: 2 / 6; 44 | grid-row: 1; 45 | opacity: 0.6; 46 | } 47 | } 48 | 49 | @media only screen and (max-width: $tablet-breakpoint) { 50 | .Teaser { 51 | padding: $tablet-padding; 52 | } 53 | } 54 | 55 | @media only screen and (max-width: $mobile-breakpoint) { 56 | .Teaser { 57 | display: flex; 58 | flex-direction: column-reverse; 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/base/Container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {fetchData} from '../../utils/fetchData'; 3 | import Text from './Text'; 4 | import Title from './Title'; 5 | import Image from './Image'; 6 | 7 | const Container = ({ resource, type, isComponent = "" }) => { 8 | const [components, setComponents] = React.useState(null); 9 | 10 | const createChildComponents = (items, itemid) => { 11 | const components = []; 12 | for(let key in items) { 13 | const item = items[key]; 14 | const type = item["sling:resourceType"]?.split("/").pop(); 15 | if (type === undefined) { 16 | continue; 17 | } 18 | 19 | let itemType, Component; 20 | switch(type) { 21 | case "image": 22 | itemType = "media"; 23 | Component = Image; 24 | break; 25 | case "text": 26 | itemType = item.textIsRich ? "richtext" : "text"; 27 | Component = item.type ? Title : Text; 28 | break; 29 | case "title": 30 | itemType = "text"; 31 | Component = Title; 32 | break; 33 | case "container": 34 | itemType = "container"; 35 | Component = Container; 36 | break; 37 | default: 38 | itemType = "component"; 39 | Component = () => (<div/>); 40 | break; 41 | } 42 | 43 | const props = { 44 | resource: `${itemid}/${key}`, 45 | type: itemType, 46 | data: item, 47 | isComponent: "component" 48 | }; 49 | components.push(<Component key={key} {...props} />) 50 | } 51 | return components; 52 | } 53 | 54 | React.useEffect(() => { 55 | if(!resource) return; 56 | fetchData(resource).then((data) => { 57 | setComponents(createChildComponents(data, resource)); 58 | }); 59 | }, [resource]); 60 | 61 | return ( 62 | <div className="container" data-aue-filter="container" data-aue-model="container" data-aue-behavior={isComponent} data-aue-resource={resource} data-aue-type={type}> 63 | {components} 64 | </div> 65 | ) 66 | }; 67 | 68 | export default Container; -------------------------------------------------------------------------------- /src/components/base/Error.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React from 'react'; 10 | 11 | const Error = ({ errorMessage }) => ( 12 | <div className="error"> 13 | <span className="error-message">{`Error: ${errorMessage}`}</span> 14 | </div> 15 | ); 16 | 17 | export default Error; -------------------------------------------------------------------------------- /src/components/base/Image.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React, {useEffect, useMemo} from 'react'; 10 | import {fetchData, getImageURL} from '../../utils/fetchData'; 11 | 12 | const Image = (props) => { 13 | const {resource, prop = "fileReference", type, className, data: initialData, isComponent = false} = props; 14 | 15 | const editorProps = useMemo(() => true && { 16 | "data-aue-resource": resource, 17 | "data-aue-prop":prop, 18 | "data-aue-type": type, 19 | "data-aue-behavior": isComponent 20 | }, [resource, prop, type, isComponent]); 21 | 22 | const [data,setData] = React.useState(initialData || {}); 23 | useEffect(() => { 24 | if(!resource || !prop || initialData) return; 25 | fetchData(resource).then((data) => setData(data)); 26 | }, [resource, prop, initialData]); 27 | const path = data?.["fileReference"]; 28 | 29 | return ( 30 | <img {...editorProps} data-aue-model="image" data-aue-label={data.id} src={path ? `${getImageURL(path)}` : ""} className={className} alt={data.alt} /> 31 | ); 32 | }; 33 | 34 | export default Image; 35 | -------------------------------------------------------------------------------- /src/components/base/Loading.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React from 'react'; 10 | import loadingIcon from '../../images/icon-loading.svg'; 11 | 12 | const Loading = () => ( 13 | <div className="loading"> 14 | <img src={loadingIcon} alt="Loading..." /> 15 | </div> 16 | ); 17 | 18 | export default Loading; -------------------------------------------------------------------------------- /src/components/base/Text.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React, {useEffect} from 'react'; 10 | import {fetchData} from '../../utils/fetchData'; 11 | 12 | const Text = (props) => { 13 | const {resource, prop = "text", type, className, data: initialData, isComponent = false} = props; 14 | const [data,setData] = React.useState(initialData); 15 | 16 | const editorProps = { 17 | "data-aue-resource": resource, 18 | "data-aue-prop":prop, 19 | "data-aue-type": type, 20 | "data-aue-behavior": isComponent, 21 | }; 22 | 23 | useEffect(() => { 24 | if(!resource || !prop ) return; 25 | if(!data) { fetchData(resource).then((data) => setData(data)) }; 26 | }, [resource, prop, data]); 27 | 28 | 29 | return data ? ( 30 | type !== "richtext" ?( 31 | <div {...editorProps} data-aue-model="text" className={className} data-aue-label={data?.id}> 32 | {data[prop]} 33 | </div> 34 | ) : <div {...editorProps} data-aue-model="richtext" className={className} data-aue-label={data?.id} dangerouslySetInnerHTML={{__html: data[prop]}}/> 35 | ): <></>; 36 | }; 37 | 38 | export default Text; 39 | -------------------------------------------------------------------------------- /src/components/base/Title.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | import React, {useEffect, useMemo} from 'react'; 10 | import {fetchData} from '../../utils/fetchData'; 11 | 12 | const Title = (props) => { 13 | const {resource, prop = "jcr:title", type, className = "test", data: initialData, isComponent = false} = props; 14 | const editorProps = useMemo(() => true && { 15 | "data-aue-resource": resource, 16 | "data-aue-prop":prop, 17 | "data-aue-type": type, 18 | "data-aue-behavior": isComponent 19 | }, [resource, prop, type, isComponent]); 20 | 21 | const [data,setData] = React.useState(initialData); 22 | 23 | useEffect(() => { 24 | if(!resource || !prop) return; 25 | if (!data) { fetchData(resource, "model").then((data) => setData(data)) }; 26 | }, [resource, prop, data]); 27 | 28 | useEffect(() => { 29 | const handleUpdate = (e) => { 30 | const { itemids = [] } = e.detail; 31 | if(itemids.indexOf(resource) >= 0) { 32 | setData(null); 33 | } 34 | e.stopPropagation(); 35 | }; 36 | document.addEventListener("editor-update", handleUpdate); 37 | return () => { 38 | document.removeEventListener("editor-update", handleUpdate); 39 | } 40 | },[resource]); 41 | 42 | const TitleTag = data?.type ? `${data.type}` : "h1"; 43 | return data ? ( 44 | <TitleTag {...editorProps} data-aue-model="title" data-aue-label={"title"} className={className}>{data["jcr:title"] ?? "Default Title"}</TitleTag> 45 | ):<></>; 46 | }; 47 | 48 | export default Title; 49 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { useSparkleAppUrl } from "./useSparkleAppUrl"; 2 | -------------------------------------------------------------------------------- /src/hooks/useSparkleAppUrl.js: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from "react-router-dom"; 2 | import { useMemo } from "react"; 3 | 4 | const useSparkleAppUrl = () => { 5 | const [queryParams] = useSearchParams(); 6 | const queryParamsString = queryParams.toString(); 7 | const sparkleUrl = useMemo(() => { 8 | const sparkleBaseUrl = "https://ue-sparkle-app.adobe.net/"; 9 | if (queryParamsString) { 10 | return `${sparkleBaseUrl}?${decodeURIComponent(queryParamsString)}`; 11 | } else { 12 | return sparkleBaseUrl; 13 | } 14 | }, [queryParamsString]); 15 | return sparkleUrl; 16 | } 17 | 18 | export { useSparkleAppUrl }; -------------------------------------------------------------------------------- /src/images/Back.svg: -------------------------------------------------------------------------------- 1 | <svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <g clip-path="url(#clip0_794_13288)"> 3 | <path d="M19.6937 8.00391H0.306641" stroke="#514051" stroke-linecap="round" stroke-linejoin="round"/> 4 | <path d="M7.99282 0.308594L0.306641 7.99866L7.99282 15.6949" stroke="#514051" stroke-linecap="round" stroke-linejoin="round"/> 5 | </g> 6 | <defs> 7 | <clipPath id="clip0_794_13288"> 8 | <rect width="20" height="16" fill="white"/> 9 | </clipPath> 10 | </defs> 11 | </svg> 12 | -------------------------------------------------------------------------------- /src/images/footer.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/src/images/footer.jpeg -------------------------------------------------------------------------------- /src/images/icon-close.svg: -------------------------------------------------------------------------------- 1 | <svg width="24px" height="24px" xmlns="http://www.w3.org/2000/svg"><path d="M11.778 11.778L4 4l7.778 7.778L4 19.556l7.778-7.778zm0 0l7.778 7.778-7.778-7.778L19.556 4l-7.778 7.778z" stroke="#fff" stroke-width="2" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg> -------------------------------------------------------------------------------- /src/images/icon-loading.svg: -------------------------------------------------------------------------------- 1 | <svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 2 | width="40px" height="40px" viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve"> 3 | <path fill="#696969" d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z"> 4 | <animateTransform attributeType="xml" 5 | attributeName="transform" 6 | type="rotate" 7 | from="0 25 25" 8 | to="360 25 25" 9 | dur="0.6s" 10 | repeatCount="indefinite"/> 11 | </path> 12 | </svg> -------------------------------------------------------------------------------- /src/images/wknd-card.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/src/images/wknd-card.jpeg -------------------------------------------------------------------------------- /src/images/wknd-logo-dk.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 239.35 89.09"><title>Asset 2 -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | code { 6 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 7 | monospace; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById("root")); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | 13 | // enable hot module replacement 14 | if (module.hot) { 15 | module.hot.accept(); 16 | } 17 | 18 | // If you want to start measuring performance in your app, pass a function 19 | // to log results (for example: reportWebVitals(console.log)) 20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 21 | // reportWebVitals(); 22 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/styles/AktivGroteskCorp-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/src/styles/AktivGroteskCorp-Medium.woff -------------------------------------------------------------------------------- /src/styles/AktivGroteskCorp-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/src/styles/AktivGroteskCorp-Regular.woff -------------------------------------------------------------------------------- /src/styles/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/src/styles/Poppins-Medium.ttf -------------------------------------------------------------------------------- /src/styles/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adobe/universal-editor-sample-editable-app/6b20d8c146cc79015f668c9255ff2efd9acec50d/src/styles/Poppins-Regular.ttf -------------------------------------------------------------------------------- /src/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 Adobe. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | * 12 | */ 13 | 14 | @font-face { 15 | font-family: 'Aktiv Grotesk'; 16 | font-style: normal; 17 | font-display: swap; 18 | src: url(AktivGroteskCorp-Regular.woff) format('woff'); 19 | } 20 | 21 | @font-face { 22 | font-family: 'Aktiv Grotesk Medium'; 23 | font-style: normal; 24 | font-display: swap; 25 | src: url(AktivGroteskCorp-Medium.woff) format('woff'); 26 | } 27 | 28 | 29 | @font-face { 30 | font-family: 'Poppins'; 31 | font-style: normal; 32 | font-display: swap; 33 | src: url(Poppins-Regular.ttf); 34 | } 35 | 36 | @font-face { 37 | font-family: 'Poppins Medium'; 38 | font-style: normal; 39 | font-display: swap; 40 | src: url(Poppins-Medium.ttf); 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adobe 3 | All Rights Reserved. 4 | 5 | NOTICE: Adobe permits you to use, modify, and distribute this file in 6 | accordance with the terms of the Adobe license agreement accompanying 7 | it. 8 | */ 9 | //_variables.scss 10 | 11 | //== Colors 12 | // 13 | //## Gray and brand colors for use across theme. 14 | 15 | $black: #141414; 16 | $gray: #696969; 17 | $gray-light: #EBEBEB; 18 | $gray-lighter: #F7F7F7; 19 | $white: #FFFFFF; 20 | $yellow: #FFEA00; 21 | $blue: #0045FF; 22 | $red: #ff0048; 23 | $lime: #DBFF00; 24 | //== Typography 25 | // 26 | //## Font, line-height, and color for body text, headings, and more. 27 | 28 | $font-family-sans-serif: "Poppins", "Aktiv Grotesk Medium", sans-serif; 29 | $font-family-serif: "Asar",Georgia, "Times New Roman", Times, serif; 30 | $font-family-base: system-ui; 31 | 32 | $font-size-xsmall: 12px; 33 | $font-size-small: 14px; 34 | $font-size-medium: 16px; 35 | $font-size-base: 1rem; 36 | $font-size-large: 24px; 37 | $font-size-xlarge: 48px; 38 | 39 | $font-size-h1: 48px; 40 | $font-size-h2: 36px; 41 | $font-size-h3: 28px; 42 | $font-size-h4: 16px; 43 | $font-size-h5: 14px; 44 | $font-size-h6: 10px; 45 | 46 | $font-weight-normal: normal; 47 | $font-weight-bold: 600; 48 | 49 | $line-height-base: 1.1; 50 | $line-height-computed: floor(($font-size-base * $line-height-base)); 51 | 52 | // Functional Colors 53 | $brand-primary: $yellow; 54 | $body-bg: $white; 55 | $text-color: $black; 56 | $text-color-inverse: $gray-light; 57 | $link-color: $blue; 58 | $button-color: $lime; 59 | //Layout 60 | $max-width: 1280px; 61 | 62 | // Spacing 63 | $gutter-padding: 12px; 64 | 65 | // Breakpoints 66 | $mobile-breakpoint: 768px; 67 | $tablet-breakpoint: 1024px; 68 | 69 | $tablet-padding: 40px; -------------------------------------------------------------------------------- /src/utils/commons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to get the first adventure from the response 3 | * @param {*} response 4 | */ 5 | function getArticle(data) { 6 | if (data && data.articleList && data.articleList.items) { 7 | // expect there only to be a single adventure in the array 8 | if (data.articleList.items.length === 1) { 9 | return data.articleList.items[0]; 10 | } 11 | } 12 | return undefined; 13 | } 14 | 15 | export { getArticle }; -------------------------------------------------------------------------------- /src/utils/fetchData.js: -------------------------------------------------------------------------------- 1 | const {REACT_APP_DEFAULT_AUTHOR_HOST} = process.env; 2 | 3 | export const fetchData = async (path) => { 4 | const url = `${getAuthorHost()}/${path.split(":/")[1]}.infinity.json`; 5 | const data = await fetch(url, {headers: {"X-Aem-Affinity-Type": "api"}, credentials: "include"}); 6 | const json = await data.json(); 7 | return json; 8 | }; 9 | export const getAuthorHost = () => { 10 | const url = new URL(window.location.href); 11 | const searchParams = new URLSearchParams(url.search); 12 | if (searchParams.has("authorHost")) { 13 | return searchParams.get("authorHost"); 14 | } else { 15 | return REACT_APP_DEFAULT_AUTHOR_HOST; 16 | } 17 | } 18 | 19 | export const getImageURL = (obj) => { 20 | if (obj === null || obj === undefined) { 21 | return undefined; 22 | } 23 | 24 | if (typeof obj === "string") { 25 | if (obj.startsWith("https://")) { 26 | return obj; 27 | } 28 | return `${getAuthorHost()}${obj}`; 29 | } 30 | 31 | if (obj._authorUrl !== undefined) { 32 | return obj._authorUrl; 33 | } 34 | 35 | if (obj.repositoryId !== undefined && obj.assetId !== undefined) { 36 | return `https://${obj.repositoryId}/adobe/assets/${obj.assetId}`; 37 | } 38 | return undefined; 39 | } 40 | 41 | export const getProtocol = () => { 42 | const url = new URL(window.location.href); 43 | const searchParams = new URLSearchParams(url.search); 44 | if (searchParams.has("protocol")) { 45 | return searchParams.get("protocol"); 46 | } else { 47 | return "aem"; 48 | } 49 | } 50 | 51 | export const getService = () => { 52 | const url = new URL(window.location.href); 53 | const searchParams = new URLSearchParams(url.search); 54 | if (searchParams.has("service")) { 55 | return searchParams.get("service"); 56 | } 57 | return null; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/utils/renderRichText.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Adobe 3 | All Rights Reserved. 4 | NOTICE: Adobe permits you to use, modify, and distribute this file in 5 | accordance with the terms of the Adobe license agreement accompanying 6 | it. 7 | */ 8 | import React, { isValidElement, cloneElement } from 'react'; 9 | 10 | /** 11 | * Map of JSON nodeTypes to HTML formats 12 | */ 13 | const defaultNodeMap = { 14 | 'header': (node, children, style) => style[node.style]?.(node, children), 15 | 'paragraph': (node, children) =>

    {children}

    , 16 | 'span': ({ format } , children) => {children}, 17 | 'unordered-list': (node, children) =>
      {children}
    , 18 | 'ordered-list': (node, children) =>
      {children}
    , 19 | 'list-item': (node, children) =>
  • {children}
  • , 20 | 'table': (node, children) => {children}
    , 21 | 'table-body': (node, children) => {children}, 22 | 'table-row': (node, children) => {children}, 23 | 'table-data': (node, children) => {children}, 24 | 'link': node => {node.value}, 25 | 'text': (node, format) => defaultRenderText(node, format), 26 | 'reference': (node) => defaultRenderImage(node), 27 | } 28 | 29 | /** 30 | * Map of JSON format variants to HTML equivalents 31 | */ 32 | const defaultTextFormat = { 33 | 'bold': (value) => {value}, 34 | 'italic': (value) => {value}, 35 | 'underline': (value) => {value}, 36 | 'strong': (value) => {value}, 37 | 'emphasis': (value) => {value}, 38 | } 39 | 40 | /** 41 | * Map of Header styles 42 | */ 43 | const defaultHeaderStyle = { 44 | 'h1': (node, children) =>

    {children}

    , 45 | 'h2': (node, children) =>

    {children}

    , 46 | 'h3': (node, children) =>

    {children}

    47 | } 48 | 49 | /** 50 | * Default renderer of Text nodeTypes 51 | * @param {*} node 52 | * @returns 53 | */ 54 | function defaultRenderText(node, format) { 55 | // iterate over variants array to append formatting 56 | if (node.format?.variants?.length > 0) { 57 | return node.format.variants.reduce((previousValue, currentValue) => { 58 | return format[currentValue]?.(previousValue) ?? null; 59 | }, node.value); 60 | } 61 | // if no formatting, simply return the value of the text 62 | return node.value; 63 | } 64 | 65 | /** 66 | * Renders an image based on a reference 67 | * @param {*} node 68 | */ 69 | function defaultRenderImage(node) { 70 | const mimeType = node.data?.mimetype; 71 | if(mimeType && mimeType.startsWith('image')) { 72 | return {'reference'} 73 | } 74 | return null; 75 | } 76 | 77 | /** 78 | * Appends a key to valid React Elements 79 | * (avoids having to pass an index everywhere) 80 | * @param {*} element 81 | * @param {*} key 82 | * @returns 83 | */ 84 | function addKeyToElement(element, key) { 85 | if (isValidElement(element) && element.key === null) { 86 | return cloneElement(element, { key }); 87 | } 88 | return element; 89 | } 90 | 91 | /** 92 | * Iterates over an array of nodes and renders each node 93 | * @param {*} childNodes array of 94 | * @returns 95 | */ 96 | function renderNodeList(childNodes, options) { 97 | if(childNodes && options) { 98 | return childNodes.map((node, index) => { 99 | return addKeyToElement(renderNode(node, options), index); 100 | }); 101 | } 102 | 103 | return null; 104 | } 105 | 106 | /** 107 | * Renders an individual node based on nodeType. 108 | * Makes a recursive call to render any children of the current node (node.content) 109 | * @param {*} node 110 | * @param {*} options 111 | * @returns 112 | */ 113 | function renderNode(node, options) { 114 | const {nodeMap, textFormat, headerStyle} = options; 115 | 116 | // null check 117 | if(!node || !options) { 118 | return null; 119 | } 120 | 121 | const children = node.content ? renderNodeList(node.content, options) : null; 122 | 123 | // special case for header, since it requires processing of header styles 124 | if(node.nodeType === 'header') { 125 | return nodeMap[node.nodeType]?.(node, children, headerStyle); 126 | } 127 | 128 | // special case for text, since it may require formatting (i.e bold, italic, underline) 129 | if(node.nodeType === 'text') { 130 | return nodeMap[node.nodeType]?.(node, textFormat); 131 | } 132 | 133 | // use a map to render the current node based on its nodeType 134 | // pass the children (if they exist) 135 | return nodeMap[node.nodeType]?.(node, children) ?? null; 136 | } 137 | 138 | /** 139 | * Expose the utility as a public function mapJsonRichText. 140 | * Calling functions can choose to override various mappings and/or formatting 141 | * by passing in an `options` object that may contain overrides for `nodeMap`, `textFormat` and `headerStyle` 142 | * @param {*} json - the json response of a Multi Line rich text field 143 | * @param {*} options {nodeMap, - override defaultNodeMap 144 | * textFormat, - override defaultTextFormat 145 | * headerStyle, - override defaultHeaderStyle 146 | * } 147 | * @returns a JSX representation of the JSON object 148 | */ 149 | export function mapJsonRichText(json, options={}) { 150 | // merge options override with default options for nodeMap, textFormat, and headerStyle 151 | return renderNodeList(json , { 152 | nodeMap: { 153 | ...defaultNodeMap, 154 | ...options.nodeMap, 155 | }, 156 | textFormat: { 157 | ...defaultTextFormat, 158 | ...options.textFormat, 159 | }, 160 | headerStyle: { 161 | ...defaultHeaderStyle, 162 | ...options.headerStyle 163 | } 164 | }); 165 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const buffer = require('buffer'); 3 | const stream = require('stream-browserify'); 4 | const webpack = require('webpack'); 5 | 6 | 7 | module.exports = { 8 | plugins: [ 9 | new webpack.ProvidePlugin({ 10 | Buffer: ['buffer', 'Buffer'], 11 | }), 12 | new webpack.ProvidePlugin({ 13 | process: 'process/browser', 14 | }), 15 | ], 16 | resolve: { 17 | extensions: [ '.ts', '.js' ], 18 | fallback: { 19 | "util": util, 20 | "buffer": buffer, 21 | "stream": stream 22 | } 23 | } 24 | }; --------------------------------------------------------------------------------