Improve homepage (#245)

* Improve how generators are listed on home

* Add some icons for generators

* Remove debug

* Refactor cachedFetch and use generated changelogs

* Add limit to how many changes are shown by default

* Add more generator icons

* Refactor cards

* Fix generator icons for light theme

* Add more worldgen icons

* Add remaining generator icons

* Refactor navigation and badges style

* Group on homepage for guides and tools

* Fix header button style

* Add versions and technical changelog to homepage

* Make it clear that not all changes could be documented
This commit is contained in:
Misode
2022-07-01 23:48:38 +02:00
committed by GitHub
parent 29031bb375
commit d0bae089d1
40 changed files with 791 additions and 460 deletions

View File

@@ -1,70 +1,5 @@
import { isObject } from '../Utils.js'
const repo = 'https://raw.githubusercontent.com/misode/technical-changes/main'
export type Change = {
group: string,
version: string,
order: number,
tags: string[],
content: string,
}
let Changelogs: Change[] | Promise<Change[]> | 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, i) => fetchGroup(parseVersion(group), i))
)).flat().map<Change>(change => ({
...change,
tags: [change.group, ...change.tags],
}))
}
return Changelogs
}
async function fetchGroup(group: string, groupIndex: number) {
const index = await (await fetch(`${repo}/${group}/index.json`)).json() as string[]
return (await Promise.all(
index.map((version, i) => fetchChangelog(group, parseVersion(version), groupIndex, i))
)).flat()
}
async function fetchChangelog(group: string, version: string, groupIndex: number, versionIndex: number) {
const text = await (await fetch(`${repo}/${group}/${version}.md`)).text()
return parseChangelog(text).map(change => ({
version,
group,
order: groupIndex * 1000 + versionIndex,
...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()
.replaceAll('->', '→')
.replaceAll('\n...\n', '\n\n'),
}
})
}
function parseVersion(version: unknown): string {
if (typeof version === 'string') {
return version
} else if (isObject(version)) {
return version.id
}
return 'unknown'
}
const ARTICLE_PREFIX = 'https://www.minecraft.net/article/'
const ARTICLE_OVERRIDES = new Map(Object.entries({
'1.16-pre2': 'minecraft-1-16-pre-release-1',
'1.16-pre4': 'minecraft-1-16-pre-release-3',

View File

@@ -16,6 +16,7 @@ type Version = {
declare var __LATEST_VERSION__: string
const latestVersion = __LATEST_VERSION__ ?? ''
const mcmetaUrl = 'https://raw.githubusercontent.com/misode/mcmeta'
const changesUrl = 'https://raw.githubusercontent.com/misode/technical-changes'
type McmetaTypes = 'summary' | 'data' | 'assets' | 'registries'
@@ -52,7 +53,7 @@ export async function fetchData(versionId: string, collectionTarget: CollectionR
async function fetchRegistries(version: Version, target: CollectionRegistry) {
console.debug(`[fetchRegistries] ${version.id}`)
try {
const data = await getData(`${mcmeta(version, 'summary')}/registries/data.min.json`)
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/registries/data.min.json`)
for (const id in data) {
target.register(id, data[id].map((e: string) => 'minecraft:' + e))
}
@@ -64,7 +65,7 @@ async function fetchRegistries(version: Version, target: CollectionRegistry) {
async function fetchBlockStateMap(version: Version, target: BlockStateRegistry) {
console.debug(`[fetchBlockStateMap] ${version.id}`)
try {
const data = await getData(`${mcmeta(version, 'summary')}/blocks/data.min.json`)
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/blocks/data.min.json`)
for (const id in data) {
target['minecraft:' + id] = {
properties: data[id][0],
@@ -99,10 +100,10 @@ export async function fetchAllPresets(versionId: VersionId, registry: string) {
const version = config.versions.find(v => v.id === versionId)!
await validateCache(version)
try {
const entries = await getData(`${mcmeta(version, 'registries')}/${registry}/data.min.json`)
const entries = await cachedFetch<any>(`${mcmeta(version, 'registries')}/${registry}/data.min.json`)
return new Map<string, unknown>(await Promise.all(
entries.map(async (e: string) =>
[e, await getData(`${mcmeta(version, 'data')}/data/minecraft/${registry}/${e}.json`)])
[e, await cachedFetch(`${mcmeta(version, 'data')}/data/minecraft/${registry}/${e}.json`)])
))
} catch (e) {
throw new Error(`Error occurred while fetching all ${registry} presets: ${message(e)}`)
@@ -119,7 +120,7 @@ export async function fetchSounds(versionId: VersionId): Promise<SoundEvents> {
await validateCache(version)
try {
const url = `${mcmeta(version, 'summary')}/sounds/data.min.json`
return await getData(url)
return await cachedFetch(url)
} catch (e) {
throw new Error(`Error occurred while fetching sounds for ${version}: ${message(e)}`)
}
@@ -148,7 +149,7 @@ export async function fetchVersions(): Promise<VersionMeta[]> {
const version = config.versions[config.versions.length - 1]
await validateCache(version)
try {
return getData(`${mcmeta(version, 'summary')}/versions/data.min.json`)
return cachedFetch(`${mcmeta(version, 'summary')}/versions/data.min.json`, { refresh: true })
} catch (e) {
throw new Error(`Error occured while fetching versions: ${message(e)}`)
}
@@ -159,32 +160,84 @@ export function getTextureUrl(versionId: VersionId, path: string): string {
return `${mcmeta(version, 'assets')}/assets/minecraft/textures/${path}.png`
}
async function getData<T = any>(url: string, fn: (v: any) => T = (v: any) => v): Promise<T> {
export interface Change {
group: string,
version: string,
order: number,
tags: string[],
content: string,
}
export async function fetchChangelogs(): Promise<Change[]> {
try {
const [changes, versions] = await Promise.all([
cachedFetch<Omit<Change, 'order'>[]>(`${changesUrl}/generated/changes.json`, { refresh: true }),
fetchVersions(),
])
const versionMap = new Map(versions.map((v, i) => [v.id, versions.length - i]))
return changes.map(c => ({ ...c, order: versionMap.get(c.version) ?? 0 }))
} catch (e) {
throw new Error(`Error occured while fetching technical changes: ${message(e)}`)
}
}
interface FetchOptions<D> {
decode?: (r: Response) => Promise<D>
refresh?: boolean
}
const REFRESHED = new Set<string>()
async function cachedFetch<D = unknown>(url: string, { decode = (r => r.json()), refresh }: FetchOptions<D> = {}): Promise<D> {
try {
const cache = await caches.open(CACHE_NAME)
console.debug(`[getData] Opened cache ${CACHE_NAME} ${url}`)
console.debug(`[cachedFetch] Opened cache ${CACHE_NAME} ${url}`)
const cacheResponse = await cache.match(url)
if (cacheResponse && cacheResponse.ok) {
console.debug(`[getData] Retrieving cached data ${url}`)
return await cacheResponse.json()
}
console.debug(`[getData] fetching data ${url}`)
const fetchResponse = await fetch(url)
const responseData = fn(await fetchResponse.json())
await cache.put(url, new Response(JSON.stringify(responseData)))
return responseData
} catch (e) {
console.warn(`[getData] Failed to open cache ${CACHE_NAME}: ${message(e)}`)
console.debug(`[getData] fetching data ${url}`)
if (refresh) {
if (REFRESHED.has(url)) {
refresh = false
} else {
REFRESHED.add(url)
}
}
if (refresh) {
try {
return await fetchAndCache(cache, url, decode)
} catch (e) {
if (cacheResponse && cacheResponse.ok) {
console.debug(`[cachedFetch] Cannot refresh, using cache ${url}`)
return await decode(cacheResponse)
}
throw new Error('Failed to fetch')
}
} else {
if (cacheResponse && cacheResponse.ok) {
console.debug(`[cachedFetch] Retrieving cached data ${url}`)
return await decode(cacheResponse)
}
return await fetchAndCache(cache, url, decode)
}
} catch (e: any) {
console.warn(`[cachedFetch] Failed to open cache ${CACHE_NAME}: ${e.message}`)
console.debug(`[cachedFetch] Fetching data ${url}`)
const fetchResponse = await fetch(url)
const responseData = fn(await fetchResponse.json())
return responseData
const fetchData = await decode(fetchResponse)
return fetchData
}
}
async function fetchAndCache<D>(cache: Cache, url: string, decode: (r: Response) => Promise<D>) {
console.debug(`[cachedFetch] Fetching data ${url}`)
const fetchResponse = await fetch(url)
const fetchClone = fetchResponse.clone()
const fetchData = await decode(fetchResponse)
await cache.put(url, fetchClone)
return fetchData
}
async function deleteMatching(matches: (url: string) => boolean) {
try {
const cache = await caches.open(CACHE_NAME)

View File

@@ -0,0 +1,20 @@
export interface Guide {
id: string,
title: string,
versions?: string[],
tags?: string[],
}
declare var __GUIDES__: Guide[]
export function getGuides() {
return __GUIDES__
}
export function getGuide(id: string): Guide {
const guide = getGuides().find(g => g.id === id)
if (guide === undefined) {
return { id, title: 'Unknown Guide' }
}
return guide
}

View File

@@ -1,4 +1,4 @@
export * from './Changelogs.js'
export * from './Article.js'
export * from './DataFetcher.js'
export * from './Schemas.js'
export * from './Sharing.js'