schema meta :)

This commit is contained in:
lovebird 2025-03-31 23:47:40 +02:00
parent 0f38b9553a
commit cd335a1a12
44 changed files with 65 additions and 21060 deletions

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-03-31T21:16:41.669Z"
"default": "2025-03-31T21:38:59.997Z"
},
"description": {
"type": "string",

File diff suppressed because one or more lines are too long

View File

@ -670,7 +670,7 @@
},
"https://scholarworks.uni.edu/cgi/viewcontent.cgi?article=3680%5C&context=grp": {
"isValid": false,
"timestamp": 1743455646211
"timestamp": 1743457141688
},
"https://pmc.ncbi.nlm.nih.gov/articles/PMC10489002/": {
"isValid": true,
@ -754,7 +754,7 @@
},
"https://journals.plos.org/plosone/article?id=10.1371%252Fjournal.pone.0288696": {
"isValid": false,
"timestamp": 1743455646737
"timestamp": 1743457142056
},
"https://www.youtube.com/watch?v=_a7usMe_K38": {
"isValid": true,
@ -841,7 +841,7 @@
},
"https://www.toraytac.com/media/c3feb206-1398-4e0e-bca6-df7780f11745/tcCurg/TenCate%2520Advanced%2520Composites/Documents/Technical%2520papers/TenCate_chopped_fiber_thermoplastics_compression_molding_technical_paper.pdf": {
"isValid": false,
"timestamp": 1743455646939
"timestamp": 1743457142209
},
"https://youtu.be/qtZv96cifIU": {
"isValid": true,

22
bom.md
View File

@ -1,22 +0,0 @@
Here's a comparative overview of open-source SBOM tools presented in a Markdown table format:
## Open Source SBOM Tools Comparison
| Tool Name | Supported Formats | Key Features | Source Link |
|----------------------------|-------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| **Syft** | SPDX, CycloneDX, Syft | CLI tool for container/image analysis, supports multiple Linux distros | [GitHub](https://github.com/anchore/syft) [6][14] |
| **Microsoft SBOM Tool** | SPDX 2.2 | Enterprise-ready, Docker image support, component detection library | [GitHub](https://github.com/microsoft/sbom-tool) [2][3][6] |
| **Dependency-Track** | CycloneDX | Vulnerability visualization, component analysis platform | [GitHub](https://github.com/DependencyTrack/dependency-track) [2] |
| **CycloneDX Generator** | CycloneDX | Multi-language support, API server integration, dependency tree analysis | [GitHub](https://github.com/CycloneDX/cdxgen) [6][12] |
| **SPDX SBOM Generator** | SPDX | Supports 15+ package managers, CLI interface | [GitHub](https://github.com/spdx/spdx-sbom-generator) [6] |
| **DISTRO2SBOM** | SPDX, CycloneDX | Linux package detection, OS-agnostic analysis | [GitHub](https://github.com/ossie-git/DISTRO2SBOM) [6] |
| **Tern** | SPDX, CycloneDX, YAML | Container layer analysis, license compliance focus | [GitHub](https://github.com/tern-tools/tern) [6] |
| **IBM SBOM Utility** | CycloneDX, SPDX | Validation against JSON schemas, license policy management | [GitHub](https://github.com/IBM/sbom-utility) [9][11] |
Key technical differentiators:
- **Format specialization**: Syft and cdxgen offer multi-format support[6][14][12], while Microsoft's tool focuses exclusively on SPDX[3][6]
- **Containerization**: Syft and Tern specialize in container/image analysis[6][14]
- **Language support**: CycloneDX Generator supports 30+ programming languages[6][12]
- **Enterprise features**: IBM's utility offers schema validation and policy management[9][11], Microsoft's tool integrates with build pipelines[6]
For developers working with TypeScript ecosystems, Syft and SPDX SBOM Generator offer native npm/yarn support[6][14], while the IBM utility provides API integration capabilities[11] that could complement CI/CD pipelines.

View File

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 238 KiB

View File

@ -1,58 +0,0 @@
{
"$schema": "https://beta.frontmatter.codes/frontmatter.schema.json",
"frontMatter.taxonomy.contentTypes": [
{
"name": "default",
"pageBundle": false,
"previewPath": null,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "draft"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}
],
"frontMatter.framework.id": "astro",
"frontMatter.preview.host": "http://localhost:4321",
"frontMatter.content.pageFolders": [
{
"title": "pm-site",
"path": "[[workspace]]"
}
],
"frontMatter.content.publicFolder": "public"
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import { Img } from "imagetools/components";
import { i18n as Translate } from "@polymech/astro-base";
import BaseLayout from "@/layouts/BaseLayout.astro";
import Wrapper from "@/components/containers/Wrapper.astro";
import GalleryK from "@/components/polymech/Gallery.astro";
import GalleryK from "@/components/polymech/gallery.astro";
import { files, forward_slash } from "@polymech/commons";
import pMap from "p-map";
import { sync as exists } from "@polymech/fs/exists";

View File

@ -1,9 +1,8 @@
---
import { default_image, IMAGE_SETTINGS } from "config/config.js";
import { IMAGE_SETTINGS } from "config/config.js";
import Translate from "@/components/polymech/i18n.astro";
import { Img } from "imagetools/components";
import Sidebar from "@/components/howtos/sidebar2.astro";
import { asset_local_abs,asset_local_rel, IHowto } from "@/model/howto/howto.js";
import { asset_local_rel, IHowto } from "@/model/howto/howto.js";
const { title, url, model, selected = false } = Astro.props;
const item: IHowto = model.item;
const classes = `group relative bg-white overflow-hidden group rounded-xl ${selected ? "ring-2 ring-orange-500" : ""}`;

View File

@ -1,7 +1,6 @@
---
import { getCollection } from 'astro:content';
import { IHowto } from "@/model/howto/howto.js";
import { i18n as Translate } from "@polymech/astro-base";
const locale = Astro.currentLocale;
const items = await getCollection('howtos')
const { category } = Astro.props;
@ -44,7 +43,8 @@ const categoryKeys = Object.keys(categories).sort();
<span class="inline-flex items-center justify-center px-2 py-0.5 ms-2 text-xs font-medium text-gray-800 bg-gray-100 rounded-full dark:bg-gray-700 dark:text-gray-300">{items.length}</span>
</a>
</li>
{categoryKeys.map((category) => (
{
categoryKeys.map((category) => (
<li key={category}>
<div class="mt-4 mb-2">
<a href={`/${locale}/howto-category/${category.toLowerCase()}`} class="flex items>
@ -52,7 +52,8 @@ const categoryKeys = Object.keys(categories).sort();
</a>
</div>
</li>
))}
))
}
</ul>
</div>
</aside>

View File

@ -1,188 +0,0 @@
{
"_createdBy": "gus-merckel",
"mentions": [],
"_deleted": false,
"fileLink": "",
"slug": "cut-out-shapes-out-of-plastic-sheets-with-a-cnc-",
"_modified": "2023-10-27T18:09:36.519Z",
"previousSlugs": [
"cut-out-shapes-out-of-plastic-sheets-with-a-cnc-"
],
"_created": "2023-08-23T18:20:09.098Z",
"description": "In this how to, I will show you our process to cut HDPE Sheets using a X-Carve CNC.\n\nHere is the full video in spanish with subtitles https://www.youtube.com/watch?v=4LrrFz802To ",
"votedUsefulBy": [
"sigolene",
"mattia",
"uillinoispreciousplastics"
],
"creatorCountry": "mx",
"total_downloads": 0,
"title": "Cut out shapes out of plastic sheets with a CNC ",
"time": "< 5 hours",
"files": [],
"difficulty_level": "Medium",
"_id": "038gjWgLjiyYknbEjDeI",
"tags": {
"RTCBJAFa05YBVVBy0KeO": true
},
"category":"machines",
"total_views": 232,
"_contentModifiedTimestamp": "2023-08-23T18:20:09.098Z",
"cover_image": {
"name": "IMG_20200605_142311.jpg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=c272c174-1adc-45af-967b-771adce7295d",
"type": "image/jpeg",
"fullPath": "uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg",
"updated": "2021-04-05T15:09:00.605Z",
"size": 124661,
"timeCreated": "2021-04-05T15:09:00.605Z",
"contentType": "image/jpeg"
},
"comments": [],
"moderatorFeedback": "",
"steps": [
{
"title": "Measure the plastic sheet",
"text": "For this step we need to measure our plastic sheet: Height, Width and Thickness. Our X-Carve machine works with the CAM Software EASEL, for me, the easiest software for CNC milling out there. \n\nThe cool thing about Easel (https://easel.inventables.com/) is that you can \"simulate\" your actual material and THEY EVEN HAVE HDPE 2-Colors in their cutting material lists!!\n\n\n",
"images": [
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/1.jpg",
"name": "1.jpg",
"size": 74095,
"type": "image/jpeg",
"timeCreated": "2021-03-26T19:42:05.766Z",
"contentType": "image/jpeg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F1.jpg?alt=media&token=293d733d-05a5-494a-9340-47f4564f1939",
"updated": "2021-03-26T19:42:05.766Z"
},
{
"contentType": "image/jpeg",
"timeCreated": "2021-03-26T19:42:05.669Z",
"updated": "2021-03-26T19:42:05.669Z",
"size": 69665,
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F2.jpg?alt=media&token=004f50f1-97ac-4df4-9ba9-f463aa4cbca3",
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/2.jpg",
"name": "2.jpg",
"type": "image/jpeg"
}
],
"_animationKey": "unique1"
},
{
"text": "Using the CNC clamps from the X-Carve, secure the sheet to the table, ",
"_animationKey": "unique2",
"images": [
{
"updated": "2021-03-26T19:42:06.249Z",
"size": 55544,
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/3.jpg",
"timeCreated": "2021-03-26T19:42:06.249Z",
"name": "3.jpg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F3.jpg?alt=media&token=0b9c1914-1c75-429e-b34a-1e2b3706edef",
"contentType": "image/jpeg",
"type": "image/jpeg"
}
],
"title": "Secure sheet "
},
{
"title": "Choosing a file to cut ",
"text": "Now we go to our illustrator, such as Inkscape to design a vector file or download and open source one frome https://thenounproject.com/.\n\nWe download the SVG file, which is an open source vector format and import it to Easel. \n",
"images": [
{
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F4.jpg?alt=media&token=1cd2d49d-9335-4bb1-ac2a-e625322ca604",
"contentType": "image/jpeg",
"timeCreated": "2021-03-26T19:42:06.727Z",
"updated": "2021-03-26T19:42:06.727Z",
"name": "4.jpg",
"size": 42952,
"type": "image/jpeg",
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/4.jpg"
},
{
"size": 69255,
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/5.jpg",
"updated": "2021-03-26T19:42:06.833Z",
"timeCreated": "2021-03-26T19:42:06.833Z",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F5.jpg?alt=media&token=7cca786a-7d47-43bb-900b-b8d101c276b4",
"name": "5.jpg",
"contentType": "image/jpeg",
"type": "image/jpeg"
}
],
"_animationKey": "unique3"
},
{
"text": "Now with the file we can choose the width we want to carve/cut and then we go to cut and start the wizzard:\n- We check that the sheet is fixed.\n- We also specify the cutting bit, we are using a 1/8 flat flute bit. \n- We tell the machine where the coordinate 0-0 is, which we always choose as the down left corner.\n- We raise the bit, turn on the Router!!!\n\nAND PUM THE MAGIC BEGINS!!",
"title": "Follow the cutting Wizzard",
"images": [
{
"timeCreated": "2021-03-26T19:42:07.493Z",
"size": 72226,
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/6.jpg",
"updated": "2021-03-26T19:42:07.493Z",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F6.jpg?alt=media&token=ba7195dd-7771-435f-a188-057457697332",
"contentType": "image/jpeg",
"type": "image/jpeg",
"name": "6.jpg"
},
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/7.jpg",
"size": 52424,
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F7.jpg?alt=media&token=a3d5820c-cfe2-484e-8f76-f861ab8b756d",
"contentType": "image/jpeg",
"type": "image/jpeg",
"timeCreated": "2021-03-26T19:42:07.308Z",
"updated": "2021-03-26T19:42:07.308Z",
"name": "7.jpg"
},
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/8.jpg",
"name": "8.jpg",
"type": "image/jpeg",
"timeCreated": "2021-03-26T19:42:07.346Z",
"size": 55264,
"contentType": "image/jpeg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F8.jpg?alt=media&token=1c9816d7-3a99-4f41-8d3c-acc2670240f6",
"updated": "2021-03-26T19:42:07.346Z"
}
],
"_animationKey": "uniquenisc2v"
},
{
"text": "You take now your glasses or object and postprocess them and of course show it to your friends, family and so on.\n\n\n",
"images": [
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/9.jpg",
"contentType": "image/jpeg",
"timeCreated": "2021-03-26T19:42:08.147Z",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F9.jpg?alt=media&token=4dcfe37d-e1ad-41e5-a590-40b4c37c5e1a",
"name": "9.jpg",
"updated": "2021-03-26T19:42:08.147Z",
"type": "image/jpeg",
"size": 82214
}
],
"_animationKey": "uniquesgl34",
"title": "Post-production and show case"
},
{
"_animationKey": "uniquem4y0yi",
"title": "Hack it and try it yourself",
"text": "You can try this project with other types of CNC machines, even manual Routers or manual saw, as I did on this video: https://youtu.be/gxkcffQD3eQ, but the important thing is that you share what you do and help this community to grow!!!\n\nShare your ideas and comments!",
"images": [
{
"contentType": "image/jpeg",
"timeCreated": "2021-04-05T15:09:01.445Z",
"fullPath": "uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg",
"type": "image/jpeg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=f94152ff-f923-4054-a3ad-d8ec588856fa",
"size": 124661,
"updated": "2021-04-05T15:09:01.445Z",
"name": "IMG_20200605_142311.jpg"
}
]
}
],
"moderation": "accepted"
}

View File

@ -1,339 +0,0 @@
{
"_createdBy": "gus-merckel",
"mentions": [],
"_deleted": false,
"fileLink": "",
"slug": "cut-out-shapes-out-of-plastic-sheets-with-a-cnc-",
"_modified": "2023-10-27T18:09:36.519Z",
"previousSlugs": [
"cut-out-shapes-out-of-plastic-sheets-with-a-cnc-"
],
"_created": "2023-08-23T18:20:09.098Z",
"description": "In this how to, I will show you our process to cut HDPE Sheets using a X-Carve CNC.\n\nHere is the full video in spanish with subtitles https://www.youtube.com/watch?v=4LrrFz802To ",
"votedUsefulBy": [
"sigolene",
"mattia",
"uillinoispreciousplastics"
],
"creatorCountry": "mx",
"total_downloads": 0,
"title": "Cut out shapes out of plastic sheets with a CNC ",
"time": "< 5 hours",
"files": [],
"difficulty_level": "Medium",
"_id": "038gjWgLjiyYknbEjDeI",
"tags": [
"HDPE"
],
"total_views": 232,
"_contentModifiedTimestamp": "2023-08-23T18:20:09.098Z",
"cover_image": {
"name": "IMG_20200605_142311.jpg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=c272c174-1adc-45af-967b-771adce7295d",
"type": "image/jpeg",
"fullPath": "uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg",
"updated": "2021-04-05T15:09:00.605Z",
"size": 124661,
"timeCreated": "2021-04-05T15:09:00.605Z",
"contentType": "image/jpeg"
},
"comments": [],
"moderatorFeedback": "",
"steps": [
{
"title": "Measure the plastic sheet",
"text": "For this step we need to measure our plastic sheet: Height, Width and Thickness. Our X-Carve machine works with the CAM Software EASEL, for me, the easiest software for CNC milling out there. \n\nThe cool thing about Easel (https://easel.inventables.com/) is that you can \"simulate\" your actual material and THEY EVEN HAVE HDPE 2-Colors in their cutting material lists!!\n\n\n",
"images": [
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/1.jpg",
"name": "1.jpg",
"size": 74095,
"type": "image/jpeg",
"timeCreated": "2021-03-26T19:42:05.766Z",
"contentType": "image/jpeg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F1.jpg?alt=media&token=293d733d-05a5-494a-9340-47f4564f1939",
"updated": "2021-03-26T19:42:05.766Z",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/1.jpg",
"alt": "1.jpg"
},
{
"contentType": "image/jpeg",
"timeCreated": "2021-03-26T19:42:05.669Z",
"updated": "2021-03-26T19:42:05.669Z",
"size": 69665,
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F2.jpg?alt=media&token=004f50f1-97ac-4df4-9ba9-f463aa4cbca3",
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/2.jpg",
"name": "2.jpg",
"type": "image/jpeg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/2.jpg",
"alt": "2.jpg"
}
],
"_animationKey": "unique1"
},
{
"text": "Using the CNC clamps from the X-Carve, secure the sheet to the table, ",
"_animationKey": "unique2",
"images": [
{
"updated": "2021-03-26T19:42:06.249Z",
"size": 55544,
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/3.jpg",
"timeCreated": "2021-03-26T19:42:06.249Z",
"name": "3.jpg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F3.jpg?alt=media&token=0b9c1914-1c75-429e-b34a-1e2b3706edef",
"contentType": "image/jpeg",
"type": "image/jpeg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/3.jpg",
"alt": "3.jpg"
}
],
"title": "Secure sheet "
},
{
"title": "Choosing a file to cut ",
"text": "Now we go to our illustrator, such as Inkscape to design a vector file or download and open source one frome https://thenounproject.com/.\n\nWe download the SVG file, which is an open source vector format and import it to Easel. \n",
"images": [
{
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F4.jpg?alt=media&token=1cd2d49d-9335-4bb1-ac2a-e625322ca604",
"contentType": "image/jpeg",
"timeCreated": "2021-03-26T19:42:06.727Z",
"updated": "2021-03-26T19:42:06.727Z",
"name": "4.jpg",
"size": 42952,
"type": "image/jpeg",
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/4.jpg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/4.jpg",
"alt": "4.jpg"
},
{
"size": 69255,
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/5.jpg",
"updated": "2021-03-26T19:42:06.833Z",
"timeCreated": "2021-03-26T19:42:06.833Z",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F5.jpg?alt=media&token=7cca786a-7d47-43bb-900b-b8d101c276b4",
"name": "5.jpg",
"contentType": "image/jpeg",
"type": "image/jpeg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/5.jpg",
"alt": "5.jpg"
}
],
"_animationKey": "unique3"
},
{
"text": "Now with the file we can choose the width we want to carve/cut and then we go to cut and start the wizzard:\n- We check that the sheet is fixed.\n- We also specify the cutting bit, we are using a 1/8 flat flute bit. \n- We tell the machine where the coordinate 0-0 is, which we always choose as the down left corner.\n- We raise the bit, turn on the Router!!!\n\nAND PUM THE MAGIC BEGINS!!",
"title": "Follow the cutting Wizzard",
"images": [
{
"timeCreated": "2021-03-26T19:42:07.493Z",
"size": 72226,
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/6.jpg",
"updated": "2021-03-26T19:42:07.493Z",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F6.jpg?alt=media&token=ba7195dd-7771-435f-a188-057457697332",
"contentType": "image/jpeg",
"type": "image/jpeg",
"name": "6.jpg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/6.jpg",
"alt": "6.jpg"
},
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/7.jpg",
"size": 52424,
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F7.jpg?alt=media&token=a3d5820c-cfe2-484e-8f76-f861ab8b756d",
"contentType": "image/jpeg",
"type": "image/jpeg",
"timeCreated": "2021-03-26T19:42:07.308Z",
"updated": "2021-03-26T19:42:07.308Z",
"name": "7.jpg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/7.jpg",
"alt": "7.jpg"
},
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/8.jpg",
"name": "8.jpg",
"type": "image/jpeg",
"timeCreated": "2021-03-26T19:42:07.346Z",
"size": 55264,
"contentType": "image/jpeg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F8.jpg?alt=media&token=1c9816d7-3a99-4f41-8d3c-acc2670240f6",
"updated": "2021-03-26T19:42:07.346Z",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/8.jpg",
"alt": "8.jpg"
}
],
"_animationKey": "uniquenisc2v"
},
{
"text": "You take now your glasses or object and postprocess them and of course show it to your friends, family and so on.\n\n\n",
"images": [
{
"fullPath": "uploads/howtos/pbo0Pe44aTngvlD04kGf/9.jpg",
"contentType": "image/jpeg",
"timeCreated": "2021-03-26T19:42:08.147Z",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2Fpbo0Pe44aTngvlD04kGf%2F9.jpg?alt=media&token=4dcfe37d-e1ad-41e5-a590-40b4c37c5e1a",
"name": "9.jpg",
"updated": "2021-03-26T19:42:08.147Z",
"type": "image/jpeg",
"size": 82214,
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/9.jpg",
"alt": "9.jpg"
}
],
"_animationKey": "uniquesgl34",
"title": "Post-production and show case"
},
{
"_animationKey": "uniquem4y0yi",
"title": "Hack it and try it yourself",
"text": "You can try this project with other types of CNC machines, even manual Routers or manual saw, as I did on this video: https://youtu.be/gxkcffQD3eQ, but the important thing is that you share what you do and help this community to grow!!!\n\nShare your ideas and comments!",
"images": [
{
"contentType": "image/jpeg",
"timeCreated": "2021-04-05T15:09:01.445Z",
"fullPath": "uploads/howtos/038gjWgLjiyYknbEjDeI/IMG_20200605_142311.jpg",
"type": "image/jpeg",
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fhowtos%2F038gjWgLjiyYknbEjDeI%2FIMG_20200605_142311.jpg?alt=media&token=f94152ff-f923-4054-a3ad-d8ec588856fa",
"size": 124661,
"updated": "2021-04-05T15:09:01.445Z",
"name": "IMG_20200605_142311.jpg",
"src": "/resources/howtos/cut-out-shapes-out-of-plastic-sheets-with-a-cnc-/IMG_20200605_142311.jpg",
"alt": "IMG_20200605_142311.jpg"
}
]
}
],
"moderation": "accepted",
"user": {
"_modified": "2024-01-08T13:28:33.484Z",
"_id": "gus-merckel",
"subType": "mix",
"moderation": "accepted",
"_deleted": false,
"verified": false,
"type": "workspace",
"location": {
"lat": 19.3935,
"lng": -99.1656
},
"_created": "2024-01-08T13:28:33.484Z",
"geo": {
"latitude": 19.3935,
"lookupSource": "coordinates",
"longitude": -99.1656,
"localityLanguageRequested": "en",
"continent": "North America",
"continentCode": "NA",
"countryName": "Mexico",
"countryCode": "MX",
"principalSubdivision": "Ciudad de Mexico",
"principalSubdivisionCode": "MX-CMX",
"city": "Mexico City",
"locality": "Benito Juarez",
"postcode": "03103",
"plusCode": "76F29RVM+CQ",
"localityInfo": {
"administrative": [
{
"name": "Mexico",
"description": "country in North America",
"isoName": "Mexico",
"order": 2,
"adminLevel": 2,
"isoCode": "MX",
"wikidataId": "Q96",
"geonameId": 3996063
},
{
"name": "Mexico City",
"description": "capital and largest city of Mexico",
"order": 5,
"adminLevel": 4,
"wikidataId": "Q1489",
"geonameId": 3530597
},
{
"name": "Ciudad de Mexico",
"description": "capital and largest city of Mexico",
"isoName": "Ciudad de Mexico",
"order": 6,
"adminLevel": 4,
"isoCode": "MX-CMX",
"wikidataId": "Q1489",
"geonameId": 3527646
},
{
"name": "Benito Juarez",
"description": "territorial demarcation of the Mexico City in Mexico",
"order": 7,
"adminLevel": 6,
"wikidataId": "Q2356998",
"geonameId": 3827406
}
],
"informative": [
{
"name": "North America",
"description": "continent and northern subcontinent of the Americas",
"isoName": "North America",
"order": 1,
"isoCode": "NA",
"wikidataId": "Q49",
"geonameId": 6255149
},
{
"name": "America/Mexico_City",
"description": "time zone",
"order": 3
},
{
"name": "Greater Mexico City",
"description": "geographical object",
"order": 4,
"wikidataId": "Q665894"
},
{
"name": "03103",
"description": "postal code",
"order": 8
}
]
}
},
"data": {
"urls": [
{
"name": "Email",
"url": "mailto:gustavomerckel@gmail.com"
},
{
"name": "Facebook",
"url": "https://www.facebook.com/pl%c3%a1stico-chido-110888520718193"
},
{
"name": "sponsor the work",
"url": "https://www.patreon.com/one_army"
}
],
"description": "Plástico Chido builds and modifies the PP machines, and also experiments with CNC and Laser Cut",
"services": [
{
"welding": false,
"assembling": false,
"machining": false,
"electronics": false,
"molds": false
}
],
"title": "Plástico Chido",
"images": []
},
"detail": {
"services": [],
"urls": []
}
},
"category": {
"label": "uncategorized"
}
}

View File

@ -1,58 +0,0 @@
---
// Import the required interfaces and utilities
import { IHowto } from "@/model/howto/howto.js";
import { i18n as Translate } from "@polymech/astro-base";
import { getCollection } from 'astro:content';
const items = await getCollection('howtos');
// Group howtos by category
const categoriesMap:Record<string,any[]> = {};
items.forEach(item => {
const howto = item.data;
const category = howto.category || 'uncategorized';
if (!categoriesMap[category]) {
categoriesMap[category] = [];
}
categoriesMap[category].push(item);
});
const categories = Object.entries(categoriesMap)
.sort(([a], [b]) => a.localeCompare(b));
---
<aside id="howto-sidebar" class="fixed top-0 left-0 z-40 w-64 h-screen transition-transform -translate-x-full sm:translate-x-0" aria-label="Sidebar">
<div class="h-full px-3 py-4 overflow-y-auto bg-gray-50 dark:bg-gray-800">
<ul class="space-y-2 font-medium">
{categories.map(([category, categoryItems]) => (
<li>
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls={`dropdown-${category}`} data-collapse-toggle={`dropdown-${category}`}>
<svg class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 22 21">
<path d="M16.975 11H10V4.025a1 1 0 0 0-1.066-.998 8.5 8.5 0 1 0 9.039 9.039.999.999 0 0 0-1-1.066h.002Z"/>
<path d="M12.5 0c-.157 0-.311.01-.565.027A1 1 0 0 0 11 1.02V10h8.975a1 1 0 0 0 1-.935c.013-.188.028-.374.028-.565A8.51 8.51 0 0 0 12.5 0Z"/>
</svg>
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap"><Translate>{category}</Translate></span>
<span class="inline-flex items-center justify-center px-2 ms-3 text-sm font-medium text-gray-800 bg-gray-100 rounded-full dark:bg-gray-700 dark:text-gray-300">{categoryItems.length}</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
</svg>
</button>
<ul id={`dropdown-${category}`} class="hidden py-2 space-y-2">
{categoryItems.map((item) => (
<li>
<a href={`/howtos/${item.slug}`} class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">{item.data.title}</a>
</li>
))}
</ul>
</li>
))}
</ul>
</div>
</aside>
<!-- Mobile toggle button -->
<button data-drawer-target="howto-sidebar" data-drawer-toggle="howto-sidebar" aria-controls="howto-sidebar" type="button" class="inline-flex items-center p-2 mt-2 ms-3 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
<span class="sr-only"><Translate>Open sidebar</Translate></span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clip-rule="evenodd" fill-rule="evenodd" d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"></path>
</svg>
</button>

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
## Todos
- for Astro, tailwind
- no react or additional dependencies
- dont comment, just return the entire code
- skip checked tasks
- leave everything else intact, dont optimize !
## Todos
Detail.astro:
- [ ] make step titles linkable

View File

@ -1,9 +0,0 @@
kbotd --preferences ./todos.md \
--include=./Detail.astro \
--include=./howto_sample.json \
--disable=terminal,git,npm,user,interact,search,email,web \
--disableTools=read_file,read_files,list_files,file_exists,web \
--model=gpt-4.5-preview \
--router=openai \
--mode=completion \
--dst=./Detail2.astro

View File

@ -1,187 +0,0 @@
---
const pricingPlans = [
{
name: "Individual",
monthlyPrice: "149",
annualPrice: "100",
description: "For freelancers and independent designers",
features: [
"Unlimited access to all design assets",
"Good plan for a freelancer and solo designer",
"Support for basic web development",
"Work on up to 3 product design projects",
"Only one user per account",
"Commercial use",
],
unavailableFeatures: [
"Licensed for teams, startups, agencies and corporates",
"Several users per account",
"Commercial use",
],
},
{
name: "Startup",
monthlyPrice: "249",
annualPrice: "300",
description: " Best choice for any size team or agency",
features: [
"Everything included in the Individual plan",
"Licensed for teams, startups, agencies and corporates",
"Several users per account",
"Commercial use",
],
unavailableFeatures: [
"Missing advanced 3D design tools",
"Without premium system collaboration",
"Standard support",
],
},
{
name: "Company",
monthlyPrice: "50",
annualPrice: "600",
description: "For businesses aiming to lead in innovation.",
features: [
"Everything included in the Individual plan",
"Everything included in the Startup plan",
"Unlimited licensed for teams, startups, agencies and corporates",
"Unlimited users per account",
"Commercial use",
],
unavailableFeatures: [],
},
];
---
<section x-data="{annual: false}">
<div
class="flex flex-col gap-12 h-full justify-between p-4 text-center pt-20">
<div class="max-w-xl mx-auto">
<h1
class="text-lg text-neutral-600 font-mono tracking-tight text-balance">
Become a member today and unlock unlimited access.
</h1>
<p class="text-sm text-balance text-neutral-500">
Enroll in our membership program today at a discounted rate and enjoy
full access to our incredible lineup of products.
</p>
<div class="max-w-sm mx-auto">
<div
class="overflow-hidden inline-flex mt-6 z-0 h-14 rounded-lg p-0.5 w-full bg-white">
<button
class="text-xs text-black w-full block font-medium px-8 py-2 transition rounded-lg border border-transparent"
@click="annual = false"
:class="annual == false ? 'bg-neutral-100 border text-black ' : ''"
type="button"
>Monthly</button
>
<button
class="text-xs text-black w-full block font-medium px-8 py-2 transition rounded-lg border border-transparent"
@click="annual = true"
:class="annual == true ? 'bg-neutral-100 border text-black' : ''"
type="button"
>Annual ( save 25% )</button
>
</div>
</div>
</div>
</div>
<div class="grid lg:grid-cols-3 gap-2 py-2">
{
pricingPlans.map((plan) => (
<>
<div class="flex flex-col h-full bg-white p-4 rounded-xl">
<div class=" h-full flex flex-col ">
<div>
<div>
<div class="flex justify-between items-center">
<h3
id="tier-essential"
class="text-lg text-neutral-600 font-mono tracking-tight uppercase">
{plan.name}
</h3>
<p class="flex items-baseline gap-x-1 text-lg text-neutral-600 font-mono tracking-tight">
<span>
<span x-show="!annual">${plan.monthlyPrice}</span>
<span x-show="annual">${plan.annualPrice}</span>
</span>
<span class="text-xs">
/m
<span
x-show="annual"
style="display: none;">
(billed annually)
</span>
</span>
</p>
</div>
<p class="mt-4 text-sm text-neutral-500">
{plan.description}
</p>
</div>
<div class="mt-8">
<button
type="button"
title="link to your page"
aria-label="your label"
class="relative group overflow-hidden pl-4 font-mono h-14 flex space-x-6 items-center bg-orange-500 hover:bg-black duration-300 rounded-xl w-full justify-between">
<span class="relative uppercase text-xs text-white">
Add to cart
</span>
<div
aria-hidden="true"
class="w-12 text-white transition duration-300 -translate-y-7 group-hover:translate-y-7">
<div class="h-14 flex">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 m-auto fill-white">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
/>
</svg>
</div>
<div class="h-14 flex">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 m-auto fill-white">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 8.25 21 12m0 0-3.75 3.75M21 12H3"
/>
</svg>
</div>
</div>
</button>
</div>
</div>
<ul class="mt-6 text-xs space-y-1 font-mono uppercase text-neutral-500">
{plan.features.map((feature) => (
<li class="inline-flex items-start gap-3 ">
<span>+</span>
{feature}
</li>
))}
{plan.unavailableFeatures.map((feature) => (
<li class="inline-flex items-start gap-3 opacity-60">
<span></span> {feature}
</li>
))}
</ul>
</div>
</div>
</>
))
}
</div>
</section>

View File

@ -1,24 +0,0 @@
## Map
- skip checked todos
- dont comment
- simple markup, for errors, use a dedicated error component
- [ ] complete & self-containing Astro component, tailwind: Google Maps Static API, pass location array & Options as props, default to 2 locations in Lamu, save in ./map.astro
- [ ] document all in ./map.md, including API key setup
- [ ] save all TS types in ./map-types.ts
export interface GeoPos {
lon: number
lat: number
}
export interface Location {
geo:GeoPos
title:string
}
export interface Options{
zoom?:number
api_key?:string
}

View File

@ -1,9 +0,0 @@
## Maps
#kbotd --prompt=./todos-map.md --preferences=./preferences.md --mode=completion --dst=./maps.md --model=anthropic/claude-3.7-sonnet:thinking
#kbotd --prompt=./todos-map.md --preferences=./preferences.md --model=anthropic/claude-3.7-sonnet:thinking
#kbotd --prompt=./todos-map.md --preferences=./preferences.md --router=openai --model=gpt-4.5-preview
kbotd --prompt=./todos-map.md --preferences=./preferences.md --model=anthropic/claude-3.7-sonnet:thinking

View File

@ -1,34 +0,0 @@
## GalleryL.astro
skip checked todos
- [x] analyse GalleryL.astro, spot problems, make recommendations, with links to code, store as markdown raw
- [x] description & title overlay in lightbox mode: use rounded background, black, for better visibilty
## resources.astro
- [x] render each group in a column, 2, on mobile 1
- [x] render group only when links are present
- [x] add another group "authors", when present
data.authors": [
{
"name": "PlasticHub S.L.",
"url": "${author_link}"
}
]
- [ ] dont comment, just update the component ("resources.astro")
- [x] add another group "Extras", its a new type of rendering, the markup is in data.extra_resources or/and data.tests, use the component function to render those fields (provided by array)
- [x] change layout
- [x] filteredGoups : 2 columns on desktop, 1 column on mobile
- [x] extraContent : 1 columns on desktop, 1 column on mobile
- [x] leave all other functions intact if possible
## readme.astro
- [x] update "readme.astro" component
- [x] processImageUrls: complete, use a markdown parser (mdast) to adjust the url for images
- [x] for local/relative paths, prefix the url with a given url (astro props)
- [x] for images, use Img (imagetools/components) component
- [ ] translate only when enabled

View File

@ -1,9 +0,0 @@
#kbotd --prompt=./Gallery.md --mode=completion --dst=./GalleryK.astro --filters=code --router=openai --model=gpt-4o
#kbotd --prompt=./Gallery.md --mode=completion --dst=./GalleryK2.astro --filters=code --include=./GalleryS.astro --router=openai --model=gpt-4o
#kbotd --prompt=./Lightbox.md --mode=completion --dst=./GalleryL.astro --filters=code --include=./GalleryM.astro --include=./lightbox.html --router=openai --model=gpt-4o
#kbotd --prompt=./todos.md --mode=completion --dst=./GalleryL2.astro --filters=code --include=./GalleryL.astro --model=openai/o1
#kbotd --prompt=./todos.md --include=resources.astro --filters=code
#kbotd --prompt=./todos.md --include=readme.astro --filters=code
kbotd --prompt=./Gallery.md --mode=completion --dst=./GalleryK.astro --filters=code --include=./Gallery.astro --model=anthropic/claude-3.7-sonnet:thinking

View File

@ -191,10 +191,10 @@ export const default_image = () => {
}
export const DEFAULT_LICENSE = `CERN Open Source Hardware License`
export const DEFAULT_CONTACT = `sales@plastic-hub.com`
/////////////////////////////////////////////
//
// Optimization
export const O_IMAGE = IMAGE_PRESET[E_BROADBAND_SPEED.MEDIUM]
export const IMAGE_SETTINGS =
{

View File

@ -1,12 +1,9 @@
import { defineCollection, z } from "astro:content"
import { ComponentConfigSchema } from '@polymech/commons/component'
import { loader } from './model/component/component.js'
import { loader as howtoLoader } from './model/howto/howto.js'
import { loader as directorLoader } from './model/directory/item.js'
import { RETAIL_PRODUCT_BRANCH, PROJECTS_BRANCH } from 'config/config.js'
import { glob } from 'astro/loaders'
const store = defineCollection({

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,141 +0,0 @@
---
import fs from 'fs';
import path from 'path';
import { IHowto,asset_local_rel } from "@/model/howto/howto/howto";
import { Img } from 'imagetools/components';
import { i18n as Translate } from "@polymech/astro-base";
import { files,forward_slash } from "@polymech/commons"
import BaseLayout from "@/layouts/BaseLayout.astro";
import Wrapper from "@/components/containers/Wrapper.astro";
import GalleryK from "@/components/polymech/Gallery.astro";
import { HOWTO_FILES_WEB, HOWTO_FILES_ABS } from "config/config.js"
import Sidebar from "@/components/howtos/sidebar2.astro";
interface Props {
howto: IHowto;
}
const { howto } = Astro.props;
const coverLocaleRel = await asset_local_rel(howto, howto.cover_image);
const howto_abs = HOWTO_FILES_ABS(howto.slug)
let model_files: any = [ ...files(howto_abs, '**/**/*.(step|stp)')]
model_files = model_files.map((f) => forward_slash(`${howto.slug}/${path.relative(path.resolve(howto_abs), f)}`))
---
<BaseLayout class="markdown-content">
<Wrapper>
<div class="howto-container max-w-4xl mx-auto p-4 ml-4 pl-8">
<header class="mb-8">
<h1 class="text-3xl font-bold mb-2">
<Translate>{howto.title}</Translate>
</h1>
<div class="bg-gray-50 p-4 rounded-lg m-4">
{
howto.tags.map((tag) => (
<span class="inline-block text-gray-700 px-2 py-1 rounded-full text-sm mr-2">
<Translate>{tag}</Translate>
</span>
))
}
</div>
<!-- Cover image -->
<div class="mb-4">
<Img
src={coverLocaleRel}
alt={"none"}
attributes={{
img: { class: "w-full h-64 object-cover rounded-lg" }
}}
/>
</div>
<!-- Metadata -->
<div class="flex flex-wrap gap-4 mb-4 text-sm text-gray-600">
<div>
<span class="font-semibold">Difficulty:</span>
<Translate>{howto.difficulty_level}</Translate>
</div>
<div>
<span class="font-semibold">Time:</span>
<Translate>{howto.time}</Translate>
</div>
<div>
<span class="font-semibold">Views:</span> {howto.total_views}
</div>
<div>
<span class="font-semibold">Created by:</span> {howto._createdBy}
</div>
<div>
<span class="font-semibold">Country:</span> {howto.creatorCountry}
</div>
</div>
<!-- Description -->
<div class="bg-gray-50 p-4 rounded-lg">
<p class="whitespace-pre-line white-space: pre-line;">
<Translate><div set:html={howto.description.replace(/\n/g, '<br>')}></div></Translate>
</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-semibold mb-4"> <Translate>Resources</Translate></h3>
<div class="">
{
howto.files.map((file) => (
<a href={`${file.downloadUrl}`} target="_blank" rel="noopener noreferrer">{file.name}</a>
))
}
</div>
<a href={`${HOWTO_FILES_WEB(howto.slug)}`} target="_blank" rel="noopener noreferrer"><Translate>Browse Files</Translate></a>
</div>
</header>
<!-- Steps -->
<div class="steps-container space-y-12">
{howto.steps.map((step, index) => (
<div class="step-item" id={`step-${index + 1}`}>
<h2 class="text-2xl font-semibold mb-4">
<span class="bg-orange-500/75 text-white w-8 h-8 rounded-full text-center leading-8 mr-2 inline-block float-left">
{index + 1}
</span>
<Translate>{step.title}</Translate>
</h2>
<div class="step-content mb-6">
<p class="whitespace-pre-line mb-4">
<Translate><div set:html={step.text.replace(/\n/g, '<br>')}></div></Translate>
</p>
</div>
{step.images && step.images.length > 0 && (
<div class="step-images">
<GalleryK images={step.images} />
</div>
)}
</div>
))}
</div>
<!-- Footer information -->
<footer class="mt-12 pt-6 border-t border-gray-200">
<div class="flex justify-between items-center">
<div>
<span class="text-sm text-gray-500">
<Translate>Created</Translate>: {new Date(howto._created).toLocaleDateString()}
</span>
</div>
<div class="text-sm text-gray-500">
<span>
<Translate>Found useful by</Translate>: {howto.votedUsefulBy.length} <Translate>people</Translate>
</span>
</div>
</div>
</footer>
</div>
</Wrapper>
</BaseLayout>

View File

@ -7,7 +7,7 @@ import Wrapper from "@/components/containers/Wrapper.astro";
import Translate from "@/components/polymech/i18n.astro";
import Readme from "@/components/polymech/readme.astro";
import GalleryK from "@/components/polymech/Gallery.astro";
import Gallery from "@/components/polymech/gallery.astro";
import Resources from "@/components/polymech/resources.astro";
import Specs from "@/components/polymech/specs.astro";
import TabButton from "@/components/polymech/tab-button.astro";
@ -223,7 +223,7 @@ const others = await group_by_path(items, Astro.currentLocale);
class="flex-1 h-full bg-white"
style="height: 100%; width: 100%;"
>
<GalleryK
<Gallery
images={item.assets.renderings}
gallerySettings={{ SHOW_TITLE: false }}
item={item}
@ -238,7 +238,7 @@ const others = await group_by_path(items, Astro.currentLocale);
item.assets.showcase && item.assets.showcase.length > 0 && (
<section>
<div class="mb-2 md:mb-16 mt-0 md:mt-16 p-2 md:p-4 border-b border-gray-200 dark:border-gray-700 bg-white rounded-xl">
<GalleryK
<Gallery
images={item.assets.showcase}
lightboxSettings={{
SHOW_TITLE: false,
@ -300,7 +300,7 @@ const others = await group_by_path(items, Astro.currentLocale);
{
SHOW_GALLERY && (
<TabContent title="Gallery" class="p-4 md:p-4 rounded-lg bg-white">
<GalleryK images={item.assets.gallery} item={item} />{" "}
<Gallery images={item.assets.gallery} item={item} />{" "}
</TabContent>
)
}
@ -310,7 +310,7 @@ const others = await group_by_path(items, Astro.currentLocale);
title="Samples"
class="p-4 bg-white rounded-xl dark:bg-gray-800"
>
<GalleryK images={item.assets.samples} item={item} />
<Gallery images={item.assets.samples} item={item} />
</TabContent>
)
}

View File

@ -1,765 +0,0 @@
import { read_fragments } from '../lib';
import { getScope, Scope } from '../lib/puppeteer';
import { DEFAULT_ROOTS } from '@plastichub/osr-cli-commons'
import * as CLI from 'yargs';
import * as path from 'path';
import * as url from 'url';
import { Promise as BPromise } from 'bluebird';
import * as download from 'download';
import axios from 'axios';
import { execFileSync, execFile } from "child_process";
import { Converter } from 'showdown';
const YAML = require('json-to-pretty-yaml');
const _sanitize = require("sanitize-filename");
// import { filenamifyPath } from 'filenamify';
const filenamify = require('filenamify');
// import { filenamifyPath as filenamify } from 'filenamify';
//import filenamify from 'filenamify';
const slugify = require('slugify');
const extract = require('extract-zip');
const fg = require('fast-glob');
const pretty = require('pretty');
var Unrar = require('node-unrar');
var shellEscape = require('shell-escape');
const URI = require("uri-js");
var escapeHtml = require('escape-html');
import * as cheerio from 'cheerio';
import { sync as mkdir } from "@plastichub/fs/dir";
import { replaceAll } from '@plastichub/core/utils';
import { sync as read } from '@plastichub/fs/read';
import { sync as exists } from '@plastichub/fs/exists';
import { sync as dir } from '@plastichub/fs/dir';
import { sync as write } from '@plastichub/fs/write';
import { sync as remove } from '@plastichub/fs/remove';
import { sync as rm } from '@plastichub/fs/remove';
import { resolveConfig, substitute } from '@plastichub/core'
import { files, forward_slash } from '@plastichub/osr-cli-commons'
import { logger } from '../index'
export var scope: Scope;
const descriptionClass = '.css-13jwego'
const USE_USER_CACHE = true
const reverse = async (user) => {
try {
const q = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${user.location.lat}&longitude=${user.location.lng}&localityLanguage=en`
return axios.get(q).then((d) => {
user.geo = d.data || { continent: 'unknown', countryName: 'unknown' };
})
} catch (e) {
user.geo = { continent: 'unknown', countryName: 'unknown' };
}
}
export const sanitize = (f) => {
let str: string = filenamify(_sanitize(f)).replace(/[^\x00-\x7F]/g, "");
if (str.startsWith('_')) {
str = str.substring(1);
}
return str;
}
export const sanitize_ex = (f) => {
let str: string = filenamify(_sanitize(f)).replace(/[^\x00-\x7F]/g, "").replace('_', '');
return str;
}
export const filename = (_url) => {
return path.basename(url.parse(_url).path);
}
export const removeEmojis = (string) => {
return string.replace(/([#0-9]\u20E3)|[\xA9\xAE\u203C\u2047-\u2049\u2122\u2139\u3030\u303D\u3297\u3299][\uFE00-\uFEFF]?|[\u2190-\u21FF][\uFE00-\uFEFF]?|[\u2300-\u23FF][\uFE00-\uFEFF]?|[\u2460-\u24FF][\uFE00-\uFEFF]?|[\u25A0-\u25FF][\uFE00-\uFEFF]?|[\u2600-\u27BF][\uFE00-\uFEFF]?|[\u2900-\u297F][\uFE00-\uFEFF]?|[\u2B00-\u2BF0][\uFE00-\uFEFF]?|(?:\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDEFF])[\uFE00-\uFEFF]?/g, '');
}
export const imageName = (url) => {
if (!url) {
return ""
}
try {
const parsed = URI.parse(decodeURIComponent(url));
const pParsed = path.parse(parsed.path);
return sanitize(decodeURIComponent(pParsed.base));
} catch (error) {
logger.error('error image name : ', url)
return ""
}
}
export const downloadImage = async (userRoot, url, altName = null) => {
try {
const parsed = URI.parse(decodeURIComponent(url));
const pParsed = path.parse(parsed.path);
const fileName = sanitize(decodeURIComponent(pParsed.base));
if (!exists(userRoot)) {
dir(userRoot);
}
const p = path.resolve(`${userRoot}/${altName || fileName}`);
if (!exists(p)) {
try {
logger.debug(`download ${url} to ${p}`, url);
await download(url, userRoot, {
filename: altName || imageName(url)
});
} catch (e) {
return 'error'
}
}
return p;
} catch (error) {
logger.error('err downloadImage : ', url)
}
}
const crawlUser = async (user) => {
if (!user) {
logger.error('error users : ', user);
return;
}
if (!user.detail) {
user.detail = {
profileUrl: `https://community.preciousplastic.com/u/${user._id}`
}
}
if (user.data && !user.detail.heroImageUrl && user.data.images && user.data.images.length) {
logger.debug('fix image');
user.detail.heroImageUrl = user.data.images[0].url;
}
if (user.data && user.data.title) {
return user.data;
}
if (user.data && user.data.error) {
// return;
}
const url = user.detail.profileUrl;
logger.debug(`open page ${url}`);
const options = {
headless: true,
url: url
};
let timeout = 5000;
if (!scope) {
// scope.onResponse = onResponse;
// scope.onRequest = onRequest;
try {
scope = await getScope(options);
await scope.init();
} catch (e) {
debugger;
logger.error("Invalid scope - abort", e);
return;
}
timeout = 9000;
scope.page.on("pageerror", function (err) {
logger.error('page-error!', err);
})
scope.page.on("error", function (err) {
logger.error('brower-error!', err);
});
scope.page.on('console', msg => {
if ((msg as any)._type === 'error') {
logger.error('Browser error:', msg);
(scope.page as any).isError = true;
}
});
}
const parse = (resolve) => {
scope.page.content().then((c) => {
if (!c) {
logger.error('error user page ', options.url);
resolve(null);
return;
}
const $ = cheerio.load(c as string, {
xmlMode: true
});
const _as = $('a');
const urls = [];
_as.each(function (elem) {
const url = $(this).attr('href');
if (url) {
if (!url.includes('discord') &&
!url.includes('https://platform.onearmy.earth/') &&
(url.startsWith('http') || url.startsWith('mailto')) &&
url !== 'https://onearmy.earth/') {
// logger.debug($(this).text(), $(this).attr('href'));
urls.push({
name: $(this).text(),
url: $(this).attr('href')
});
}
}
});
const _imgs = $('.slick-list');
const _slides = [];
_imgs.each(function (elem) {
const slides = $(this).find('[style="width: 100%; display: inline-block;"]');
slides.each(function (s) {
const classes = $(this).attr('class').split(' ');
classes.forEach((c) => {
_slides.push(c);
// const backgroundImage = scope.page.evaluate(el => window.getComputedStyle(el).backgroundImage, await page.$('.home-masthead'));
})
});
})
const t = $(descriptionClass);
let description = null;
t.each(function (elem) {
// logger.debug($(this).text());
description = $(this).text();
});
if (!description || !description.length) {
logger.error('error downloading user detail ', user._id);
resolve({
jsError: true
});
}
let title = $('[style="word-break: break-word;"]');
let _title = null;
title.each(function (elem) {
_title = $(this).text();
});
if (!_title) {
title = $('[style="overflow-wrap: break-word;"]');
_title = null;
title.each(function (elem) {
// logger.debug($(this).text());
_title = $(this).text();
});
}
// const p = path.resolve(`${root}/data/user/${user['_id']}.html`);
// write(p, c);
const services = [
{
welding: c.includes('welding'),
assembling: c.includes('assembling'),
machining: c.includes('machining'),
electronics: c.includes('electronics'),
molds: c.includes('mould-making'),
}
];
resolve({
urls,
description,
slides: _slides,
scope: scope,
services: services,
title: _title
});
})
}
return new Promise((resolve) => {
scope.page.goto(options.url, {
timeout: timeout,
waitUntil: 'networkidle0'
}).then((v) => {
parse(resolve);
}).catch((e) => {
parse(resolve);
});
});
}
export const completeUser = async (user, userData) => {
const slides = userData.slides;
if (!slides) {
return;
}
if (userData.images) {
return;
}
userData.images = [];
for (var i = 0; i < slides.length; i++) {
if (slides[i].startsWith('sc')) {
continue;
}
const backgroundImage = await scope.page.evaluate(el => window.getComputedStyle(el).backgroundImage, await userData.scope.page.$('.' + slides[i]));
if (backgroundImage) {
try {
userData.images.push({
url: backgroundImage.match(/url\("(.*)"/)[1]
});
} catch (e) {
logger.error('error background image', backgroundImage);
}
}
}
}
export const downloadUser = async (src, data, data_last: any[], user) => {
if (user.data) {
// return;
}
if (user.data && user.data.error) {
logger.error('skip ' + user._id + ' | error')
//return
user.data = null;
}
if (user.data && user.data.title && data.geo) {
return;
}
const last = data_last.find((u) => {
return u._id == user._id
})
if (!user.geo) {
try {
await reverse(user)
} catch (e) {
logger.error('error reverse', e)
}
write(src, data)
}
if (USE_USER_CACHE && last && last.data && last.geo) {
user.data = last.data
user.geo = last.geo
write(src, data);
logger.debug('used cached ' + user._id)
return
}
if (user.data && user.data.jsError) {
logger.error('skip ' + user._id + ' | jsError')
return
}
logger.debug('download user ' + user._id);
const userData: any = await crawlUser(user);
if (userData && userData.jsError) {
user.data = userData
write(src, data);
return;
}
if (userData) {
await completeUser(user, userData);
if (!user.geo) {
try {
await reverse(user);
} catch (e) {
logger.error('error reverse', e);
}
}
delete userData.slides;
delete userData.scope;
user.data = userData;
write(src, data);
} else {
logger.error('unknown error', user._id);
user.error = true;
write(src, data);
}
}
export async function downloadStep(dst, s) {
return await BPromise.resolve(s.images).map((i: any) => {
const image = path.resolve(`${dst}/${sanitize(i.name)}`);
const image_ex = path.resolve(`${dst}/${sanitize_ex(i.name)}`);
if (exists(image_ex) && image !== image_ex) {
logger.debug('remove old : : ' + i.name + ": ", image_ex);
rm(image_ex);
}
if (!exists(image)) {
try {
logger.debug(`download step image ` + i.name + ' to : ' + image);
return download(i.downloadUrl, dst, {
filename: sanitize(i.name)
});
} catch (e) {
logger.error('error download step image', e);
}
}
}, { concurrency: 1 });
}
export async function downloadFiles(dst, h: any, fetchFiles) {
return await BPromise.resolve(h.files).map((i) => {
if (!fetchFiles) {
return Promise.resolve();
}
const image = path.resolve(`${dst}/${sanitize((i as any).name)}`);
const file = path.resolve(`${dst}/${(i as any).name}`);
if (!exists(image)) {
if (fetchFiles) {
try {
logger.info(`Download step file ${(i as any).name}`);
return download((i as any).downloadUrl, dst, {
filename: sanitize((i as any).name)
}) as any;
} catch (e) {
logger.error('error download step file', e);
}
}
} else {
if (fetchFiles) {
const parts = path.parse(image);
let folder = `${(sanitize((i as any).name)).replace(parts.ext, '')}`;
folder = replaceAll('-', '_', folder);
folder = replaceAll(' ', '_', folder);
folder = replaceAll('+', '_', folder);
const zipout = path.resolve(`${dst}/files/${folder}`);
if (exists(zipout)) {
// rm(zipout);
return;
};
if (parts.ext === '.zip') {
if (!exists(zipout)) {
mkdir(zipout);
logger.info(`extract zip to ${zipout}`);
try {
return extract(image, {
dir: zipout
});
} catch (e) {
logger.error('Error unzipping : ' + image, e);
}
}
}
if (parts.ext === '.rar') {
logger.info(`extract rar ${image} to ${zipout}`);
try {
if (!exists(image)) {
return logger.error("File doesnt exists" + file);
}
return new Promise((resolve, reject) => {
execFile("7z", ["e", "" + (image), "-o" + (zipout)], function (err, stdout) {
if (err) {
logger.error(err.message);
}
// cb(null, stdout);
logger.info(`extracted rar to ${zipout}`);
return resolve(1);
});
/*
child_process.execFile('unrar', execCommand.split(' '), function (err, stdout) {
if (err) cb(new Error(err));
if (stdout.length > 0 && stdout.match(g)) {
return cb(new Error('Unsupported RAR.'));
}
cb(null, stdout);
});
*/
/*
rar.extract(zipout + "/", null, function (err) {
//file extracted successfully.
if (err) {
logger.error('error unpackaging ' + image, err);
}
logger.info(`extracted rar to ${zipout}`);
resolve(1);
});
*/
});
} catch (e) {
logger.error("error unrar", e);
}
}
}
}
}, { concurrency: 1 });
}
function createTextLinks_(text) {
return (text || "").replace(
/([^\S]|^)(((https?\:\/\/)|(www\.))(\S+))/gi,
function (match, space, url) {
var hyperlink = url;
if (!hyperlink.match('^https?:\/\/')) {
hyperlink = 'http://' + hyperlink;
}
return space + '<a href="' + hyperlink + '">' + url + '</a>';
}
);
};
export async function downloadHowto(dst, howto, data, fragments, templates, fetchFiles = false) {
if (howto.downloaded || !howto.cover_image) {
return;
}
const out = path.resolve(`${dst}/${howto.slug}`);
if (exists(out)) {
// return;
}
!exists(out) && dir(out);
const cover = path.resolve(`${dst}/${howto.slug}/${howto.cover_image.name}`);
if (!exists(cover)) {
try {
fetchFiles && await download(howto.cover_image.downloadUrl, out, {
filename: sanitize(howto.cover_image.name)
});
} catch (e) {
logger.error('error download cover image', e);
}
}
fetchFiles && logger.debug('download howto : ', howto.slug);
fetchFiles && await BPromise.resolve(howto.steps).map((s: any) => { downloadStep(out, s) }, { concurrency: 1 });
fetchFiles && await downloadFiles(out, howto, fetchFiles);
howto.steps.forEach((s, i) => {
const step = path.resolve(`${out}/step_${i}.md`);
const stepText = `### ${s.title} \n\n ${s.text}`;
write(step, stepText);
})
const tags = data.v3_tags;
const howtoTags = [];
for (const ht in howto.tags) {
const gt = tags.find((t) => t._id === ht);
if (gt) {
howtoTags.push(gt.label);
// logger.debug('resolved ' + ht + ' to ' + gt.label);
} else {
// logger.error('Cant resolve tag : ' + ht);
}
}
howto.slug = howto.slug.trim();
howto.tags = howtoTags;
howto.user = data.v3_mappins.find((u) => u._id == howto._createdBy);
const howtoHeader = path.resolve(`${out}/howto_in.md`);
const header = `### ${howto.title} \n\n\n${howto.description}`;
write(howtoHeader, header);
const howto_config = path.resolve(`${out}/config.yaml`);
howto.cover_image.name = sanitize(howto.cover_image.name);
howto.images = howto.images ? howto.images.map((i) => {
return {
...i,
name: sanitize(i.name)
}
}) : [];
howto.files = howto.files.map((i) => {
return {
...i,
name: sanitize(i.name)
}
})
howto.steps.forEach((s) => {
s.images = s.images.map((i) => {
return {
...i,
name: sanitize(i.name)
}
})
})
const config = YAML.stringify(howto);
write(howto_config, config);
const howto_config_json = path.resolve(`${out}/config.json`);
const config_json = JSON.stringify(howto, null, 2);
write(howto_config_json, config_json);
let s: string = '';
const index_md = path.resolve(`${dst}/../_howtos/${howto.slug.trim()}.md`);
const meta = path.resolve(`${dst}/../_includes/meta/${howto.slug.trim()}.json`);
let step_template = "" + templates.step;
const step_image = (i) => {
const image = `/howtos/${howto.slug}/${sanitize(i.name)}`;
return `
<div class="col-sm">
<a href="${image}">
<img loading=lazy class="step-image" src="${image}"/>
</a>
</div>
`
}
const step_file = (i) => {
const image = `/howtos/${howto.slug}/${encodeURIComponent(sanitize(i.name))}`;
return `<div class="col-sm">
<a href="${image}">
<p>${i.name}"<p/>
</a>
</div>`
}
const step_files = (s) => {
const files = s.files.map(step_file).join('<br/>\n');
return `<div class="row">
${files}
</div>
`
}
const step_images = (s) => {
const images = s.images.map(step_image).join('<br/>\n');
return `<div class="row">
${images}
</div>
`
}
const step = (s, i) => {
const t = substitute(step_template, {
title: s.title,
text: createTextLinks_(escapeHtml(s.text.trim()).replace(/(?:\r\n|\r|\n)/g, '<br/>\n\n')),
step_number: i + 1,
images: step_images(s)
});
return t;
}
const steps = howto.steps.map((s, i) => step(s, i)).join('\n<br/>\n\n\n');
const attachments = howto.files.map((f) => {
return `<li><a href="${f.downloadUrl}">${sanitize(f.name)}</a></li>`
})
howto.description = removeEmojis(howto.description);
howto.description = createTextLinks_(howto.description);
let authorName = (howto.user && howto.user.data && howto.user.data && howto.user.data.title) ? howto.user.data.title : (howto.user ? howto.user._id : 'OSR-Plastic');
if (authorName === 'Precious Plastic Headquarters') {
authorName = 'Precious Plastic Nantes';
}
let _3dFiles: any = [...files(path.resolve(`${out}`), '**/**/*.step'), ...files(path.resolve(`${out}`), '**/**/*.STEP'), ...files(path.resolve(`${out}`), '**/**/*.stp')];
_3dFiles = _3dFiles.map((f) => {
return forward_slash(`${howto.slug}/${path.relative(path.resolve(out), f)}`);
})
let previews = '';
if (_3dFiles.length) {
previews += '<div class="container">';
previews += '<div class="row"><div class="col-12"><ul class="list-group">';
_3dFiles = _3dFiles.map((f) => {
return `
<li class="list-group-item">
<span>3D Step File: ${f.replace(howto.slug, '')} -
<a href="javascript:void(0);" class="iframe-lightbox-link" data-src="/howtos/${encodeURIComponent(f).replace('.STEP', '.html').
replace('.step', '.html').replace('.stp', '.html')}">
Preview</a>
</span>
</li>`
})
previews += _3dFiles.join('');
previews += '</ul></div></div></div>';
}
let index = substitute(templates.howto, {
...fragments,
image: `/howtos/${howto.slug}/${encodeURIComponent(sanitize(howto.cover_image.name))}`,
title: howto.title.trim(),
description: escapeHtml(howto.description.trim()).replace(/(?:\r\n|\r|\n)/g, '<br/>') || "",
config: YAML.stringify({
tags: howto.tags,
hasPreviews: previews.length > 0
}),
enabled: howto.moderation == "accepted" ? true : false,
steps: pretty(steps, { ocd: true }),
keywords: ['Precious plastic', 'Preciousplastic', 'plastichub', 'osr', ...howtoTags].join(','),
user: howto._createdBy,
files: `<ul>${attachments.join('\n')}</ul>`,
authorName: authorName,
authorUrl: `https://osr-plastic.org/users/${howto.user ? howto.user._id : 'https://osr-plastic.org/users/plastichub'}.html`,
short: escapeHtml(howto.description.trim()).substring(0, 100) + '....',
slug: howto.slug,
previews3D: previews,
micro: `meta/${howto.slug.trim()}.json`
});
logger.info("write howto " + index_md);
const $ = cheerio.load(index as string, {
xmlMode: true
});
/*
$('a').each(function () {
const url = $(this).attr("href");
logger.debug('url : ' + url);
if(url.indexOf('dropbox')){
}
});*/
write(index_md, pretty(index, { ocd: true }));
const micro = {
"@context": "http://schema.org",
"@type": "HowTo",
"name": howto.title.trim(),
"description": howto.description.trim(),
"image": {
"url": `https://osr-plastic.org/howtos/${howto.slug}/${encodeURIComponent(sanitize(howto.cover_image.name))}`,
"@type": "ImageObject",
},
"step": howto.steps.map((s, i) => {
// logger.debug('s',s);
return {
"@type": "HowToStep",
"name": s.text.substring(0, 100),
"text": s.text,
"url": `https://osr-plastic.org/howtos/${howto.slug}.html#step-${i}`,
"image": s.images && s.images[0] ? `https://osr-plastic.org/howtos/${howto.slug}/${s.images[0].name}` : ''
}
})
}
write(meta, micro);
if (howto.moderation !== "accepted") {
rm(index_md);
}
if (howto.title === 'Make a bench with beams') {
logger.debug('howto tags -', howto.tags);
logger.debug('howto tags', howtoTags);
}
howto.downloaded = true;
}

View File

@ -1,212 +0,0 @@
import * as CLI from 'yargs';
import * as path from 'path';
import { Promise as BPromise } from 'bluebird';
import * as download from 'download';
import axios from 'axios';
const YAML = require('json-to-pretty-yaml');
const _sanitize = require("sanitize-filename");
const slugify = require('slugify');
const extract = require('extract-zip');
const fg = require('fast-glob');
const pretty = require('pretty');
var Unrar = require('node-unrar');
var shellEscape = require('shell-escape');
const URI = require("uri-js");
import { html_beautify } from 'js-beautify';
var escapeHtml = require('escape-html');
import * as cheerio from 'cheerio';
import { capitalize } from 'lodash';
import { sync as read } from '@plastichub/fs/read';
import { sync as exists } from '@plastichub/fs/exists';
import { sync as dir } from '@plastichub/fs/dir';
import { sync as write } from '@plastichub/fs/write';
import { sync as rm } from '@plastichub/fs/remove';
import { resolveConfig, substitute } from '@plastichub/core'
import { files, forward_slash } from '@plastichub/osr-cli-commons'
import { logger } from '../'
import { read_fragments } from '../lib';
import { DEFAULT_ROOTS } from '@plastichub/osr-cli-commons'
import { resolve } from '@plastichub/osr-cli-commons/fs'
import {
sanitize,
removeEmojis,
imageName,
downloadFiles,
downloadStep,
downloadHowto,
downloadUser
} from '../lib/download'
import { createMap } from '../lib/map'
import filenamify from 'filenamify/filenamify';
let root: string;
const USER_DIRECTORY_INTRO = "${KB_ROOT}/src/directory/intro.md"
const defaultOptions = (yargs: CLI.Argv) => {
return yargs.option('output', {
default: '${OA_ROOT}/howtos',
describe: 'The output directory'
}).option('onearmy', {
default: '${OA_ROOT}',
describe: 'location of onearmy folder'
}).option('src', {
default: '${OA_ROOT}/data/latest.json',
describe: 'The source file'
}).option('kb', {
default: '${OSR_KB}/'
}).option('debug', {
default: 'false',
describe: 'Enable internal debug message'
}).option('download', {
default: 'true',
describe: 'download users',
type: 'boolean'
}).option('type', {
default: 'user'
})
};
let options = (yargs: CLI.Argv) => defaultOptions(yargs);
const index = (data, argv) => {
let howtos = data.v3_howtos as any[];
howtos = howtos.filter((h) => h.moderation == 'accepted');
const dst = path.resolve('' + argv.output || './');
const tags = data.v3_tags;
let output = {
tags: {},
howtows: {}
}
howtos.forEach((howto) => {
const howtoTags = [];
for (const ht in howto.tags) {
const gt = tags.find((t) => t._id === ht);
if (gt) {
howtoTags.push(gt.label);
if (!output.tags[gt.label]) {
output.tags[gt.label] = []
}
output.tags[gt.label].push(howto.slug.trim());
}
}
howto.user = data.v3_mappins.find((u) => u._id == howto._createdBy);
howto.tags = howtoTags;
howto.image = `/howtos/${howto.slug}/${encodeURIComponent(sanitize(howto.cover_image.name))}`,
howto.cover_image.name = sanitize(howto.cover_image.name);
output.howtows[howto.slug.trim()] = howto;
});
write(path.resolve(dst + '/../indexed.json'), JSON.stringify(output, null, 2));
}
// node ./dist/main.js download_hugo --type=howto --src="../../../ph3/pp-next2/data/2022/05/raw.json" --output=../../../ph3/pp-next2/howtos --download=true --onearmy="../../../ph3/pp-next2/"
// node ./dist/main.js download_hugo user --src="../../../ph3/pp-next2/data/2022/04/raw.json" --output=../../onearmy/user --download=true --onearmy="../../../ph3/pp-next2/"
// node ./dist/main.js download_hugo --type=howto --src="../../../ph3/pp-next2/data/2022/05/raw.json" --output=../../../ph3/pp-next2/howtos --download=true --onearmy="../../../ph3/pp-next2/"
// node ./dist/main.js download_hugo --type=map --src="../../../ph3/pp-next2/data/2022/04/raw.json" --output=../../../ph3/pp-next2/kmz --download=false --onearmy="../../../ph3/pp-next2/"
export const register = (cli: CLI.Argv) => {
return cli.command('download_hugo_howto <verb>', 'Download howto data', options, async (argv: CLI.Arguments) => {
if (argv.help) { return; }
const src = path.resolve(substitute(argv.src, DEFAULT_ROOTS));
const kb_out = path.resolve(substitute("${KB_ROOT}", DEFAULT_ROOTS));
const isDebug = argv.debug === 'true';
const verb = argv.verb;
const data = read(src, 'json') as any;
const dst = path.resolve(substitute(argv.dst || "", DEFAULT_ROOTS) || './');
if (!exists(dst)) {
dir(dst);
}
const options = {
verb,
root,
dst,
src,
kb_out
}
logger.debug('opts', options);
if (verb === 'howto') {
let howtos = data.v3_howtos as any[]
// howtos = howtos.filter((h) => h.moderation == 'accepted')
const download: any = argv.download
const root = path.resolve(substitute("${KB_ROOT}", DEFAULT_ROOTS))
const dstHowtos = path.resolve(root + '/src/howtos')
const cPath = path.resolve(`${root}/templates/hugo/config.json`)
isDebug && logger.info(`read config at ${cPath}`);
const config = read(cPath, 'json') as any;
const templatesPath = path.resolve(`${root}/templates/hugo`);
if (!exists(templatesPath)) {
logger.error(`\t Cant find templates at ${templatesPath}, path doesn't exists`);
return;
}
let fragments: any = { ...config };
read_fragments(templatesPath, fragments, "product_rel_path_name", "machine");
let template = read(path.resolve(`${templatesPath}/howto.md`), 'string');
let step = read(path.resolve(`${templatesPath}/step.md`), 'string');
resolveConfig(fragments);
await BPromise.resolve(howtos).map((h: any) => {
return downloadHowto(
dstHowtos,
h,
data,
fragments,
{
howto: template,
step: '' + step
},
download);
}, { concurrency: 1 });
console.log('download howtos!');
/*
await Promise.all(howtos.map((h) => downloadHowto(
dstHowtos, h,
data,
fragments,
{ howto: template, step: '' + step },
download)));
*/
}
});
};

View File

@ -1,51 +0,0 @@
export * from './crawler'
export * from './types'
import * as path from 'path';
export { writeUsers } from './user'
import { sync as read } from '@plastichub/fs/read'
import { sync as exists } from '@plastichub/fs/exists'
import { sync as mkdir } from '@plastichub/fs/dir'
import { files } from '@plastichub/osr-cli-commons'
import { html_beautify } from 'js-beautify'
import { Converter } from 'showdown'
export const md_edit_wrap = (content, f, prefix = '', context = '') => {
return html_beautify(`<div prefix="${prefix}" file="${path.parse(f).base}" context="${context}" class="fragment">${content}</div>`);
}
export const md2html = (content) => {
let converter = new Converter({ tables: true });
converter.setOption('literalMidWordUnderscores', 'true');
return converter.makeHtml(content);
}
export const toHTML = (path, markdown) => {
const content = read(path, 'string') as string;
if (!markdown) {
let converter = new Converter({ tables: true });
converter.setOption('literalMidWordUnderscores', 'true');
return converter.makeHtml(content);
} else {
return content;
}
}
export const read_fragments = (src, config, prefix = '', context = '') => {
if (!exists(src)) {
//debug.warn(`Create template folder ${src}`);
mkdir(src);
}
let fragments = files(src, '*.html');
fragments.map((f) => {
config[path.parse(f).name] = md_edit_wrap(toHTML(f, true), f, prefix, context);
});
fragments = files(src, '*.md');
fragments.map((f) => {
config[path.parse(f).name] = md_edit_wrap(toHTML(f, false), f, prefix, context);
});
return config;
}

View File

@ -1,355 +0,0 @@
import * as path from 'path'
import xlsx from 'node-xlsx'
import { Promise as BPromise } from 'bluebird';
import { sync as read } from "@plastichub/fs/read"
import { sync as write } from "@plastichub/fs/write"
import { resolve } from "@plastichub/osr-cli-commons/fs"
import { logger } from '..'
import { IOptions } from '../types'
import { IUser, I_OSR_USER, I_USER_SHORT } from '../lib/types'
import { generate } from 'csv-generate/sync'
const extension = (file: string) => path.parse(file).ext;
const writerTXT = (_products: any[], opts: IOptions) => {
if (opts.dst) {
let _path = path.resolve(opts.dst);
_products = _products.map((p) => {
return `${p.product_id} \t | ${p.status} | ${p.product}`;
}).sort((a: any, b: any) => parseInt(a.product_id) < parseInt(b.product_id) ? 1 : -1);
write(_path, _products.join('\n'));
opts.debug && logger.debug(`Writing products to ${_path} `, _products);
}
}
const writerXLS = (_users: any[], opts: IOptions, extraProps = [], extraCols = []) => {
let _path = path.resolve(resolve(opts.dst));
_users = _users.map((u) => {
return [u.email, u.name, u.bazar, u.web, u.social, u.status, u.lastActive, u.ig, ...extraProps];
})
let columns = ['EMail', 'Name', 'Bazar', 'Web', 'Social', 'Status', 'Last TS', 'IG', ...extraCols];
let data =
[
columns,
..._users
];
const sheetOptions = { '!cols': [{ wch: 30 }, { wch: 30 }, { wch: 30 }, { wch: 30 }, { wch: 30 }, { wch: 40 }] };
const buffer = xlsx.build([{ name: 'raw', data: data, options: sheetOptions }]);
write(_path, buffer);
opts.debug && logger.debug(`Writing users to ${_path} `, _users);
}
const writerCSV = (_users: any[], opts: IOptions) => {
if (opts.dst) {
let _path = path.resolve(opts.dst);
_users = _users.map((u) => {
return [u.email, u.name, u.bazar, u.web, u.social, u.censored, u.lastActive];
})
let columns = ['EMail', 'Name', 'Bazar', 'Web', 'Social', 'Censored', 'Last TS', 'IG'];
// let colsTB = [First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Screen Name,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes
let data =
[
columns,
..._users
];
const records = generate({
seed: 1,
objectMode: true,
columns: 2,
length: 2,
});
/*
records = [
['OMH', 'ONKCHhJmjadoA'],
['D', 'GeACHiN']
];
*/
write(_path, '');
opts.debug && logger.debug(`Writing users to ${_path} `, _users);
}
}
const writerJSON = (_users: any, opts: IOptions) => {
if (opts.dst) {
let _path = path.resolve(opts.dst);
write(_path, _users);
opts.debug && logger.debug(`Writing products to ${_path} `, _users);
}
}
const WRITERS =
{
'.xls': writerXLS,
'.xlsx': writerXLS,
'.txt': writerTXT,
'.json': writerJSON,
'.csv': writerCSV
}
const filter_valid = (users: any[]) => {
return users.filter((user) => {
if (!user.data) {
return false;
}
if (!user.geo) {
return false;
}
if (!user.data.urls) {
return false;
}
if (user.data && user.data.jsError) {
return false;
}
return true;
})
}
const filter_email_only = (users: any[]) => {
return users.filter((user) => {
if (!user.data) {
return false;
}
if (!user.geo) {
return false;
}
if (!user.data.urls) {
return false;
}
if (user.data && user.data.jsError) {
return false;
}
if (user.data.urls.find((l) => l.name == 'Email') == undefined) {
return false;
}
return true;
})
}
const filter_ig_only = (users: any[]) => {
return users.filter((user) => {
if (!user.data) {
return false;
}
if (!user.geo) {
return false;
}
if (!user.data.urls) {
return false;
}
if (user.data && user.data.jsError) {
return false;
}
if (user.data.urls.find((l) => l.name == 'Social media') == undefined && user.data.urls.find((l) => l.name == 'Instagram') == undefined) {
return false;
}
return true;
})
}
const filter_bazar_only = (users: any[]) => {
return users.filter((user) => {
if (!user.data) {
return false;
}
if (!user.data.urls) {
return false;
}
if (user.data && user.data.jsError) {
return false;
}
const bUrl = bazar_url(user);
if (bUrl && bUrl.indexOf('bazar') !== -1) {
return true;
}
return false;
})
}
export const users = (src: string): I_OSR_USER[] => {
const raw = read(src, 'json') as any;
return raw.v3_mappins.filter((f) => f.data != null);
}
const url = (user, field) => {
const url = (user.data.urls.find((u) => u.name === field) as any);
return url ? url.url : '';
}
const bazar_url = (user) => url(user, 'Bazar');
const ig_url = (user) => {
const _url = url(user, 'Social media') || url(user, 'Instagram');
return _url.indexOf('instagram.com') !== -1 ? _url : '';
}
export const toShort = (user: I_OSR_USER, extra: any = {}) => {
return {
name: user.data.title,
email: url(user, 'Email').replace('mailto:', ''),
bazar: url(user, 'Bazar'),
web: url(user, 'Website'),
social: url(user, 'Social media'),
status: user.moderation,
lastActive: user._modified,
ig: ig_url(user),
...extra
}
}
export const writeUsers = (users: any, opts: IOptions, path: string = null) =>
WRITERS[extension(path || opts.dst)](users, opts);
export const convert = (opts: any) => {
let pins = filter_valid(users(opts.src));
pins = filter_email_only(pins);
pins = pins.map(toShort);
logger.debug('users', pins.length);
writeUsers(pins, opts);
}
export const get_ig = (opts: any) => {
let pins = filter_ig_only(users(opts.src));
logger.debug('pins short', pins.length);
pins = pins.map(toShort);
pins = pins.filter((u) => !!u.ig)
logger.debug('users', pins.length);
if (opts.dst) {
let _path = path.resolve(opts.dst);
logger.debug(`Write IG users `);
write(_path, pins.map((u) => {
return {
ig: u.ig,
followed: false,
followFailed: false
}
}));
}
}
export const get_bazar = (opts: any) => {
let pins = filter_bazar_only(users(opts.src));
pins = filter_email_only(pins);
pins = pins.map(toShort);
pins = pins.filter((u) => !!u.bazar)
logger.debug('users', pins.length);
if (opts.dst) {
let _path = path.resolve( resolve(opts.dst));
logger.debug(`Write Bazar users `, _path);
const users = pins.map((u) => {
return {
...u,
registered: false,
registeredFailed: false
}
});
write(_path, users );
opts.dst = _path.replace('.json','.xlsx');
writerXLS(users, opts)
}
}
/////////////////////////////////////////////////////////////////////
//
// old map
//
const mapOld = (u) =>{
return {
"detail": {
"profilePicUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/avatars%2Fplastichub.jpg?alt=media",
"verifiedBadge": false,
"displayName": "PlasticHub",
"profileUrl": "https://community.preciousplastic.com/u/plastichub",
"shortDescription": "machine shop & lab",
"heroImageUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fusers%2Fplastichub%2Fmorecult-18418ccf804.jpg?alt=media&token=4e582731-2cca-4896-9ee5-879a31f5a32e",
"name": "plastichub",
"lastActive": "2022-11-26T20:57:17.257Z"
},
"moderation": "rejected",
"location": {
"lng": -7.845697790524478,
"lat": 40.008617694777925
},
"_deleted": false,
"verified": false,
"_modified": "2022-11-28T20:48:01.532Z",
"_created": "2022-10-27T09:36:40.271Z",
"type": "machine-builder",
"_id": "plastichub",
"data": {
"urls": [
{
"name": "Website",
"url": "https://google.com/"
},
{
"name": "sponsor the work",
"url": "https://www.patreon.com/one_army"
}
],
"description": "plastichubPlasticHubI got you, Babe !We offer the following services:machiningweldingassemblingelectronicsmould-makingContact & LinksWebsite",
"services": [
{
"welding": false,
"assembling": false,
"machining": false,
"electronics": false,
"molds": false
}
],
"title": "PlasticHub",
"images": [
{
"url": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fusers%2Fplastichub%2Fmorecult-18418ccf804.jpg?alt=media&token=4e582731-2cca-4896-9ee5-879a31f5a32e"
}
]
}
}
}
export const convert_old = (opts: any) => {
let pins = read(opts.src, 'json') as any;
// pins = pins.filter((u) => !!u.bazar)
logger.debug('users', pins.length);
if (opts.dst) {
logger.debug(`Convert old users `, opts.dst)
write(opts.dst, pins )
}
}

View File

@ -1 +0,0 @@
export const HOWTO_ROOT = () => '/test/howto';

View File

@ -133,6 +133,7 @@ const complete = async (item: IUser) => {
}
const onStoreItem = async (store: any) => {
return store
let item = store.data.item as IUser
item = await complete(item)
const configPath = path.join(item_path(item), 'config.json')

View File

@ -1,12 +1,11 @@
import * as path from 'path';
const slugify = require('slugify');
import { sync as read } from '@plastichub/fs/read';
import { sync as write } from '@plastichub/fs/write';
import { substitute } from '@plastichub/core'
import { logger } from '../'
import { logger } from '@base/index.js'
const slugify = require('slugify');
import {
sanitize,

View File

@ -0,0 +1,42 @@
import { z } from 'zod'
import * as path from 'path'
import * as env from 'env-var'
import { sync as writeFS } from '@polymech/fs/write'
import { generate_interfaces, write, ZodMetaMap } from '@polymech/commons'
export const get_var = (key: string ='') => env.get(key).asString() || env.get(key.replace(/-/g, '_')).asString() || env.get(key.replace(/_/g, '-')).asString()
export const HOME = (sub = '') => path.join(process.env[(process.platform == 'win32' ) ? 'USERPROFILE' : 'HOME'] || '', sub)
// https://rjsf-team.github.io/react-jsonschema-form/docs/usage/widgets#file-widgets
export type ModelSchemaMeta = Record<string, unknown>
let map: ZodMetaMap
export const OptionsSchema = (opts?: any) => {
map = ZodMetaMap.create<ModelSchemaMeta>()
map.add(
'path',
z.string()
.min(1)
.default('.')
.describe('Target directory')
, {'ui:widget': 'file'})
.add(
'output',
z.string()
.optional()
.describe('Optional output path for modified files (Tool mode only)')
)
return map.root()
.passthrough()
.describe('IKBotOptions')
}
export const types = () => {
generate_interfaces([OptionsSchema()], 'src/zod_types.ts')
schemas()
}
export const schemas = () => {
write([OptionsSchema()], 'schema.json', 'model', {})
writeFS('schema_ui.json', map.getUISchema())
}

View File

@ -1,21 +0,0 @@
## Todos
- Typescript ESM
## Todos
Create an annotation system, providing alternative content for IHowto (main description, step description)
- [ ] modify ./annotation.ts
- [ ] IAnnotation
- [ ] move all AI related settings into a new "user" field
- [ ] implement a custom cache key generation, thats model, prompt, date, original content
- [ ] title field is optional
- [ ] use Zod (with defaults) for Ihowto & user settings, adjust createDefaultAnnotation
- [ ] no need for id
- [ ] rename target to 'path'
- [ ] rename howtoId to 'owner'
- [ ] adjust all other code :)
- [ ] create examples, in annotation-examples.ts
- [ ] load annotations from path (array), patch an Howto (and steps), using jsonpath-plus
- [ ] cache an annotation

View File

@ -45,7 +45,7 @@ export async function getStaticPaths() {
}
const { page: page, ...rest } = Astro.props;
import Sidebar2 from "@/components/howtos/sidebar2.astro";
import Sidebar2 from "@/components/howtos/Nav.astro";
---
<List {...rest} />

File diff suppressed because one or more lines are too long

View File

@ -1,6 +0,0 @@
{
"debug": true,
"matching": [
"**"
]
}

View File

@ -1,17 +0,0 @@
{
"debug": true,
"matching": [
"src/**",
"*.md",
"*.json",
"*.cjs",
"docs",
"packages/imagetools/**",
"scripts",
"meta/**",
"tests/**",
"*.mjs",
"!node_modules/**",
"!ref/**"
]
}

View File

@ -1,17 +0,0 @@
{
"debug": true,
"matching": [
"src/**",
"*.md",
"*.json",
"*.cjs",
"docs",
"packages/imagetools/**",
"scripts",
"meta/**",
"tests/**",
"*.mjs",
"!node_modules/**",
"!ref/**"
]
}