mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-24 07:37:10 +00:00
Add versions explorer page
This commit is contained in:
14
src/app/components/forms/Checkbox.tsx
Normal file
14
src/app/components/forms/Checkbox.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { hexId } from '../../Utils'
|
||||
|
||||
interface Props {
|
||||
label: string,
|
||||
value: boolean,
|
||||
onChange: (value: boolean) => unknown,
|
||||
}
|
||||
export function Checkbox({ label, value, onChange }: Props) {
|
||||
const id = hexId()
|
||||
return <label class="checkbox">
|
||||
<input id={id} type="checkbox" checked={value} onClick={() => onChange(!value)} />
|
||||
{label}
|
||||
</label>
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './Checkbox'
|
||||
export * from './Input'
|
||||
export * from './SearchList'
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './previews'
|
||||
export * from './sounds'
|
||||
export * from './ToolCard'
|
||||
export * from './TreeView'
|
||||
export * from './versions'
|
||||
|
||||
27
src/app/components/versions/ChangelogEntry.tsx
Normal file
27
src/app/components/versions/ChangelogEntry.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { marked } from 'marked'
|
||||
import { ChangelogTag } from '.'
|
||||
import type { Change, ChangelogVersion } from '../../services'
|
||||
|
||||
type Props = {
|
||||
change: Change,
|
||||
activeTags?: string[],
|
||||
toggleTag?: (tag: string) => unknown,
|
||||
}
|
||||
export function ChangelogEntry({ change, activeTags, toggleTag }: Props) {
|
||||
return <div class="changelog-entry">
|
||||
<div class="changelog-version">
|
||||
<ArticleLink {...change.version}/>
|
||||
<ArticleLink {...change.group}/>
|
||||
</div>
|
||||
<div class="changelog-tags">
|
||||
{change.tags.map(tag => <ChangelogTag label={tag} onClick={toggleTag ? () => toggleTag(tag) : undefined} 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>
|
||||
}
|
||||
66
src/app/components/versions/ChangelogList.tsx
Normal file
66
src/app/components/versions/ChangelogList.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { Btn, TextInput } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import type { Change } from '../../services'
|
||||
import { ChangelogEntry } from './ChangelogEntry'
|
||||
import { ChangelogTag } from './ChangelogTag'
|
||||
|
||||
interface Props {
|
||||
changes: Change[] | undefined,
|
||||
defaultOrder: 'asc' | 'desc',
|
||||
}
|
||||
export function ChangelogList({ changes, defaultOrder }: Props) {
|
||||
const { locale } = useLocale()
|
||||
|
||||
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 changes
|
||||
return changes?.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)
|
||||
})
|
||||
})
|
||||
}, [changes, search, tags])
|
||||
|
||||
const [sort, setSort] = useState(defaultOrder === 'desc')
|
||||
|
||||
const sortedChangelogs = useMemo(() => {
|
||||
return filteredChangelogs?.sort((a, b) => sort ? b.order - a.order : a.order - b.order)
|
||||
}, [filteredChangelogs, sort])
|
||||
|
||||
return <>
|
||||
<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 => <ChangelogTag label={tag} onClick={() => setTags(tags.filter(t => t !== tag))} />)}
|
||||
</div>}
|
||||
<div class="changelog-list">
|
||||
{sortedChangelogs === undefined
|
||||
? <span>{locale('loading')}</span>
|
||||
: sortedChangelogs.length === 0
|
||||
? <span>{locale('changelog.no_results')}</span>
|
||||
: sortedChangelogs.map(change =>
|
||||
<ChangelogEntry change={change} activeTags={tags} toggleTag={toggleTag} />)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
15
src/app/components/versions/ChangelogTag.tsx
Normal file
15
src/app/components/versions/ChangelogTag.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Octicon } from '..'
|
||||
import { hashString } from '../../Utils'
|
||||
|
||||
type TagProps = {
|
||||
label: string,
|
||||
active?: boolean,
|
||||
onClick?: () => unknown,
|
||||
}
|
||||
export function ChangelogTag({ 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>
|
||||
}
|
||||
48
src/app/components/versions/VersionDetail.tsx
Normal file
48
src/app/components/versions/VersionDetail.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { VersionMetaData } from '.'
|
||||
import { useLocale } from '../../contexts'
|
||||
import type { Change, VersionMeta } from '../../services'
|
||||
import { getChangelogs } from '../../services'
|
||||
import { ChangelogList } from './ChangelogList'
|
||||
|
||||
interface Props {
|
||||
version: VersionMeta
|
||||
}
|
||||
export function VersionDetail({ version }: Props) {
|
||||
const { locale } = useLocale()
|
||||
|
||||
const [changelogs, setChangelogs] = useState<Change[] | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
getChangelogs()
|
||||
.then(changelogs => setChangelogs(
|
||||
changelogs.map(c => ({ ...c, tags: c.tags.filter(t => t !== c.group.id) }))
|
||||
))
|
||||
.catch(e => console.error(e))
|
||||
}, [])
|
||||
|
||||
const filteredChangelogs = useMemo(() =>
|
||||
changelogs?.filter(c => c.version.id === version.id || c.group.id === version.id),
|
||||
[version.id, changelogs])
|
||||
|
||||
return <>
|
||||
<div class="version-detail">
|
||||
<h2>{version.name}</h2>
|
||||
<div class="version-info">
|
||||
<VersionMetaData label={locale('versions.released')} value={releaseDate(version)} />
|
||||
<VersionMetaData label={locale('versions.release_target')} value={version.release_target} link={version.id !== version.release_target ? `/versions/?id=${version.release_target}` : undefined} />
|
||||
<VersionMetaData label={locale('versions.data_version')} value={version.data_version} />
|
||||
<VersionMetaData label={locale('versions.protocol_version')} value={version.protocol_version} />
|
||||
<VersionMetaData label={locale('versions.data_pack_format')} value={version.data_pack_version} />
|
||||
<VersionMetaData label={locale('versions.resource_pack_format')} value={version.resource_pack_version} />
|
||||
</div>
|
||||
<h3>{locale('versions.technical_changes')}</h3>
|
||||
<div class="version-changes">
|
||||
<ChangelogList changes={filteredChangelogs} defaultOrder="asc" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export function releaseDate(version: VersionMeta) {
|
||||
return new Date(version.release_time).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })
|
||||
}
|
||||
18
src/app/components/versions/VersionEntry.tsx
Normal file
18
src/app/components/versions/VersionEntry.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { releaseDate, VersionMetaData } from '.'
|
||||
import { useLocale } from '../../contexts'
|
||||
import type { VersionMeta } from '../../services'
|
||||
|
||||
interface Props {
|
||||
version: VersionMeta,
|
||||
link?: string,
|
||||
}
|
||||
export function VersionEntry({ version, link }: Props) {
|
||||
const { locale } = useLocale()
|
||||
|
||||
return <a class="version-entry" href={link}>
|
||||
<span class="version-id">{version.id}</span>
|
||||
<VersionMetaData label={locale('versions.released')} value={releaseDate(version)} compact />
|
||||
<VersionMetaData label={locale('versions.data_version')} value={version.data_version} optional />
|
||||
<VersionMetaData label={locale('versions.pack_format')} value={version.data_pack_version} optional />
|
||||
</a>
|
||||
}
|
||||
36
src/app/components/versions/VersionList.tsx
Normal file
36
src/app/components/versions/VersionList.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { Checkbox, TextInput } from '..'
|
||||
import { useLocale } from '../../contexts'
|
||||
import type { VersionMeta } from '../../services'
|
||||
import { VersionEntry } from './VersionEntry'
|
||||
|
||||
interface Props {
|
||||
versions: VersionMeta[]
|
||||
link?: (id: string) => string
|
||||
}
|
||||
export function VersionList({ versions, link }: Props) {
|
||||
const { locale } = useLocale()
|
||||
|
||||
const [snapshots, setSnapshots] = useState(true)
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filteredVersions = useMemo(() => versions.filter(v => {
|
||||
if (v.type === 'snapshot' && !snapshots) return false
|
||||
return v.id.includes(search)
|
||||
}), [versions, snapshots, search])
|
||||
|
||||
|
||||
return <>
|
||||
<div class="versions-controls">
|
||||
<TextInput class="btn btn-input version-search" list="sound-list" placeholder={locale('versions.search')}
|
||||
value={search} onChange={setSearch} />
|
||||
<Checkbox label="Include snapshots" value={snapshots} onChange={setSnapshots} />
|
||||
</div>
|
||||
<div class="version-list">
|
||||
{filteredVersions.map(v => <VersionEntry version={v} link={link?.(v.id)} />)}
|
||||
{filteredVersions.length === 0 && <span>
|
||||
{locale('versions.no_results')}
|
||||
</span>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
16
src/app/components/versions/VersionMetaData.tsx
Normal file
16
src/app/components/versions/VersionMetaData.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Octicon } from '..'
|
||||
|
||||
interface Props {
|
||||
label: string,
|
||||
value: string | number,
|
||||
link?: string,
|
||||
compact?: boolean,
|
||||
optional?: boolean,
|
||||
}
|
||||
export function VersionMetaData({ label, value, link, compact, optional }: Props) {
|
||||
return <div class={`version-metadata${optional ? ' version-metadata-hide' : ''}`}>
|
||||
<span class={compact ? 'version-metadata-hide' : undefined}>{label}: </span>
|
||||
<span class="version-metadata-value">{value}</span>
|
||||
{link && <a href={link} class="version-metadata-link">{Octicon.link_external}</a>}
|
||||
</div>
|
||||
}
|
||||
7
src/app/components/versions/index.ts
Normal file
7
src/app/components/versions/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './ChangelogEntry'
|
||||
export * from './ChangelogList'
|
||||
export * from './ChangelogTag'
|
||||
export * from './VersionDetail'
|
||||
export * from './VersionEntry'
|
||||
export * from './VersionList'
|
||||
export * from './VersionMetaData'
|
||||
Reference in New Issue
Block a user