diff --git a/package-lock.json b/package-lock.json index 1b89de10..1fa36dc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "deepslate": "^0.9.0-beta.6", "deepslate-rs": "^0.1.6", "howler": "^2.2.3", + "marked": "^3.0.8", "rfdc": "^1.3.0" }, "devDependencies": { @@ -25,6 +26,7 @@ "@rollup/plugin-html": "^0.2.3", "@types/google.analytics": "0.0.40", "@types/howler": "^2.2.4", + "@types/marked": "^3.0.2", "@types/seedrandom": "^2.4.28", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", @@ -514,6 +516,12 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "node_modules/@types/marked": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-3.0.2.tgz", + "integrity": "sha512-mGYI9qFs+i5eYaytWKBbtEMbIkrXGKuhsDpDcj70ogKS2gk1NmgEy9Z3VEKz922Lfms6eITXXqv5idlX7C/irw==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -1920,6 +1928,17 @@ "node": ">=10" } }, + "node_modules/marked": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz", + "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -3128,6 +3147,12 @@ "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", "dev": true }, + "@types/marked": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-3.0.2.tgz", + "integrity": "sha512-mGYI9qFs+i5eYaytWKBbtEMbIkrXGKuhsDpDcj70ogKS2gk1NmgEy9Z3VEKz922Lfms6eITXXqv5idlX7C/irw==", + "dev": true + }, "@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -4170,6 +4195,11 @@ "yallist": "^4.0.0" } }, + "marked": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz", + "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==" + }, "md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", diff --git a/package.json b/package.json index 632b50b4..a4cecc38 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "deepslate": "^0.9.0-beta.6", "deepslate-rs": "^0.1.6", "howler": "^2.2.3", + "marked": "^3.0.8", "rfdc": "^1.3.0" }, "devDependencies": { @@ -31,6 +32,7 @@ "@rollup/plugin-html": "^0.2.3", "@types/google.analytics": "0.0.40", "@types/howler": "^2.2.4", + "@types/marked": "^3.0.2", "@types/seedrandom": "^2.4.28", "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", diff --git a/src/app/Analytics.ts b/src/app/Analytics.ts index b4496dd6..a55dedbb 100644 --- a/src/app/Analytics.ts +++ b/src/app/Analytics.ts @@ -43,7 +43,6 @@ export namespace Analytics { export function setGenerator(generator: string) { dimension(DIM_GENERATOR, generator) - console.log(generator) } export function generatorEvent(action: string, label?: string) { diff --git a/src/app/Main.tsx b/src/app/Main.tsx index 355433ae..c3cb34d3 100644 --- a/src/app/Main.tsx +++ b/src/app/Main.tsx @@ -8,7 +8,7 @@ import '../styles/nodes.css' import { Analytics } from './Analytics' import { Header } from './components' import { loadLocale, locale, Locales } from './Locales' -import { Generator, Home, Sounds, Worldgen } from './pages' +import { Changelog, Generator, Home, Sounds, Worldgen } from './pages' import type { VersionId } from './Schemas' import { Store } from './Store' import { cleanUrl } from './Utils' @@ -72,6 +72,7 @@ function Main() { + diff --git a/src/app/pages/Changelog.tsx b/src/app/pages/Changelog.tsx new file mode 100644 index 00000000..aec33bda --- /dev/null +++ b/src/app/pages/Changelog.tsx @@ -0,0 +1,87 @@ +import marked from 'marked' +import { useEffect, useMemo, useState } from 'preact/hooks' +import { Ad, ErrorPanel, TextInput } from '../components' +import { locale } from '../Locales' +import type { VersionId } from '../Schemas' +import type { ChangelogEntry } from '../services/Changelogs' +import { getChangelogs } from '../services/Changelogs' +import { hashString } from '../Utils' + +type ChangelogProps = { + path?: string, + lang: string, + changeTitle: (title: string, versions?: VersionId[]) => unknown, +} +export function Changelog({ lang, changeTitle }: ChangelogProps) { + const loc = locale.bind(null, lang) + const [error, setError] = useState(null) + changeTitle(loc('title.changelog')) + + const [changelogs, setChangelogs] = useState([]) + useEffect(() => { + getChangelogs() + .then(changelogs => setChangelogs(changelogs)) + .catch(e => { console.error(e); setError(e) }) + }, []) + + const [search, setSearch] = useState('') + const [tags, setTags] = useState([]) + const addTag = (tag: string) => { + if (!tags.includes(tag)) { + setTags([...tags, tag]) + } + } + + const filteredChangelogs = useMemo(() => { + const query = search.split(' ').map(q => q.trim().toLowerCase()).filter(q => q.length > 0) + if (query.length === 0 && tags.length === 0) return changelogs + return changelogs.filter(change => { + if (!tags.every(tag => change.tags.includes(tag))) { + return false + } + const content = change.tags.join(' ') + ' ' + change.content.toLowerCase() + return query.every(q => content.includes(q)) + }) + }, [changelogs, search, tags]) + + return
+ + {error && setError(null)} />} +
+ + {tags.length > 0 &&
+ {tags.map(tag => setTags(tags.filter(t => t !== tag))} />)} +
} +
+
+ {filteredChangelogs.map(change => + )} +
+
+} + +type ChangeProps = { + change: ChangelogEntry, + activeTags: string[], + addTag: (tag: string) => unknown, +} +function Change({ change, activeTags, addTag }: ChangeProps) { + return
+
+ {change.tags.map(tag => addTag(tag)} active={activeTags.includes(tag)} />)} + {change.version} +
+
+
+} + +type TagProps = { + label: string, + active?: boolean, + onClick?: () => unknown, +} +function Tag({ label, active, onClick }: TagProps) { + const color = hashString(label) % 360 + return
{label}
+} diff --git a/src/app/pages/Home.tsx b/src/app/pages/Home.tsx index 478e1fab..f02ef773 100644 --- a/src/app/pages/Home.tsx +++ b/src/app/pages/Home.tsx @@ -27,6 +27,7 @@ export function Home({ lang, changeTitle }: HomeProps) {

Convert your 1.16 data packs to 1.17

+
} diff --git a/src/app/pages/index.ts b/src/app/pages/index.ts index 2d435cf4..41d98aeb 100644 --- a/src/app/pages/index.ts +++ b/src/app/pages/index.ts @@ -1,3 +1,4 @@ +export * from './Changelog' export * from './Generator' export * from './Home' export * from './Sounds' diff --git a/src/app/services/Changelogs.ts b/src/app/services/Changelogs.ts new file mode 100644 index 00000000..01456747 --- /dev/null +++ b/src/app/services/Changelogs.ts @@ -0,0 +1,47 @@ +const repo = 'https://raw.githubusercontent.com/misode/technical-changes/main' + +export type ChangelogEntry = { + group: string, + version: string, + tags: string[], + content: string, +} + +let Changelogs: ChangelogEntry[] | Promise | null = null + +export async function getChangelogs() { + if (!Changelogs) { + const index = await (await fetch(`${repo}/index.json`)).json() as string[] + Changelogs = (await Promise.all( + index.map(group => fetchGroup(group)) + )).flat() + } + return Changelogs +} + +async function fetchGroup(group: string) { + const index = await (await fetch(`${repo}/${group}/index.json`)).json() as string[] + return (await Promise.all( + index.map(version => fetchChangelog(group, version)) + )).flat() +} + +async function fetchChangelog(group: string, version: string) { + const text = await (await fetch(`${repo}/${group}/${version}.md`)).text() + return parseChangelog(text).map(change => ({ + version, + group, + ...change, + })) +} + +function parseChangelog(text: string) { + return text.split('\n\n') + .map(entry => { + const i = entry.indexOf('|') + return { + tags: entry.substring(0, i).trim().split(' '), + content: entry.slice(i + 1).trim(), + } + }) +} diff --git a/src/locales/en.json b/src/locales/en.json index 45232b7e..c698ad85 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -3,6 +3,7 @@ "add_bottom": "Add to bottom", "add_top": "Add to top", "advancement": "Advancement", + "changelog.search": "Search changes", "collapse": "Collapse", "collapse_all": "Hold %0% to collapse all", "configure_layers": "Configure layers", @@ -63,6 +64,7 @@ "theme.dark": "Dark", "theme.light": "Light", "theme.system": "System", + "title.changelog": "Technical Changelog", "title.generator": "%0% Generator", "title.generator_category": "%0% Generators", "title.home": "Data Pack Generators", diff --git a/src/styles/global.css b/src/styles/global.css index ab57c809..3e951e23 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -22,6 +22,8 @@ --errors-background: #62190f; --errors-text: #ffffffcc; --invalid-text: #fd7951; + --text-saturation: 60%; + --text-lightness: 45%; } :root[data-theme=light] { @@ -48,6 +50,8 @@ --errors-background: #f66653; --errors-text: #000000cc; --invalid-text: #a32600; + --text-saturation: 100%; + --text-lightness: 30%; } @media (prefers-color-scheme: light) { @@ -75,6 +79,8 @@ --errors-background: #f66653; --errors-text: #000000cc; --invalid-text: #a32600; + --text-saturation: 100%; + --text-lightness: 35%; } } @@ -379,11 +385,18 @@ main.has-preview { display: flex; flex-direction: column; position: absolute; - right: 0; top: 100%; margin-top: 8px; } +.btn-menu.menu-sw .btn-group { + right: 0; +} + +.btn-menu.menu-se .btn-group { + left: 0; +} + .btn-group { border-radius: 6px; box-shadow: 0 0 7px -2px #000; @@ -975,6 +988,83 @@ hr { color: var(--invalid-text); } +.changelog { + display: flex; + flex-direction: column; + padding: 16px; +} + +.changelog-entry { + background: var(--background-2); + border-radius: 4px; + margin-bottom: 8px; + padding: 8px; + color: var(--text-2); +} + +.changelog-tags { + display: flex; + margin-bottom: 8px; +} + +.changelog-tag { + --color: hsl(var(--tint, 0), var(--text-saturation), var(--text-lightness)); + margin-right: 8px; + border: 1.5px solid var(--color); + height: 24px; + border-radius: 12px; + padding: 0 8px; + color: var(--color); + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.changelog-tag.clickable { + cursor: pointer; +} + +.changelog-tag.active { + background-color: var(--color); + color: var(--background-2); +} + +.changelog-version { + margin-left: auto; + font-size: 15px; + color: var(--text-3); + text-decoration: none; +} + +.changelog-version:hover { + text-decoration: underline; +} + +.changelog-entry code { + background-color: var(--background-5); + padding: 1px 4px; + border-radius: 4px; + color: var(--text-1); +} + +.changelog-controls { + display: flex; + flex-direction: column; + padding: 0 16px; +} + +.changelog-search { + flex-basis: 100%; + padding: 8px; + background-color: var(--background-2); + border-radius: 6px; +} + +.changelog-controls .changelog-tags { + margin: 8px 0 0; +} + @media screen and (max-width: 720px) { .sound-search-group { margin-bottom: 8px; diff --git a/vite.config.js b/vite.config.js index 5a03329d..137aea4e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -21,6 +21,11 @@ export default defineConfig({ title: getTitle({ id: 'title.sounds', page: true }), template: template, }), + html({ + fileName: `changelog/index.html`, + title: getTitle({ id: 'title.changelog', page: true }), + template: template, + }), ...config.generators.map(m => html({ fileName: `${m.url}/index.html`, title: getTitle(m),