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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"build:search": "tsx ./src/scripts/builders/search.ts",
"build:p5-version": "tsx ./src/scripts/p5-version.ts",
"custom:dev": "tsx ./src/scripts/branchTest.ts",
"custom:cleanup": "tsx ./src/scripts/resetBranchTest.ts"
"custom:cleanup": "tsx ./src/scripts/resetBranchTest.ts",
"sync:openprocessing": "tsx ./src/scripts/sync-openprocessing.ts"
},
"engines": {
"node": ">=22.0.0"
Expand Down
200 changes: 107 additions & 93 deletions src/layouts/ExampleLayout.astro
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
---
import type { CollectionEntry } from "astro:content";
import { render } from "astro:content";

import EditableSketch from "@components/EditableSketch/index.astro";
import Head from "@components/Head/index.astro";
import RelatedItems from "@components/RelatedItems/index.astro";

import { getCurrentLocale, getUiTranslator } from "../i18n/utils";
import {
generateJumpToState,
getFallbackRemixData,
getRelatedEntriesinCollection,
} from "../pages/_utils";
import BaseLayout from "./BaseLayout.astro";
import EditableSketch from "@components/EditableSketch/index.astro";
import RelatedItems from "@components/RelatedItems/index.astro";
import {
EXAMPLE_ATTRIBUTION,
getExampleAttributionData,
} from "../utils/exampleAttribution";

import { getFallbackRemixData } from "../pages/_utils";
import BaseLayout from "./BaseLayout.astro";

interface Props {
example: CollectionEntry<"examples">;
Expand Down Expand Up @@ -43,29 +49,17 @@ const relatedReferences =

const { Content } = await render(example);

// Use the fallback function to retrieve English remix data if the current locale doesn't have any
let remixData = (await getFallbackRemixData(
// Use the fallback function to retrieve English remix data if the current
// locale doesn't have any.
const remixData = (await getFallbackRemixData(
example.id,
currentLocale,
example.data.remix
)) as typeof example.data.remix;

// Extract the collective attribution year. If multiple provided, uses last shown.
const collectivelyAttributedSince = remixData?.reduce(
(acc: number | null, item) => {
if (item.collectivelyAttributedSince) {
return item.collectivelyAttributedSince;
}
return acc;
},
null
);

// Boolean value on whether the remix history contains links to code
const remixHistoryHasCodeLinks = remixData?.some(
(item) => Array.isArray(item.code) && item.code.length > 0
);

// Shared attribution rules used by both the website layout and
// OpenProcessing synchronization.
const attributionData = getExampleAttributionData(remixData ?? []);
---

<Head
Expand All @@ -88,88 +82,107 @@ const remixHistoryHasCodeLinks = remixData?.some(
<div class="rendered-markdown">
<Content />
</div>

<div class="rendered-markdown">
<img src="/images/by-nc-sa.svg" alt="Creative Commons BY-NC-SA license badge" />
<img
src="/images/by-nc-sa.svg"
alt="Creative Commons BY-NC-SA license badge"
/>

<p>
<a href={Astro.url.pathname}>{example.data.title}</a>:{" "}

{
attributionData.remixes.map((item, i) => (
<span>
{i > 0 && " "}
{t("attribution", item.description)}{" "}
{item.attribution.map((author, j) => (
<>
{author.URL ? (
<a href={author.URL}>{t(author.name)}</a>
) : (
t(author.name)
)}
{j < item.attribution.length - 1 ? ", " : "."}
</>
))}
</span>
))
}

<a href={Astro.url.pathname}>{example.data.title}</a>:{" "}

{remixData?.map((item, i) => {
const parts = [];

// Each remix entry requires at least one attribution
// If a remix entry contains a collective attribution starting year, it is ignored here
{
attributionData.collectivelyAttributedSince ? (
<>
{t(
"attribution",
`From ${attributionData.collectivelyAttributedSince} onwards, edited and maintained by`
)}{" "}
</>
) : (
<>
{t("attribution", "Edited and maintained by")}{" "}
</>
)
}

if (!item.collectivelyAttributedSince && item.attribution) {
parts.push(<>{t("attribution", item.description)}</>);
<span>
<a href={EXAMPLE_ATTRIBUTION.contributors.URL}>
{EXAMPLE_ATTRIBUTION.contributors.name}
</a>{" "}
{t("attribution", "and")}{" "}
<a href={EXAMPLE_ATTRIBUTION.foundation.URL}>
{EXAMPLE_ATTRIBUTION.foundation.name}
</a>.
{" "}Licensed under{" "}
<a href={EXAMPLE_ATTRIBUTION.license.URL}>
{EXAMPLE_ATTRIBUTION.license.name}
</a>.
</span>
</p>

if (item.attribution?.length) {
parts.push(
<>
{" "}
{item.attribution.map((a, j) => (
<p>
{
attributionData.codeLinks.length > 0 ? (
<>
{t(
"attribution",
"You can find the code history of these examples here"
)}
{": "}

{attributionData.codeLinks.map(
(codeItem, i, codeItemsList) => (
<>
{a.URL ? <a href={a.URL}>{t(a.name)}</a> : t(a.name)}
{
item.attribution?.length
? j < item.attribution.length - 1 ? ", " : "."
: ""
}
<a href={codeItem.URL}>{codeItem.label}</a>
{i < codeItemsList.length - 1 ? ", " : ". "}
</>
))}
</>
);
}
)
)}

{t(
"attribution",
"You can suggest improvements by"
)}{" "}
<a href="https://github.com/processing/p5.js-website">
{t(
"attribution",
"contributing to the current website"
)}
</a>
!
</>
) : (
<></>
)
}
</p>
</div>

return <span>{i > 0 && " "}{parts}</span>;
})}

{collectivelyAttributedSince ? (
<>{t("attribution", `From ${collectivelyAttributedSince} onwards, edited and maintained by`)}{" "}</>
) : (
<>{t("attribution", "Edited and maintained by")}{" "}</>
)}
<span>
<a href="https://github.com/processing/p5.js?tab=readme-ov-file#contributors">p5.js Contributors</a>{" "}
{t("attribution", "and")}{" "}
<a href="https://processingfoundation.org/people">Processing Foundation</a>.
Licensed under{" "}
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.
</span>
</p>

<p>
{remixHistoryHasCodeLinks ? (
<>
{t("attribution", "You can find the code history of these examples here")}{": "}
{remixData
.map(item => item?.code)
.flat()
.filter(codeItem => codeItem && codeItem.URL)
.map((codeItem, i, codeItemsList) => (
<>
<a href={codeItem?.URL}>{codeItem?.label}</a>{
i < (codeItemsList?.length ?? 0) - 1 ? ", " : ". "
}
</>
))}
{t("attribution", "You can suggest improvements by")}{" "}
<a href="https://github.com/processing/p5.js-website">{t("attribution", "contributing to the current website")}</a>!
</>
) : (
<></>
)}
</p>


<EditableSketch code={code} />
<p>{example.data.arialabel}</p>
</div>

<EditableSketch code={code} />
<p>{example.data.arialabel}</p>

</div>
<div class="grid gap-y-4xl lg:gap-y-mb-3xl mt-2xl">
{
relatedReferences.length > 0 ? (
Expand All @@ -179,9 +192,10 @@ const remixHistoryHasCodeLinks = remixData?.some(
/>
) : null
}

<RelatedItems
title={t("Related Examples") as string}
items={relatedExamples}
/>
</div>
</BaseLayout>
</BaseLayout>
121 changes: 121 additions & 0 deletions src/scripts/openprocessing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const OPENPROCESSING_BASE_URL = "https://openprocessing.org/api";

export interface OpenProcessingSketch {
visualID: number;
title?: string;
[key: string]: unknown;
}

export interface OpenProcessingCuration {
visualID?: number;
id?: number;
sketches?: OpenProcessingSketch[];
[key: string]: unknown;
}

interface OpenProcessingConfig {
token: string;
curationId: string;
}

function getConfig(): OpenProcessingConfig {
const token = process.env.OPENPROCESSING_TOKEN;
const curationId = process.env.OPENPROCESSING_CURATION_ID;

if (!token) {
throw new Error(
"Missing OPENPROCESSING_TOKEN environment variable."
);
}

if (!curationId) {
throw new Error(
"Missing OPENPROCESSING_CURATION_ID environment variable."
);
}

return {
token,
curationId,
};
}

async function request<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const { token } = getConfig();

const response = await fetch(`${OPENPROCESSING_BASE_URL}${path}`, {
...options,
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
...options.headers,
},
});

if (!response.ok) {
const body = await response.text();

throw new Error(
`OpenProcessing request failed: ${response.status} ${response.statusText}\n${body}`
);
}

if (response.status === 204) {
return undefined as T;
}

const text = await response.text();

if (!text) {
return undefined as T;
}

return JSON.parse(text) as T;
}

export async function getCuration(): Promise<OpenProcessingCuration> {
const { curationId } = getConfig();

return request<OpenProcessingCuration>(
`/curation/${curationId}`
);
}

export async function getCurationSketches(): Promise<
OpenProcessingSketch[]
> {
const { curationId } = getConfig();

return request<OpenProcessingSketch[]>(
`/curation/${curationId}/sketches`
);
}

export async function addSketchToCuration(
visualID: number
): Promise<void> {
const { curationId } = getConfig();

await request<void>(
`/curation/${curationId}/sketches/${visualID}`,
{
method: "POST",
}
);
}

export async function removeSketchFromCuration(
visualID: number
): Promise<void> {
const { curationId } = getConfig();

await request<void>(
`/curation/${curationId}/sketches/${visualID}`,
{
method: "DELETE",
}
);
}
Loading