generated from polymech/site-template
howtos store
This commit is contained in:
parent
97eb1c5e8a
commit
68376435d8
18
.astro/collections/howtos.schema.json
Normal file
18
.astro/collections/howtos.schema.json
Normal 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#"
|
||||
}
|
||||
@ -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
8
.astro/content.d.ts
vendored
@ -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
@ -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`
|
||||
|
||||
/////////////////////////////////////////////
|
||||
|
||||
@ -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
|
||||
@ -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
120
src/layouts/Howto.astro
Normal 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>
|
||||
@ -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, {
|
||||
|
||||
@ -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[] = [];
|
||||
|
||||
32
src/pages/[locale]/howtos/[...path].astro
Normal file
32
src/pages/[locale]/howtos/[...path].astro
Normal 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}/>
|
||||
@ -17,6 +17,7 @@
|
||||
"baseUrl": ".",
|
||||
"lib": ["DOM", "ES2015"],
|
||||
"resolveJsonModule": true,
|
||||
"inlineSourceMap": true,
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"site/*": ["src/*"],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user