Files
markone/packages/web/src/app/bookmarks.tsx

621 lines
16 KiB
TypeScript
Raw Normal View History

2025-05-06 11:00:35 +01:00
import type { LinkBookmark } from "@markone/core/bookmark"
2025-05-07 23:40:03 +01:00
import { createFileRoute, useNavigate } from "@tanstack/react-router"
2025-05-06 11:00:35 +01:00
import clsx from "clsx"
2025-05-07 23:09:14 +01:00
import { useEffect, useId, useState } from "react"
2025-05-06 11:00:35 +01:00
import { create } from "zustand"
2025-05-07 23:09:14 +01:00
import { fetchApi, useAuthenticatedQuery, BadRequestError, ApiErrorCode } from "~/api"
import { useCreateBookmark, useDeleteBookmark } from "~/bookmark/api"
2025-05-06 11:00:35 +01:00
import { Button } from "~/components/button"
2025-05-07 23:09:14 +01:00
import { Dialog, DialogActionRow, DialogBody, DialogTitle } from "~/components/dialog"
import { Message, MessageVariant } from "~/components/message"
import { FormField } from "~/components/form-field"
2025-05-07 16:57:09 +01:00
import { LoadingSpinner } from "~/components/loading-spinner"
2025-05-07 15:47:08 +01:00
import { useMnemonics } from "~/hooks/use-mnemonics"
2025-05-07 23:40:03 +01:00
import { useLogOut } from "~/auth"
2025-05-03 23:27:36 +01:00
const LAYOUT_MODE = {
popup: "popup",
sideBySide: "side-by-side",
2025-05-06 11:00:35 +01:00
} as const
type LayoutMode = (typeof LAYOUT_MODE)[keyof typeof LAYOUT_MODE]
2025-05-03 23:27:36 +01:00
2025-05-07 15:47:08 +01:00
enum ActiveDialog {
None = "None",
AddBookmark = "AddBookmark",
DeleteBookmark = "DeleteBookmark",
}
2025-05-03 23:27:36 +01:00
interface BookmarkPageState {
2025-05-07 15:47:08 +01:00
bookmarkCount: number
selectedBookmarkId: string
2025-05-06 11:00:35 +01:00
selectedBookmarkIndex: number
isBookmarkItemExpanded: boolean
isBookmarkPreviewOpened: boolean
2025-05-07 15:47:08 +01:00
bookmarkToBeDeleted: LinkBookmark | null
2025-05-06 11:00:35 +01:00
layoutMode: LayoutMode
2025-05-07 15:47:08 +01:00
activeDialog: ActiveDialog
2025-05-06 11:00:35 +01:00
2025-05-07 15:47:08 +01:00
setActiveDialog: (dialog: ActiveDialog) => void
2025-05-06 11:00:35 +01:00
setBookmarkItemExpanded: (isExpanded: boolean) => void
setBookmarkPreviewOpened: (isOpened: boolean) => void
setLayoutMode: (mode: LayoutMode) => void
2025-05-07 15:47:08 +01:00
selectBookmark: (bookmark: LinkBookmark, index: number) => void
reconcileSelection: (bookmarks: LinkBookmark[]) => void
markBookmarkForDeletion: (bookmark: LinkBookmark | null) => void
2025-05-03 23:27:36 +01:00
}
const useBookmarkPageStore = create<BookmarkPageState>()((set, get) => ({
2025-05-07 15:47:08 +01:00
bookmarkCount: 0,
bookmarks: [],
selectedBookmarkId: "",
2025-05-03 23:27:36 +01:00
selectedBookmarkIndex: 0,
isBookmarkItemExpanded: false,
isBookmarkPreviewOpened: false,
2025-05-07 15:47:08 +01:00
bookmarkToBeDeleted: null,
2025-05-03 23:27:36 +01:00
layoutMode: LAYOUT_MODE.popup,
2025-05-07 15:47:08 +01:00
activeDialog: ActiveDialog.None,
setActiveDialog(dialog: ActiveDialog) {
set({ activeDialog: dialog })
},
2025-05-03 23:27:36 +01:00
setBookmarkItemExpanded(isExpanded: boolean) {
2025-05-06 11:00:35 +01:00
set({ isBookmarkItemExpanded: isExpanded })
2025-05-03 23:27:36 +01:00
},
setBookmarkPreviewOpened(isOpened: boolean) {
2025-05-06 11:00:35 +01:00
set({ isBookmarkPreviewOpened: isOpened })
2025-05-03 23:27:36 +01:00
},
setLayoutMode(mode: LayoutMode) {
2025-05-06 11:00:35 +01:00
set({ layoutMode: mode })
2025-05-03 23:27:36 +01:00
},
2025-05-07 15:47:08 +01:00
selectBookmark(bookmark: LinkBookmark, index: number) {
set({ selectedBookmarkId: bookmark.id, selectedBookmarkIndex: index })
2025-05-03 23:27:36 +01:00
},
2025-05-07 15:47:08 +01:00
reconcileSelection(bookmarks: LinkBookmark[]) {
const { selectedBookmarkId, selectedBookmarkIndex } = get()
const newIndex = bookmarks.findIndex((bookmark) => bookmark.id === selectedBookmarkId)
if (newIndex !== selectedBookmarkIndex) {
if (newIndex >= 0) {
set({ selectedBookmarkIndex: newIndex })
} else if (selectedBookmarkIndex >= bookmarks.length - 1) {
set({ selectedBookmarkIndex: bookmarks.length - 1 })
} else if (selectedBookmarkIndex === 0) {
set({ selectedBookmarkIndex: 0 })
} else {
set({ selectedBookmarkIndex: selectedBookmarkIndex + 1 })
2025-05-03 23:27:36 +01:00
}
}
2025-05-07 15:47:08 +01:00
},
2025-05-03 23:27:36 +01:00
2025-05-07 15:47:08 +01:00
markBookmarkForDeletion(bookmark: LinkBookmark | null) {
set({ bookmarkToBeDeleted: bookmark })
},
}))
2025-05-03 23:27:36 +01:00
2025-05-07 15:47:08 +01:00
function Page() {
const setLayoutMode = useBookmarkPageStore((state) => state.setLayoutMode)
2025-05-03 23:27:36 +01:00
useEffect(() => {
function mediaQueryListener(this: MediaQueryList) {
if (this.matches) {
2025-05-06 11:00:35 +01:00
setLayoutMode(LAYOUT_MODE.sideBySide)
2025-05-03 23:27:36 +01:00
} else {
2025-05-06 11:00:35 +01:00
setLayoutMode(LAYOUT_MODE.popup)
2025-05-03 23:27:36 +01:00
}
}
2025-05-06 11:00:35 +01:00
const q = window.matchMedia("(width >= 64rem)")
q.addEventListener("change", mediaQueryListener)
2025-05-03 23:27:36 +01:00
2025-05-06 11:00:35 +01:00
mediaQueryListener.call(q)
2025-05-03 23:27:36 +01:00
return () => {
2025-05-06 11:00:35 +01:00
q.removeEventListener("change", mediaQueryListener)
}
}, [setLayoutMode])
2025-05-03 23:27:36 +01:00
return (
2025-05-07 15:47:08 +01:00
<div className="relative">
2025-05-03 23:27:36 +01:00
<Main>
2025-05-07 15:47:08 +01:00
<div className="flex flex-col md:flex-row justify-center py-16 lg:py-32">
2025-05-03 23:27:36 +01:00
<header className="mb-4 md:mb-0 md:mr-16 text-start">
<h1 className="font-bold text-start">
<span className="invisible md:hidden">&gt;&nbsp;</span>
YOUR BOOKMARKS
</h1>
</header>
2025-05-07 15:47:08 +01:00
<BookmarkListSection />
2025-05-03 23:27:36 +01:00
</div>
<BookmarkPreview />
</Main>
2025-05-07 15:47:08 +01:00
<ActionBar />
<PageDialog />
2025-05-03 23:27:36 +01:00
</div>
2025-05-06 11:00:35 +01:00
)
2025-05-03 23:27:36 +01:00
}
function Main({ children }: React.PropsWithChildren) {
2025-05-06 11:00:35 +01:00
const isPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
2025-05-03 23:27:36 +01:00
return (
<main
className={clsx(
"px-4 lg:px-8 2xl:px-0 grid flex justify-center relative w-full",
2025-05-06 11:00:35 +01:00
isPreviewOpened && layoutMode === LAYOUT_MODE.sideBySide ? "grid-cols-2" : "grid-cols-1",
2025-05-03 23:27:36 +01:00
)}
>
{children}
</main>
2025-05-06 11:00:35 +01:00
)
2025-05-03 23:27:36 +01:00
}
2025-05-07 15:47:08 +01:00
function PageDialog() {
const dialog = useBookmarkPageStore((state) => state.activeDialog)
switch (dialog) {
case ActiveDialog.None:
return null
case ActiveDialog.AddBookmark:
2025-05-07 23:09:14 +01:00
return <AddBookmarkDialog />
2025-05-07 15:47:08 +01:00
case ActiveDialog.DeleteBookmark:
return <DeleteBookmarkDialog />
}
}
function DeleteBookmarkDialog() {
// biome-ignore lint/style/noNonNullAssertion: this cannot be null when delete bookmark dialog is visible
const bookmark = useBookmarkPageStore((state) => state.bookmarkToBeDeleted!)
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
const markBookmarkForDeletion = useBookmarkPageStore((state) => state.markBookmarkForDeletion)
const deleteBookmarkMutation = useDeleteBookmark()
useMnemonics(
{
2025-05-07 16:57:09 +01:00
y: proceed,
n: cancel,
2025-05-07 15:47:08 +01:00
},
{ active: true },
)
async function proceed() {
try {
await deleteBookmarkMutation.mutateAsync({ bookmark })
setActiveDialog(ActiveDialog.None)
markBookmarkForDeletion(null)
} catch (error) {
console.error(error)
}
}
function cancel() {
setActiveDialog(ActiveDialog.None)
markBookmarkForDeletion(null)
}
2025-05-07 16:57:09 +01:00
function body() {
switch (deleteBookmarkMutation.status) {
case "pending":
return (
<p>
Deleting <LoadingSpinner />
</p>
)
case "idle":
return (
<p>
The bookmark titled{" "}
<strong>
<em>"{bookmark.title}"</em>
</strong>{" "}
will be deleted. Proceed?
</p>
)
case "error":
return <p className="text-red-500">Failed to delete the bookmark!</p>
}
}
function title() {
switch (deleteBookmarkMutation.status) {
case "pending":
return "PLEASE WAIT"
case "idle":
return "CONFIRM"
case "error":
return "ERROR OCCURRED"
}
}
2025-05-07 15:47:08 +01:00
return (
<Dialog>
2025-05-07 16:57:09 +01:00
<DialogTitle>{title()}</DialogTitle>
<DialogBody>{body()}</DialogBody>
2025-05-07 15:47:08 +01:00
<DialogActionRow>
<Button disabled={deleteBookmarkMutation.isPending} onClick={proceed}>
2025-05-07 16:57:09 +01:00
{deleteBookmarkMutation.isError ? "Retry" : "Proceed"} (y)
2025-05-07 15:47:08 +01:00
</Button>
<Button disabled={deleteBookmarkMutation.isPending} onClick={cancel}>
2025-05-07 16:57:09 +01:00
Cancel (n)
2025-05-07 15:47:08 +01:00
</Button>
</DialogActionRow>
</Dialog>
)
}
2025-05-07 23:09:14 +01:00
function AddBookmarkDialog() {
const [isWebsiteUnreachable, setIsWebsiteUnreachable] = useState(false)
const createBookmarkMutation = useCreateBookmark()
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
const formId = useId()
useMnemonics(
{
c: cancel,
},
{ active: true },
)
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const url = formData.get("link")
if (url && typeof url === "string") {
try {
await createBookmarkMutation.mutateAsync({ url, force: isWebsiteUnreachable })
} catch (error) {
if (error instanceof BadRequestError && error.code === ApiErrorCode.WebsiteUnreachable) {
setIsWebsiteUnreachable(true)
}
}
}
}
function cancel() {
setActiveDialog(ActiveDialog.None)
}
function message() {
if (createBookmarkMutation.isPending) {
return (
<p>
Loading <LoadingSpinner />
</p>
)
}
if (isWebsiteUnreachable) {
return (
<Message variant={MessageVariant.Warning} className="px-4">
The link does not seem to be reachable. Click "SAVE" to save anyways.
</Message>
)
}
if (createBookmarkMutation.status === "error") {
return (
<Message variant={MessageVariant.Error} className="px-4">
An error occurred when saving bookmark
</Message>
)
}
return null
}
return (
<Dialog>
<DialogTitle>NEW BOOKMARK</DialogTitle>
<DialogBody>
{message()}
<form id={formId} className="px-8" onSubmit={onSubmit}>
<FormField
type="text"
name="link"
label="LINK"
className="w-full"
labelClassName="bg-stone-300 dark:bg-stone-800"
/>
</form>
</DialogBody>
<DialogActionRow>
<Button type="submit" disabled={createBookmarkMutation.isPending} form={formId}>
SAVE
</Button>
<Button type="button" disabled={createBookmarkMutation.isPending} onClick={cancel}>
<span className="underline">C</span>ANCEL
</Button>
</DialogActionRow>
</Dialog>
)
}
2025-05-07 15:47:08 +01:00
function BookmarkListSection() {
2025-05-07 16:57:09 +01:00
const { data: bookmarks, status } = useAuthenticatedQuery(["bookmarks"], () =>
2025-05-07 23:09:14 +01:00
fetchApi("/bookmarks").then((res) => res.json()),
2025-05-07 16:57:09 +01:00
)
2025-05-07 15:47:08 +01:00
switch (status) {
case "pending":
2025-05-07 16:57:09 +01:00
return (
<p className="container max-w-2xl">
Loading&nbsp;
<LoadingSpinner />
</p>
)
2025-05-07 15:47:08 +01:00
case "success":
2025-05-07 16:57:09 +01:00
if (bookmarks) {
return <BookmarkList bookmarks={bookmarks} />
}
return null
2025-05-07 15:47:08 +01:00
default:
return null
}
}
function BookmarkList({ bookmarks }: { bookmarks: LinkBookmark[] }) {
const reconcileSelection = useBookmarkPageStore((state) => state.reconcileSelection)
useEffect(() => {
reconcileSelection(bookmarks)
}, [bookmarks, reconcileSelection])
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
const state = useBookmarkPageStore.getState()
switch (event.key) {
case "ArrowDown": {
const nextIndex = state.selectedBookmarkIndex + 1
if (nextIndex < bookmarks.length) {
state.selectBookmark(bookmarks[nextIndex], nextIndex)
}
break
}
case "ArrowUp": {
const prevIndex = state.selectedBookmarkIndex - 1
if (prevIndex >= 0) {
state.selectBookmark(bookmarks[prevIndex], prevIndex)
}
break
}
case "ArrowLeft":
state.setBookmarkItemExpanded(false)
break
case "ArrowRight":
state.setBookmarkItemExpanded(true)
break
default:
break
}
}
window.addEventListener("keydown", onKeyDown)
return () => {
window.removeEventListener("keydown", onKeyDown)
}
}, [bookmarks])
return (
<div className="flex flex-col container max-w-2xl -mt-2">
{bookmarks.length === 0 ? (
<p className="mt-2">You have not saved any bookmark!</p>
) : (
bookmarks.map((bookmark, i) => <BookmarkListItem key={bookmark.id} index={i} bookmark={bookmark} />)
)}
</div>
)
}
2025-05-03 23:27:36 +01:00
function BookmarkPreview() {
2025-05-06 11:00:35 +01:00
const isVisible = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
const layoutMode = useBookmarkPageStore((state) => state.layoutMode)
2025-05-03 23:27:36 +01:00
if (!isVisible) {
2025-05-06 11:00:35 +01:00
return null
2025-05-03 23:27:36 +01:00
}
return (
<div
className={clsx(
"h-screen flex justify-center items-center border-l border-stone-700 dark:border-stone-300 flex dark:bg-stone-900",
{
"absolute inset-0 border-l-0": layoutMode === LAYOUT_MODE.popup,
},
)}
>
<p>Content here</p>
</div>
2025-05-06 11:00:35 +01:00
)
2025-05-03 23:27:36 +01:00
}
2025-05-06 11:00:35 +01:00
function BookmarkListItem({ bookmark, index }: { bookmark: LinkBookmark; index: number }) {
const url = new URL(bookmark.url)
2025-05-07 15:47:08 +01:00
const selectedBookmarkIndex = useBookmarkPageStore((state) => state.selectedBookmarkIndex)
2025-05-06 11:00:35 +01:00
const isBookmarkItemExpanded = useBookmarkPageStore((state) => state.isBookmarkItemExpanded)
2025-05-07 15:47:08 +01:00
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
2025-05-06 11:00:35 +01:00
const setBookmarkItemExpanded = useBookmarkPageStore((state) => state.setBookmarkItemExpanded)
2025-05-07 15:47:08 +01:00
const selectBookmark = useBookmarkPageStore((state) => state.selectBookmark)
2025-05-06 11:00:35 +01:00
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
2025-05-07 15:47:08 +01:00
const markBookmarkForDeletion = useBookmarkPageStore((state) => state.markBookmarkForDeletion)
const isSelected = selectedBookmarkIndex === index
useMnemonics(
{
d: deleteItem,
},
{ active: isSelected },
)
function deleteItem() {
if (isSelected) {
markBookmarkForDeletion(bookmark)
setActiveDialog(ActiveDialog.DeleteBookmark)
}
}
2025-05-03 23:27:36 +01:00
function expandOrOpenPreview() {
2025-05-06 11:00:35 +01:00
setBookmarkItemExpanded(true)
2025-05-03 23:27:36 +01:00
if (useBookmarkPageStore.getState().layoutMode === LAYOUT_MODE.sideBySide) {
2025-05-06 11:00:35 +01:00
console.log(useBookmarkPageStore.getState().layoutMode)
setBookmarkPreviewOpened(true)
2025-05-03 23:27:36 +01:00
}
}
return (
<div
className={clsx("group flex flex-row justify-start py-2", {
"bg-teal-600 text-stone-100": isBookmarkItemExpanded && isSelected,
"text-teal-600": isSelected && !isBookmarkItemExpanded,
})}
onMouseEnter={() => {
if (!isBookmarkItemExpanded) {
2025-05-07 15:47:08 +01:00
selectBookmark(bookmark, index)
2025-05-03 23:27:36 +01:00
}
}}
>
<button
2025-05-06 11:00:35 +01:00
type="button"
2025-05-03 23:27:36 +01:00
disabled={!isSelected}
2025-05-06 11:00:35 +01:00
className={clsx("select-none flex items-start font-bold hover:bg-teal-600 hover:text-stone-100", {
invisible: !isSelected,
})}
2025-05-03 23:27:36 +01:00
onClick={() => {
2025-05-06 11:00:35 +01:00
setBookmarkItemExpanded(!isBookmarkItemExpanded)
setBookmarkPreviewOpened(false)
2025-05-03 23:27:36 +01:00
}}
>
<span className="sr-only">Options for this bookmark</span>
<span>&nbsp;</span>
<span className={isBookmarkItemExpanded ? "rotate-90" : ""}>&gt;</span>
<span>&nbsp;</span>
</button>
<div className="flex flex-col w-full">
2025-05-06 11:00:35 +01:00
<button type="button" className="text-start font-bold" onClick={expandOrOpenPreview}>
2025-05-03 23:27:36 +01:00
{bookmark.title}
</button>
<p className="opacity-80 text-sm">{url.host}</p>
{isBookmarkItemExpanded && isSelected ? (
<div className="flex flex-col space-y-1 md:flex-row md:space-y-0 md:space-x-2 items-end justify-between pt-2">
2025-05-06 11:00:35 +01:00
<p className="text-sm">#dev #devops #devops #devops #devops #devops #devops</p>
2025-05-03 23:27:36 +01:00
<div className="flex space-x-2">
<OpenBookmarkPreviewButton />
2025-05-07 15:47:08 +01:00
<Button variant="light" className="text-sm">
2025-05-03 23:27:36 +01:00
<span className="underline">E</span>dit
</Button>
2025-05-07 15:47:08 +01:00
<Button variant="light" className="text-sm" onClick={deleteItem}>
2025-05-03 23:27:36 +01:00
<span className="underline">D</span>elete
</Button>
<span className="-ml-2">&nbsp;</span>
</div>
</div>
) : null}
</div>
</div>
2025-05-06 11:00:35 +01:00
)
2025-05-03 23:27:36 +01:00
}
function OpenBookmarkPreviewButton() {
2025-05-06 11:00:35 +01:00
const isBookmarkPreviewOpened = useBookmarkPageStore((state) => state.isBookmarkPreviewOpened)
const setBookmarkPreviewOpened = useBookmarkPageStore((state) => state.setBookmarkPreviewOpened)
const setBookmarkItemExpanded = useBookmarkPageStore((state) => state.setBookmarkItemExpanded)
2025-05-03 23:27:36 +01:00
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (isBookmarkPreviewOpened && event.key === "c") {
2025-05-06 11:00:35 +01:00
closePreview()
2025-05-03 23:27:36 +01:00
} else if (!isBookmarkPreviewOpened && event.key === "o") {
2025-05-06 11:00:35 +01:00
openPreview()
2025-05-03 23:27:36 +01:00
}
}
2025-05-06 11:00:35 +01:00
window.addEventListener("keydown", onKeyDown)
2025-05-03 23:27:36 +01:00
return () => {
2025-05-06 11:00:35 +01:00
window.removeEventListener("keydown", onKeyDown)
}
}, [isBookmarkPreviewOpened])
2025-05-03 23:27:36 +01:00
function closePreview() {
2025-05-06 11:00:35 +01:00
setBookmarkPreviewOpened(false)
setBookmarkItemExpanded(false)
2025-05-03 23:27:36 +01:00
}
function openPreview() {
2025-05-06 11:00:35 +01:00
setBookmarkPreviewOpened(true)
2025-05-03 23:27:36 +01:00
}
return (
<Button
2025-05-07 15:47:08 +01:00
variant="light"
2025-05-03 23:27:36 +01:00
className="text-sm"
onClick={() => {
if (isBookmarkPreviewOpened) {
2025-05-06 11:00:35 +01:00
closePreview()
2025-05-03 23:27:36 +01:00
} else {
2025-05-06 11:00:35 +01:00
openPreview()
2025-05-03 23:27:36 +01:00
}
}}
>
{isBookmarkPreviewOpened ? (
<>
<span className="underline">C</span>lose
</>
) : (
<>
<span className="underline">O</span>pen
</>
)}
</Button>
2025-05-06 11:00:35 +01:00
)
2025-05-03 23:27:36 +01:00
}
2025-05-07 15:47:08 +01:00
function ActionBar() {
2025-05-07 23:09:14 +01:00
const setActiveDialog = useBookmarkPageStore((state) => state.setActiveDialog)
useMnemonics(
{
a: addBookmark,
},
{ active: true },
)
function addBookmark() {
setActiveDialog(ActiveDialog.AddBookmark)
}
2025-05-07 15:47:08 +01:00
return (
<div className="fixed z-10 bottom-0 left-0 right-0 border-t-1 flex flex-row justify-center py-4 space-x-4">
2025-05-07 23:09:14 +01:00
<Button onClick={addBookmark}>
2025-05-07 15:47:08 +01:00
<span className="underline">A</span>DD
</Button>
<Button>
<span className="underline">S</span>EARCH
</Button>
2025-05-07 23:40:03 +01:00
<LogOutButton />
2025-05-07 15:47:08 +01:00
</div>
)
}
2025-05-07 23:40:03 +01:00
function LogOutButton() {
const logOutMutation = useLogOut()
const navigate = useNavigate()
function logOut() {
logOutMutation.mutate()
navigate({ to: "/", replace: true })
}
return (
<Button disabled={logOutMutation.isPending} onClick={logOut}>
LOG OUT
</Button>
)
}
2025-05-06 11:00:35 +01:00
export const Route = createFileRoute("/bookmarks")({
2025-05-03 23:27:36 +01:00
component: Page,
2025-05-06 11:00:35 +01:00
})