feat: column manager
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import { Col } from "@/components/col";
|
||||
import { Toolbar } from "@/components/toolbar";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { Column } from "@lume/ui";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { VList, VListHandle } from "virtua";
|
||||
|
||||
export const Route = createFileRoute("/$account/home")({
|
||||
component: Screen,
|
||||
pendingComponent: Pending,
|
||||
loader: async () => {
|
||||
const columns = [
|
||||
{ name: "Tauri v2", content: "https://beta.tauri.app" },
|
||||
{ name: "Tauri v1", content: "https://tauri.app" },
|
||||
{ name: "Lume", content: "https://lume.nu" },
|
||||
{ name: "Snort", content: "https://snort.social" },
|
||||
{ name: "Newsfeed", content: "/columns/newsfeed" },
|
||||
{ name: "Default", content: "/columns/default" },
|
||||
];
|
||||
return columns;
|
||||
},
|
||||
@@ -19,22 +19,71 @@ export const Route = createFileRoute("/$account/home")({
|
||||
|
||||
function Screen() {
|
||||
const data = Route.useLoaderData();
|
||||
const search = Route.useSearch();
|
||||
const vlistRef = useRef<VListHandle>(null);
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [isScroll, setIsScroll] = useState(false);
|
||||
|
||||
const moveLeft = () => {
|
||||
const prevIndex = Math.max(selectedIndex - 1, 0);
|
||||
setSelectedIndex(prevIndex);
|
||||
vlistRef.current.scrollToIndex(prevIndex, {
|
||||
align: "start",
|
||||
});
|
||||
};
|
||||
|
||||
const moveRight = () => {
|
||||
const nextIndex = Math.min(selectedIndex + 1, data.length - 1);
|
||||
setSelectedIndex(nextIndex);
|
||||
vlistRef.current.scrollToIndex(nextIndex, {
|
||||
align: "end",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div
|
||||
onScroll={() => setIsScroll((state) => !state)}
|
||||
className="flex h-full w-full flex-nowrap gap-3 overflow-x-auto px-3 pb-3 pt-1.5 focus:outline-none"
|
||||
<div className="h-full w-full">
|
||||
<VList
|
||||
ref={vlistRef}
|
||||
horizontal
|
||||
itemSize={440}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (!vlistRef.current) return;
|
||||
switch (e.code) {
|
||||
case "ArrowUp":
|
||||
case "ArrowLeft": {
|
||||
e.preventDefault();
|
||||
moveLeft();
|
||||
break;
|
||||
}
|
||||
case "ArrowDown":
|
||||
case "ArrowRight": {
|
||||
e.preventDefault();
|
||||
moveRight();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}}
|
||||
onScroll={() => {
|
||||
setIsScroll(true);
|
||||
}}
|
||||
onScrollEnd={() => {
|
||||
setIsScroll(false);
|
||||
}}
|
||||
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
|
||||
>
|
||||
{data.map((column, index) => (
|
||||
<Column
|
||||
<Col
|
||||
key={column.name + index}
|
||||
column={column}
|
||||
// @ts-ignore, yolo !!!
|
||||
account={search.acccount}
|
||||
isScroll={isScroll}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</VList>
|
||||
<Toolbar moveLeft={moveLeft} moveRight={moveRight} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ComposeFilledIcon, PlusIcon } from "@lume/icons";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ComposeFilledIcon,
|
||||
PlusIcon,
|
||||
} from "@lume/icons";
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||
import { cn } from "@lume/utils";
|
||||
import { Accounts } from "@/components/accounts";
|
||||
@@ -10,7 +15,7 @@ export const Route = createFileRoute("/$account")({
|
||||
|
||||
function App() {
|
||||
const ark = useArk();
|
||||
const context = Route.useRouteContext();
|
||||
const { platform } = Route.useRouteContext();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col">
|
||||
@@ -18,7 +23,7 @@ function App() {
|
||||
data-tauri-drag-region
|
||||
className={cn(
|
||||
"flex h-11 shrink-0 items-center justify-between pr-2",
|
||||
context.platform === "macos" ? "ml-2 pl-20" : "pl-4",
|
||||
platform === "macos" ? "ml-2 pl-20" : "pl-4",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -40,6 +45,7 @@ function App() {
|
||||
<ComposeFilledIcon className="size-4" />
|
||||
New post
|
||||
</button>
|
||||
<div id="toolbar" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
|
||||
16
apps/desktop2/src/routes/default.lazy.tsx
Normal file
16
apps/desktop2/src/routes/default.lazy.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Column } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/default")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Content className="flex flex-col items-center justify-center">
|
||||
<p>TODO</p>
|
||||
</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
||||
}
|
||||
85
apps/desktop2/src/routes/newsfeed.lazy.tsx
Normal file
85
apps/desktop2/src/routes/newsfeed.lazy.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { RepostNote } from "@/components/repost";
|
||||
import { Suggest } from "@/components/suggest";
|
||||
import { TextNote } from "@/components/text";
|
||||
import { useEvents } from "@lume/ark";
|
||||
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||
import { Event, Kind } from "@lume/types";
|
||||
import { Column } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
export const Route = createLazyFileRoute("/newsfeed")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
export function Screen() {
|
||||
// @ts-ignore, just work!!!
|
||||
const { name, account } = Route.useSearch();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
hasNextPage,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useEvents("local", account);
|
||||
|
||||
const renderItem = (event: Event) => {
|
||||
if (!event) return;
|
||||
switch (event.kind) {
|
||||
case Kind.Repost:
|
||||
return <RepostNote key={event.id} event={event} />;
|
||||
default:
|
||||
return <TextNote key={event.id} event={event} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Column.Root>
|
||||
<Column.Header name={name} />
|
||||
<Column.Content>
|
||||
{isLoading || isRefetching ? (
|
||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
||||
<InfoIcon className="size-6" />
|
||||
<div>
|
||||
<p className="leading-tight">{t("emptyFeedTitle")}</p>
|
||||
<p className="leading-tight">{t("emptyFeedSubtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Suggest />
|
||||
</div>
|
||||
) : (
|
||||
<Virtualizer overscan={3}>
|
||||
{data.map((item) => renderItem(item))}
|
||||
</Virtualizer>
|
||||
)}
|
||||
<div className="flex h-20 items-center justify-center">
|
||||
{hasNextPage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchNextPage()}
|
||||
disabled={!hasNextPage || isFetchingNextPage}
|
||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
<LoaderIcon className="size-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowRightCircleIcon className="size-5" />
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</Column.Content>
|
||||
</Column.Root>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user