Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/.local_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.29.0
0.49.0
1 change: 1 addition & 0 deletions .beads/deletions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
{"id":"interfacer-gui-9lv.13","ts":"2025-12-11T13:40:28.675257Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"interfacer-gui-9lv.12","ts":"2025-12-11T13:40:28.681444Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"interfacer-gui-9lv.14","ts":"2025-12-11T13:40:28.687578Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
{"id":"interfacer-gui-yig.14","ts":"2026-02-06T15:05:42.2713Z","by":"git-history-backfill","reason":"recovered from git history (pruned from manifest)"}
9 changes: 9 additions & 0 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .beads/last-touched
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
interfacer-gui-zsv
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
name: 🐳 Docker image
on:
push:
branches: ["main", "dtec"]
branches: ["main", "dtec", "tchibo"]

env:
REGISTRY: ghcr.io
Expand All @@ -37,4 +37,4 @@ jobs:
uses: interfacerproject/workflows/.github/workflows/publish-ghcr.yml@main
secrets: inherit
with:
image_name: ${{ github.ref == 'refs/heads/dtec' && format('{0}-dtec', github.repository) || github.repository }}
image_name: ${{ github.ref == 'refs/heads/dtec' && format('{0}-dtec', github.repository) || github.ref == 'refs/heads/tchibo' && format('{0}-tchibo', github.repository) || github.repository }}
2 changes: 1 addition & 1 deletion components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const Footer = () => {
<Text as="h3" variant="headingLg">
{t("Projects")}
</Text>
<NextLink href="/projects">
<NextLink href="/products">
<a>
<Text as="p" color="subdued" variant="headingMd">
{t("All projects")}
Expand Down
15 changes: 10 additions & 5 deletions components/GeneralCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,18 @@ const LicenseFooter = () => {
const { project } = useCardProject();

// Check multiple possible license locations
const license = project.license || project.metadata?.license;
const license = project.license;
const metadataLicenses = project.metadata?.licenses;

// Don't render if no license available
if (!license) return null;

const licenseText = `${t("LICENSE")}: ${license}`;

if (!license && !metadataLicenses) return null;

const licenseText =
license == undefined
? `${t("LICENSE")}: ${license}`
: metadataLicenses
?.map((l: { scope: string; licenseId: string }) => `${t("LICENSE")} (${l.scope}): ${l.licenseId}`)
.join(", ");
return (
<div className="px-4 py-3 border-t-1 border-t-gray-200">
<Text as="p" variant="bodySm" fontWeight="medium">
Expand Down
50 changes: 48 additions & 2 deletions components/ProductsActiveFiltersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useQuery } from "@apollo/client";
import { Tag } from "@bbtgnn/polaris-interfacer";
import { useResourceSpecs } from "hooks/useResourceSpecs";
import { QUERY_MACHINES } from "lib/QueryAndMutation";
import { isPrefixedTag, prefixedTag, TAG_PREFIX } from "lib/tagging";
import { isPrefixedTag, prefixedTag, REPAIRABILITY_AVAILABLE_TAG, TAG_PREFIX } from "lib/tagging";
import { useTranslation } from "next-i18next";
import { useRouter } from "next/router";

Expand Down Expand Up @@ -99,6 +99,8 @@ export default function ProductsActiveFiltersBar() {
TAG_PREFIX.POWER_COMPAT,
TAG_PREFIX.POWER_REQ,
TAG_PREFIX.REPLICABILITY,
TAG_PREFIX.RECYCLABILITY,
TAG_PREFIX.REPAIRABILITY,
TAG_PREFIX.ENV_ENERGY,
TAG_PREFIX.ENV_CO2,
])
Expand Down Expand Up @@ -139,7 +141,10 @@ export default function ProductsActiveFiltersBar() {
Boolean(energyMax) ||
Boolean(co2Min) ||
Boolean(co2Max) ||
asCsvArray(router.query.replicability).length > 0;
asCsvArray(router.query.replicability).length > 0 ||
Boolean(asString(router.query.recyclabilityMin)) ||
Boolean(asString(router.query.recyclabilityMax)) ||
asString(router.query.repairability) === "true";

if (!hasAnyActive) return null;

Expand Down Expand Up @@ -411,6 +416,47 @@ export default function ProductsActiveFiltersBar() {
});
}

const recyclabilityMin = asString(router.query.recyclabilityMin);
const recyclabilityMax = asString(router.query.recyclabilityMax);

const recyclabilityLabel = (() => {
const min = recyclabilityMin ? `${recyclabilityMin}%` : "";
const max = recyclabilityMax ? `${recyclabilityMax}%` : "";
if (min && max) return `${min}–${max}`;
if (min) return `β‰₯${min}`;
if (max) return `≀${max}`;
return "";
})();

if (recyclabilityLabel) {
chips.push({
key: `recyclability:${recyclabilityMin}:${recyclabilityMax}`,
label: `${t("Recyclability")}: ${recyclabilityLabel}`,
onRemove: () => {
const next = { ...router.query };
delete next.recyclabilityMin;
delete next.recyclabilityMax;
const nextTags = removeTagsByPrefix(rawTags, TAG_PREFIX.RECYCLABILITY);
next.tags = nextTags.length > 0 ? nextTags.join(",") : undefined;
pushQuery(next);
},
});
}

if (asString(router.query.repairability) === "true") {
chips.push({
key: "repairability:true",
label: t("Available for repair"),
onRemove: () => {
const next = { ...router.query };
delete next.repairability;
const nextTags = rawTags.filter(tg => tg !== REPAIRABILITY_AVAILABLE_TAG);
next.tags = nextTags.length > 0 ? nextTags.join(",") : undefined;
pushQuery(next);
},
});
}

return (
<div className="mb-6 bg-white rounded-lg border border-[#C9CCCF] p-4">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
Expand Down
132 changes: 132 additions & 0 deletions components/ProductsFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import {
POWER_COMPATIBILITY_OPTIONS,
POWER_REQUIREMENT_THRESHOLDS_W,
prefixedTag,
RECYCLABILITY_THRESHOLDS_PCT,
rangeFilterTags,
REPAIRABILITY_AVAILABLE_TAG,
REPLICABILITY_OPTIONS,
TAG_PREFIX,
} from "lib/tagging";
Expand Down Expand Up @@ -63,6 +65,9 @@ export interface ProductsFiltersState {
powerRequirementMin: string;
powerRequirementMax: string;
replicability: string[];
recyclabilityMin: string;
recyclabilityMax: string;
repairability: boolean;
energyMin: string;
energyMax: string;
co2Min: string;
Expand Down Expand Up @@ -142,6 +147,8 @@ export default function ProductsFilters() {
powerCompatibility: false,
powerRequirement: false,
replicability: false,
recyclability: false,
repairability: false,
environmentalImpact: false,
});

Expand All @@ -156,6 +163,9 @@ export default function ProductsFilters() {
powerRequirementMin: "",
powerRequirementMax: "",
replicability: [],
recyclabilityMin: "",
recyclabilityMax: "",
repairability: false,
energyMin: "",
energyMax: "",
co2Min: "",
Expand All @@ -177,6 +187,8 @@ export default function ProductsFilters() {
TAG_PREFIX.POWER_COMPAT,
TAG_PREFIX.POWER_REQ,
TAG_PREFIX.REPLICABILITY,
TAG_PREFIX.RECYCLABILITY,
TAG_PREFIX.REPAIRABILITY,
TAG_PREFIX.ENV_ENERGY,
TAG_PREFIX.ENV_CO2,
])
Expand All @@ -201,6 +213,9 @@ export default function ProductsFilters() {
powerRequirementMin: (query.powerMin as string) || "",
powerRequirementMax: (query.powerMax as string) || "",
replicability: query.replicability ? (query.replicability as string).split(",") : [],
recyclabilityMin: (query.recyclabilityMin as string) || "",
recyclabilityMax: (query.recyclabilityMax as string) || "",
repairability: query.repairability === "true",
energyMin: (query.energyMin as string) || "",
energyMax: (query.energyMax as string) || "",
co2Min: (query.co2Min as string) || "",
Expand Down Expand Up @@ -255,6 +270,17 @@ export default function ProductsFilters() {
.map(value => prefixedTag(TAG_PREFIX.REPLICABILITY, value))
.filter((t): t is string => Boolean(t));

const recyclabilityMin = filters.recyclabilityMin ? Number(filters.recyclabilityMin) : undefined;
const recyclabilityMax = filters.recyclabilityMax ? Number(filters.recyclabilityMax) : undefined;
const recyclabilityTags = rangeFilterTags(
TAG_PREFIX.RECYCLABILITY,
recyclabilityMin,
recyclabilityMax,
RECYCLABILITY_THRESHOLDS_PCT
);

const repairabilityTags = filters.repairability ? [REPAIRABILITY_AVAILABLE_TAG] : [];

const powerMin = filters.powerRequirementMin ? Number(filters.powerRequirementMin) : undefined;
const powerMax = filters.powerRequirementMax ? Number(filters.powerRequirementMax) : undefined;
const powerReqTags = rangeFilterTags(TAG_PREFIX.POWER_REQ, powerMin, powerMax, POWER_REQUIREMENT_THRESHOLDS_W);
Expand All @@ -276,6 +302,8 @@ export default function ProductsFilters() {
materialTags,
powerCompatTags,
replicabilityTags,
recyclabilityTags,
repairabilityTags,
powerReqTags,
energyTags,
co2Tags
Expand All @@ -297,6 +325,9 @@ export default function ProductsFilters() {
if (filters.powerRequirementMin) query.powerMin = filters.powerRequirementMin;
if (filters.powerRequirementMax) query.powerMax = filters.powerRequirementMax;
if (filters.replicability.length > 0) query.replicability = filters.replicability.join(",");
if (filters.recyclabilityMin) query.recyclabilityMin = filters.recyclabilityMin;
if (filters.recyclabilityMax) query.recyclabilityMax = filters.recyclabilityMax;
if (filters.repairability) query.repairability = "true";
if (filters.energyMin) query.energyMin = filters.energyMin;
if (filters.energyMax) query.energyMax = filters.energyMax;
if (filters.co2Min) query.co2Min = filters.co2Min;
Expand All @@ -316,6 +347,9 @@ export default function ProductsFilters() {
powerRequirementMin: "",
powerRequirementMax: "",
replicability: [],
recyclabilityMin: "",
recyclabilityMax: "",
repairability: false,
energyMin: "",
energyMax: "",
co2Min: "",
Expand Down Expand Up @@ -686,6 +720,104 @@ export default function ProductsFilters() {
)}
</div>

{/* Recyclability (%) */}
<div className="border-b border-[#C9CCCF] pb-4">
<button
onClick={() => toggleSection("recyclability")}
className="flex items-center justify-between w-full text-left"
>
<span className="font-medium text-gray-900" style={{ fontFamily: "'IBM Plex Sans', sans-serif" }}>
{t("Recyclability (%)")}
</span>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${openSections.recyclability ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openSections.recyclability && (
<div className="mt-3">
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs text-gray-600 mb-1">{t("Min")}</label>
<div className="flex items-center gap-2">
<input
type="number"
value={filters.recyclabilityMin}
onChange={e => setFilters(prev => ({ ...prev, recyclabilityMin: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#036A53] focus:border-transparent text-sm"
placeholder="0"
min={0}
max={100}
/>
<span className="text-xs text-gray-600">{"%"}</span>
</div>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">{t("Max")}</label>
<div className="flex items-center gap-2">
<input
type="number"
value={filters.recyclabilityMax}
onChange={e => setFilters(prev => ({ ...prev, recyclabilityMax: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#036A53] focus:border-transparent text-sm"
placeholder="100"
min={0}
max={100}
/>
<span className="text-xs text-gray-600">{"%"}</span>
</div>
</div>
</div>
</div>
)}
</div>

{/* Repairability */}
<div className="border-b border-[#C9CCCF] pb-4">
<button
onClick={() => toggleSection("repairability")}
className="flex items-center justify-between w-full text-left"
>
<span className="font-medium text-gray-900" style={{ fontFamily: "'IBM Plex Sans', sans-serif" }}>
{t("Repairability")}
</span>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${openSections.repairability ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openSections.repairability && (
<div className="mt-3">
<label className="flex items-center gap-3 text-sm cursor-pointer">
<button
type="button"
role="switch"
aria-checked={filters.repairability}
onClick={() => setFilters(prev => ({ ...prev, repairability: !prev.repairability }))}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-[#036A53] focus:ring-offset-2 ${
filters.repairability ? "bg-[#036A53]" : "bg-gray-300"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
filters.repairability ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
<span className="text-sm text-gray-700">{t("Available for repair")}</span>
</label>
</div>
)}
</div>

{/* Environmental Impact */}
<div className="border-b border-[#C9CCCF] pb-4">
<button
Expand Down
2 changes: 1 addition & 1 deletion components/ProjectTypeChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function ProjectTypeChip(props: Props) {
const { project, projectType, introduction = false, link = true } = props;

const name = (project?.conformsTo?.name as ProjectType) || projectType || ProjectType.DESIGN;
const href = `/projects?conformsTo=${project?.conformsTo?.id}`;
const href = `/products?conformsTo=${project?.conformsTo?.id}`;

const renderProps = ProjectTypeRenderProps[name];

Expand Down
2 changes: 1 addition & 1 deletion components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function Sidebar() {
// Dropdown -> Projects
latestProjects: {
text: t("Projects"),
link: "/projects",
link: "/products",
},
resources: {
text: t("Import from LOSH"),
Expand Down
6 changes: 4 additions & 2 deletions components/brickroom/BrBreadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ type Crumb = {
const BrBreadcrumb = ({ crumbs }: { crumbs: Crumb[] }) => {
return (
<div className="text-primary breadcrumbs">
<ul>
<ul className="flex flex-wrap gap-1">
{crumbs.map((crumb, i) => {
return (
<li key={i}>
<Link href={crumb.href}>
<a>{crumb.name}</a>
<a>
{crumb.name} {i < crumbs.length - 1 ? "/" : ""}
</a>
</Link>
</li>
);
Expand Down
2 changes: 2 additions & 0 deletions components/brickroom/BrMdEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export default function BrMdEditor(props: BrMdEditorProps) {

<div className="pt-2">
<MdEditor
view={{ menu: true, md: true, html: false }}
plugins={["mode-toggle", "header", "font-bold", "font-italic", "font-underline"]}
className={editorClass}
renderHTML={text => MdParser.render(text)}
onChange={onChange}
Expand Down
2 changes: 1 addition & 1 deletion components/brickroom/BrTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function BrTag(props: { tag: string }) {
const classes = classNames("py-1 px-2", "bg-primary/5 hover:bg-primary/20", "border-1 border-primary/20 rounded-md");

return (
<Link href={`/projects?tags=${tag}`}>
<Link href={`/products?tags=${tag}`}>
<a key={tag} className={classes}>
<Text as="span" variant="bodyMd">
<span className="text-primary whitespace-nowrap">{decodeURI(tag)}</span>
Expand Down
Loading