Implement changelog dialog
Some checks failed
Docs / build (push) Has been cancelled

This commit is contained in:
Полина 2025-01-25 22:31:59 +03:00
parent a3daff891f
commit 67292719a7
7 changed files with 395 additions and 70 deletions

View file

@ -18,6 +18,8 @@
"@mtcute/convert": "^0.19.8", "@mtcute/convert": "^0.19.8",
"@mtcute/web": "^0.19.5", "@mtcute/web": "^0.19.5",
"@nanostores/persistent": "^0.10.2", "@nanostores/persistent": "^0.10.2",
"@tanstack/solid-query": "^5.64.2",
"@tanstack/solid-query-persist-client": "^5.64.2",
"@tanstack/solid-table": "^8.20.5", "@tanstack/solid-table": "^8.20.5",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View file

@ -1,9 +1,15 @@
import type { RunnerController } from './components/runner/Runner.tsx' import type { RunnerController } from './components/runner/Runner.tsx'
import { ColorModeProvider, ColorModeScript } from '@kobalte/core' import { ColorModeProvider, ColorModeScript } from '@kobalte/core'
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/solid-query'
import { LucidePartyPopper } from 'lucide-solid'
import { workerInit } from 'mtcute-repl-worker/client' import { workerInit } from 'mtcute-repl-worker/client'
import { createSignal, lazy, onCleanup, onMount, Show } from 'solid-js' import { createSignal, lazy, onCleanup, onMount, Show } from 'solid-js'
import { toast } from 'solid-sonner' import { toast } from 'solid-sonner'
import { ChangelogDialog } from './components/changelog/Changelog.tsx'
import { EditorTabs } from './components/editor/EditorTabs.tsx' import { EditorTabs } from './components/editor/EditorTabs.tsx'
import { NavbarMenu } from './components/nav/NavbarMenu.tsx' import { NavbarMenu } from './components/nav/NavbarMenu.tsx'
import { Runner } from './components/runner/Runner.tsx' import { Runner } from './components/runner/Runner.tsx'
@ -14,8 +20,11 @@ import { Toaster } from './lib/components/ui/sonner.tsx'
const Editor = lazy(() => import('./components/editor/Editor.tsx')) const Editor = lazy(() => import('./components/editor/Editor.tsx'))
const queryClient = new QueryClient()
export function App() { export function App() {
const [updating, setUpdating] = createSignal(true) const [updating, setUpdating] = createSignal(true)
const [showChangelog, setShowChangelog] = createSignal(false)
const [showSettings, setShowSettings] = createSignal(false) const [showSettings, setShowSettings] = createSignal(false)
const [settingsTab, setSettingsTab] = createSignal<SettingsTab>('accounts') const [settingsTab, setSettingsTab] = createSignal<SettingsTab>('accounts')
const [runnerController, setRunnerController] = createSignal<RunnerController>() const [runnerController, setRunnerController] = createSignal<RunnerController>()
@ -27,6 +36,32 @@ export function App() {
let workerIframe!: HTMLIFrameElement let workerIframe!: HTMLIFrameElement
onMount(() => { onMount(() => {
const localBuild = localStorage.getItem('repl:buildVersion')
const latestBuild: string = import.meta.env.BUILD_VERSION
if (localBuild === null || new Date(localBuild) !== new Date(latestBuild)) {
localStorage.setItem('repl:buildVersion', latestBuild)
setTimeout(() => {
toast.custom(t => (
<div
class="flex cursor-pointer items-center rounded-md border p-6"
onClick={() => {
setShowChangelog(true)
toast.dismiss(t)
}}
>
<LucidePartyPopper class="ml-1.5 mr-4" />
<div class="flex flex-col">
<div class="text-sm font-semibold">Playground updated!</div>
<div class="text-sm opacity-90">Click here to see the latest changes.</div>
</div>
</div>
), {
important: true,
})
}, 1000)
}
workerInit(workerIframe).then(() => { workerInit(workerIframe).then(() => {
setIframeLoading(false) setIframeLoading(false)
}) })
@ -46,6 +81,7 @@ export function App() {
<div class="flex h-screen w-screen flex-col overflow-hidden"> <div class="flex h-screen w-screen flex-col overflow-hidden">
<Toaster /> <Toaster />
<ColorModeScript /> <ColorModeScript />
<QueryClientProvider client={queryClient}>
<ColorModeProvider> <ColorModeProvider>
<iframe <iframe
ref={workerIframe} ref={workerIframe}
@ -68,13 +104,16 @@ export function App() {
setShowSettings(true) setShowSettings(true)
setSettingsTab('accounts') setSettingsTab('accounts')
}} }}
onShowChangelog={() => {
setShowChangelog(true)
}}
onShowSettings={() => { onShowSettings={() => {
setShowSettings(true) setShowSettings(true)
}} }}
/> />
</div> </div>
</nav> </nav>
<div class="h-px shrink-0 bg-border" /> <div class="bg-border h-px shrink-0" />
<Show <Show
when={!updating()} when={!updating()}
fallback={( fallback={(
@ -112,6 +151,11 @@ export function App() {
</Resizable> </Resizable>
</Show> </Show>
<ChangelogDialog
show={showChangelog()}
onClose={() => setShowChangelog(false)}
/>
<SettingsDialog <SettingsDialog
show={showSettings()} show={showSettings()}
onClose={() => setShowSettings(false)} onClose={() => setShowSettings(false)}
@ -119,6 +163,7 @@ export function App() {
onTabChange={setSettingsTab} onTabChange={setSettingsTab}
/> />
</ColorModeProvider> </ColorModeProvider>
</QueryClientProvider>
</div> </div>
) )
} }

View file

@ -0,0 +1,217 @@
import {
createQuery,
} from '@tanstack/solid-query'
import { createSignal, ErrorBoundary, For, onMount, Show, Suspense } from 'solid-js'
import { Avatar, AvatarFallback, AvatarImage, makeAvatarFallbackText } from '../../lib/components/ui/avatar'
import { Button } from '../../lib/components/ui/button'
import { Dialog, DialogContent } from '../../lib/components/ui/dialog'
import { Skeleton } from '../../lib/components/ui/skeleton'
interface Commit {
message?: string
authorName?: string
authorUsername?: string
authorAvatar?: string
authorUrl?: string
date?: string
commitHash?: string
commitUrl?: string
}
function getRandomSizes(len: number) {
return Array.from({ length: len }, () => ({
msg: Math.random() * 192 + 128,
sub: [
Math.random() * 32 + 32,
Math.random() * 32 + 32,
Math.random() * 32 + 32,
],
}))
}
const lastPageRegex = /page=(\d*)>; rel="last"/g
export function ChangelogDialog(props: {
show: boolean
onClose: () => void
}) {
const commitsPerPage = 15
const [lastPage, setLastPage] = createSignal(1)
onMount(async () => {
const result = await fetch(`https://api.github.com/repos/mtcute/repl/commits?per_page=${commitsPerPage}`, {
headers: {
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json',
},
})
const link = result.headers.get('link')
if (link === null) return
const match = lastPageRegex.exec(link)
if (match !== null && match.length > 1) {
setLastPage(Number.parseInt(match[1]))
}
})
const [page, setPage] = createSignal(1)
const commitsQuery = createQuery<Commit[]>(() => ({
queryKey: ['repo commits', page()],
queryFn: async () => {
const result = await fetch(`https://api.github.com/repos/mtcute/repl/commits?per_page=${commitsPerPage}&page=${page()}`, {
headers: {
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json',
},
})
if (!result.ok) throw new Error('Failed to fetch data')
return result.json().then(e => e.map((e: any) => ({
message: e?.commit?.message?.split(':').at(-1)?.trim(),
authorName: e?.commit?.author?.name,
authorUsername: e?.author?.login,
authorAvatar: e?.committer?.avatar_url,
authorUrl: e?.author?.html_url,
date: e?.commit?.author?.date,
commitHash: e?.sha,
commitUrl: e?.html_url,
})))
},
staleTime: 1000 * 60 * 10, // 10 minutes
throwOnError: true, // Throw an error if the query fails
}))
const [skeletonSizes, setSkeletonSizes] = createSignal<{ msg: number, sub: number[] }[]>(getRandomSizes(commitsPerPage))
return (
<Dialog
open={props.show}
onOpenChange={open => !open && props.onClose()}
>
<DialogContent class="flex w-[calc(100vw-96px)] max-w-[960px] flex-col gap-2 overflow-auto p-2">
<div class="flex flex-row items-center gap-3 px-2">
<h1 class="text-lg font-bold">Changelog</h1>
<div class="text-sm opacity-75">
<Show when={commitsQuery.data !== null}>
{(() => {
const date = commitsQuery.data?.at(0)?.date
if (date) return new Date(date).toLocaleDateString()
return '???'
})()}
&nbsp;to&nbsp;
{(() => {
const date = commitsQuery.data?.at(-1)?.date
if (date) return new Date(date).toLocaleDateString()
return '???'
})()}
</Show>
</div>
</div>
<div class="relative flex flex-col rounded-md border">
<ErrorBoundary fallback={(
<>
<For each={Array.from({ length: commitsPerPage }, (_, index) => index)}>
{_ => (
<div class="hover:bg-muted/50 box-content flex h-[24px] items-center justify-between px-3 py-1.5 [&:not(:last-child)]:border-b" />
)}
</For>
<div class="absolute flex size-full items-center justify-center">
<div class="bg-background rounded-md border p-4 shadow-sm">
Error while loading the changelog.
</div>
</div>
</>
)}
>
<Suspense fallback={(
<For each={Array.from({ length: commitsPerPage }, (_, index) => index)}>
{i => (
<div class="hover:bg-muted/50 box-content flex h-[24px] items-center justify-between px-3 py-1.5 [&:not(:last-child)]:border-b">
<Skeleton class="h-4" style={{ width: `${skeletonSizes()[i].msg}px` }} />
<div class="flex items-center text-xs">
<div class="flex opacity-50">
<Skeleton class="h-4" style={{ width: `${skeletonSizes()[i].sub[0]}px` }} />
&nbsp;&nbsp;
<Skeleton class="h-4" style={{ width: `${skeletonSizes()[i].sub[1]}px` }} />
&nbsp;&nbsp;
</div>
<div class="flex cursor-pointer items-center gap-1.5 hover:underline">
<div class="opacity-50">
<Skeleton class="h-4" style={{ width: `${skeletonSizes()[i].sub[2]}px` }} />
</div>
<Skeleton class="size-4 rounded-full" />
</div>
</div>
</div>
)}
</For>
)}
>
<For each={commitsQuery.data}>
{commit => (
<div class="hover:bg-muted/50 flex justify-between px-3 py-1.5 [&:not(:last-child)]:border-b">
<div>
<a href={commit.commitUrl} target="_blank" class="text-sm font-semibold capitalize hover:underline">
{commit.message}
</a>
</div>
<div class="flex items-center text-xs">
<div class="opacity-50">
{commit.date ? new Date(commit.date).toLocaleDateString() : 'unknown'}
&nbsp;&nbsp;
{commit.commitHash?.slice(0, 7) ?? 'unknown'}
&nbsp;&nbsp;
</div>
<a href={commit.authorUrl} target="_blank" class="flex cursor-pointer items-center gap-1.5 hover:underline">
<div class="opacity-50">
{commit.authorUsername ?? 'unknown'}
</div>
<Avatar class="size-4">
<AvatarImage src={commit.authorAvatar} />
<AvatarFallback class="whitespace-nowrap rounded-none">
{makeAvatarFallbackText(commit.authorUsername ?? 'unknown')}
</AvatarFallback>
</Avatar>
</a>
</div>
</div>
)}
</For>
<For each={Array.from({ length: commitsPerPage - (commitsQuery.data?.length ?? 0) })}>
{_ => (
<div class="hover:bg-muted/50 box-content flex h-[24px] justify-between px-3 py-1.5 [&:not(:last-child)]:border-b" />
)}
</For>
</Suspense>
</ErrorBoundary>
</div>
<div class="flex justify-center gap-4">
<Button
variant="ghost"
disabled={page() === 1}
onClick={() => {
setSkeletonSizes(getRandomSizes(commitsPerPage))
setPage(page() - 1)
}}
>
Previous
</Button>
<Button
variant="ghost"
disabled={page() === lastPage()}
onClick={() => {
setSkeletonSizes(getRandomSizes(commitsPerPage))
setPage(page() + 1)
}}
>
Next
</Button>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -1,7 +1,7 @@
import type { ConfigColorMode, MaybeConfigColorMode } from '@kobalte/core' import type { ConfigColorMode, MaybeConfigColorMode } from '@kobalte/core'
import type { DropdownMenuTriggerProps } from '@kobalte/core/dropdown-menu' import type { DropdownMenuTriggerProps } from '@kobalte/core/dropdown-menu'
import { useColorMode } from '@kobalte/core' import { useColorMode } from '@kobalte/core'
import { LucideCheck, LucideChevronRight, LucideExternalLink, LucideLaptop, LucideLogIn, LucideMoon, LucideSun, LucideUsers } from 'lucide-solid' import { LucideCheck, LucideChevronRight, LucideExternalLink, LucideLaptop, LucideLogIn, LucideMoon, LucideNotebook, LucideSun, LucideUsers } from 'lucide-solid'
import { SiGithub } from 'solid-icons/si' import { SiGithub } from 'solid-icons/si'
import { createSignal, For, Show } from 'solid-js' import { createSignal, For, Show } from 'solid-js'
import { Button } from '../../lib/components/ui/button.tsx' import { Button } from '../../lib/components/ui/button.tsx'
@ -26,6 +26,7 @@ import { AccountAvatar } from '../AccountAvatar.tsx'
export function NavbarMenu(props: { export function NavbarMenu(props: {
onShowAccounts: () => void onShowAccounts: () => void
onShowSettings: () => void onShowSettings: () => void
onShowChangelog: () => void
iframeLoading: boolean iframeLoading: boolean
}) { }) {
const { setColorMode } = useColorMode() const { setColorMode } = useColorMode()
@ -145,6 +146,10 @@ export function NavbarMenu(props: {
Manage accounts Manage accounts
<DropdownMenuShortcut> ,</DropdownMenuShortcut> <DropdownMenuShortcut> ,</DropdownMenuShortcut>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={props.onShowChangelog}>
<LucideNotebook class="mr-2 size-4" />
Changelog
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
as="a" as="a"
class="cursor-pointer" class="cursor-pointer"

View file

@ -0,0 +1,13 @@
import { type ComponentProps, splitProps } from 'solid-js'
import { cn } from '../../utils.ts'
export function Skeleton(props: ComponentProps<'div'>) {
const [local, rest] = splitProps(props, ['class'])
return (
<div
class={cn('animate-pulse rounded-md bg-primary/10', local.class)}
{...rest}
/>
)
}

View file

@ -10,6 +10,9 @@ export default defineConfig((env): UserConfig => {
} }
return { return {
define: {
'import.meta.env.BUILD_VERSION': JSON.stringify(new Date()),
},
optimizeDeps: { optimizeDeps: {
exclude: ['@mtcute/wasm'], exclude: ['@mtcute/wasm'],
}, },

View file

@ -88,6 +88,12 @@ importers:
'@nanostores/persistent': '@nanostores/persistent':
specifier: ^0.10.2 specifier: ^0.10.2
version: 0.10.2(nanostores@0.11.3) version: 0.10.2(nanostores@0.11.3)
'@tanstack/solid-query':
specifier: ^5.64.2
version: 5.64.2(solid-js@1.9.4)
'@tanstack/solid-query-persist-client':
specifier: ^5.64.2
version: 5.64.2(@tanstack/solid-query@5.64.2(solid-js@1.9.4))(solid-js@1.9.4)
'@tanstack/solid-table': '@tanstack/solid-table':
specifier: ^8.20.5 specifier: ^8.20.5
version: 8.20.5(solid-js@1.9.4) version: 8.20.5(solid-js@1.9.4)
@ -1171,6 +1177,23 @@ packages:
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tanstack/query-core@5.64.2':
resolution: {integrity: sha512-hdO8SZpWXoADNTWXV9We8CwTkXU88OVWRBcsiFrk7xJQnhm6WRlweDzMD+uH+GnuieTBVSML6xFa17C2cNV8+g==}
'@tanstack/query-persist-client-core@5.64.2':
resolution: {integrity: sha512-yhBClzqwReV7pA1J6FsyWCND++ceqv0nqv8NOrC9rTsou9XfAJ0PlSl7XFupu9ZwsBF6Ooalc9KSHa25WxJ0/Q==}
'@tanstack/solid-query-persist-client@5.64.2':
resolution: {integrity: sha512-EyX5+MJqgU4ZCiDG0l531zcs7kYi7JyDRJ1U82GS4e872WldEMmJh9PHCG6mLmrZy9DhYiQkTaiIv5XXUm1Mnw==}
peerDependencies:
'@tanstack/solid-query': ^5.64.2
solid-js: ^1.6.0
'@tanstack/solid-query@5.64.2':
resolution: {integrity: sha512-SxYRxU4hU4fBNBrFujxpohVdAcjeivCY0hZh+YkdBsyZ7NphJq6VulS4TKGt8LsGyOtMSullvsCvIlOza0n/iQ==}
peerDependencies:
solid-js: ^1.6.0
'@tanstack/solid-table@8.20.5': '@tanstack/solid-table@8.20.5':
resolution: {integrity: sha512-LsB/g/24CjBpccOcok+u+tfyqtU9SIQg5wf7ne54jRdEsy5YQnrpb5ATWZileHBduIG0p/1oE7UOA+DyjtnbDQ==} resolution: {integrity: sha512-LsB/g/24CjBpccOcok+u+tfyqtU9SIQg5wf7ne54jRdEsy5YQnrpb5ATWZileHBduIG0p/1oE7UOA+DyjtnbDQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -3734,6 +3757,23 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@tanstack/query-core@5.64.2': {}
'@tanstack/query-persist-client-core@5.64.2':
dependencies:
'@tanstack/query-core': 5.64.2
'@tanstack/solid-query-persist-client@5.64.2(@tanstack/solid-query@5.64.2(solid-js@1.9.4))(solid-js@1.9.4)':
dependencies:
'@tanstack/query-persist-client-core': 5.64.2
'@tanstack/solid-query': 5.64.2(solid-js@1.9.4)
solid-js: 1.9.4
'@tanstack/solid-query@5.64.2(solid-js@1.9.4)':
dependencies:
'@tanstack/query-core': 5.64.2
solid-js: 1.9.4
'@tanstack/solid-table@8.20.5(solid-js@1.9.4)': '@tanstack/solid-table@8.20.5(solid-js@1.9.4)':
dependencies: dependencies:
'@tanstack/table-core': 8.20.5 '@tanstack/table-core': 8.20.5