18 changed files with 3259 additions and 10951 deletions
@ -1,238 +0,0 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
import {Command} from 'commander'; |
|||
import path from 'path'; |
|||
import fetch from "node-fetch"; |
|||
import fs from "fs"; |
|||
import http from "http"; |
|||
import https from "https"; |
|||
import StreamZip from "node-stream-zip"; |
|||
import {fileURLToPath} from 'url'; |
|||
import memFs from 'mem-fs'; |
|||
import editor from 'mem-fs-editor'; |
|||
|
|||
export const defaultGitRepo = 'git+https://git.devdevdev.life/axe-web-public/block-dev-tool.git#master'; |
|||
|
|||
const __filename = fileURLToPath(import.meta.url); |
|||
const __dirname = path.dirname(__filename); |
|||
|
|||
const isDev = process.env.NODE_ENV === 'development'; // Check README file in case you get "missing files" error.
|
|||
const blocksRegistry = isDev ? 'http://localhost:3020' : 'https://axe-web-blocks-registry.captain.devdevdev.life'; |
|||
const blocksDirectory = isDev ? 'blocks/' : ''; |
|||
|
|||
if (isDev) { |
|||
console.log(`Development Mode Active`); |
|||
} |
|||
|
|||
try { |
|||
const blockName = await init(); |
|||
console.log(`🎉 Done! \n\nCheck the "${blocksDirectory}${blockName}" directory. \n`); |
|||
} catch (e) { |
|||
console.log(`❌ Fail.`); |
|||
} |
|||
|
|||
async function init() { |
|||
const program = new Command(); |
|||
|
|||
program |
|||
.name('create-block') |
|||
.description('AXE-WEB Platform Blocks'); |
|||
|
|||
const promise = new Promise((resolve, reject) => { |
|||
program.command('pull') |
|||
.argument('<string>', 'Provide a full name of required block, for example: @axe-web/hero-block') |
|||
.action(async (blockName, options) => { |
|||
console.log('📦 Block to download:', blockName) |
|||
|
|||
const status = await getBlockSourceFiles(blockName); |
|||
if (status) { |
|||
resolve(blockName); |
|||
} else { |
|||
reject(); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
program.parse(); |
|||
return promise; |
|||
} |
|||
|
|||
async function getBlockSourceFiles(blockName) { |
|||
const queryString = new URLSearchParams(); |
|||
queryString.append('blockName', blockName); |
|||
queryString.append('includeDevConfig', 'true'); |
|||
|
|||
const response = await fetch(`${blocksRegistry}?${queryString.toString()}`); |
|||
const responseData = await response.json(); |
|||
if (!responseData || !responseData.name) { |
|||
console.log("⚠️ Block not found, please contact administrator."); |
|||
return; |
|||
} |
|||
|
|||
if (responseData.status && responseData.status !== 200) { |
|||
console.log("⚠️ [ERROR]", responseData.message || "Server side error."); |
|||
throw new Error("⚠️ [ERROR]", responseData.message || "Server side error."); |
|||
} |
|||
|
|||
const data = { |
|||
title: responseData.title, |
|||
name: responseData.name, |
|||
preview_image: responseData.preview_image, |
|||
remToPx: responseData.config.remToPx ?? 16, |
|||
blockFilename: responseData.name, |
|||
blockClassName: responseData.name, |
|||
blockGroupName: responseData.project, |
|||
version: responseData.version, |
|||
devToolSource: defaultGitRepo, |
|||
}; |
|||
|
|||
if (responseData.downloadUrl) { |
|||
console.log(`⬇️ Downloading v${responseData.version}`); |
|||
const zipFile = await downloadFile(responseData.downloadUrl, responseData.name + '.zip'); |
|||
|
|||
// Download, Extract and Remove downloaded file.
|
|||
try { |
|||
const zip = new StreamZip.async({file: zipFile}); |
|||
await zip.extract(null, `${blocksDirectory}${responseData.name}/src`); |
|||
await zip.close(); |
|||
|
|||
await fs.promises.access(zipFile, fs.constants.W_OK); |
|||
await fs.promises.unlink(zipFile); |
|||
} catch (e) { |
|||
throw e; |
|||
} |
|||
} else { |
|||
await createSourceFiles(data, __dirname, `${blocksDirectory}${responseData.name}`); |
|||
} |
|||
|
|||
await createTechnicalFiles(data, __dirname, `${blocksDirectory}${responseData.name}`); |
|||
|
|||
if (responseData.config.design_files) { |
|||
await downloadDesignFiles(responseData.name, responseData.config.design_files); |
|||
} |
|||
|
|||
await createDataFiles(responseData.name, responseData.config.data_sources); |
|||
|
|||
await createConfigFile(responseData.name, responseData.config); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
async function createConfigFile(blockName, config = {}) { |
|||
const obj = { |
|||
blockName, |
|||
baseView: config.baseView, |
|||
remToPx: config.remToPx, |
|||
}; |
|||
|
|||
const cssAssets = config.assets.filter(item => item.type === 'css').map(item => item.url); |
|||
if (cssAssets.length) { |
|||
obj.cssUrl = cssAssets; |
|||
} |
|||
|
|||
const jsAssets = config.assets.filter(item => item.type === 'js').map(item => item.url); |
|||
if (jsAssets.length) { |
|||
obj.jsUrl = jsAssets; |
|||
} |
|||
|
|||
await setupPath(`${blocksDirectory}${blockName}/config`); |
|||
await fs.promises.writeFile(`${blocksDirectory}${blockName}/config/default.json`, JSON.stringify(obj, null, 2)); |
|||
} |
|||
|
|||
async function downloadDesignFiles(blockName = '', files) { |
|||
for (let url of files) { |
|||
const fileName = url.split('/').pop(); |
|||
await setupPath(`${blocksDirectory}${blockName}/design`); |
|||
await downloadFile(url, `${blocksDirectory}${blockName}/design/${fileName}`) |
|||
} |
|||
} |
|||
|
|||
async function createDataFiles(blockName, dataSources = []) { |
|||
for (let source of dataSources) { |
|||
await setupPath(`${blocksDirectory}${blockName}/data`); |
|||
|
|||
await fs.promises.writeFile(`${blocksDirectory}${blockName}/data/${source.name}.json`, JSON.stringify(source.data, null, 2)); |
|||
|
|||
if (typeof source.preview_images !== 'undefined' && source.preview_images.length) { |
|||
for (let preview_image of source.preview_images) { |
|||
await setupPath(`${blocksDirectory}${blockName}/design/preview`); |
|||
await downloadFile(preview_image.url, `${blocksDirectory}${blockName}/design/preview/${source.name}.${preview_image.width}.${preview_image.extension}`) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
//
|
|||
// Helpers
|
|||
//
|
|||
|
|||
async function setupPath(path) { |
|||
if (!fs.existsSync(path)) { |
|||
await fs.promises.mkdir(path); |
|||
} |
|||
} |
|||
|
|||
async function downloadFile(url, fileName) { |
|||
const file = fs.createWriteStream(fileName); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const protocol = url.startsWith('https://') ? https : http; |
|||
protocol.get(url, function (response) { |
|||
response.pipe(file); |
|||
|
|||
// Loading Indicator.
|
|||
const loadingInterval = setInterval(() => console.log('🕐 Download in progress...'), 3000); |
|||
|
|||
// after download completed close filestream
|
|||
file.on("finish", () => { |
|||
clearInterval(loadingInterval); |
|||
|
|||
file.close(); |
|||
resolve(fileName); |
|||
}); |
|||
}); |
|||
}) |
|||
} |
|||
|
|||
export async function createSourceFiles(data, baseDir, distPath) { |
|||
const pathDist = distPath + '/src'; //path.join(baseDir, distPath);
|
|||
const generatorsPath = path.join(baseDir, 'generators/block/templates/src'); |
|||
|
|||
const store = memFs.create(); |
|||
const filesystem = editor.create(store); |
|||
|
|||
const files = [ |
|||
{from: 'template.template.hbs', to: `${data.name}.template.hbs`}, |
|||
{from: 'styles/template.scss', to: `styles/${data.name}.scss`}, |
|||
{from: 'scripts/template.js', to: `scripts/${data.name}.js`}, |
|||
'images/demo.jpeg', |
|||
]; |
|||
|
|||
for (let file of files) { |
|||
const from = typeof file !== 'string' ? `${generatorsPath}/${file.from}` : `${generatorsPath}/${file}`; |
|||
const to = typeof file !== 'string' ? `${pathDist}/${file.to}` : `${pathDist}/${file}`; |
|||
|
|||
await filesystem.copyTplAsync(from, to, data); |
|||
} |
|||
|
|||
return filesystem.commit(); // Promise
|
|||
} |
|||
|
|||
export async function createTechnicalFiles(data, baseDir, distPath) { |
|||
const pathDist = distPath; //path.join(baseDir, distPath);
|
|||
const generatorsPath = path.join(baseDir, 'generators/block/templates'); |
|||
|
|||
const store = memFs.create(); |
|||
const filesystem = editor.create(store); |
|||
|
|||
const files = ['package.json', 'README.md', '.editorconfig', {from: 'gitignore', to: '.gitignore'}, 'block.json']; |
|||
|
|||
for (let file of files) { |
|||
const from = typeof file !== 'string' ? `${generatorsPath}/${file.from}` : `${generatorsPath}/${file}`; |
|||
const to = typeof file !== 'string' ? `${pathDist}/${file.to}` : `${pathDist}/${file}`; |
|||
|
|||
await filesystem.copyTplAsync(from, to, data); |
|||
} |
|||
|
|||
return filesystem.commit(); // Promise
|
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
# This file is for unifying the coding style for different editors and IDEs |
|||
# editorconfig.org |
|||
|
|||
# WordPress Coding Standards |
|||
# https://make.wordpress.org/core/handbook/coding-standards/ |
|||
|
|||
root = true |
|||
|
|||
[*] |
|||
charset = utf-8 |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
indent_style = space |
|||
indent_size = 2 |
|||
|
|||
[*.json] |
|||
indent_style = tab |
|||
indent_size = 4 |
|||
|
|||
[*.md] |
|||
trim_trailing_whitespace = false |
|||
|
|||
@ -1,7 +0,0 @@ |
|||
# Basic |
|||
.idea |
|||
.DS_Store |
|||
node_modules |
|||
vendor |
|||
|
|||
# Custom |
|||
@ -1,167 +0,0 @@ |
|||
# Development Setup |
|||
|
|||
Install required modules: |
|||
|
|||
```sh |
|||
npm install |
|||
``` |
|||
|
|||
Run development environment: |
|||
|
|||
```sh |
|||
npm start |
|||
``` |
|||
|
|||
## How it works ⚙️ |
|||
|
|||
The project runs `gulp` and `browsersync` beyond the scenes. |
|||
Each change in template, styling or script files will reload the page. The idea is to have a comfortable development |
|||
environment. |
|||
|
|||
# Block Structure |
|||
|
|||
You will find all the block files in `/src` folder. |
|||
|
|||
``` |
|||
/src/images/ |
|||
*.png, *jpg, *.svg |
|||
|
|||
/src/scritps/ |
|||
*.mjs, *js |
|||
|
|||
/src/styles/ |
|||
*.scss |
|||
|
|||
/src/*.template.hbs |
|||
``` |
|||
|
|||
# Block rules |
|||
|
|||
We have a “blocks system”, means you will have to follow strict rules and use listed technologies: |
|||
|
|||
## Template Layout & Data |
|||
|
|||
### Data |
|||
|
|||
You will find multiple `*.json` files in `/data` folder after the first run of `npm start`. |
|||
|
|||
These files are used as data sources in layout. By reviewing these files you can understand what parameters you have and |
|||
how the data is stored. |
|||
|
|||
There are multiple data sources since the block can be reused in different situations with different data. We have to |
|||
make sure that the block is always rendered properly. |
|||
|
|||
> Don't change or edit these files - use the data as it is! |
|||
|
|||
### Handlebars |
|||
|
|||
[Handlebars](https://handlebarsjs.com/guide/ "What is Handlebars") used as a template engine in the project. |
|||
|
|||
All the template layout must be in a single `*.hbs` file that located in `/src` folder. |
|||
|
|||
> Use the data that is available in `/data` folder. |
|||
> Avoid using any kind of "freestyle" input/content. Your template file must contain only HTML tags and Handlebars parameters. |
|||
|
|||
### Static Media Files 🖼️ |
|||
|
|||
Use the `/src/images` folder in case you need to upload images or video files to the template. |
|||
These files will be available by the next URL: http://localhost:3000/images/${filename} |
|||
|
|||
### Class Naming Convention |
|||
|
|||
Use BEM naming system for class names: http://getbem.com/naming/ |
|||
|
|||
## Styling rules 🎨 |
|||
|
|||
### Mobile First 📱 |
|||
|
|||
Use Mobile First approach. Start with mobile device styling and then focus on other screen sizes by using @media ( |
|||
min-width: ...px). |
|||
|
|||
### CSS Units |
|||
|
|||
Use `rem` units instead of pixels. |
|||
For example, `1rem = 16pixels` depends on project. |
|||
|
|||
No need to use rem in situations of `1px`, `2px` or `3px`. (usually used for borders width). |
|||
It's important to mention that the block is "elastic". By using `rem` units we are able to scale the layout and keep the |
|||
aspect ratio. |
|||
|
|||
### Global Styling |
|||
|
|||
The development environment includes CSS rules of the original project. This is including fonts, CSS variables, |
|||
container logic etc... |
|||
|
|||
The CSS file is already embedded in served HTML. |
|||
|
|||
# JavaScript |
|||
|
|||
## Sliders |
|||
|
|||
For any kind of slides animations - use [SwiperJS](https://swiperjs.com/get-started). |
|||
|
|||
The lib is included in the project and ready for use. Test with `window.Swiper` in browser console. |
|||
|
|||
# Summary |
|||
|
|||
## Short Video Review |
|||
Check the [Video Guide](https://www.loom.com/share/1c707a4ea14e48b7a35a49d7b0a6f1e0) |
|||
> PRO TIP |
|||
> Use x2 speed in the player 😜 |
|||
|
|||
|
|||
## Testing ✅ |
|||
|
|||
### Development Toolbar ✔️ |
|||
|
|||
Use development toolbar to switch between data sources. |
|||
|
|||
### Conditional Statements |
|||
Make sure the data is available in Handlebars template. |
|||
If there is a chance that some data/property will be missing - wrap the HTML layout with conditional verification: |
|||
|
|||
Example: |
|||
Render the link only if URL exists |
|||
``` |
|||
{{#if link_cta_url }} |
|||
<a href="{{link_cta_url}}">Click Here</a> |
|||
{{ endif }} |
|||
``` |
|||
|
|||
### Hover/Focus States ✔️ |
|||
|
|||
Make sure all "clickable" elements have `&:hover` & `&:focus` states in CSS rules. |
|||
This might be related to links, buttons or any other interactive elements. |
|||
|
|||
### Responsiveness ✔️ |
|||
|
|||
Make sure the layout is rendered correctly on all standard breakpoint size: |
|||
|
|||
1. 1920px in width... |
|||
2. 1680px |
|||
3. 1440px |
|||
4. 1360px |
|||
5. 1280px |
|||
6. 1024px |
|||
7. 980px |
|||
8. 768px |
|||
9. 600px |
|||
10. 414px |
|||
11. 375px |
|||
|
|||
If you follow the rules of REM unit - your layout must be "elastic" and most of the breakpoints should be rendered |
|||
properly. |
|||
|
|||
### Pixel Perfect Test ✔️ |
|||
|
|||
Compare the final result with provided screenshots. |
|||
|
|||
You can use |
|||
available [Browser Extensions](https://chrome.google.com/webstore/detail/perfectpixel-by-welldonec/dkaagdgjmgdmbnecmcefdhjekcoceebi?hl=en) |
|||
or any other tool/approach that you're familiar with. |
|||
The idea is to have "pretty close" result to requested design. |
|||
|
|||
## Delivery |
|||
Once you finished with the task, before letting me know - please review the next [Google Form link](https://forms.gle/w4sJjAyRDSSBRuGLA](https://forms.gle/w4sJjAyRDSSBRuGLA) |
|||
|
|||
If everything is OK - send a Pull Request to the git project or share a ZIP file with me directly. |
|||
@ -1,34 +0,0 @@ |
|||
{ |
|||
"name": "<%= blockGroupName %>/<%= blockFilename %>", |
|||
"version": "<%= version %>", |
|||
"title": "<%= title %>", |
|||
"categories": [], |
|||
"icon": "shortcode", |
|||
"preview_image": "<%= preview_image %>", |
|||
"field_groups": [ |
|||
{ |
|||
"name": "content", |
|||
"label": "Content", |
|||
"fields": { |
|||
"title": { |
|||
"type": "text", |
|||
"label": "Title" |
|||
}, |
|||
"content": { |
|||
"type": "wysiwyg", |
|||
"label": "Content" |
|||
}, |
|||
"cta": { |
|||
"type": "link", |
|||
"label": "CTA" |
|||
} |
|||
} |
|||
}, |
|||
{ |
|||
"name": "styling", |
|||
"label": "Styling", |
|||
"fields": { |
|||
} |
|||
} |
|||
] |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
module.exports = { |
|||
cssUrl: "https://", |
|||
jsUrl: "https://", |
|||
blockName: "<%= blockFilename %>", |
|||
baseView: "<%= baseView %>", |
|||
remToPx: <%= remToPx %>, |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
{ |
|||
"title": "Option Two", |
|||
"content": "<p><b>Delivering top results to industry leaders:</b></p>", |
|||
"image_url": "https://fakeimg.pl/400x400/", |
|||
"cta_text": "Our Success Stories", |
|||
"url": "https://google.com" |
|||
} |
|||
|
|||
/** |
|||
* Tips for good data.json example: |
|||
* - Provide Short and Long version of text (title/content). |
|||
* - Put <a> and <strong> elements in content. |
|||
* - Upload different sizes and ratios of images. |
|||
* - Provide options with and without CTA to test conditional logic. |
|||
*/ |
|||
@ -1,7 +0,0 @@ |
|||
{ |
|||
"title": "As a Global Salesforce Partner,\n we deliver Service Cloud solutions\n and complex Field Service Management\n in diverse industries worldwide", |
|||
"content": "<p><b>Delivering top results to industry leaders:</b></p>", |
|||
"image_url": "https://fakeimg.pl/400x400/", |
|||
"cta_text": "Our Success Stories", |
|||
"url": "https://google.com" |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
# Basic |
|||
.idea |
|||
.DS_Store |
|||
node_modules |
|||
vendor |
|||
|
|||
# Custom |
|||
@ -1,11 +0,0 @@ |
|||
{ |
|||
"name": "<%= blockFilename %>", |
|||
"version": "<%= version %>", |
|||
"scripts": { |
|||
"start": "component-dev", |
|||
"build": "component-build" |
|||
}, |
|||
"devDependencies": { |
|||
"create-block-dev-tool": "<%= devToolSource %>" |
|||
} |
|||
} |
|||
|
Before Width: | Height: | Size: 9.3 KiB |
@ -1,7 +0,0 @@ |
|||
(function ($) { |
|||
|
|||
console.log('Ready!'); |
|||
console.log('jQuery =', $); |
|||
console.log('Swiper =', window.Swiper); |
|||
|
|||
})(window.jQuery); |
|||
@ -1,40 +0,0 @@ |
|||
/** |
|||
* Use "rem" instead of pixels. 1rem = <%= remToPx %>pixels |
|||
* No need to use rem in situations of "1px". (usually used for borders width). |
|||
* |
|||
* Use BEM naming system for class names: http://getbem.com/naming/ |
|||
* |
|||
* Use Mobile First approach. |
|||
* Start with mobile device styling and then focus on other screen sizes by using @media (min-width: ...px). |
|||
* |
|||
*/ |
|||
|
|||
.<%= blockClassName %> { |
|||
|
|||
&__header { |
|||
// Header Mobile. |
|||
|
|||
@media (min-width: 1024px) { |
|||
// Header Desktop. |
|||
} |
|||
|
|||
&-heading { |
|||
// Child Element. |
|||
} |
|||
} |
|||
|
|||
&__visual { |
|||
&-image { |
|||
|
|||
} |
|||
} |
|||
|
|||
&__content { |
|||
|
|||
} |
|||
|
|||
&__footer { |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
<section class="<%= blockClassName %> <%= blockClassName %>--{{ config.activeDataFile }}"> |
|||
|
|||
{{! |
|||
Remove Everything Below: |
|||
}} |
|||
|
|||
<style> |
|||
code { |
|||
background-color: #eee; |
|||
color: #333; |
|||
padding: 1rem 1.5rem; |
|||
border: 2px solid #d8d8d8; |
|||
display: inline-block; |
|||
border-radius: 2px; |
|||
font-size: 14px; |
|||
white-space: pre-line; |
|||
} |
|||
</style> |
|||
|
|||
<header class="<%= blockClassName %>__header"> |
|||
<h1 class="<%= blockClassName %>__header-heading">Ready!</h1> |
|||
</header> |
|||
|
|||
<div class="<%= blockClassName %>__content"> |
|||
<p> |
|||
Review the `/data` folder with JSON data files.<br> |
|||
Don't change data JSON files - use the data as it is. |
|||
</p> |
|||
<p>Build the layout based on provided data structure.</p> |
|||
|
|||
<br><br> |
|||
<hr> |
|||
|
|||
<h2>Image Example</h2> |
|||
<div> |
|||
<img src="/images/demo.jpeg" alt=""> |
|||
<br> |
|||
<code><img src="/images/demo.jpeg" alt=""> |
|||
</code> |
|||
</div> |
|||
|
|||
<br><br> |
|||
<hr> |
|||
|
|||
<h2>Available Buttons Styling</h2> |
|||
|
|||
<div> |
|||
<a href="#" class="btn btn--primary">Primary</a> |
|||
<br> |
|||
<code><a href="#" class="btn btn--primary">Primary</a> |
|||
</code> |
|||
</div> |
|||
|
|||
<br> |
|||
|
|||
<div> |
|||
<a href="#" class="btn btn--secondary">Secondary</a> |
|||
<br> |
|||
<code><a href="#" class="btn btn--secondary">Secondary</a> |
|||
</code> |
|||
</div> |
|||
</div> |
|||
|
|||
{{! |
|||
Remove END |
|||
}} |
|||
|
|||
</section> |
|||
@ -1,14 +0,0 @@ |
|||
export function capitalize(str) { |
|||
if (typeof str !== 'string') { |
|||
return ''; |
|||
} |
|||
|
|||
return str |
|||
.toLowerCase() |
|||
.split(/[ -_]/g) |
|||
.filter((word) => !!word) |
|||
.map((word) => { |
|||
return word.charAt(0).toUpperCase() + word.slice(1); |
|||
}) |
|||
.join(' '); |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue