Add versions explorer page

This commit is contained in:
Misode
2022-03-02 02:34:41 +01:00
parent fd46bc4360
commit cb24e61cf0
20 changed files with 526 additions and 115 deletions

View File

@@ -1,10 +1,8 @@
import { marked } from 'marked'
import { useEffect, useMemo, useState } from 'preact/hooks'
import { Ad, Btn, ErrorPanel, Octicon, TextInput } from '../components'
import { useEffect, useState } from 'preact/hooks'
import { Ad, ChangelogList, ErrorPanel } from '../components'
import { useLocale, useTitle } from '../contexts'
import type { ChangelogEntry, ChangelogVersion } from '../services'
import type { Change } from '../services'
import { getChangelogs } from '../services'
import { hashString } from '../Utils'
interface Props {
path?: string,
@@ -14,99 +12,19 @@ export function Changelog({}: Props) {
const [error, setError] = useState<string | null>(null)
useTitle(locale('title.changelog'))
const [changelogs, setChangelogs] = useState<ChangelogEntry[]>([])
const [changelogs, setChangelogs] = useState<Change[]>([])
useEffect(() => {
getChangelogs()
.then(changelogs => setChangelogs(changelogs))
.catch(e => { console.error(e); setError(e) })
}, [])
const [search, setSearch] = useState('')
const [tags, setTags] = useState<string[]>([])
const toggleTag = (tag: string) => {
if (!tags.includes(tag)) {
setTags([...tags, tag])
} else {
setTags(tags.filter(t => t !== 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 => {
if (q.startsWith('!')) {
return q.length === 1 || !content.includes(q.slice(1))
}
return content.includes(q)
})
})
}, [changelogs, search, tags])
const [sort, setSort] = useState(true)
const sortedChangelogs = useMemo(() => {
return filteredChangelogs.sort((a, b) => sort ? b.order - a.order : a.order - b.order)
}, [filteredChangelogs, sort])
return <main>
<Ad type="text" id="changelog" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
<div class="changelog-controls">
<div class="changelog-query">
<TextInput class="btn btn-input changelog-search" list="sound-list" placeholder={locale('changelog.search')}
value={search} onChange={setSearch} />
<Btn icon={sort ? 'sort_desc' : 'sort_asc'} label={sort ? 'Newest first' : 'Oldest first'} onClick={() => setSort(!sort)} />
</div>
{tags.length > 0 && <div class="changelog-tags">
{tags.map(tag => <Tag label={tag} onClick={() => setTags(tags.filter(t => t !== tag))} />)}
</div>}
</div>
<div class="changelog">
{sortedChangelogs.map(change =>
<Change change={change} activeTags={tags} toggleTag={toggleTag} />)}
<ChangelogList changes={changelogs} defaultOrder="desc" />
</div>
</main>
}
type ChangeProps = {
change: ChangelogEntry,
activeTags: string[],
toggleTag: (tag: string) => unknown,
}
function Change({ change, activeTags, toggleTag }: ChangeProps) {
return <div class="changelog-entry">
<div class="changelog-version">
<ArticleLink {...change.version}/>
<ArticleLink {...change.group}/>
</div>
<div class="changelog-tags">
{change.tags.map(tag => <Tag label={tag} onClick={() => toggleTag(tag)} active={activeTags.includes(tag)} />)}
</div>
<div class="changelog-content" dangerouslySetInnerHTML={{ __html: marked(change.content) }} />
</div>
}
function ArticleLink({ id, article }: ChangelogVersion) {
return article === null
? <span>{id}</span>
: <a href={`https://www.minecraft.net/en-us/article/${article}`} target="_blank">{id}</a>
}
type TagProps = {
label: string,
active?: boolean,
onClick?: () => unknown,
}
function Tag({ label, active, onClick }: TagProps) {
const color = label === 'breaking' ? 5 : hashString(label) % 360
return <div class={`changelog-tag${active ? ' active' : ''}${onClick ? ' clickable' : ''}`} style={`--tint: ${color}`} onClick={onClick}>
{label === 'breaking' && Octicon.alert}
{label}
</div>
}

View File

@@ -32,6 +32,7 @@ export function Home({}: Props) {
link="https://misode.github.io/upgrader/"
desc="Convert your data packs from 1.16 to 1.17 to 1.18" />
<ToolCard title="Technical Changelog" link="/changelog/" />
<ToolCard title="Minecraft Versions" link="/versions/" />
</div>
</main>
}

View File

@@ -0,0 +1,64 @@
import { getCurrentUrl } from 'preact-router'
import { useEffect, useState } from 'preact/hooks'
import { Ad, ErrorPanel, Octicon, VersionDetail, VersionList } from '../components'
import { useLocale, useTitle } from '../contexts'
import type { VersionMeta } from '../services'
import { fetchVersions } from '../services'
import { getSearchParams } from '../Utils'
interface Props {
path?: string,
}
export function Versions({}: Props) {
const { locale } = useLocale()
const [error, setError] = useState<string | null>(null)
useTitle(locale('title.versions'))
const [versions, setVersions] = useState<VersionMeta[]>([])
useEffect(() => {
fetchVersions()
.then(versions => setVersions(versions))
.catch(e => { console.error(e); setError(e) })
}, [])
const selectedId = getSearchParams(getCurrentUrl()).get('id')
const selected = versions.find(v => v.id === selectedId)
useTitle(selected ? selected.name : 'Versions Explorer', selected ? [] : undefined)
const nextVersion = selected && getOffsetVersion(versions, selected, -1)
const previousVersion = selected && getOffsetVersion(versions, selected, 1)
return <main>
<Ad type="text" id="versions" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
<div class="versions">
{selected ? <>
<div class="version-navigation">
<a class="btn btn-link" href="/versions/">
{Octicon.three_bars}
{locale('versions.all')}
</a>
<a class="btn btn-link" {...previousVersion ? {href: `/versions/?id=${previousVersion.id}`} : {disabled: true}}>
{Octicon.arrow_left}
{locale('versions.previous')}
</a>
<a class="btn btn-link" {...nextVersion ? {href: `/versions/?id=${nextVersion.id}`} : {disabled: true}}>
{locale('versions.next')}
{Octicon.arrow_right}
</a>
</div>
<VersionDetail version={selected} />
</> : <VersionList versions={versions} link={id => `/versions/?id=${id}`} />}
</div>
</main>
}
function getOffsetVersion(versions: VersionMeta[], current: VersionMeta, offset: number) {
const currentIndex = versions.findIndex(v => v.id === current.id)
const offsetIndex = currentIndex + offset
if (offsetIndex < 0 || offsetIndex >= versions.length) {
return undefined
}
return versions[offsetIndex]
}

View File

@@ -4,3 +4,4 @@ export * from './Generator'
export * from './Home'
export * from './Project'
export * from './Sounds'
export * from './Versions'