howtos store

This commit is contained in:
lovebird 2025-03-20 17:04:02 +01:00
parent 97eb1c5e8a
commit 68376435d8
12 changed files with 315 additions and 95 deletions

View File

@ -0,0 +1,18 @@
{
"$ref": "#/definitions/howtos",
"definitions": {
"howtos": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"$schema": {
"type": "string"
}
},
"additionalProperties": true
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-03-20T07:39:49.013Z"
"default": "2025-03-20T16:02:01.103Z"
},
"description": {
"type": "string",

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

@ -168,6 +168,14 @@ declare module 'astro:content' {
rendered?: RenderedContent;
filePath?: string;
}>;
"howtos": Record<string, {
id: string;
body?: string;
collection: "howtos";
data: InferEntrySchema<"howtos">;
rendered?: RenderedContent;
filePath?: string;
}>;
"infopages": Record<string, {
id: string;
render(): Render[".md"];

File diff suppressed because one or more lines are too long

View File

@ -18,6 +18,10 @@ export const I18N_SOURCE_LANGUAGE = 'en'
export const I18N_CACHE = true
export const I18N_ASSET_PATH = "${SRC_DIR}/${SRC_NAME}-${DST_LANG}${SRC_EXT}"
// Products
export const HOWTO_MIGRATION = () => path.resolve(resolve("./data/last.json"))
// Products
export const PRODUCT_ROOT = () => path.resolve(resolve("${OSR_ROOT}/products"))
export const PRODUCT_BRANCHES = read(path.join(PRODUCT_ROOT(), 'config/machines.json'), 'json')
@ -146,7 +150,6 @@ export const default_image = () => {
}
export const DEFAULT_LICENSE = `CERN Open Source Hardware License`
export const DEFAULT_CONTACT = `sales@plastic-hub.com`
/////////////////////////////////////////////

View File

@ -1,7 +0,0 @@
git checkout --orphan latest_branch
git add -A
git commit -am "init v0.1.3"
git branch -D master
git branch -m master
git push -f origin master
git gc --aggressive --prune=all

View File

@ -1,6 +1,9 @@
import { defineCollection, z } from "astro:content"
import { loader } from './model/component.js'
import { ComponentConfigSchema } from '@polymech/commons/component'
import { loader as howtoLoader } from './model/howto.js'
import { RETAIL_PRODUCT_BRANCH, PROJECTS_BRANCH } from 'config/config.js'
import { glob } from 'astro/loaders'
@ -13,7 +16,6 @@ const projects = defineCollection({
loader: loader(PROJECTS_BRANCH) as any,
schema: ComponentConfigSchema.passthrough(),
})
const helpcenter = defineCollection({
schema: z.object({
title: z.string(),
@ -26,7 +28,12 @@ const infopages = defineCollection({
intro: z.string().optional(),
}).passthrough(),
})
const howtos = defineCollection({
loader: howtoLoader(),
schema: z.object({
title: z.string().optional()
}).passthrough()
})
const resources = defineCollection({
loader: glob({ base: './src/content/resources', pattern: '*.{md,mdx}' }),
schema: z.object({
@ -47,5 +54,6 @@ export const collections = {
projects,
resources,
helpcenter,
infopages
infopages,
howtos
};

120
src/layouts/Howto.astro Normal file
View File

@ -0,0 +1,120 @@
---
import { IHowto } from "@/model/howto";
import { Gallery } from "@polymech/astro-base";
import { Img } from 'imagetools/components';
import { i18n as Translate } from "@polymech/astro-base";
interface Props {
howto: IHowto;
}
const { howto } = Astro.props;
debugger
import BaseLayout from "@/layouts/BaseLayout.astro";
import Wrapper from "@/components/containers/Wrapper.astro";
const _url = Astro.url
const canonicalUrl = _url.origin
function getProxyUrl(imageUrl: string): string {
const ret = `${canonicalUrl}/api/image-proxy?url=${encodeURIComponent(imageUrl)}`;
return ret
}
---
<BaseLayout class="markdown-content">
<Wrapper>
<div class="howto-container max-w-4xl mx-auto p-4">
<!-- Header section -->
<header class="mb-8">
<h1 class="text-3xl font-bold mb-2">
<Translate>{howto.title}</Translate>
</h1>
<!-- Cover image -->
<div class="mb-4">
<Img
src={(getProxyUrl(howto.cover_image.downloadUrl))}
alt={"none"}
attributes={{
img: { class: "w-full h-64 object-cover rounded-lg" }
}}
/>
</div>
<!-- Metadata -->
<div class="flex flex-wrap gap-4 mb-4 text-sm text-gray-600">
<div>
<span class="font-semibold">Difficulty:</span>
<Translate>{howto.difficulty_level}</Translate>
</div>
<div>
<span class="font-semibold">Time:</span>
<Translate>{howto.time}</Translate>
</div>
<div>
<span class="font-semibold">Views:</span> {howto.total_views}
</div>
<div>
<span class="font-semibold">Created by:</span> {howto._createdBy}
</div>
<div>
<span class="font-semibold">Country:</span> {howto.creatorCountry}
</div>
</div>
<!-- Description -->
<div class="bg-gray-50 p-4 rounded-lg">
<p class="whitespace-pre-line">
<Translate>{howto.description}</Translate>
</p>
</div>
</header>
<!-- Steps -->
<div class="steps-container space-y-12">
{howto.steps.map((step, index) => (
<div class="step-item" id={`step-${index + 1}`}>
<h2 class="text-2xl font-semibold mb-4">
<span class="inline-block bg-blue-500 text-white w-8 h-8 rounded-full text-center leading-8 mr-2">
{index + 1}
</span>
<Translate>{step.title}</Translate>
</h2>
<!-- Step content -->
<div class="step-content mb-6">
<p class="whitespace-pre-line mb-4">
<Translate>{step.text}</Translate>
</p>
</div>
{step.images && step.images.length > 0 && (
<div class="step-images">
{step.images.map(img => (
<img src={img.downloadUrl} alt="alt" />
))
}
</div>
)}
</div>
))}
</div>
<!-- Footer information -->
<footer class="mt-12 pt-6 border-t border-gray-200">
<div class="flex justify-between items-center">
<div>
<span class="text-sm text-gray-500">
<Translate>Created</Translate>: {new Date(howto._created).toLocaleDateString()}
</span>
</div>
<div class="text-sm text-gray-500">
<span>
<Translate>Found useful by</Translate>: {howto.votedUsefulBy.length} <Translate>people</Translate>
</span>
</div>
</div>
</footer>
</div>
</Wrapper>
</BaseLayout>

View File

@ -2,11 +2,12 @@ import * as path from 'path'
import { findUp } from 'find-up'
import { sync as read } from '@polymech/fs/read'
import { sync as exists } from '@polymech/fs/exists'
import { filesEx, forward_slash, resolveConfig } from '@polymech/commons'
import { filesEx, forward_slash, resolveConfig, resolve } from '@polymech/commons'
import { ICADNodeSchema, IComponentConfig } from '@polymech/commons/component'
import { RenderedContent, DataEntry } from "astro:content"
import type { Loader, LoaderContext } from 'astro/loaders'
import { get } from '@polymech/commons/component'
import {
CAD_MAIN_MATCH, PRODUCT_BRANCHES,
CAD_EXTENSIONS, CAD_MODEL_EXT, PRODUCT_DIR, PRODUCT_GLOB,
@ -30,84 +31,124 @@ import { logger as log } from '@/base/index.js'
import { translate } from "@/base/i18n.js"
import { I18N_SOURCE_LANGUAGE } from "config/config.js"
import { slugify } from "@/base/strings.js"
import { HOWTO_MIGRATION } from '@/app/config.js'
export const ITEM_TYPE = 'howto'
export const items = (branch: string) => get(`${HOWTO_ROOT()}/${HOWTO_GLOB}`, HOWTO_ROOT(), ITEM_TYPE)
export const defaults = async (data: any, cwd: string, root:string): Promise<IComponentConfigEx> => {
let defaultsJSON = await findUp('defaults.json', {
stopAt: root,
cwd: cwd
});
try {
if (defaultsJSON) {
data = {
...read(defaultsJSON, 'json') as any,
...data,
};
//export const load = () => get(`${HOWTO_ROOT()}/${HOWTO_GLOB}`, HOWTO_ROOT(), ITEM_TYPE)
export const howtos = async () => {
const src = HOWTO_MIGRATION()
//const kb_out = path.resolve(substitute("${KB_ROOT}", DEFAULT_ROOTS));
const data = read(src, 'json') as any;
let howtos = data.v3_howtos as any[]
howtos = howtos.filter((h) => h.moderation == 'accepted');
const tags = data.v3_tags;
let output = {
tags: {},
howtows: {}
}
howtos.forEach((howto) => {
const howtoTags: any = [];
for (const ht in howto.tags) {
const gt: any = tags.find((t) => t._id === ht) || { label: 'untagged' };
if (gt) {
howtoTags.push(gt.label || "");
if (!output.tags[gt.label]) {
output.tags[gt.label] = []
}
} catch (error) {
output.tags[gt.label].push(howto.slug.trim());
}
}
return data;
howto.user = data.v3_mappins.find((u) => u._id == howto._createdBy);
howto.tags = howtoTags;
/*
howto.image = `/howtos/${howto.slug}/${encodeURIComponent(sanitize(howto.cover_image.name))}`,
howto.cover_image.name = sanitize(howto.cover_image.name);
*/
//output.howtows[howto.slug.trim()] = howto;
});
return howtos
}
export const defaults = async (data: any, cwd: string, root: string) => {
let defaultsJSON = await findUp('defaults.json', {
stopAt: root,
cwd: cwd
});
try {
if (defaultsJSON) {
data = {
...read(defaultsJSON, 'json') as any,
...data,
};
}
} catch (error) {
}
return data;
};
const onItem = async (item: any, ctx: LoaderContext) => {
return item
if (!item || !item.data) {
ctx.logger.error(`Error completing ${''}: no data`);
return
}
let data: any = item.data
const itemRel = data.rel
const itemRelMin = itemRel.replace('products/', '')
const itemDir = PRODUCT_DIR(itemRel)
const default_profile = env(itemRel)
data.product_rel = itemRelMin
data.assets = {
renderings: [],
gallery: []
}
data.body = (read(path.join(itemDir, 'templates/shared', 'body.md')) as string) || ""
data.resources = (read(path.join(itemDir, 'templates/shared', 'resources.md')) as string) || ""
//////////////////////////////////////////
//
// Item Shared Resources
//
let resourcesDefaultPath = await findUp('resources.md', {
stopAt: PRODUCT_ROOT(),
cwd: itemDir
}) || ""
data.shared = (read(resourcesDefaultPath) as string) || ""
//////////////////////////////////////////
//
// Readme
//
let readmePath = path.join(itemDir, 'Readme.md')
data.readme = (read(readmePath) as string) || ""
//////////////////////////////////////////
//
// Variables
//
data = await defaults(data, itemDir, PRODUCT_ROOT());
data = {
...data,
...default_profile.variables,
product_rel_min: itemRelMin,
}
// data = resolveConfig(data as Record<string, string>) as IComponentConfigEx
item.data = data
//////////////////////////////////////////
//
// Extensions, CAD, Media, etcetera etcetera :)
//
//data.cad = await cad(item)
data.assets.gallery = await gallery('media/gallery', data.rel)
if (!item || !item.data) {
ctx.logger.error(`Error completing ${''}: no data`);
return
}
let data: any = item.data
const itemRel = data.rel
const itemRelMin = itemRel.replace('products/', '')
const itemDir = PRODUCT_DIR(itemRel)
const default_profile = env(itemRel)
data.product_rel = itemRelMin
data.assets = {
renderings: [],
gallery: []
}
data.body = (read(path.join(itemDir, 'templates/shared', 'body.md')) as string) || ""
data.resources = (read(path.join(itemDir, 'templates/shared', 'resources.md')) as string) || ""
//////////////////////////////////////////
//
// Item Shared Resources
//
let resourcesDefaultPath = await findUp('resources.md', {
stopAt: PRODUCT_ROOT(),
cwd: itemDir
}) || ""
data.shared = (read(resourcesDefaultPath) as string) || ""
//////////////////////////////////////////
//
// Readme
//
let readmePath = path.join(itemDir, 'Readme.md')
data.readme = (read(readmePath) as string) || ""
//////////////////////////////////////////
//
// Variables
//
data = await defaults(data, itemDir, PRODUCT_ROOT());
data = {
...data,
...default_profile.variables,
product_rel_min: itemRelMin,
}
// data = resolveConfig(data as Record<string, string>) as IComponentConfigEx
item.data = data
//////////////////////////////////////////
//
// Extensions, CAD, Media, etcetera etcetera :)
//
//data.cad = await cad(item)
data.assets.gallery = await gallery('media/gallery', data.rel)
}
export function loader(branch: string): Loader {
export function loader(): Loader {
const load = async ({
config,
logger,
@ -116,27 +157,23 @@ export function loader(branch: string): Loader {
store,
generateDigest }: LoaderContext) => {
store.clear();
let products = items(branch)
for (const item of products) {
const product: any = item.config
const id = product.slug;
store.clear()
let items = await howtos()
for (const item of items) {
const id = item.slug
const data = {
rel: item.rel,
slug: id,
slug: item.slug,
id,
title: product.name,
title: item.title,
type: ITEM_TYPE,
highlights: [],
components: [],
...product,
item
}
//const parsedData = await parseData({ id, data: data });
const storeItem = {
digest: await generateDigest(data),
filePath: id,
assetImports: [],
id: `${item.rel}`,
id: `${item.slug}`,
data: data
}
await onItem(storeItem, {

View File

@ -9,10 +9,10 @@ import StoreEntries from "@/components/store/StoreEntries.astro";
import CtaOne from "@/components/cta/CtaOne.astro";
import { group_by_path } from "@/model/component.js";
const view = "store";
const items = await getCollection(view);
const view = "store"
const items = await getCollection(view)
const locale = Astro.currentLocale;
const store = `/${locale}/${view}/`;
const store = `/${locale}/${view}/`
export function getStaticPaths() {
const all: unknown[] = [];

View File

@ -0,0 +1,32 @@
---
import Layout from '@/layouts/Howto.astro'
import { getCollection } from 'astro:content'
import { LANGUAGES_PROD as LANGUAGES } from "config/config.js"
export async function getStaticPaths()
{
const view = 'howtos'
const items = await getCollection(view)
const all: unknown[] = []
LANGUAGES.forEach((lang) => {
items.forEach((product) => {
all.push({
params: {
locale: lang,
path: product.id,
},
props: {
page: product,
locale: lang,
path: product.id,
view
}
})
})
})
return all
}
const { page:page, ...rest } = Astro.props
---
<Layout howto={page.data.item} {...rest}/>

View File

@ -17,6 +17,7 @@
"baseUrl": ".",
"lib": ["DOM", "ES2015"],
"resolveJsonModule": true,
"inlineSourceMap": true,
"paths": {
"@/*": ["src/*"],
"site/*": ["src/*"],