stuff like that :)

This commit is contained in:
babayaga 2025-12-26 18:54:44 +01:00
parent d9acb49f47
commit 70e6c1d37b
37 changed files with 3881 additions and 2837 deletions

View File

@ -23,7 +23,7 @@
"format": "unix-time" "format": "unix-time"
} }
], ],
"default": "2025-08-19T18:25:54.314Z" "default": "2025-12-26T17:53:03.149Z"
}, },
"description": { "description": {
"type": "string", "type": "string",

View File

@ -1,5 +1,4 @@
export default new Map([ export default new Map([
["src/content/resources/workflow.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fresources%2Fworkflow.mdx&astroContentModuleFlag=true")], ["src/content/resources/workflow.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fresources%2Fworkflow.mdx&astroContentModuleFlag=true")]]);
["src/content/infopages/contact.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fcontact.mdx&astroContentModuleFlag=true")]]);

81
.astro/content.d.ts vendored
View File

@ -56,6 +56,10 @@ declare module 'astro:content' {
collection: C; collection: C;
slug: E; slug: E;
}; };
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
/** @deprecated Use `getEntry` instead. */ /** @deprecated Use `getEntry` instead. */
export function getEntryBySlug< export function getEntryBySlug<
@ -84,6 +88,13 @@ declare module 'astro:content' {
filter?: (entry: CollectionEntry<C>) => unknown, filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>; ): Promise<CollectionEntry<C>[]>;
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter?: LiveLoaderCollectionFilterType<C>,
): Promise<
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>;
export function getEntry< export function getEntry<
C extends keyof ContentEntryMap, C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}), E extends ValidContentEntrySlug<C> | (string & {}),
@ -120,6 +131,10 @@ declare module 'astro:content' {
? Promise<DataEntryMap[C][E]> | undefined ? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]> : Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>; : Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter: string | LiveLoaderEntryFilterType<C>,
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */ /** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>( export function getEntries<C extends keyof ContentEntryMap>(
@ -158,25 +173,7 @@ declare module 'astro:content' {
}; };
type DataEntryMap = { type DataEntryMap = {
"directory": Record<string, { "howtos": Record<string, {
id: string;
body?: string;
collection: "directory";
data: InferEntrySchema<"directory">;
rendered?: RenderedContent;
filePath?: string;
}>;
"helpcenter": Record<string, {
id: string;
render(): Render[".md"];
slug: string;
body: string;
collection: "helpcenter";
data: InferEntrySchema<"helpcenter">;
rendered?: RenderedContent;
filePath?: string;
}>;
"howtos": Record<string, {
id: string; id: string;
body?: string; body?: string;
collection: "howtos"; collection: "howtos";
@ -184,24 +181,6 @@ declare module 'astro:content' {
rendered?: RenderedContent; rendered?: RenderedContent;
filePath?: string; filePath?: string;
}>; }>;
"infopages": Record<string, {
id: string;
render(): Render[".md"];
slug: string;
body: string;
collection: "infopages";
data: InferEntrySchema<"infopages">;
rendered?: RenderedContent;
filePath?: string;
}>;
"projects": Record<string, {
id: string;
body?: string;
collection: "projects";
data: InferEntrySchema<"projects">;
rendered?: RenderedContent;
filePath?: string;
}>;
"resources": Record<string, { "resources": Record<string, {
id: string; id: string;
body?: string; body?: string;
@ -223,5 +202,33 @@ declare module 'astro:content' {
type AnyEntryMap = ContentEntryMap & DataEntryMap; type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData,
infer TEntryFilter,
infer TCollectionFilter,
infer TError
>
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
LiveContentConfig['collections'][C]['loader']
>;
export type ContentConfig = typeof import("./../src/content.config.js"); export type ContentConfig = typeof import("./../src/content.config.js");
export type LiveContentConfig = never;
} }

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"_variables": { "_variables": {
"lastUpdateCheck": 1755620245882 "lastUpdateCheck": 1766761372436
} }
} }

View File

@ -10512,7 +10512,7 @@
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://linktr.ee/plastmakers": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://linktr.ee/plastmakers": {
"isValid": false, "isValid": false,
"timestamp": 1755627954986 "timestamp": 1755628616096
}, },
"%0A%0ALaser-Cut%20Parts%20for%20Pressing%20Plates:%0A%5BPressing%20Plate%20Parts%5D(%0A%0AFor%20updates%20and%20tips%20on%20compression%20molding,%20visit:%0A%0A-%20~~%5BYouTube%20and%20Instagram%20Resources%5D(%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://linktr.ee/plastmakers": { "%0A%0ALaser-Cut%20Parts%20for%20Pressing%20Plates:%0A%5BPressing%20Plate%20Parts%5D(%0A%0AFor%20updates%20and%20tips%20on%20compression%20molding,%20visit:%0A%0A-%20~~%5BYouTube%20and%20Instagram%20Resources%5D(%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://linktr.ee/plastmakers": {
"isValid": false, "isValid": false,
@ -10552,7 +10552,7 @@
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.qitech.de/en/industries": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.qitech.de/en/industries": {
"isValid": false, "isValid": false,
"timestamp": 1755628185764 "timestamp": 1755628632676
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.qitech.de/en/ind/academy-area/filament-extruder-comparision": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.qitech.de/en/ind/academy-area/filament-extruder-comparision": {
"isValid": false, "isValid": false,
@ -10612,19 +10612,19 @@
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.darigovresearch.com/donate": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.darigovresearch.com/donate": {
"isValid": false, "isValid": false,
"timestamp": 1755628451499 "timestamp": 1755628664978
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.patreon.com/darigovresearch": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.patreon.com/darigovresearch": {
"isValid": false, "isValid": false,
"timestamp": 1755628451507 "timestamp": 1755628664988
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.darigovresearch.com/": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.darigovresearch.com/": {
"isValid": false, "isValid": false,
"timestamp": 1755628451514 "timestamp": 1755628664995
}, },
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.youtube.com/channel/UCb34hWA6u2Lif92aljhV4HA": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.youtube.com/channel/UCb34hWA6u2Lif92aljhV4HA": {
"isValid": false, "isValid": false,
"timestamp": 1755628451521 "timestamp": 1755628665004
}, },
"https://www.youtube.com/watch?v=IoSn84Axao8": { "https://www.youtube.com/watch?v=IoSn84Axao8": {
"isValid": true, "isValid": true,
@ -10663,5 +10663,41 @@
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.dropbox.com/sh/xlts122wcb905q6/AABRgMZTki8gH1NqQ5SvOS-Ia?dl=0": { "%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.dropbox.com/sh/xlts122wcb905q6/AABRgMZTki8gH1NqQ5SvOS-Ia?dl=0": {
"isValid": false, "isValid": false,
"timestamp": 1755628607796 "timestamp": 1755628607796
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://photos.app.goo.gl/7ALBkSbWRh6VzzaK9": {
"isValid": false,
"timestamp": 1755628624522
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://youtu.be/6Ae6oDKhqiE": {
"isValid": false,
"timestamp": 1755628632667
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.youtube.com/watch?v=6u6y6gD17rk&feature=youtu.be": {
"isValid": false,
"timestamp": 1755628640416
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.freecadweb.org/downloads.php": {
"isValid": false,
"timestamp": 1755628664967
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.youtube.com/watch?v=BTiQqPFE9vs": {
"isValid": false,
"timestamp": 1755628691506
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.youtube.com/watch?v=YzjTm3FRLVY&t=5s": {
"isValid": false,
"timestamp": 1755628745050
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.fosbarcelona.com/": {
"isValid": false,
"timestamp": 1755628754193
},
"%3Ca%20class=%22text-orange-600%20underline%22%20href=%22https://www.instagram.com/": {
"isValid": false,
"timestamp": 1755628754202
},
"%0A%0A%0AUser%20Location:%20Gandia,%20Spain%0A%0A##%20Guide%20Overview%0A%0AThis%20guide%20covers%20the%20creation%20of%20two%20products%20using%20panels%20or%20boards%20made%20from%20polypropylene%20plastic%20and%20fishing%20nets:%0A%0A-%20Urban%20Bench%0A-%20Dumbbells%0A%0AFor%20detailed%20instructions%20on%20creating%20panels%20with%20a%20sheet%20press,%20refer%20to%20the%20expanded%20guide%20titled%20%22Boards%20Made%20from%20Marine%20Litter.%22%0A%0AThis%20guide%20will%20focus%20on%20the%20product%20creation%20and%20manufacturing%20process.%0A%0A##%20How-To%20Guide%20for%20CNC%20Machining%20with%20CAD%20and%203D%20Modeling%20Software%0A%0AWe%20utilize%20CAD%20and%203D%20modeling%20software%20to%20design%20products%20and%20define%20cutting%20lines%20for%20CNC%20machining.%20For%203D%20modeling,%20Solidworks%20and%20Rhinoceros%20are%20used,%20while%20AutoCAD%20and%20Illustrator%20handle%202D%20cutting%20lines.%0A%0AOnce%20the%20design%20is%20finalized,%20we%20prepare%20the%20material%20for%20CNC%20machining.%0A%0A###%20Tips%20for%20CNC%20Machining%0A%0A-%20Efficiently%20utilize%20material%20to%20minimize%20waste%20by%20optimizing%20the%20cutting%20layout.%0A-%20Plastic%20boards,%20being%20generally%20softer%20than%20wood,%20can%20often%20be%20cut%20in%20one%20pass%20with%20the%20same%20drill%20bit,%20improving%20production%20time%20and%20finish%20quality.%20Conduct%20initial%20test%20cuts%20to%20ensure%20optimal%20settings.%0A-%20Collect%20shavings%20after%20cutting;%20these%20can%20be%20reused%20as%20raw%20material%20in%20other%20processes%20like%20sheet%20pressing%20or%20extrusion.%0A%0AAfter%20cutting%20the%20parts,%20proceed%20with%20assembling%20the%20bench%20and%20its%20framework.%20%0A%0AFor%20urban%20furniture,%20we%20have%20created%20a%20metal%20framework%20to%20enhance%20stability%20and%20durability.%0A%0A**Tips**%0A%0AUse%20galvanized%20or%20stainless%20steel%20screws%20and%20nuts%20to%20connect%20plastic%20elements%20to%20the%20metal%20structure.%20Stainless%20steel%20is%20more%20costly%20but%20more%20resistant%20to%20oxidation.%0A%0AWhen%20attaching%20plastic%20parts,%20drill%20a%20hole%20matching%20the%20screw%20size%20to%20prevent%20the%20plastic%20from%20breaking%20or%20deforming%20under%20pressure.%0A%0ATo%20create%20the%20round%20weight%20plates%20for%20dumbbells,%20we%20utilize%20marine%20litter%20boards,%20similar%20to%20those%20used%20for%20the%20bench,%20and%20cut%20them%20using%20a%20CNC%20machine.%0A%0AFor%20the%20handles,%20we%20employ%20a%20mix%20of%20recycled%20plastic%20from%20bottle%20caps.%0A%0AAn%20extrusion%20machine%20is%20used%20to%20form%20plastic%20rods%20measuring%203%20meters%20(9.8%20feet": {
"isValid": false,
"timestamp": 1755628762741
} }
} }

View File

@ -3,7 +3,7 @@
"messages": [ "messages": [
{ {
"role": "user", "role": "user",
"content": "use a formal tone\nspell & grammar fix the text,\nremove emojis\nremove personal preferences or biases\nshorten text if possible but preserve personality\nremove references to preciousplastic, bazar and Discord\nremove any brain/green washing, eg: sustainable, circular, recycling ... inflated prospects\nRewrite the following text to remove any inflated or empty language, sugar-coated filler, and needless repetition. The result should be concise, direct, and preserve only the essential ideas.\nContext: howto tutorials, for makers\nConvert units, from metric to imperial and vice versa (in braces)\ndont comment just return as Markdown\n\nText to process:\nMaking clock is not difficult and you are able to recycle about 300g per clock in less than 2 hours. With creative design can be clock nice present for your friends or family. These hand made products can be done with electric oven, mini press and a simple compression mould. Clock diameter is 30 cm with thickness of 5mm. Material cost is about 6 EUR/ clock." "content": "Return a list of max. 10 keywords that can be used for SEO purposes, separated by commas (dont comment, just the list) : \n\nText to process:\nDeck rails have been used by skateboarders since the 80s to (1) help boards slide better on handrails, coping, curbs, etc., and (2) to protect board graphics. These recycled rails succeed at both of those things, but offer something that skateboarding has never seen before: a set of deck rails that is made from 100% post-consumer waste.\n\n\nUser Location: Los Angeles, United States of America (the)\n\nYou can buy my mold, or my mold design from the [filtered] bazar or my website (links below), or design a version yourself.\n(with anything [filtered] related that sells on my website, I donate 5% of the sales to \n\n\n<a class=\"text-orange-600 underline\" href=\"https://skatehyena.com/\" target=\"_blank\" rel=\"noopener noreferrer\">skatehyena.com: skatehyena.com</a>\n\nIf you buy my mold, then youll receive it in about 4 weeks. \n\nIf you buy my mold design (or design it yourself), then youll have the digital file, but youll still need to have the mold made, which leaves two options:\n - Make it yourself\n - Send the file to someone to make the mold:\n - Your local CNC machinist \n - Whoever is the most local mold maker to you on the [filtered] Bazar \n\n\nCollect used plastic to shred and shred it, or buy pre-shredded plastic:\n\n \n \n\n(Ive found that type #2 HDPE has worked best for me for durability and boardsliding, but Id love to hear what other people find if another plastic type works better/differently for them)\n\n\nBuy or build an injection machine\n\n\n<a class=\"text-orange-600 underline\" href=\"https://youtu.be/qtZv96ciFIU\" target=\"_blank\" rel=\"noopener noreferrer\">youtu.be: youtu.be/qtZv96ciFIU</a>\n\n(also, I realize that an extruder might be a better [filtered] machine for this product. That said, I cant afford an extruder, so Ive been using the V3 injection machine. Id love to hear any feedback if someone out there makes these rails with an extruder.)\n\n\nLearn how to use your new injection machine and mold and get a crash course on plastics (link below). When going through all of this educational info, if you have any questions feel free to email me at preciousplasticpasadena@gmail.com\n\n\n\nI've been using the [filtered] V3 injection machine with a carjack (because the mold is too wide to screw onto the injection machine all the way). See link below for [filtered]'s How-To for using the V3 injection machine.\n\nThe rail mold takes about 80 grams of molten plastic (this varies depending on the plastic type), so you'll end up using about 80% of the plastic from an injection machine that's been filled to the brim.\n\nI also pre-heat the mold for 15 minutes at 250°F / 121°C, so that when the molten plastic hits the mold, it's not hitting a lukewarm surface and allows for better melt-flow. \n\n\n\nAfter you've made the rails, screws are needed to attach the rails to the bottom of a skateboard. Order screws that fit the rails and work with skateboard decks (this took a lot of trial and error to figure out which screws work best).\n\nHere's the options I found that work best:\n - Order these: <a class=\"text-orange-600 underline\" href=\"https://www.mcmaster.com/91555A101/\" target=\"_blank\" rel=\"noopener noreferrer\">mcmaster.com: mcmaster.com/91555A101</a> \n - If youre not able to order through McMaster, find screws that match the image attached to this step.\n\nI recommend using a plain non-powered phillips head screwdriver to screw the rails onto a board and not strip out the wood. But an electric drill can work if youre delicate.\n\n\nMake your own recycled rails, and anything else that you can think of to have injection molds made of! And happy recycling!"
}, },
{ {
"role": "user", "role": "user",

118
app-config.json Normal file
View File

@ -0,0 +1,118 @@
{
"site": {
"title": "Polymech Library",
"base_url": "https://library.polymech.info/",
"description": "",
"base_path": "/",
"trailing_slash": false,
"favicon": "/images/favicon.png",
"logo": "/images/logo.png",
"logo_darkmode": "/images/logo-darkmode.png",
"logo_width": "150",
"logo_height": "33",
"logo_text": "Polymech Library",
"image": {
"default": "/images/default-image.png",
"error": "/images/error-image.png",
"alt": "Polymech Library"
}
},
"footer_left": [
{
"href": "/${LANG}/resources/info/contact",
"text": "Contact"
},
{
"href": "https://forum.polymech.info/",
"text": "Forum"
},
{
"href": "https://files.polymech.info/",
"text": "Files"
},
{
"href": "https://git.polymech.info/explore/repos",
"text": "Github"
}
],
"footer_right": [],
"settings": {
"search": true,
"account": true,
"sticky_header": true,
"theme_switcher": true,
"default_theme": "system"
},
"params": {
"contact_form_action": "#",
"copyright": "Designed And Developed by [Themefisher](https://themefisher.com/)"
},
"navigation": {
"top": [
{
"href": "/${LANG}",
"text": "Home"
},
{
"href": "/${LANG}/resources",
"text": "Resources"
},
{
"href": "/${LANG}/library",
"text": "Library"
},
{
"href": "/${LANG}/tutorials",
"text": "Tutorials"
},
{
"href": "https://service.polymech.info/",
"text": "Media"
},
{
"href": "https://forum.polymech.info/",
"text": "Forum"
},
{
"href": "/${LANG}/resources/info/contact",
"text": "Contact"
}
]
},
"navigation_button": {
"enable": true,
"label": "Get Started",
"link": "https://github.com/themefisher/astrofront"
},
"ecommerce": {
"brand": "Polymech",
"currencySymbol": "",
"currencyCode": "EU"
},
"metadata": {
"country": "Spain",
"city": "Barcelona",
"author": "Polymech",
"author_bio": "I am in, if its true",
"author_url": "https://polymech.info/",
"image": "/images/og-image.png",
"description": "Polymech is a plastic prototyping company that offers product design services.",
"keywords": "Plastic, Prototyping, Product Design, Opensource"
},
"shopify": {
"currencySymbol": "",
"currencyCode": "EU",
"collections": {
"hero_slider": "hidden-homepage-carousel",
"featured_products": "featured-products"
}
},
"pages": {
"home": {
"hero": "https://assets.osr-plastic.org/machines//assets/newsletter/common/products/extruders/overview-3.jpg",
"_blog": {
"store": "resources"
}
}
}
}

View File

@ -1,7 +1,6 @@
import { defineConfig } from 'astro/config' import { defineConfig } from 'astro/config'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import { imagetools } from "imagetools" import { imagetools } from "imagetools"
// import domainExpansion from '@domain-expansion/astro';
import react from "@astrojs/react" import react from "@astrojs/react"
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
@ -9,6 +8,46 @@ import sitemap from "@astrojs/sitemap";
import { rehypeAccessibleEmojis } from 'rehype-accessible-emojis'; import { rehypeAccessibleEmojis } from 'rehype-accessible-emojis';
import getReadingTime from 'reading-time'; import getReadingTime from 'reading-time';
import { toString } from 'mdast-util-to-string'; import { toString } from 'mdast-util-to-string';
import { PolymechInstance } from "@polymech/astro-base/registry";
// import domainExpansion from '@domain-expansion/astro';
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
const argv = yargs(hideBin(process.argv)).argv;
PolymechInstance.setConfig({
//...config,
productCategory: 'Consumer Electronics2',
languages: ['en', 'de', 'fr', 'es', 'it'],
products: ['laptop', 'phone', 'tablet', 'headphones', 'smartwatch', 'camera'],
features: ['multi-language', 'dynamic-content', 'seo-optimized', 'analytics'],
apiEndpoints: {
products: '/api/products',
translations: '/api/translations',
categories: '/api/categories',
search: '/api/search'
},
callbacks: {
onConfigUpdate: (config) => {
//console.log('MyPages config updated:', config);
},
onLanguageChange: (lang) => {
console.log('Language changed to:', lang);
},
onRouteRender: (path, { lang, slug }) => {
console.log('Route rendered:', path, { lang, slug });
},
get: (path, { lang, slug }) => {
//console.log('Getting data for:2', path, { lang, slug });
return {
title: 'Product Page',
description: 'This is a product page',
sharedPageText: 'This is shared text'
};
}
}
});
export function remarkReadingTime() { export function remarkReadingTime() {
return function (tree, { data }) { return function (tree, { data }) {
@ -17,7 +56,7 @@ export function remarkReadingTime() {
data.astro.frontmatter.minutesRead = readingTime.text; data.astro.frontmatter.minutesRead = readingTime.text;
}; };
} }
//locales: ['en','es','fr','it','de'],
export default defineConfig({ export default defineConfig({
site: 'https://creava.org', site: 'https://creava.org',
devToolbar: { devToolbar: {
@ -50,7 +89,7 @@ export default defineConfig({
tailwindcss({ tailwindcss({
config: './tailwind.config.cjs', config: './tailwind.config.cjs',
jit: true jit: true
}), }),
], ],
build: { build: {
target: 'esnext', target: 'esnext',
@ -80,7 +119,7 @@ export default defineConfig({
mdx({ mdx({
rehypePlugins: [ rehypePlugins: [
rehypeAccessibleEmojis, rehypeAccessibleEmojis,
remarkReadingTime remarkReadingTime
], ],
}), }),
//AstroPWA({}), //AstroPWA({}),

View File

@ -3,14 +3,8 @@
{ {
"path": "." "path": "."
}, },
{
"path": "../polymech-mono/packages"
},
{ {
"path": "../polymech-astro" "path": "../polymech-astro"
},
{
"path": "../site2"
} }
], ],
"settings": { "settings": {

3897
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,24 @@
{ {
"name": "@plastichub/astro-site-template", "name": "@polymech/library",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "astro dev --mode dev --host=0.0.0.0", "generate:config": "npx vite-node scripts/generate-app-config.ts",
"dev": "npm run generate:config && astro dev --mode dev --host=0.0.0.0",
"dev:all": "concurrently \"npm run dev\" \"npm run serve:products\"",
"start": "astro dev", "start": "astro dev",
"build": "astro build", "build": "npm run generate:config && astro build -- --logLevel=info --branch=test",
"test:build": "astro build ; cd dist ; serve", "test:build": "astro build ; cd dist ; serve",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"serve:products": "npx vite-node scripts/serve-products.ts",
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/logo.svg", "generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/logo.svg",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"lint": "eslint . --ext .ts,.tsx --fix", "lint": "eslint . --ext .ts,.tsx --fix",
"lint:check": "eslint . --ext .ts,.tsx", "lint:check": "eslint . --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx}\"", "format": "prettier --write \"src/**/*.{ts,tsx}\"",
"test:lighthouse": "lighthouse https://polymech.io/en --output json --output html --output-path ./dist/reports/report.html --save-assets --chrome-flags=\"--window-size=1440,700 --headless\"", "test:lighthouse": "lighthouse https://polymech.info/en --output json --output html --output-path ./dist/reports/report.html --save-assets --chrome-flags=\"--window-size=1440,700 --headless\"",
"test:debug": "playwright test", "test:debug": "playwright test",
"test:ui": "playwright test --ui", "test:ui": "playwright test --ui",
"clean": "rm -rf dist", "clean": "rm -rf dist",
@ -54,6 +57,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"axios": "^1.7.9", "axios": "^1.7.9",
"cacache": "^19.0.1", "cacache": "^19.0.1",
"concurrently": "^9.2.1",
"env-var": "^7.5.0", "env-var": "^7.5.0",
"exifreader": "^4.26.1", "exifreader": "^4.26.1",
"file-type": "^20.0.0", "file-type": "^20.0.0",
@ -64,7 +68,7 @@
"glob": "^11.0.1", "glob": "^11.0.1",
"got": "^14.4.6", "got": "^14.4.6",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"imagetools": "file:../polymech-astro/packages/imagetools", "imagetools": "file:../polymech-astro/packages/imagetools_3",
"jsonpath-plus": "^10.3.0", "jsonpath-plus": "^10.3.0",
"lighthouse": "^12.3.0", "lighthouse": "^12.3.0",
"link-preview-js": "^3.0.14", "link-preview-js": "^3.0.14",
@ -82,6 +86,7 @@
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"potrace": "^2.1.8", "potrace": "^2.1.8",
"puppeteer": "^22.3.0", "puppeteer": "^22.3.0",
"quicktype-core": "^23.2.6",
"react-jsx-parser": "^2.3.0", "react-jsx-parser": "^2.3.0",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-accessible-emojis": "^0.3.2", "rehype-accessible-emojis": "^0.3.2",
@ -92,11 +97,14 @@
"remark-toc": "^9.0.0", "remark-toc": "^9.0.0",
"sanitize-html": "^2.14.0", "sanitize-html": "^2.14.0",
"schema-dts": "^1.1.2", "schema-dts": "^1.1.2",
"sharp": "^0.29.3", "serve": "^14.2.5",
"serve-handler": "^6.1.6",
"sharp": "^0.34.5",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
"type-fest": "^4.34.1", "type-fest": "^4.34.1",
"vite": "^6.1.1", "vite": "^6.1.1",
"vite-node": "^5.2.0",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"write-file-atomic": "^6.0.0", "write-file-atomic": "^6.0.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
@ -121,4 +129,4 @@
"ts-jest": "^29.3.0", "ts-jest": "^29.3.0",
"vitest": "^1.3.1" "vitest": "^1.3.1"
} }
} }

35
scripts/commit.sh Normal file
View File

@ -0,0 +1,35 @@
#!/bin/bash
# Define repositories to commit to
# . refers to the current directory (polymech/site2)
# ../polymech-astro refers to the sibling directory
REPOS=("." "../polymech-astro")
# Store the optional commit message
MSG="$1"
# Iterate over each repository
for repo in "${REPOS[@]}"; do
echo "--------------------------------------------------"
echo "Processing repository: $repo"
echo "--------------------------------------------------"
# Execute in a subshell to preserve current directory
(
cd "$repo" || { echo "Failed to enter $repo"; exit 1; }
# Add all changes
git add -A .
# Commit
if [ -n "$MSG" ]; then
git commit -m "$MSG"
else
# If no message provided, let git open the editor
git commit
fi
# Pushing changes
git push
)
done

View File

@ -0,0 +1,64 @@
import fs from 'fs';
import path from 'path';
import { quicktype, InputData, jsonInputForTargetLanguage } from 'quicktype-core';
const CONFIG_PATH = path.resolve('./app-config.json');
const OUTPUT_SCHEMA_PATH = path.resolve('./src/app/config.schema.ts');
const OUTPUT_DTS_PATH = path.resolve('./src/app/config.d.ts');
async function main() {
console.log(`Reading config from ${CONFIG_PATH}...`);
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8');
// 1. Generate TypeScript Definitions (d.ts) FIRST
console.log('Generating TypeScript definitions...');
const tsInput = jsonInputForTargetLanguage("ts");
await tsInput.addSource({
name: "AppConfig",
samples: [configContent]
});
const tsInputData = new InputData();
tsInputData.addInput(tsInput);
const tsResult = await quicktype({
inputData: tsInputData,
lang: "ts",
rendererOptions: {
"just-types": "true",
"acronym-style": "original"
}
});
const tsCode = tsResult.lines.join('\n');
fs.writeFileSync(OUTPUT_DTS_PATH, tsCode);
console.log(`Wrote TypeScript definitions to ${OUTPUT_DTS_PATH}`);
// 2. Generate Zod Schema from Types using ts-to-zod
console.log('Generating Zod schema from types...');
try {
const { execSync } = await import('child_process');
// ts-to-zod <input> <output>
// Use relative paths to avoid Windows path concatenation issues with ts-to-zod
const relDts = path.relative(process.cwd(), OUTPUT_DTS_PATH);
const relSchema = path.relative(process.cwd(), OUTPUT_SCHEMA_PATH);
execSync(`npx ts-to-zod "${relDts}" "${relSchema}"`, { stdio: 'inherit', cwd: process.cwd() });
// Append export type AppConfig
fs.appendFileSync(OUTPUT_SCHEMA_PATH, `\nexport type AppConfig = z.infer<typeof appConfigSchema>;\n`);
console.log(`Wrote Zod schema to ${OUTPUT_SCHEMA_PATH}`);
} catch (error) {
console.error('Failed to generate Zod schema:', error);
throw error;
}
}
main().catch(err => {
console.error('Error fetching/generating config:', err);
process.exit(1);
});

25
scripts/serve-products.ts Normal file
View File

@ -0,0 +1,25 @@
import handler from 'serve-handler';
import http from 'http';
import { PRODUCT_ROOT, FILE_SERVER_DEV } from '../src/app/config';
// Parse host and port from FILE_SERVER_DEV
const [host, portStr] = FILE_SERVER_DEV.split(':');
const port = parseInt(portStr, 10);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const server = http.createServer((request: any, response: any) => {
return handler(request, response, {
public: PRODUCT_ROOT(),
headers: [
{
source: '**/*',
headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }]
}
]
});
});
server.listen(port, host, () => {
console.log(`Running at http://${host}:${port}`);
console.log(`Serving files from: ${PRODUCT_ROOT()}`);
});

103
src/app/config.d.ts vendored Normal file
View File

@ -0,0 +1,103 @@
export interface AppConfig {
site: Site;
footer_left: FooterLeft[];
footer_right: any[];
settings: Settings;
params: Params;
navigation: Navigation;
navigation_button: NavigationButton;
ecommerce: Ecommerce;
metadata: Metadata;
shopify: Shopify;
pages: Pages;
}
export interface Ecommerce {
brand: string;
currencySymbol: string;
currencyCode: string;
}
export interface FooterLeft {
href: string;
text: string;
}
export interface Metadata {
country: string;
city: string;
author: string;
author_bio: string;
author_url: string;
image: string;
description: string;
keywords: string;
}
export interface Navigation {
top: FooterLeft[];
}
export interface NavigationButton {
enable: boolean;
label: string;
link: string;
}
export interface Pages {
home: Home;
}
export interface Home {
hero: string;
_blog: Blog;
}
export interface Blog {
store: string;
}
export interface Params {
contact_form_action: string;
copyright: string;
}
export interface Settings {
search: boolean;
account: boolean;
sticky_header: boolean;
theme_switcher: boolean;
default_theme: string;
}
export interface Shopify {
currencySymbol: string;
currencyCode: string;
collections: Collections;
}
export interface Collections {
hero_slider: string;
featured_products: string;
}
export interface Site {
title: string;
base_url: string;
description: string;
base_path: string;
trailing_slash: boolean;
favicon: string;
logo: string;
logo_darkmode: string;
logo_width: string;
logo_height: string;
logo_text: string;
image: Image;
}
export interface Image {
default: string;
error: string;
alt: string;
}

108
src/app/config.schema.ts Normal file
View File

@ -0,0 +1,108 @@
// Generated by ts-to-zod
import { z } from "zod";
export const footerLeftSchema = z.object({
href: z.string(),
text: z.string()
});
export const settingsSchema = z.object({
search: z.boolean(),
account: z.boolean(),
sticky_header: z.boolean(),
theme_switcher: z.boolean(),
default_theme: z.string()
});
export const paramsSchema = z.object({
contact_form_action: z.string(),
copyright: z.string()
});
export const navigationSchema = z.object({
top: z.array(footerLeftSchema)
});
export const navigationButtonSchema = z.object({
enable: z.boolean(),
label: z.string(),
link: z.string()
});
export const ecommerceSchema = z.object({
brand: z.string(),
currencySymbol: z.string(),
currencyCode: z.string()
});
export const metadataSchema = z.object({
country: z.string(),
city: z.string(),
author: z.string(),
author_bio: z.string(),
author_url: z.string(),
image: z.string(),
description: z.string(),
keywords: z.string()
});
export const blogSchema = z.object({
store: z.string()
});
export const collectionsSchema = z.object({
hero_slider: z.string(),
featured_products: z.string()
});
export const imageSchema = z.object({
default: z.string(),
error: z.string(),
alt: z.string()
});
export const siteSchema = z.object({
title: z.string(),
base_url: z.string(),
description: z.string(),
base_path: z.string(),
trailing_slash: z.boolean(),
favicon: z.string(),
logo: z.string(),
logo_darkmode: z.string(),
logo_width: z.string(),
logo_height: z.string(),
logo_text: z.string(),
image: imageSchema
});
export const shopifySchema = z.object({
currencySymbol: z.string(),
currencyCode: z.string(),
collections: collectionsSchema
});
export const homeSchema = z.object({
hero: z.string(),
_blog: blogSchema
});
export const pagesSchema = z.object({
home: homeSchema
});
export const appConfigSchema = z.object({
site: siteSchema,
footer_left: z.array(footerLeftSchema),
footer_right: z.array(z.any()),
settings: settingsSchema,
params: paramsSchema,
navigation: navigationSchema,
navigation_button: navigationButtonSchema,
ecommerce: ecommerceSchema,
metadata: metadataSchema,
shopify: shopifySchema,
pages: pagesSchema
});
export type AppConfig = z.infer<typeof appConfigSchema>;

View File

@ -5,14 +5,14 @@ import { sync as read } from '@polymech/fs/read'
import { sanitizeUri } from 'micromark-util-sanitize-uri' import { sanitizeUri } from 'micromark-util-sanitize-uri'
export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}")) export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}"))
export const FILE_SERVER_DEV = 'localhost:5000'
export const LOGGING_NAMESPACE = 'polymech-site' export const LOGGING_NAMESPACE = 'polymech-site'
export const TRANSLATE_CONTENT = true export const TRANSLATE_CONTENT = true
export const LANGUAGES = ['en', 'ar', 'de', 'ja', 'es', 'zh', 'fr'] export const LANGUAGES = ['en', 'ar', 'de', 'ja', 'es', 'zh', 'fr']
//export const LANGUAGES_PROD = ['en', 'es', 'ar', 'de', 'ja', 'zh', 'fr', 'nl', 'it', 'pt'] //export const LANGUAGES_PROD = ['en', 'es', 'ar', 'de', 'ja', 'zh', 'fr', 'nl', 'it', 'pt']
export const LANGUAGES_PROD = ['en', 'es'] export const LANGUAGES_PROD = ['en', 'es', 'fr']
export const isRTL = (lang) => lang === 'ar' export const isRTL = (lang) => lang === 'ar'
// i18n constants // i18n constants
export const I18N_STORE = (root, lang) => `${root}/i18n-store/store-${lang}.json` export const I18N_STORE = (root, lang) => `${root}/i18n-store/store-${lang}.json`
export const I18N_SOURCE_LANGUAGE = 'en' export const I18N_SOURCE_LANGUAGE = 'en'
@ -63,7 +63,7 @@ export const TASK_LOG_DIRECTORY = './logs/'
// Task - Retail Config // Task - Retail Config
export const REGISTER_PRODUCT_TASKS = true export const REGISTER_PRODUCT_TASKS = true
export const RETAIL_PRODUCT_BRANCH = 'site' export const LIBARY_BRANCH = 'site-prod'
export const PROJECTS_BRANCH = 'projects' export const PROJECTS_BRANCH = 'projects'
export const RETAIL_COMPILE_CACHE = false export const RETAIL_COMPILE_CACHE = false
export const RETAIL_MEDIA_CACHE = true export const RETAIL_MEDIA_CACHE = true
@ -95,9 +95,11 @@ export const CAD_URL = (file: string, variables: Record<string, string>) =>
export const ASSET_URL = (file: string, variables: Record<string, string>) => export const ASSET_URL = (file: string, variables: Record<string, string>) =>
sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/products/${product_rel_min}/${file}", { file, ...variables })) sanitizeUri(template("${OSR_MACHINES_ASSETS_URL}/products/${product_rel_min}/${file}", { file, ...variables }))
export const ITEM_ASSET_URL = (variables: Record<string, string>) => export const ITEM_ASSET_URL_R = (variables: Record<string, string>) =>
template("${OSR_MACHINES_ASSETS_URL}/${ITEM_REL}/${assetPath}/${filePath}", variables) template("${OSR_MACHINES_ASSETS_URL}/${ITEM_REL}/${assetPath}/${filePath}", variables)
export const ITEM_ASSET_URL = (variables: Record<string, string>) =>
template("http://${FILE_SERVER_DEV}/${ITEM_REL}/${assetPath}/${filePath}", variables)
//back compat - osr-cad //back compat - osr-cad
export const parseBoolean = (value: string): boolean => { export const parseBoolean = (value: string): boolean => {
@ -122,7 +124,8 @@ export const SHOW_DEBUG = false
export const SHOW_SAMPLES = true export const SHOW_SAMPLES = true
export const SHOW_README = false export const SHOW_README = false
export const SHOW_RELATED = true export const SHOW_RELATED = true
export const SHOW_SHOWCASE = true
export const SHOW_SCREENSHOTS = true
///////////////////////////////////////////// /////////////////////////////////////////////
// //

View File

@ -6,27 +6,27 @@
"CACHE": "${root}/cache/", "CACHE": "${root}/cache/",
"CACHE_URL": "${abs_url}/cache/", "CACHE_URL": "${abs_url}/cache/",
"GIT_REPO": "https://git.polymech.io/", "GIT_REPO": "https://git.polymech.io/",
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org", "OSR_MACHINES_ASSETS_URL": "https://assets.osr-plastic.org",
"PRODUCTS_ASSETS_URL":"https://assets.osr-plastic.org/${product_rel}", "PRODUCTS_ASSETS_URL": "https://assets.osr-plastic.org/${product_rel}",
"OSR_FILES_WEB":"https://files.polymech.io/files/machines", "OSR_FILES_WEB": "https://files.polymech.info/files/machines",
"PRODUCTS_FILES_URL":"${OSR_FILES_WEB}/${product_rel}", "PRODUCTS_FILES_URL": "${OSR_FILES_WEB}/${product_rel}",
"DISCORD":"https://discord.gg/s8K7yKwBRc" "DISCORD": "https://discord.gg/s8K7yKwBRc"
}, },
"env": { "env": {
"astro-release":{ "astro-release": {
"includes": [ "includes": [
"${PRODUCT_ROOT}" "${PRODUCT_ROOT}"
], ],
"variables": { "variables": {
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org/" "OSR_MACHINES_ASSETS_URL": "https://assets.osr-plastic.org/"
} }
}, },
"astro-debug":{ "astro-debug": {
"includes": [ "includes": [
"${PRODUCT_ROOT}" "${PRODUCT_ROOT}"
], ],
"variables": { "variables": {
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org", "OSR_MACHINES_ASSETS_URL": "https://assets.osr-plastic.org",
"showCart": false, "showCart": false,
"showPrice": false, "showPrice": false,
"showResources": false, "showResources": false,
@ -37,7 +37,5 @@
"debug": true "debug": true
} }
} }
} }
} }

View File

@ -122,6 +122,7 @@ export const createTemplates = (context: TemplateContext = TemplateContext.COMMO
}), {}); }), {});
}; };
export type { TemplateConfig, LLMConfig, Prompt, PromptRegistry } export type { TemplateConfig, LLMConfig, Prompt, PromptRegistry }
export { export {
InstructionSchema, InstructionSchema,
InstructionSetSchema, InstructionSetSchema,

View File

@ -70,11 +70,13 @@ export const filter = async (content: string, tpl: string = 'howto', opts: Props
logger.info(`kbot-result: template:${tpl} : context:${context} @ ${options.model} : ${result[0]}`) logger.info(`kbot-result: template:${tpl} : context:${context} @ ${options.model} : ${result[0]}`)
return result[0] as string; return result[0] as string;
}; };
export const template_filter = async (text: string, template: string, context: TemplateContext = TemplateContext.COMMONS) => { export const template_filter = async (text: string, template: string, context: TemplateContext = TemplateContext.COMMONS) => {
if (!text || text.length < 20) { if (!text || text.length < 20) {
return text; return text;
} }
const templates = createTemplates(context); const templates = createTemplates(context);
debugger
if (!templates[template]) { if (!templates[template]) {
logger.warn(`No template found for ${template}`); logger.warn(`No template found for ${template}`);
return text; return text;

View File

@ -13,11 +13,11 @@ import { logger } from '@/base/index.js'
import { env } from './index.js' import { env } from './index.js'
import { GalleryImage, MetaJSON } from './images.js' import { GalleryImage, MetaJSON } from './images.js'
import { import {
removeArrayValues, removeArrayValues,
removeArrays, removeArrays,
removeBufferValues, removeBufferValues,
removeEmptyObjects removeEmptyObjects
} from '@/base/objects.js' } from '@/base/objects.js'
import { import {
@ -26,7 +26,7 @@ import {
ASSETS_GLOB ASSETS_GLOB
} from 'config/config.js' } from 'config/config.js'
export const default_sanitizer = (files:string[]) => files.map((f) => sanitizeFilename(f)) export const default_sanitizer = (files: string[]) => files.map((f) => sanitizeFilename(f))
export const default_sort = (files: string[]): string[] => { export const default_sort = (files: string[]): string[] => {
const getSortableParts = (filename: string) => { const getSortableParts = (filename: string) => {
@ -80,8 +80,8 @@ export const image_url = async (src, fallback = DEFAULT_IMAGE_URL) => {
return safeSrc return safeSrc
} }
export const gallery = async ( assetPath, item): Promise<GalleryImage[]> => { export const gallery = async (assetPath, item): Promise<GalleryImage[]> => {
const root = resolve(PRODUCT_ROOT()) const root = resolve(PRODUCT_ROOT())
const profile = env() const profile = env()
const assetSlug = path.parse(assetPath).name const assetSlug = path.parse(assetPath).name
@ -93,7 +93,6 @@ export const gallery = async ( assetPath, item): Promise<GalleryImage[]> => {
} }
const mediaPath = `${root}/${item}/${assetPath}/` const mediaPath = `${root}/${item}/${assetPath}/`
if (!exists(mediaPath)) { if (!exists(mediaPath)) {
logger.warn(`item gallery : item ${item} media path not found ${mediaPath}!`)
return [] return []
} }
const galleryGlob = (itemConfig.gallery || {})[assetSlug]?.glob || ASSETS_GLOB const galleryGlob = (itemConfig.gallery || {})[assetSlug]?.glob || ASSETS_GLOB
@ -127,7 +126,7 @@ export const gallery = async ( assetPath, item): Promise<GalleryImage[]> => {
galleryFiles = default_sort(galleryFiles) galleryFiles = default_sort(galleryFiles)
} else { } else {
galleryFiles = galleryFiles.filter(default_filter_locale) galleryFiles = galleryFiles.filter(default_filter_locale)
} }
galleryFiles = default_sort(galleryFiles) galleryFiles = default_sort(galleryFiles)
return await pMap(galleryFiles, async (file: string) => { return await pMap(galleryFiles, async (file: string) => {

View File

@ -1,16 +1,15 @@
--- ---
import path from "path"; import path from "path";
import { decode } from "html-entities"; import { decode } from "html-entities";
import { IHowto, asset_local_rel } from "@/model/howto/howto.js"; import { IHowto, IStep, asset_local_rel } from "@/model/howto/howto.js";
import { translate } from "@polymech/astro-base/base/i18n.js"; import { translate } from "@polymech/astro-base/base/i18n.js";
import Translate from "@polymech/astro-base/components/i18n.astro"; import Translate from "@polymech/astro-base/components/i18n.astro";
import BaseLayout from "@/layouts/BaseLayout.astro"; import BaseLayout from "@/layouts/BaseLayout.astro";
import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro" import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro";
import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro" import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro";
import Breadcrumb from "@polymech/astro-base/components/Breadcrumb.astro"; import Breadcrumb from "@polymech/astro-base/components/Breadcrumb.astro";
import { getSidebarConfig } from '@polymech/astro-base/config/sidebar'; import { getSidebarConfig } from "@polymech/astro-base/config/sidebar";
import type { MarkdownHeading } from 'astro'; import type { MarkdownHeading } from "astro";
import Wrapper from "@/components/containers/Wrapper.astro"; import Wrapper from "@/components/containers/Wrapper.astro";
import GalleryK from "@polymech/astro-base/components/GalleryK.astro"; import GalleryK from "@polymech/astro-base/components/GalleryK.astro";
import { files, forward_slash } from "@polymech/commons"; import { files, forward_slash } from "@polymech/commons";
@ -33,6 +32,7 @@ import {
HOWTO_ADD_REFERENCES, HOWTO_ADD_REFERENCES,
HOWTO_EDIT_URL, HOWTO_EDIT_URL,
} from "config/config.js"; } from "config/config.js";
import { filter } from "@/base/kbot.js"; import { filter } from "@/base/kbot.js";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { group_by_cat } from "@/model/howto/howto.js"; import { group_by_cat } from "@/model/howto/howto.js";
@ -51,29 +51,27 @@ model_files = model_files.map((f) =>
const content = async (str: string) => const content = async (str: string) =>
await translate(str, I18N_SOURCE_LANGUAGE, Astro.currentLocale); await translate(str, I18N_SOURCE_LANGUAGE, Astro.currentLocale);
const component = async (str: string) => const component = async (str: string) =>
await createMarkdownComponent(await content(str)); await createMarkdownComponent(await content(str));
const componentHTML = async (str: string) => const componentHTML = async (str: string) =>
await createHTMLComponent(await content(str)); await createHTMLComponent(await content(str));
// Debug: Check if howto.steps exists
console.log('howto.steps:', howto.steps);
console.log('howto.steps length:', howto.steps?.length);
const stepsWithFilteredMarkdown = await pMap( const stepsWithFilteredMarkdown = await pMap(
howto.steps || [], howto.steps || [],
async (step) => ({ async (step: IStep) => ({
...step, ...step,
filteredMarkdownComponent: await component(step.text), filteredMarkdownComponent: await component(step.text),
}), }),
{ concurrency: 1 }, { concurrency: 1 },
); );
console.log('stepsWithFilteredMarkdown length:', stepsWithFilteredMarkdown.length);
// Fetch all howtos for category navigation // Fetch all howtos for category navigation
const allHowtos = await getCollection("howtos"); const allHowtos = await getCollection("howtos");
const allHowtoItems = allHowtos.map((storeItem) => storeItem.data.item) as IHowto[]; const allHowtoItems = allHowtos.map(
(storeItem) => storeItem.data.item,
) as IHowto[];
const howtosByCategory = group_by_cat(allHowtoItems); const howtosByCategory = group_by_cat(allHowtoItems);
const categories = Object.keys(howtosByCategory).sort(); const categories = Object.keys(howtosByCategory).sort();
@ -81,32 +79,42 @@ const categories = Object.keys(howtosByCategory).sort();
const organizedCategories: any[] = []; const organizedCategories: any[] = [];
// Separate and organize categories // Separate and organize categories
const uncategorizedItems = howtosByCategory['Uncategorized'] || howtosByCategory['uncategorized'] || []; const uncategorizedItems =
const categorizedItems = categories.filter(cat => howtosByCategory["Uncategorized"] || howtosByCategory["uncategorized"] || [];
cat.toLowerCase() !== 'uncategorized' && cat !== 'Uncategorized' const categorizedItems = categories
).sort(); .filter(
(cat) => cat.toLowerCase() !== "uncategorized" && cat !== "Uncategorized",
)
.sort();
// Create dynamic category structure from actual data // Create dynamic category structure from actual data
if (categorizedItems.length > 0) { if (categorizedItems.length > 0) {
organizedCategories.push({ organizedCategories.push({
label: 'Browse by Category', label: "Browse by Category",
collapsed: false, collapsed: false,
items: categorizedItems.map(category => ({ items: categorizedItems.map((category) => ({
label: `${category} (${howtosByCategory[category].length})`, label: `${category} (${howtosByCategory[category].length})`,
collapsed: !(category === howto.category?.label), // Expand current category collapsed: !(category === howto.category?.label), // Expand current category
isSubGroup: true, // This makes it a collapsible subgroup isSubGroup: true, // This makes it a collapsible subgroup
items: howtosByCategory[category].slice(0, 8).map((categoryHowto: IHowto) => ({ items: howtosByCategory[category]
label: categoryHowto.title, .slice(0, 8)
href: `/${Astro.currentLocale}/howtos/${categoryHowto.slug}`, .map((categoryHowto: IHowto) => ({
isCurrent: categoryHowto.slug === howto.slug label: categoryHowto.title,
})).concat( href: `/${Astro.currentLocale}/howtos/${categoryHowto.slug}`,
howtosByCategory[category].length > 8 ? [{ isCurrent: categoryHowto.slug === howto.slug,
label: `View all ${howtosByCategory[category].length}...`, }))
href: `/${Astro.currentLocale}/howtos/category/${category.toLowerCase().replace(/\s+/g, '-')}`, .concat(
isCurrent: false howtosByCategory[category].length > 8
}] : [] ? [
) {
})) label: `View all ${howtosByCategory[category].length}...`,
href: `/${Astro.currentLocale}/howtos/category/${category.toLowerCase().replace(/\s+/g, "-")}`,
isCurrent: false,
},
]
: [],
),
})),
}); });
} }
@ -115,60 +123,67 @@ if (uncategorizedItems.length > 0) {
organizedCategories.push({ organizedCategories.push({
label: `Uncategorized (${uncategorizedItems.length})`, label: `Uncategorized (${uncategorizedItems.length})`,
collapsed: true, collapsed: true,
items: uncategorizedItems.slice(0, 10).map((uncatHowto: IHowto) => ({ items: uncategorizedItems
label: uncatHowto.title, .slice(0, 10)
href: `/${Astro.currentLocale}/howtos/${uncatHowto.slug}`, .map((uncatHowto: IHowto) => ({
isCurrent: uncatHowto.slug === howto.slug label: uncatHowto.title,
})).concat( href: `/${Astro.currentLocale}/howtos/${uncatHowto.slug}`,
uncategorizedItems.length > 10 ? [{ isCurrent: uncatHowto.slug === howto.slug,
label: `View all ${uncategorizedItems.length} guides...`, }))
href: `/${Astro.currentLocale}/howtos/uncategorized`, .concat(
isCurrent: false uncategorizedItems.length > 10
}] : [] ? [
) {
label: `View all ${uncategorizedItems.length} guides...`,
href: `/${Astro.currentLocale}/howtos/uncategorized`,
isCurrent: false,
},
]
: [],
),
}); });
} }
// Add quick navigation // Add quick navigation
organizedCategories.unshift({ organizedCategories.unshift({
label: 'Quick Navigation', label: "Quick Navigation",
collapsed: false, collapsed: false,
items: [ items: [
{ {
label: 'All Guides', label: "All Guides",
href: `/${Astro.currentLocale}/howtos`, href: `/${Astro.currentLocale}/howtos`,
isCurrent: false isCurrent: false,
}, },
{ {
label: 'Recently Added', label: "Recently Added",
href: `/${Astro.currentLocale}/howtos/recent`, href: `/${Astro.currentLocale}/howtos/recent`,
isCurrent: false isCurrent: false,
} },
] ],
}); });
const pageNavigation = organizedCategories; const pageNavigation = organizedCategories;
// Function to extract headings from markdown content // Function to extract headings from markdown content
const extractHeadingsFromMarkdown = (markdown: string): MarkdownHeading[] => { const extractHeadingsFromMarkdown = (markdown: string): MarkdownHeading[] => {
if (!markdown) return []; if (!markdown) return [];
const headingRegex = /^(#{1,6})\s+(.+)$/gm; const headingRegex = /^(#{1,6})\s+(.+)$/gm;
const headings: MarkdownHeading[] = []; const headings: MarkdownHeading[] = [];
let match; let match;
while ((match = headingRegex.exec(markdown)) !== null) { while ((match = headingRegex.exec(markdown)) !== null) {
const depth = match[1].length as 1 | 2 | 3 | 4 | 5 | 6; const depth = match[1].length as 1 | 2 | 3 | 4 | 5 | 6;
const text = match[2].trim(); const text = match[2].trim();
const slug = text.toLowerCase() const slug = text
.replace(/[^\w\s-]/g, '') // Remove special characters .toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/[^\w\s-]/g, "") // Remove special characters
.replace(/\s+/g, "-") // Replace spaces with hyphens
.trim(); .trim();
headings.push({ depth, slug, text }); headings.push({ depth, slug, text });
} }
return headings; return headings;
}; };
@ -222,8 +237,8 @@ const dynamicHeadings: MarkdownHeading[] = [];
// Main title // Main title
dynamicHeadings.push({ dynamicHeadings.push({
depth: 1, depth: 1,
slug: 'title', slug: "title",
text: howto.title text: howto.title,
}); });
// Extract headings from description if it contains markdown headings // Extract headings from description if it contains markdown headings
@ -233,12 +248,17 @@ if (howto.description) {
// No headings in description, add it as a section // No headings in description, add it as a section
dynamicHeadings.push({ dynamicHeadings.push({
depth: 2, depth: 2,
slug: 'description', slug: "description",
text: 'Description' text: "Description",
}); });
} else { } else {
// Add description headings // Add description headings
dynamicHeadings.push(...descriptionHeadings.map(h => ({ ...h, depth: Math.max(2, h.depth) as any }))); dynamicHeadings.push(
...descriptionHeadings.map((h) => ({
...h,
depth: Math.max(2, h.depth) as any,
})),
);
} }
} }
@ -247,26 +267,26 @@ if (stepsWithFilteredMarkdown.length > 0) {
// Steps section header // Steps section header
dynamicHeadings.push({ dynamicHeadings.push({
depth: 2, depth: 2,
slug: 'steps', slug: "steps",
text: 'Steps' text: "Steps",
}); });
// Individual steps with extracted headings from their content // Individual steps with extracted headings from their content
stepsWithFilteredMarkdown.forEach((step, idx) => { stepsWithFilteredMarkdown.forEach((step, idx) => {
// Add the step title // Add the step title
dynamicHeadings.push({ dynamicHeadings.push({
depth: 3, depth: 3,
slug: `step-${idx + 1}`, slug: `step-${idx + 1}`,
text: `Step ${idx + 1}: ${step.title}` text: `Step ${idx + 1}: ${step.title}`,
}); });
// Extract any headings from the step content // Extract any headings from the step content
const stepHeadings = extractHeadingsFromMarkdown(step.text); const stepHeadings = extractHeadingsFromMarkdown(step.text);
stepHeadings.forEach(heading => { stepHeadings.forEach((heading) => {
dynamicHeadings.push({ dynamicHeadings.push({
...heading, ...heading,
depth: Math.max(4, heading.depth) as any, // Make sure step content headings are at least depth 4 depth: Math.max(4, heading.depth) as any, // Make sure step content headings are at least depth 4
slug: `step-${idx + 1}-${heading.slug}` slug: `step-${idx + 1}-${heading.slug}`,
}); });
}); });
}); });
@ -277,11 +297,16 @@ const resourcesHeadings = extractHeadingsFromMarkdown(howto_resources);
if (resourcesHeadings.length === 0) { if (resourcesHeadings.length === 0) {
dynamicHeadings.push({ dynamicHeadings.push({
depth: 2, depth: 2,
slug: 'resources', slug: "resources",
text: 'Resources' text: "Resources",
}); });
} else { } else {
dynamicHeadings.push(...resourcesHeadings.map(h => ({ ...h, depth: Math.max(2, h.depth) as any }))); dynamicHeadings.push(
...resourcesHeadings.map((h) => ({
...h,
depth: Math.max(2, h.depth) as any,
})),
);
} }
// Add references section (check if references content has headings) // Add references section (check if references content has headings)
@ -289,18 +314,23 @@ const referencesHeadings = extractHeadingsFromMarkdown(howto_references);
if (referencesHeadings.length === 0) { if (referencesHeadings.length === 0) {
dynamicHeadings.push({ dynamicHeadings.push({
depth: 2, depth: 2,
slug: 'references', slug: "references",
text: 'References' text: "References",
}); });
} else { } else {
dynamicHeadings.push(...referencesHeadings.map(h => ({ ...h, depth: Math.max(2, h.depth) as any }))); dynamicHeadings.push(
...referencesHeadings.map((h) => ({
...h,
depth: Math.max(2, h.depth) as any,
})),
);
} }
// Add metadata section // Add metadata section
dynamicHeadings.push({ dynamicHeadings.push({
depth: 2, depth: 2,
slug: 'metadata', slug: "metadata",
text: 'Metadata' text: "Metadata",
}); });
const headings = dynamicHeadings; const headings = dynamicHeadings;
@ -312,199 +342,224 @@ const EditLink = () => {
) )
} }
*/ */
--- ---
<BaseLayout class="markdown-content bg-gray-100" frontmatter={howto}> <BaseLayout class="markdown-content bg-gray-100" frontmatter={howto}>
<div class="layout-with-sidebar"> <div class="layout-with-sidebar">
<!-- Mobile Toggle --> <!-- Mobile Toggle -->
<MobileToggle /> <MobileToggle />
<!-- Sidebar --> <!-- Sidebar -->
<div class="sidebar-wrapper"> <div class="sidebar-wrapper">
<Sidebar <Sidebar
config={sidebarConfig} config={sidebarConfig}
currentUrl={Astro.url} currentUrl={Astro.url}
headings={headings} headings={headings}
pageNavigation={pageNavigation} pageNavigation={pageNavigation}
/> />
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<main class="main-content-with-sidebar"> <main class="main-content-with-sidebar">
<div class="px-4 py-4 md:px-6 md:py-6"> <div class="px-4 py-4 md:px-6 md:py-6">
{/* Breadcrumb */} {/* Breadcrumb */}
<Breadcrumb <Breadcrumb
currentPath={Astro.url.pathname} currentPath={Astro.url.pathname}
collection="howtos" collection="howtos"
title={howto.title} title={howto.title}
/> />
<Wrapper>
<article class="bg-white shadow-lg rounded-lg overflow-hidden">
<header class="p-4 ">
<h1 id="title" class="text-4xl font-bold text-gray-800 mb-4">
<Translate>{howto.title}</Translate>
</h1>
<GalleryK images={[{ src: howto.cover_image.src, alt: "" }]} />
<div class="flex flex-wrap gap-2 mb-4">
{
howto.tags.map((tag) => (
<span class="bg-orange-400 text-white text-xs px-3 py-1 rounded-full">
<Translate>{tag.toUpperCase()}</Translate>
</span>
))
}
</div>
</header>
</article>
<section class="meta-view bg-white rounded-lg p-4 mt-4 truncate"> <Wrapper>
<ul class="grid md:grid-cols-1 lg:grid-cols-2 gap-4 mt-8 mb-8"> <article class="bg-white shadow-lg rounded-lg overflow-hidden">
<li> <header class="p-4">
<strong><Translate>Difficulty:</Translate></strong> <h1 id="title" class="text-4xl font-bold text-gray-800 mb-4">
<Translate>{howto.difficulty_level}</Translate> <Translate>{howto.title}</Translate>
</li> </h1>
<li> <GalleryK images={[{ src: howto.cover_image.src, alt: "" }]} />
<strong><Translate>Time Required:</Translate></strong> <div class="flex flex-wrap gap-2 mb-4">
<Translate>{decode(howto.time)}</Translate> {
</li> howto.tags.map((tag) => (
<li> <span class="bg-orange-400 text-white text-xs px-3 py-1 rounded-full">
<strong><Translate>Views:</Translate></strong>{howto.total_views} <Translate>{tag.toUpperCase()}</Translate>
</li> </span>
<li> ))
<strong><Translate>Creator:</Translate></strong>{howto._createdBy} }
</li> </div>
<li> </header>
<strong><Translate>Country:</Translate></strong>{authorGeo.countryName } </article>
</li>
<li> <section class="meta-view bg-white rounded-lg p-4 mt-4 truncate">
<strong><Translate>Email:</Translate></strong> <ul class="grid md:grid-cols-1 lg:grid-cols-2 gap-4 mt-8 mb-8">
<a
class="text-orange-600 underline"
href={`mailto:${authorLinks.find((link) => link.name.toLowerCase() === "email")?.url.replace("mailto:", "")}`}
>
{
authorLinks
.find((link) => link.name.toLowerCase() === "email")
?.url.replace("mailto:", "")
}
</a>
</li>
{
authorLinks
.filter((l) => l.name.toLowerCase() !== "email")
.map((link) => (
<li> <li>
<strong>{link.name}:</strong> <strong><Translate>Difficulty:</Translate></strong>
<Translate>{howto.difficulty_level}</Translate>
</li>
<li>
<strong><Translate>Time Required:</Translate></strong>
<Translate>{decode(howto.time)}</Translate>
</li>
<li>
<strong><Translate>Views:</Translate></strong>{
howto.total_views
}
</li>
<li>
<strong><Translate>Creator:</Translate></strong>{
howto._createdBy
}
</li>
<li>
<strong><Translate>Country:</Translate></strong>{
authorGeo.countryName
}
</li>
<li>
<strong><Translate>Email:</Translate></strong>
<a <a
class="text-orange-600 underline" class="text-orange-600 underline"
href={link.url} href={`mailto:${authorLinks.find((link) => link.name.toLowerCase() === "email")?.url.replace("mailto:", "")}`}
target="_blank"
> >
{shortenUrl(link.url)} {
authorLinks
.find((link) => link.name.toLowerCase() === "email")
?.url.replace("mailto:", "")
}
</a> </a>
</li> </li>
)) {
} authorLinks
<li> .filter((l) => l.name.toLowerCase() !== "email")
<strong><Translate>Downloads:</Translate></strong>{ .map((link) => (
howto.total_downloads <li>
} <strong>{link.name}:</strong>
</li> <a
</ul> class="text-orange-600 underline"
</section> href={link.url}
target="_blank"
<section id="description" class="bg-white p-8"> >
<div class="mb-8 markdown-content"> {shortenUrl(link.url)}
<Description /> </a>
</div> </li>
<a ))
href={HOWTO_FILES_WEB(howto.slug)} }
class="inline-block py-2 px-4 bg-orange-500 hover:bg-orange-700 text-white rounded-full mb-8"
><Translate>Browse Files</Translate></a
>
</section>
<section id="table-of-contents" class="px-8 py-8 bg-orange-50">
<h2 class="font-bold mb-4 text-xl">
<Translate>Table of Contents</Translate>
</h2>
{stepsWithFilteredMarkdown && stepsWithFilteredMarkdown.length > 0 ? (
<ul class="grid grid-cols-1 md:grid-cols-2 gap-2 list-decimal p-4">
{
stepsWithFilteredMarkdown.map((step, idx) => (
<li> <li>
<a <strong><Translate>Downloads:</Translate></strong>{
href={`#step-${idx + 1}`} howto.total_downloads
class="text-orange-600 hover:underline" }
>
<Translate>{step.title}</Translate>
</a>
</li> </li>
)) </ul>
} </section>
</ul>
) : (
<div class="p-4 text-gray-600">
<p>No steps available for this how-to guide.</p>
<p>Debug: Steps length = {stepsWithFilteredMarkdown?.length || 0}</p>
</div>
)}
</section>
<section id="steps" class="border-gray-300 p-0 lg:p-6 mt-8"> <section id="description" class="bg-white p-8">
{stepsWithFilteredMarkdown && stepsWithFilteredMarkdown.length > 0 ? ( <div class="mb-8 markdown-content">
<ol class="space-y-10"> <Description />
{ </div>
stepsWithFilteredMarkdown.map((step, idx) => ( <a
<li href={HOWTO_FILES_WEB(howto.slug)}
id={`step-${idx + 1}`} class="inline-block py-2 px-4 bg-orange-500 hover:bg-orange-700 text-white rounded-full mb-8"
class="bg-white shadow-sm rounded-lg p-2 lg:p-6" ><Translate>Browse Files</Translate></a
> >
<div class="mb-4 flex items-center"> </section>
<span class="bg-orange-500 text-xl font-bold text-white rounded-full h-10 w-10 flex items-center justify-center mr-3">
{idx + 1} <section id="table-of-contents" class="px-8 py-8 bg-orange-50">
</span> <h2 class="font-bold mb-4 text-xl">
<h3 class="text-xl font-bold"> <Translate>Table of Contents</Translate>
<a </h2>
href={`#step-${idx + 1}`} {
class="text-orange-600 hover:underline" stepsWithFilteredMarkdown &&
stepsWithFilteredMarkdown.length > 0 ? (
<ul class="grid grid-cols-1 md:grid-cols-2 gap-2 list-decimal p-4">
{stepsWithFilteredMarkdown.map((step, idx) => (
<li>
<a
href={`#step-${idx + 1}`}
class="text-orange-600 hover:underline"
>
<Translate>{step.title}</Translate>
</a>
</li>
))}
</ul>
) : (
<div class="p-4 text-gray-600">
<p>No steps available for this how-to guide.</p>
<p>
Debug: Steps length ={" "}
{stepsWithFilteredMarkdown?.length || 0}
</p>
</div>
)
}
</section>
<section id="steps" class="border-gray-300 p-0 lg:p-6 mt-8">
{
stepsWithFilteredMarkdown &&
stepsWithFilteredMarkdown.length > 0 ? (
<ol class="space-y-10">
{stepsWithFilteredMarkdown.map((step, idx) => (
<li
id={`step-${idx + 1}`}
class="bg-white shadow-sm rounded-lg p-2 lg:p-6"
> >
<Translate>{step.title}</Translate> <div class="mb-4 flex items-center">
</a> <span class="bg-orange-500 text-xl font-bold text-white rounded-full h-10 w-10 flex items-center justify-center mr-3">
</h3> {idx + 1}
</span>
<h3 class="text-xl font-bold">
<a
href={`#step-${idx + 1}`}
class="text-orange-600 hover:underline"
>
<Translate>{step.title}</Translate>
</a>
</h3>
</div>
<div class="markdown-content">
<step.filteredMarkdownComponent />
</div>
{step.images?.length > 0 && (
<GalleryK images={step.images} />
)}
</li>
))}
</ol>
) : (
<div class="p-8 text-center text-gray-600">
<p>No detailed steps are available for this how-to guide.</p>
</div> </div>
<div class="markdown-content"> )
<step.filteredMarkdownComponent /> }
</div> </section>
{step.images?.length > 0 && <GalleryK images={step.images} />}
</li>
))
}
</ol>
) : (
<div class="p-8 text-center text-gray-600">
<p>No detailed steps are available for this how-to guide.</p>
</div>
)}
</section>
<section id="resources" class="bg-white shadow-lg rounded-lg border-gray-300 p-4 lg:p-6 mt-8 markdown-content"><Resources /></section> <section
<section id="references" class="bg-white shadow-lg rounded-lg border-gray-300 p-4 lg:p-6 mt-8 markdown-content"><References /></section> id="resources"
<footer id="metadata" class="p-8 text-sm border-t bg-white text-gray-600"> class="bg-white shadow-lg rounded-lg border-gray-300 p-4 lg:p-6 mt-8 markdown-content"
<div class="flex justify-between"> >
<span <Resources />
><Translate>Created on</Translate>: { </section>
new Date(howto._created).toLocaleDateString() <section
}</span id="references"
> class="bg-white shadow-lg rounded-lg border-gray-300 p-4 lg:p-6 mt-8 markdown-content"
<span >
>{howto.votedUsefulBy.length} <References />
<Translate>people found this useful</Translate></span </section>
> <footer
</div> id="metadata"
</footer> class="p-8 text-sm border-t bg-white text-gray-600"
>
<div class="flex justify-between">
<span
><Translate>Created on</Translate>: {
new Date(howto._created).toLocaleDateString()
}</span
>
<span
>{howto.votedUsefulBy.length}
<Translate>people found this useful</Translate></span
>
</div>
</footer>
</Wrapper> </Wrapper>
</div> </div>
</main> </main>

View File

@ -64,6 +64,7 @@ const cleanTime = decodedTime.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replac
// The URL prop already points to the correct howto page // The URL prop already points to the correct howto page
const cardUrl = url; const cardUrl = url;
--- ---
<article class="group relative bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 flex flex-col h-full"> <article class="group relative bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 flex flex-col h-full">

View File

@ -1,38 +1,47 @@
--- ---
import { default_image } from "config/config.js"; import { default_image } from "@/app/config.js";
import Translate from "@/components/polymech/i18n.astro"; const { title, url, price, model } = Astro.props;
import { Img } from "imagetools/components" const thumbnail =
const { title, url, price, model, selected = false } = Astro.props; model?.assets?.main_image?.url ||
const thumbnail = model?.assets?.renderings[0]?.src || default_image(); model?.assets?.gallery[0]?.url ||
const classes = `group relative bg-white overflow-hidden group rounded-xl ${selected ? "ring-2 ring-orange-500" : ""}`; default_image();
const hash = model?.assets?.main_image?.hash || model?.assets?.gallery[0]?.hash;
import Img from "@polymech/astro-base/components/polymech/image.astro";
--- ---
<div class={classes}> <div
<div class="group relative overflow-hidden rounded-xl shadow-sm hover:shadow-md transition-shadow duration-300"
class="p-4 overflow-hidden group-hover:opacity-75 duration-300 transition-all" >
> <div class="aspect-square overflow-hidden">
<a href={url} title={title} aria-label={title}> <a href={url} title={title} aria-label={title} class="block w-full h-full">
<Img <div class="w-full h-full flex items-center justify-center p-0 md:p-1">
src={thumbnail} <Img
alt={title} src={thumbnail}
format="avif" s={hash}
objectFit="cover" alt={title}
placeholder="blurred" format="avif"
sizes="(min-width: 220px) 220px" objectFit="contain"
breakpoints={{ count: 2, minWidth: 120, maxWidth: 430 }} placeholder="blurred"
/> sizes="(min-width: 220px) 220px"
breakpoints={{ count: 2, minWidth: 120, maxWidth: 430 }}
class="max-w-full max-h-full object-contain group-hover:scale-105 transition-transform duration-300"
/>
</div>
</a> </a>
</div> </div>
<div class="p-4 text-xs text-neutral-500"> <div class="p-4">
<div class="flex items-center justify-between space-x-8"> <h3 class="text-sm font-medium line-clamp-2">
<h3> <a
<a href={url} title={title} aria-label={title}> href={url}
<span aria-hidden="true" class="absolute inset-0"></span> title={title}
<Translate>{title}</Translate> aria-label={title}
</a> class="hover:text-neutral-900 transition-colors"
</h3> >
<p class="top-4 right-4"></p> <span aria-hidden="true" class="absolute inset-0"></span>
<p class="mt-1"></p> {title}
</div> </a>
</h3>
</div> </div>
</div> </div>

View File

@ -14,7 +14,7 @@ export const LOGGING_NAMESPACE = 'polymech-site'
export const TRANSLATE_CONTENT = true export const TRANSLATE_CONTENT = true
export const LANGUAGES = ['en', 'es'] export const LANGUAGES = ['en', 'es']
export const LANGUAGES_SITE = ['en', 'ar', 'de', 'ja', 'es', 'zh', 'fr'] export const LANGUAGES_SITE = ['en', 'ar', 'de', 'ja', 'es', 'zh', 'fr']
export const LANGUAGES_PROD = ['en','es','fr','it','de'] export const LANGUAGES_PROD = ['en', 'es', 'fr', 'it', 'de']
export const isRTL = (lang) => lang === 'ar' export const isRTL = (lang) => lang === 'ar'
// i18n constants // i18n constants
@ -25,9 +25,10 @@ export const I18N_ASSET_PATH = "${SRC_DIR}/${SRC_NAME}-${DST_LANG}${SRC_EXT}"
// Library - Howtos // Library - Howtos
export const HOWTO_GLOB = '**/config.json' export const HOWTO_GLOB = '**/config.json'
export const FILES_WEB = 'https://files.polymech.io/files/machines/howtos/' export const FILES_WEB = 'https://files.polymech.info/files/machines/howtos/'
export const HOWTO_EDIT_ROOT = 'https://git.polymech.io/osr-plastic/osr-machines/src/branch/master/howtos' export const HOWTO_EDIT_ROOT = 'https://git.polymech.io/osr-plastic/osr-machines/src/branch/master/howtos'
export const HOWTO_FILTER_LLM = false export const HOWTO_FILTER_LLM = false
export const HOWTO_LLM_KEYWORDS = false
export const HOWTO_ANNOTATIONS = false export const HOWTO_ANNOTATIONS = false
export const HOWTO_ANNOTATIONS_CACHE = false export const HOWTO_ANNOTATIONS_CACHE = false
export const HOWTO_COMPLETE_RESOURCES = false export const HOWTO_COMPLETE_RESOURCES = false
@ -37,7 +38,7 @@ export const HOWTO_ADD_REFERENCES = false
export const HOWTO_COMPLETE_SKILLS = false export const HOWTO_COMPLETE_SKILLS = false
export const HOWTO_LOCAL_RESOURCES = false export const HOWTO_LOCAL_RESOURCES = false
export const HOWTO_SEO_LLM = false export const HOWTO_SEO_LLM = false
export const HOWTO_MAX_ITEMS = 100 export const HOWTO_MAX_ITEMS = 1000
export const HOWTO_MIGRATION = () => path.resolve(resolve("./data/last.json")) export const HOWTO_MIGRATION = () => path.resolve(resolve("./data/last.json"))
export const HOWTO_ROOT_INTERN = () => path.resolve(resolve("./public/resources/howtos")) export const HOWTO_ROOT_INTERN = () => path.resolve(resolve("./public/resources/howtos"))
@ -48,8 +49,8 @@ export const HOWTO_EDIT_URL = (id: string, lang: string) => `${HOWTO_EDIT_ROOT}/
// Library - Directory // Library - Directory
export const DIRECTORY_GLOB = '**/config.json' export const DIRECTORY_GLOB = '**/config.json'
export const DIRECTORY_FILES_BASE = 'https://files.polymech.io/files/directory/' export const DIRECTORY_FILES_BASE = 'https://files.polymech.info/files/directory/'
export const DIRECTORY_EDIT_ROOT = 'https://git.polymech.io/osr-plastic/osr-machines/src/branch/master/directory' export const DIRECTORY_EDIT_ROOT = 'https://git.polymech.info/polymech/machines/src/branch/master/directory'
export const DIRECTORY_FILTER_LLM = true export const DIRECTORY_FILTER_LLM = true
export const DIRECTORY_ANNOTATIONS = false export const DIRECTORY_ANNOTATIONS = false
export const DIRECTORY_ANNOTATIONS_CACHE = false export const DIRECTORY_ANNOTATIONS_CACHE = false
@ -118,8 +119,8 @@ export const TASK_LOG_DIRECTORY = './logs/'
// Task - Retail Config // Task - Retail Config
export const REGISTER_PRODUCT_TASKS = true export const REGISTER_PRODUCT_TASKS = true
export const RETAIL_PRODUCT_BRANCH = 'site' export const LIBARY_BRANCH = 'site-prod'
export const PROJECTS_BRANCH = 'projects' export const PROJECTS_BRANCH = 'projects2'
export const RETAIL_COMPILE_CACHE = false export const RETAIL_COMPILE_CACHE = false
export const RETAIL_MEDIA_CACHE = true export const RETAIL_MEDIA_CACHE = true
export const RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS = 'info' export const RETAIL_LOG_LEVEL_I18N_PRODUCT_ASSETS = 'info'
@ -135,8 +136,7 @@ export const CAD_CAM_MAIN_MATCH = (product) => `${product}/cad*/*-CNC*.+(SLDASM)
export const CAD_CACHE = true export const CAD_CACHE = true
export const CAD_EXPORT_CONFIGURATIONS = false export const CAD_EXPORT_CONFIGURATIONS = false
export const CAD_EXPORT_SUB_COMPONENTS = true export const CAD_EXPORT_SUB_COMPONENTS = true
export const CAD_MODEL_FILE_PATH = (SOURCE, CONFIGURATION = '') => export const CAD_MODEL_FILE_PATH = (SOURCE, CONFIGURATION = '') => SOURCE.replace('.json', `${CONFIGURATION ? '-' + CONFIGURATION : ''}.tree.json`)
SOURCE.replace('.json', `${CONFIGURATION ? '-' + CONFIGURATION : ''}.tree.json`)
export const CAD_DEFAULT_CONFIGURATION = 'Default' export const CAD_DEFAULT_CONFIGURATION = 'Default'
export const CAD_RENDERER = 'solidworks' export const CAD_RENDERER = 'solidworks'
export const CAD_RENDERER_VIEW = 'Render' export const CAD_RENDERER_VIEW = 'Render'
@ -155,9 +155,7 @@ export const ITEM_ASSET_URL = (variables: Record<string, string>) =>
//back compat - osr-cad //back compat - osr-cad
export const parseBoolean = (value: string): boolean => { export const parseBoolean = (value: string): boolean => { return value === '1' || value.toLowerCase() === 'true'; }
return value === '1' || value.toLowerCase() === 'true';
}
///////////////////////////////////////////// /////////////////////////////////////////////
// //
// Rendering - Store // Rendering - Store

View File

@ -6,27 +6,27 @@
"CACHE": "${root}/cache/", "CACHE": "${root}/cache/",
"CACHE_URL": "${abs_url}/cache/", "CACHE_URL": "${abs_url}/cache/",
"GIT_REPO": "https://git.polymech.io/", "GIT_REPO": "https://git.polymech.io/",
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org", "OSR_MACHINES_ASSETS_URL": "https://assets.osr-plastic.org",
"PRODUCTS_ASSETS_URL":"https://assets.osr-plastic.org/${product_rel}", "PRODUCTS_ASSETS_URL": "https://assets.osr-plastic.org/${product_rel}",
"OSR_FILES_WEB":"https://files.polymech.io/files/machines", "OSR_FILES_WEB": "https://files.polymech.info/files/machines",
"PRODUCTS_FILES_URL":"${OSR_FILES_WEB}/${product_rel}", "PRODUCTS_FILES_URL": "${OSR_FILES_WEB}/${product_rel}",
"DISCORD":"https://discord.gg/s8K7yKwBRc" "DISCORD": "https://discord.gg/s8K7yKwBRc"
}, },
"env": { "env": {
"astro-release":{ "astro-release": {
"includes": [ "includes": [
"${PRODUCT_ROOT}" "${PRODUCT_ROOT}"
], ],
"variables": { "variables": {
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org/" "OSR_MACHINES_ASSETS_URL": "https://assets.osr-plastic.org/"
} }
}, },
"astro-debug":{ "astro-debug": {
"includes": [ "includes": [
"${PRODUCT_ROOT}" "${PRODUCT_ROOT}"
], ],
"variables": { "variables": {
"OSR_MACHINES_ASSETS_URL":"https://assets.osr-plastic.org", "OSR_MACHINES_ASSETS_URL": "https://assets.osr-plastic.org",
"showCart": false, "showCart": false,
"showPrice": false, "showPrice": false,
"showResources": false, "showResources": false,
@ -37,7 +37,5 @@
"debug": true "debug": true
} }
} }
} }
} }

View File

@ -1,44 +1,53 @@
import { glob } from 'astro/loaders'
import { defineCollection, z } from "astro:content" import { defineCollection, z } from "astro:content"
import { ComponentConfigSchema } from '@polymech/commons/component' import { ComponentConfigSchema } from '@polymech/commons/component'
import { loader } from './model/component/component.js' import { loader } from '@polymech/astro-base/model/component.js'
import { loader as howtoLoader } from './model/howto/howto.js' import { loader as howtoLoader } from './model/howto/howto.js'
import { loader as directorLoader } from './model/directory/item.js' import { loader as directorLoader } from './model/directory/item.js'
import { RETAIL_PRODUCT_BRANCH, PROJECTS_BRANCH } from 'config/config.js' import { LIBARY_BRANCH } from '@/app/config.js'
import { glob } from 'astro/loaders'
const store = defineCollection({ const store = defineCollection({
loader: loader(RETAIL_PRODUCT_BRANCH) as any, loader: loader('site-dev') as any,
schema: ComponentConfigSchema.passthrough(), schema: ComponentConfigSchema.passthrough(),
}) })
/*
const projects = defineCollection({ const projects = defineCollection({
loader: loader(PROJECTS_BRANCH) as any, loader: loader(PROJECTS_BRANCH) as any,
schema: ComponentConfigSchema.passthrough(), schema: ComponentConfigSchema.passthrough(),
}) })
*/
/*
const helpcenter = defineCollection({ const helpcenter = defineCollection({
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
intro: z.string(), intro: z.string(),
}) })
}) })
const infopages = defineCollection({ const infopages = defineCollection({
schema: z.object({ schema: z.object({
title: z.string().optional(), title: z.string().optional(),
intro: z.string().optional(), intro: z.string().optional(),
}).passthrough(), }).passthrough(),
}) })
*/
const howtos = defineCollection({ const howtos = defineCollection({
loader: howtoLoader(), loader: howtoLoader(),
schema: z.object({ schema: z.object({
title: z.string().optional() title: z.string().optional()
}).passthrough() }).passthrough()
}) })
/*
const directory = defineCollection({ const directory = defineCollection({
loader: directorLoader(), loader: directorLoader(),
schema: z.object({ schema: z.object({
title: z.string().optional() title: z.string().optional()
}).passthrough() }).passthrough()
}) })
*/
const resources = defineCollection({ const resources = defineCollection({
loader: glob({ base: './src/content/resources', pattern: '*.{md,mdx}' }), loader: glob({ base: './src/content/resources', pattern: '*.{md,mdx}' }),
schema: z.object({ schema: z.object({
@ -56,10 +65,9 @@ const resources = defineCollection({
export const collections = { export const collections = {
store, store,
projects,
resources, resources,
helpcenter, //helpcenter,
infopages, //infopages,
howtos, howtos,
directory //directory
}; };

View File

@ -107,7 +107,7 @@ tags: ["community", "blogging", "c++"]
- [Machine & Components Library](https://forum.osr-plastic.org/c/machines/49) - [Machine & Components Library](https://forum.osr-plastic.org/c/machines/49)
- [Moulds - Library](https://files.polymech.io/files/machines/moulds/) - [Moulds - Library](https://files.polymech.info/files/machines/moulds/)
- [Git Repository Machines](https://git.polymech.io/osr-plastic/osr-machines) - [Git Repository Machines](https://git.polymech.io/osr-plastic/osr-machines)

View File

@ -1,27 +1,24 @@
--- ---
import { getCollection } from "astro:content";
import BaseLayout from "./BaseLayout.astro";
import { createMarkdownComponent } from "@/base/index.js";
import { translate } from "@/base/i18n.js";
import Wrapper from "@/components/containers/Wrapper.astro";
import Translate from "@/components/polymech/i18n.astro";
import Readme from "@/components/polymech/readme.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";
import TabContent from "@/components/polymech/tab-content.astro";
import StoreEntries from "@/components/store/StoreEntries.astro";
import {
IComponentConfigEx,
group_by_path,
group_path,
} from "@/model/component/component.js";
import "flowbite"; import "flowbite";
import Navigation from "@polymech/astro-base/components/global/Navigation.astro";
import { createMarkdownComponent } from "@/base/index.js";
import { translate } from "@polymech/astro-base/base/i18n.js";
import Translate from "@polymech/astro-base/components/i18n.astro";
import LGallery from "@polymech/astro-base/components/GalleryK.astro";
import BaseLayout from "./BaseLayout.astro";
import Wrapper from "@polymech/astro-base/components/containers/Wrapper.astro";
import Readme from "@polymech/astro-base/components/polymech/readme.astro";
import Breadcrumb from "@polymech/astro-base/components/Breadcrumb.astro";
import Resources from "@polymech/astro-base/components/polymech/resources.astro";
import Specs from "@polymech/astro-base/components/specs.astro";
import TabButton from "@polymech/astro-base/components/tab-button.astro";
import TabContent from "@polymech/astro-base/components/tab-content.astro";
import { import {
I18N_SOURCE_LANGUAGE, I18N_SOURCE_LANGUAGE,
SHOW_3D_PREVIEW, SHOW_3D_PREVIEW,
@ -37,25 +34,40 @@ import {
SHOW_RESOURCES, SHOW_RESOURCES,
SHOW_CHECKOUT, SHOW_CHECKOUT,
SHOW_README, SHOW_README,
SHOW_RELATED,
DEFAULT_LICENSE,
isRTL, isRTL,
} from "config/config.js"; SHOW_SHOWCASE,
SHOW_SCREENSHOTS,
} from "@/app/config.js";
const { frontmatter: item, ...rest } = Astro.props; import { substitute } from "@polymech/commons/variables";
import * as path from "path";
import { sync as read } from "@polymech/fs/read";
import { sync as exists } from "@polymech/fs/exists";
import { PRODUCT_DIR } from "@/app/config.js";
const content = await translate( const { frontmatter: data, ...rest } = Astro.props;
item.body || "", const itemDir = PRODUCT_DIR(data.rel);
const contentPath = path.join(itemDir, "templates/shared", "body.md");
if (exists(contentPath)) {
data.content = read(contentPath) as string;
}
const content = substitute(false, data.content || "", {
...data,
LANG: Astro.currentLocale,
});
const translated_content = await translate(
content,
I18N_SOURCE_LANGUAGE, I18N_SOURCE_LANGUAGE,
Astro.currentLocale, Astro.currentLocale,
); );
const Body = createMarkdownComponent(content) as any;
const Body = await createMarkdownComponent(translated_content);
const str_debug = const str_debug =
"```json\n" + "```json\n" +
JSON.stringify( JSON.stringify(
{ {
...item, ...data,
config: null, config: null,
}, },
null, null,
@ -64,50 +76,66 @@ const str_debug =
"\n```"; "\n```";
const Content_Debug = await createMarkdownComponent(str_debug); const Content_Debug = await createMarkdownComponent(str_debug);
const view = "store";
const items = await getCollection(view); //.filter((i) => item.rel !== i.id);
//const group_id = group_path(item);
const others = await group_by_path(items, Astro.currentLocale);
--- ---
<BaseLayout frontmatter={item} description={item.description} {...rest} class=""> <BaseLayout
frontmatter={data}
description={data.description}
hideNavigation={true}
{...rest}
>
<Navigation />
<Wrapper> <Wrapper>
<!-- Header with Breadcrumb on its own line -->
<div
class="flex flex-col gap-4 mb-4 py-4 border-b border-gray-200 dark:border-gray-700"
>
<!-- Breadcrumb -->
<div class="flex-1 min-w-0">
<Breadcrumb
currentPath={Astro.url.pathname}
collection="store"
title={data.title}
showHome={true}
/>
</div>
</div>
<section> <section>
<div class="grid sm:grid-cols-2 lg:grid-cols-2 just xl:grid-cols-2 gap-2 "> <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
<div class="flex flex-col gap-2 h-full justify-between"> <!-- Left Column: Description -->
<div class="flex flex-col gap-4">
<div> <div>
<article class="markdown-content bg-white rounded-xl p-4"> <h1 class="font-semibold mb-2 text-2xl">
<h1 <Translate>{`${data.title}`}</Translate>
class="text-neutral-500 font-semibold mb-2 text-2xl" </h1>
> {
<span><Translate>{`${item.title}`}</Translate></span> isRTL(Astro.currentLocale) && (
{ <div class=" font-semibold mb-2">"{data.title}"</div>
isRTL(Astro.currentLocale) && ( )
<div class="text-neutral-500 font-semibold mb-2"> }
"{item.title}"
</div>
)
}
</h1>
<Body />
</article>
</div> </div>
<div class="gap-2 flex flex-col h-full justify-end"> <article
class="markdown-content bg-white dark:bg-gray-800 rounded-xl p-4"
>
<Body />
</article>
<div class="flex flex-col gap-2">
{ {
SHOW_3D_PREVIEW && SHOW_3D_PREVIEW &&
item.Preview3d && data.Preview3d &&
item.cad && data.cad &&
item.cad[0] && data.cad[0] &&
item.cad[0][".html"] && ( data.cad[0][".html"] && (
<a <a
href={item.cad[0][".html"]} href={data.cad[0][".html"]}
title="link to your page" title="link to your page"
aria-label="your label" aria-label="your label"
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white hover:bg-neutral-200 duration-300 rounded-xl w-full justify-between" class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white dark:bg-gray-800 hover:bg-neutral-200 dark:hover:bg-gray-700 duration-300 rounded-xl w-full justify-between rounded-xl"
> >
<span class="relative uppercase text-xs text-neutral-600"> <span class="relative uppercase text-xs">
<Translate>3D Preview</Translate> <Translate>3D Preview</Translate>
</span> </span>
<div <div
@ -150,20 +178,21 @@ const others = await group_by_path(items, Astro.currentLocale);
</a> </a>
) )
} }
{ {
SHOW_CHECKOUT && item.checkout && ( SHOW_CHECKOUT && data.checkout && (
<a <a
href={item.checkout} href={data.checkout}
title="link to your page" title="link to your page"
aria-label="your label" aria-label="your label"
class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-orange-500 hover:bg-black duration-300 rounded-xl w-full justify-between" class="relative group overflow-hidden pl-4 h-14 flex space-x-6 items-center bg-white dark:bg-gray-800 hover:bg-black dark:hover:bg-gray-700 duration-300 rounded-xl w-full justify-between"
> >
<span class="relative uppercase text-xs text-white"> <span class="relative uppercase text-xs ">
<Translate>Add to cart</Translate> <Translate>Add to cart</Translate>
</span> </span>
<div <div
aria-hidden="true" aria-hidden="true"
class="w-12 text-white transition duration-300 -translate-y-7 group-hover:translate-y-7" class="w-12 transition duration-300 -translate-y-7 group-hover:translate-y-7"
> >
<div class="h-14 flex"> <div class="h-14 flex">
<svg <svg
@ -204,58 +233,34 @@ const others = await group_by_path(items, Astro.currentLocale);
</div> </div>
{ {
SHOW_LICENSE && ( SHOW_LICENSE && (
<div class="space-y-2"> <div class="bg-white dark:bg-gray-800 rounded-xl p-4">
<div class="bg-white rounded-xl p-4"> <h3 class="text-lg text-neutral-600 dark:text-gray-300 uppercase tracking-tight">
<h3 class="text-lg text-neutral-600 uppercase tracking-tight"> License
<Translate>License</Translate> </h3>
</h3> <p class=" mt-4 text-sm text-gray-700 dark:text-gray-300">
<p class="text-neutral-500 mt-4 text-sm"> {data.license}
{item.license || DEFAULT_LICENSE} </p>
</p> </div>
</div> )
}
</div>
<!-- Right Column: Gallery and Actions -->
<div class="flex flex-col gap-4">
{
SHOW_RENDERINGS && data.assets?.renderings && (
<div class="bg-white dark:bg-gray-800 rounded-xl p-4">
<LGallery
images={data.assets.gallery}
gallerySettings={{ SHOW_TITLE: false }}
/>
</div> </div>
) )
} }
</div> </div>
{
SHOW_RENDERINGS && (
<div
class="flex-1 h-full bg-white"
style="height: 100%; width: 100%;"
>
<Gallery
images={item.assets.renderings}
gallerySettings={{ SHOW_TITLE: false }}
item={item}
/>
</div>
)
}
</div> </div>
</section> </section>
{
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">
<Gallery
images={item.assets.showcase}
lightboxSettings={{
SHOW_TITLE: false,
SHOW_DESCRIPTION: false,
SIZES_THUMB: "w-32 h-32",
}}
gallerySettings={{
SHOW_TITLE: false,
SHOW_DESCRIPTION: false,
}}
item={item}
/>
</div>
</section>
)
}
<section id="tabs-view"> <section id="tabs-view">
<div class="mb-4 border-b border-gray-200 dark:border-gray-700"> <div class="mb-4 border-b border-gray-200 dark:border-gray-700">
<ul <ul
@ -266,72 +271,125 @@ const others = await group_by_path(items, Astro.currentLocale);
data-tabs-inactive-classes="dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300" data-tabs-inactive-classes="dark:border-transparent text-gray-500 hover:text-gray-600 dark:text-gray-400 border-gray-100 hover:border-gray-300 dark:border-gray-700 dark:hover:text-gray-300"
role="tablist" role="tablist"
> >
{SHOW_README && item.readme && <TabButton title="Overview" />} {SHOW_README && <TabButton title="Overview" />}
{SHOW_SPECS && <TabButton title="Specs" />} <TabButton title="Specs" />
{SHOW_GALLERY && <TabButton title="Gallery" />}
{SHOW_RESOURCES && <TabButton title="Resources" />} <TabButton title="Resources" />
{SHOW_SAMPLES && <TabButton title="Samples" />}
{data.assets.showcase.length > 0 && <TabButton title="Showcase" />}
{ {
SHOW_SAMPLES && item.assets.samples.length > 0 && ( SHOW_SCREENSHOTS &&
<TabButton title="Samples" /> data.assets.screenshots &&
) data.assets.screenshots.length > 0 && (
<TabButton title="Screenshots" />
)
} }
{SHOW_DEBUG && <TabButton title="Debug" />} {SHOW_DEBUG && <TabButton title="Debug" />}
</ul> </ul>
</div> </div>
<div id="default-styled-tab-content"> <div id="default-styled-tab-content">
{SHOW_README && item.readme && <TabContent title="Overview" class="content"> <TabContent title="Overview">
{ {
( SHOW_README && data.readme && (
<Readme markdown={item.readme} data={item} /> <Readme markdown={data.readme} data={data} />
) )
} }
</TabContent> </TabContent>
} <div
{ class="hidden bg-white rounded-xl dark:bg-gray-800"
SHOW_SPECS && ( id="specs-view"
<TabContent role="tabpanel"
title="Specs" aria-labelledby="dashboard-tab"
class="bg-white rounded-xl dark:bg-gray-800" >
> <Specs frontmatter={data} />
<Specs frontmatter={item} /> </div>
</TabContent>
)
}
{
SHOW_GALLERY && (
<TabContent title="Gallery" class="p-4 md:p-4 rounded-lg bg-white">
<Gallery images={item.assets.gallery} item={item} />{" "}
</TabContent>
)
}
{ {
SHOW_SAMPLES && ( SHOW_SAMPLES && (
<TabContent <div
title="Samples" class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
class="p-4 bg-white rounded-xl dark:bg-gray-800" id="samples-view"
role="tabpanel"
aria-labelledby="dashboard-tab"
> >
<Gallery images={item.assets.samples} item={item} /> <LGallery images={data.assets.samples} />
</TabContent> </div>
) )
} }
{ {
SHOW_RESOURCES && ( SHOW_RESOURCES && (
<TabContent <div
title="Resources" class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
class="p-4 bg-white rounded-xl dark:bg-gray-800" id="resources-view"
role="tabpanel"
aria-labelledby="dashboard-tab"
> >
<Resources frontmatter={item} /> <Resources frontmatter={data} />
</TabContent> </div>
) )
} }
{
SHOW_SHOWCASE &&
data.assets.showcase &&
data.assets.showcase.length > 0 && (
<div
class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
id="showcase-view"
role="tabpanel"
aria-labelledby="dashboard-tab"
>
<LGallery
images={data.assets.showcase}
lightboxSettings={{
SHOW_TITLE: false,
SHOW_DESCRIPTION: false,
SIZES_THUMB: "w-32 h-32",
}}
gallerySettings={{
SHOW_TITLE: false,
SHOW_DESCRIPTION: false,
//SIZES_THUMB: "w-32 h-32",
}}
/>
</div>
)
}
{
SHOW_SCREENSHOTS &&
data.assets.screenshots &&
data.assets.screenshots.length > 0 && (
<div
class="hidden p-4 bg-white rounded-xl dark:bg-gray-800"
id="screenshots-view"
role="tabpanel"
aria-labelledby="dashboard-tab"
>
<LGallery
images={data.assets.screenshots}
lightboxSettings={{
SHOW_TITLE: true,
SHOW_DESCRIPTION: true,
SIZES_THUMB: "w-32 h-32",
}}
gallerySettings={{
SHOW_TITLE: true,
SHOW_DESCRIPTION: true,
SIZES_THUMB: "w-32 h-32",
}}
/>
</div>
)
}
{ {
SHOW_DEBUG && ( SHOW_DEBUG && (
<TabContent <div
title="Debug" class="hidden rounded-lg bg-white p-4 dark:bg-gray-800"
class="rounded-lg bg-white p-4 dark:bg-gray-800" id="debug-view"
role="tabpanel"
aria-labelledby="dashboard-tab"
> >
<Content_Debug /> <Content_Debug />
</TabContent> </div>
) )
} }
</div> </div>
@ -372,38 +430,5 @@ const others = await group_by_path(items, Astro.currentLocale);
}); });
</script> </script>
</section> </section>
<hr />
<h1 class="p-4 text-xs"> <Translate>Related</Translate>
</h1>
{
SHOW_RELATED && (
<section id="item_related" class="bg-blue-50 p-4 rounded-2xl">
{Object.keys(others).map((relKey) => (
<section>
<h4
aria-hidden="true"
class="text-xs p-4 text-neutral-500"
>
{relKey}
</h4>
<div class="grid sm:grid-cols-4 lg:grid-cols-4 xl:grid-cols-4 gap-8">
{others[relKey].map((post) => (
<StoreEntries
key={post.id}
url={`/${Astro.currentLocale}/${view}/${post.id}`}
title={post.data.title}
price={post.data.price}
type={post.data.type}
alt={post.data.title}
model={post.data}
selected={post.id === item.rel}
/>
))}
</div>
</section>
))}
</section>
)
}
</Wrapper> </Wrapper>
</BaseLayout> </BaseLayout>

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

@ -15,33 +15,23 @@ import { IHowto, IImage, ITag, ITEM_TYPE } from './howto-model.js'
import { BLACKLIST, default_filters_markdown, validateLinks } from '../../base/filters.js' import { BLACKLIST, default_filters_markdown, validateLinks } from '../../base/filters.js'
import { download } from '../download.js' import { download } from '../download.js'
import { filter } from "@/base/kbot.js"
import { slugify } from "@/base/strings.js"
import { urlCache } from '../../base/url-cache.js';
const expandUrls = true const expandUrls = true
import { import {
HOWTO_FILES_WEB,
HOWTO_FILES_ABS,
HOWTO_FILTER_LLM, HOWTO_FILTER_LLM,
default_image, default_image,
HOWTO_ROOT, HOWTO_ROOT,
HOWTO_GLOB,
HOWTO_MIGRATION, HOWTO_MIGRATION,
HOWTO_ANNOTATIONS,
HOWTO_COMPLETE_RESOURCES,
HOWTO_ADD_HARDWARE,
HOWTO_COMPLETE_SKILLS,
HOWTO_LOCAL_RESOURCES,
HOWTO_ADD_RESOURCES, HOWTO_ADD_RESOURCES,
HOWTO_ADD_REFERENCES, HOWTO_ADD_REFERENCES,
HOWTO_SEO_LLM, HOWTO_SEO_LLM,
HOWTO_MAX_ITEMS HOWTO_MAX_ITEMS,
HOWTO_LLM_KEYWORDS
} from "config/config.js" } from "config/config.js"
import { logger } from '@/base/index.js' import { logger } from '@/base/index.js'
import { applyFilters, default_filters_plain, FilterFunction } from '../../base/filters.js' import { applyFilters, default_filters_plain, FilterFunction } from '../../base/filters.js'
import { TemplateContext, buildPrompt, LLMConfig, createTemplates } from '@/base/kbot-templates.js'; import { TemplateContext } from '@/base/kbot-templates.js';
import { template_filter } from '@/base/kbot.js' import { template_filter } from '@/base/kbot.js'
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
@ -73,7 +63,6 @@ export const downloadFiles = async (dst: string, howto: IHowto) => {
const parts = path.parse(asset_path); const parts = path.parse(asset_path);
const zipout = path.join(asset_root, 'files') const zipout = path.join(asset_root, 'files')
if (parts.ext === '.rar' || parts.ext === '.zip') { if (parts.ext === '.rar' || parts.ext === '.zip') {
logger.info(`Extracting RAR file ${i.name} to ${zipout}`);
try { try {
if (!exists(asset_path)) { if (!exists(asset_path)) {
logger.error(`File does not exist: ${asset_path}`); logger.error(`File does not exist: ${asset_path}`);
@ -329,7 +318,7 @@ const complete = async (item: IHowto) => {
// item = { ...item, ...config } // item = { ...item, ...config }
// Apply content filtering respecting HOWTO_FILTER_LLM flag // Apply content filtering respecting HOWTO_FILTER_LLM flag
if (HOWTO_FILTER_LLM) { if (HOWTO_FILTER_LLM) {
item.description = await content(item.description || '') item.description = await content(item.description || '')
// Process steps with content filtering // Process steps with content filtering
item.steps = await pMap( item.steps = await pMap(
item.steps, item.steps,
@ -341,7 +330,7 @@ const complete = async (item: IHowto) => {
} else { } else {
// Just apply validateLinks if LLM filtering is disabled // Just apply validateLinks if LLM filtering is disabled
item.description = await applyFilters(item.description || '', [validateLinks]) item.description = await applyFilters(item.description || '', [validateLinks])
// Apply only default filters to steps // Apply only default filters to steps
item.steps = await pMap( item.steps = await pMap(
item.steps, item.steps,
@ -359,7 +348,9 @@ const complete = async (item: IHowto) => {
...item.steps.map(step => step.text) ...item.steps.map(step => step.text)
].filter(Boolean).join('\n\n') ].filter(Boolean).join('\n\n')
item.keywords = await template_filter(item.content, 'keywords', TemplateContext.HOWTO); if (HOWTO_LLM_KEYWORDS) {
item.keywords = await template_filter(item.content, 'keywords', TemplateContext.HOWTO);
}
if (HOWTO_ADD_RESOURCES) { if (HOWTO_ADD_RESOURCES) {
// item.resources = await template_filter(item.content, 'toolsAndHardware', TemplateContext.HOWTO); // item.resources = await template_filter(item.content, 'toolsAndHardware', TemplateContext.HOWTO);
item.resources = await applyFilters(item.resources, default_filters_markdown); item.resources = await applyFilters(item.resources, default_filters_markdown);
@ -408,7 +399,6 @@ const onStoreItem = async (store: any) => {
item = await complete(item) item = await complete(item)
const configPath = path.join(item_path(item), 'config.json') const configPath = path.join(item_path(item), 'config.json')
write(configPath, JSON.stringify(item, null, 2)) write(configPath, JSON.stringify(item, null, 2))
logger.info(`Stored item ${item.slug} at ${configPath}`)
store.data.item = item store.data.item = item
return store return store
} }

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,231 +1,231 @@
--- ---
import { group_by_cat } from "@/model/howto/howto.js"; import { group_by_cat } from "@/model/howto/howto.js";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import BaseLayout from "@/layouts/BaseLayout.astro"; import BaseLayout from "@/layouts/BaseLayout.astro";
import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro" import Sidebar from "@polymech/astro-base/components/sidebar/Sidebar.astro"
import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro" import MobileToggle from "@polymech/astro-base/components/sidebar/MobileToggle.astro"
import { getSidebarConfig } from '@polymech/astro-base/config/sidebar'; import { getSidebarConfig } from '@polymech/astro-base/config/sidebar';
import HowtoCard from "@/components/howtos/HowtoCard.astro"; import HowtoCard from "@/components/howtos/HowtoCard.astro";
import Translate from "@/components/polymech/i18n.astro"; import Translate from "@/components/polymech/i18n.astro";
import { LANGUAGES_PROD as LANGUAGES } from "config/config.js"; import { LANGUAGES_PROD as LANGUAGES } from "config/config.js";
import type { IHowto } from "@/model/howto/howto.js"; import type { IHowto } from "@/model/howto/howto.js";
const view = "howtos"; const view = "howtos";
const all = await getCollection(view); const all = await getCollection(view);
const items = all.map((storeItem) => storeItem.data.item) as IHowto[]; const items = all.map((storeItem) => storeItem.data.item) as IHowto[];
const groups = group_by_cat(items); const groups = group_by_cat(items);
const locale = Astro.currentLocale; const locale = Astro.currentLocale;
const sidebarConfig = getSidebarConfig(); const sidebarConfig = getSidebarConfig();
const categories = Object.keys(groups).sort(); const categories = Object.keys(groups).sort();
// Create organized page-level navigation for categories // Create organized page-level navigation for categories
const organizedCategories: any[] = []; const organizedCategories: any[] = [];
// Separate and organize categories // Separate and organize categories
const uncategorizedItems = groups['Uncategorized'] || groups['uncategorized'] || []; const uncategorizedItems = groups['Uncategorized'] || groups['uncategorized'] || [];
const categorizedItems = categories.filter(cat => const categorizedItems = categories.filter(cat =>
cat.toLowerCase() !== 'uncategorized' && cat !== 'Uncategorized' cat.toLowerCase() !== 'uncategorized' && cat !== 'Uncategorized'
).sort(); ).sort();
// Add categorized items with better organization // Add categorized items with better organization
if (categorizedItems.length > 0) { if (categorizedItems.length > 0) {
organizedCategories.push({ organizedCategories.push({
label: 'Browse by Category', label: 'Browse by Category',
collapsed: false, collapsed: false,
items: categorizedItems.map(category => ({ items: categorizedItems.map(category => ({
label: `${category} (${groups[category].length})`, label: `${category} (${groups[category].length})`,
collapsed: true, collapsed: true,
isSubGroup: true, // This makes it a collapsible subgroup isSubGroup: true, // This makes it a collapsible subgroup
items: groups[category].slice(0, 8).map((howto: IHowto) => ({ items: groups[category].slice(0, 8).map((howto: IHowto) => ({
label: howto.title, label: howto.title,
href: `/${locale}/howtos/${howto.slug}`, href: `/${locale}/howtos/${howto.slug}`,
isCurrent: false isCurrent: false
})).concat( })).concat(
groups[category].length > 8 ? [{ groups[category].length > 8 ? [{
label: `View all ${groups[category].length} guides...`, label: `View all ${groups[category].length} guides...`,
href: `/${locale}/howtos/category/${category.toLowerCase().replace(/\s+/g, '-')}`, href: `/${locale}/howtos/category/${category.toLowerCase().replace(/\s+/g, '-')}`,
isCurrent: false isCurrent: false
}] : [] }] : []
) )
})) }))
}); });
} }
// Then, add uncategorized items if they exist // Then, add uncategorized items if they exist
if (uncategorizedItems.length > 0) { if (uncategorizedItems.length > 0) {
organizedCategories.push({ organizedCategories.push({
label: `Uncategorized (${uncategorizedItems.length})`, label: `Uncategorized (${uncategorizedItems.length})`,
collapsed: true, collapsed: true,
items: uncategorizedItems.slice(0, 10).map((uncatHowto: IHowto) => ({ items: uncategorizedItems.slice(0, 10).map((uncatHowto: IHowto) => ({
label: uncatHowto.title, label: uncatHowto.title,
href: `/${locale}/howtos/${uncatHowto.slug}`, href: `/${locale}/howtos/${uncatHowto.slug}`,
isCurrent: false isCurrent: false
})).concat( })).concat(
uncategorizedItems.length > 10 ? [{ uncategorizedItems.length > 10 ? [{
label: `View all ${uncategorizedItems.length} guides...`, label: `View all ${uncategorizedItems.length} guides...`,
href: `/${locale}/howtos/uncategorized`, href: `/${locale}/howtos/uncategorized`,
isCurrent: false isCurrent: false
}] : [] }] : []
) )
}); });
} }
// Add quick navigation // Add quick navigation
organizedCategories.unshift({ organizedCategories.unshift({
label: 'Quick Navigation', label: 'Quick Navigation',
collapsed: false, collapsed: false,
items: [ items: [
{ {
label: 'All Guides', label: 'All Guides',
href: `/${locale}/howtos`, href: `/${locale}/howtos`,
isCurrent: true // This is the current page isCurrent: true // This is the current page
}, },
{ {
label: 'Recently Added', label: 'Recently Added',
href: `/${locale}/howtos/recent`, href: `/${locale}/howtos/recent`,
isCurrent: false isCurrent: false
} }
] ]
}); });
const pageNavigation = organizedCategories; const pageNavigation = organizedCategories;
export async function getStaticPaths() { export async function getStaticPaths() {
return LANGUAGES.map((lang) => ({ return LANGUAGES.map((lang) => ({
params: { locale: lang }, params: { locale: lang },
})); }));
} }
--- ---
<BaseLayout frontmatter={{ <BaseLayout frontmatter={{
title: "How-To Guides", title: "How-To Guides",
description: "Browse our collection of step-by-step how-to guides" description: "Browse our collection of step-by-step how-to guides"
}}> }}>
<div class="layout-with-sidebar"> <div class="layout-with-sidebar">
<!-- Mobile Toggle --> <!-- Mobile Toggle -->
<MobileToggle /> <MobileToggle />
<!-- Sidebar --> <!-- Sidebar -->
<div class="sidebar-wrapper"> <div class="sidebar-wrapper">
<Sidebar <Sidebar
config={sidebarConfig} config={sidebarConfig}
currentUrl={Astro.url} currentUrl={Astro.url}
pageNavigation={pageNavigation} pageNavigation={pageNavigation}
/> />
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<main class="main-content-with-sidebar"> <main class="main-content-with-sidebar">
<div class="container mx-auto px-4 py-4 md:px-6 md:py-6"> <div class="container mx-auto px-4 py-4 md:px-6 md:py-6">
<section class="py-8"> <section class="py-8">
<!-- Header --> <!-- Header -->
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-4"> <h1 class="text-3xl font-bold text-gray-900 mb-4">
<Translate>How-To Guides</Translate> <Translate>How-To Guides</Translate>
</h1> </h1>
<p class="text-lg text-gray-600 mb-6"> <p class="text-lg text-gray-600 mb-6">
<Translate>{all.length} guide{all.length !== 1 ? 's' : ''} available across {categories.length} categories</Translate> <Translate>{all.length} guide{all.length !== 1 ? 's' : ''} available across {categories.length} categories</Translate>
</p> </p>
</div> </div>
<!-- All Howtos Grid --> <!-- All Howtos Grid -->
<div class="mb-12"> <div class="mb-12">
<h2 class="text-2xl font-semibold text-gray-900 mb-6"><Translate>All Guides</Translate></h2> <h2 class="text-2xl font-semibold text-gray-900 mb-6"><Translate>All Guides</Translate></h2>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-6">
{items.map((item) => { {items.map((item) => {
const correspondingItem = all.find(storeItem => storeItem.data.item.slug === item.slug); const correspondingItem = all.find(storeItem => storeItem.data.item.slug === item.slug);
return correspondingItem ? ( return correspondingItem ? (
<HowtoCard <HowtoCard
url={`/${locale}/howtos/${item.slug}`} url={`/${locale}/howtos/${item.slug}`}
title={item.title} title={item.title}
description={item.description} description={item.description}
alt={item.title} alt={item.title}
category={item.category?.label} category={item.category?.label}
difficulty_level={item.difficulty_level} difficulty_level={item.difficulty_level}
time={item.time} time={item.time}
total_views={item.total_views} total_views={item.total_views}
total_downloads={item.total_downloads} total_downloads={item.total_downloads}
author={item._createdBy} author={item._createdBy}
tags={item.tags?.map(tag => typeof tag === 'string' ? tag : tag.label) || []} tags={item.tags?.map(tag => typeof tag === 'string' ? tag : tag.label) || []}
locale={locale} locale={locale}
howto={item} howto={item}
/> />
) : null; ) : null;
})} })}
</div> </div>
</div> </div>
<!-- Categories Section --> <!-- Categories Section -->
<div class="space-y-12" id="categories"> <div class="space-y-12" id="categories">
{categories.map((category) => ( {categories.map((category) => (
<section class="category-section"> <section class="category-section">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 <h2
id={category.toLowerCase().replace(/\s+/g, '-')} id={category.toLowerCase().replace(/\s+/g, '-')}
class="text-2xl font-semibold text-gray-900 cursor-pointer hover:text-orange-600 transition-colors" class="text-2xl font-semibold text-gray-900 cursor-pointer hover:text-orange-600 transition-colors"
onclick={`window.location.hash = '${category.toLowerCase().replace(/\s+/g, '-')}'`} onclick={`window.location.hash = '${category.toLowerCase().replace(/\s+/g, '-')}'`}
> >
<Translate>{category}</Translate> <Translate>{category}</Translate>
</h2> </h2>
</div> </div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{groups[category].slice(0, 8).map((howto: IHowto) => { {groups[category].slice(0, 8).map((howto: IHowto) => {
const correspondingItem = all.find(storeItem => storeItem.data.item.slug === howto.slug); const correspondingItem = all.find(storeItem => storeItem.data.item.slug === howto.slug);
return correspondingItem ? ( return correspondingItem ? (
<HowtoCard <HowtoCard
url={`/${locale}/howtos/${howto.slug}`} url={`/${locale}/howtos/${howto.slug}`}
title={howto.title} title={howto.title}
description={howto.description} description={howto.description}
alt={howto.title} alt={howto.title}
category={howto.category?.label} category={howto.category?.label}
difficulty_level={howto.difficulty_level} difficulty_level={howto.difficulty_level}
time={howto.time} time={howto.time}
total_views={howto.total_views} total_views={howto.total_views}
total_downloads={howto.total_downloads} total_downloads={howto.total_downloads}
author={howto._createdBy} author={howto._createdBy}
tags={howto.tags?.map(tag => typeof tag === 'string' ? tag : tag.label) || []} tags={howto.tags?.map(tag => typeof tag === 'string' ? tag : tag.label) || []}
locale={locale} locale={locale}
howto={howto} howto={howto}
/> />
) : null; ) : null;
})} })}
</div> </div>
{groups[category].length > 8 && ( {groups[category].length > 8 && (
<div class="mt-6 text-center"> <div class="mt-6 text-center">
<a <a
href={`/${locale}/howtos/category/${category.toLowerCase().replace(/\s+/g, '-')}`} href={`/${locale}/howtos/category/${category.toLowerCase().replace(/\s+/g, '-')}`}
class="inline-flex items-center px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors" class="inline-flex items-center px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"
> >
<Translate>View all</Translate> {groups[category].length} <Translate>guides in</Translate> {category} <Translate>View all</Translate> {groups[category].length} <Translate>guides in</Translate> {category}
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg> </svg>
</a> </a>
</div> </div>
)} )}
</section> </section>
))} ))}
</div> </div>
<!-- Back to top --> <!-- Back to top -->
<div class="mt-12 text-center"> <div class="mt-12 text-center">
<a <a
href="#top" href="#top"
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
> >
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/>
</svg> </svg>
<Translate>Back to top</Translate> <Translate>Back to top</Translate>
</a> </a>
</div> </div>
</section> </section>
</div> </div>
</main> </main>
</div> </div>
</BaseLayout> </BaseLayout>

View File

@ -50,7 +50,7 @@ const categories = Object.keys(howtosByCategory).sort()
<!-- All Howtos Grid --> <!-- All Howtos Grid -->
<div class="mb-12"> <div class="mb-12">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">All Guides</h2> <h2 class="text-2xl font-semibold text-gray-900 mb-6">All Guides</h2>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-2">
{all.map((item) => ( {all.map((item) => (
<HowtoCard <HowtoCard
url={`/${locale}/howtos/${item.id}`} url={`/${locale}/howtos/${item.id}`}
@ -84,7 +84,7 @@ const categories = Object.keys(howtosByCategory).sort()
</span> </span>
</div> </div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-2">
{howtosByCategory[category].slice(0, 8).map((howto: IHowto) => { {howtosByCategory[category].slice(0, 8).map((howto: IHowto) => {
const correspondingItem = all.find(item => item.data.item.slug === howto.slug); const correspondingItem = all.find(item => item.data.item.slug === howto.slug);
return correspondingItem ? ( return correspondingItem ? (

View File

@ -1,6 +1,5 @@
--- ---
import { foo, i18n as Translate } from "@polymech/astro-base" import Translate from "@polymech/astro-base/components/i18n.astro";
import test from "@/model/howto/howto.json"
--- ---
<Translate language="es">Hellau</Translate>
<Translate language="es">Hellau</Translate>