Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
Expand Down
107 changes: 74 additions & 33 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/app/(feed)/feed/FeedLoader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import clsx from "clsx";
import { Progress } from "~/components/ui/progress";
import { useFeeds } from "~/lib/data/feeds";
import { useFeedStatusDict, useFetchFeedItemsStatus } from "~/lib/data/store";

export function FeedLoader() {
const { feeds } = useFeeds();
const feedStatusDict = useFeedStatusDict();

const fetchedFeedsCount = Object.keys(feedStatusDict).length;
const status = useFetchFeedItemsStatus();

const isFetching = status === "fetching";

const value = isFetching ? fetchedFeedsCount : 0;

return (
<div
className={clsx("w-32 transition-opacity", {
"opacity-0": !isFetching,
"opacity-100": isFetching,
})}
>
<Progress max={feeds.length || 100} value={value} className="w-full" />
</div>
);
}
2 changes: 2 additions & 0 deletions src/app/(feed)/feed/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useShortcut } from "~/lib/hooks/useShortcut";
import { TopLeftButton } from "./TopLeftButton";
import { TopRightHeaderContent } from "./TopRightHeaderContent";
import { usePathname, useRouter } from "next/navigation";
import { FeedLoader } from "./FeedLoader";

export function Header() {
const pathname = usePathname();
Expand All @@ -17,6 +18,7 @@ export function Header() {
return (
<header className="top-0 z-20 flex w-full flex-wrap items-center justify-between gap-2 bg-transparent px-6 py-6">
<TopLeftButton />
<FeedLoader />
<TopRightHeaderContent />
</header>
);
Expand Down
226 changes: 192 additions & 34 deletions src/app/(feed)/feed/SidebarFeeds.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { CircleSmall, Edit2Icon, PlusIcon } from "lucide-react";
import {
AlertCircleIcon,
AlertTriangleIcon,
CircleSmall,
Edit2Icon,
MinusIcon,
PlusIcon,
} from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { EditFeedDialog } from "~/components/AddFeedDialog";
import { ButtonWithShortcut } from "~/components/ButtonWithShortcut";
Expand All @@ -23,7 +30,19 @@ import { doesFeedItemPassFilters } from "~/lib/data/feed-items";
import { useFeeds } from "~/lib/data/feeds";
import { useDeselectViewFilter } from "~/lib/data/views";
import { useDialogStore } from "./dialogStore";
import { useFeedItemsDict, useFeedItemsOrder } from "~/lib/data/store";
import {
useFeedItemsDict,
useFeedItemsOrder,
useFeedStatusDict,
useFetchFeedItemsStatus,
} from "~/lib/data/store";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { error } from "node:console";
import { ApplicationFeed } from "~/server/db/schema";

function useCheckFilteredFeedItemsForFeed() {
const feedItemsOrder = useFeedItemsOrder();
Expand Down Expand Up @@ -91,6 +110,10 @@ function useDebouncedState(defaultValue: string, delay: number) {
return [searchQuery, setDebouncedQuery] as const;
}

function sortFeedOptions(a: ApplicationFeed, b: ApplicationFeed) {
return a.name.localeCompare(b.name);
}

export function SidebarFeeds() {
const [searchQuery, setSearchQuery] = useDebouncedState("", 300);

Expand All @@ -107,52 +130,78 @@ export function SidebarFeeds() {
const viewFilter = useAtomValue(viewFilterAtom);
const deselectViewFilter = useDeselectViewFilter();

const feedStatusDict = useFeedStatusDict();
const fetchFeedItemsStatus = useFetchFeedItemsStatus();

const checkFilteredFeedItemsForFeed = useCheckFilteredFeedItemsForFeed();

const feedOptions = feeds?.map((category) => ({
...category,
hasEntries: !!checkFilteredFeedItemsForFeed(category.id).length,
const feedOptions = feeds?.map((feed) => ({
...feed,
hasEntries: !!checkFilteredFeedItemsForFeed(feed.id).length,
}));

const preferredFeedOptions = feedOptions
?.filter((feedOption) => {
const {
preferredFeedOptions,
feedOptionsWithContent,
emptyFeedOptions,
errorFeedOptions,
} = feedOptions?.reduce(
(acc, feedOption) => {
const {
preferredFeedOptions,
feedOptionsWithContent,
emptyFeedOptions,
errorFeedOptions,
} = acc;
if (!!searchQuery) {
const lowercaseQuery = searchQuery.toLowerCase();
const lowercaseName = feedOption.name.toLowerCase();

if (lowercaseName.includes(lowercaseQuery)) {
return true;
preferredFeedOptions.push(feedOption);
preferredFeedOptions.sort(sortFeedOptions);
return acc;
}
} else {
if (feedOption.hasEntries) return true;
if (feedOption.hasEntries) {
preferredFeedOptions.push(feedOption);
preferredFeedOptions.sort(sortFeedOptions);
return acc;
}
}

if (feedOption.id === feedFilter) {
return true;
preferredFeedOptions.push(feedOption);
preferredFeedOptions.sort(sortFeedOptions);
return acc;
}

return false;
})
.toSorted((a, b) => {
if (a.id === feedFilter) {
return -1;
}
if (b.id === feedFilter) {
return 1;
}
const feedStatus = !!feedStatusDict[feedOption.id]
? feedStatusDict[feedOption.id]
: fetchFeedItemsStatus === "fetching"
? "success"
: "empty";

return a.name.localeCompare(b.name);
});
if (feedStatus === "success") {
feedOptionsWithContent.push(feedOption);
feedOptionsWithContent.sort(sortFeedOptions);
} else if (feedStatus === "empty") {
emptyFeedOptions.push(feedOption);
emptyFeedOptions.sort(sortFeedOptions);
} else if (feedStatus === "error") {
errorFeedOptions.push(feedOption);
errorFeedOptions.sort(sortFeedOptions);
}

const otherFeedOptions = feedOptions
?.filter((feedOption) => {
return !preferredFeedOptions.some(
(option) => option.id === feedOption.id,
);
})
.toSorted((a, b) => {
return a.name.localeCompare(b.name);
});
return acc;
},
{
preferredFeedOptions: [] as typeof feedOptions,
feedOptionsWithContent: [] as typeof feedOptions,
emptyFeedOptions: [] as typeof feedOptions,
errorFeedOptions: [] as typeof feedOptions,
},
);

const hasAnyItems = !!checkFilteredFeedItemsForFeed(-1).length;

Expand Down Expand Up @@ -207,6 +256,14 @@ export function SidebarFeeds() {
</SidebarMenuButton>
</SidebarMenuItem>
{preferredFeedOptions.map((feed) => {
const feedStatus = !!feedStatusDict[feed.id]
? feedStatusDict[feed.id]
: fetchFeedItemsStatus === "fetching"
? "success"
: "empty";

const isSuccess = feedStatus === "success";

return (
<SidebarMenuItem key={feed.id} className="group flex gap-1">
<SidebarMenuButton
Expand All @@ -218,10 +275,35 @@ export function SidebarFeeds() {
}
}}
>
{!feed.hasEntries && (
{feedStatus === "error" && (
<Tooltip>
<TooltipTrigger asChild>
<AlertCircleIcon
size={16}
className="text-sidebar-accent"
/>
</TooltipTrigger>
<TooltipContent className="max-w-xs text-center">
Something went wrong fetching content for this feed. If
this continues, try deleting this feed and adding it
again with the correct URL.
</TooltipContent>
</Tooltip>
)}
{feedStatus === "empty" && (
<Tooltip>
<TooltipTrigger asChild>
<MinusIcon size={16} className="text-sidebar-accent" />
</TooltipTrigger>
<TooltipContent>
This feed has no new content within the last 30 days.
</TooltipContent>
</Tooltip>
)}
{isSuccess && !feed.hasEntries && (
<CircleSmall size={16} className="text-sidebar-accent" />
)}
{feed.hasEntries && (
{isSuccess && feed.hasEntries && (
<div className="grid size-4 place-items-center">
<div className="bg-sidebar-accent size-2.5 rounded-full" />
</div>
Expand All @@ -238,10 +320,10 @@ export function SidebarFeeds() {
</SidebarMenuItem>
);
})}
{!!preferredFeedOptions.length && !!otherFeedOptions.length && (
{!!preferredFeedOptions.length && !!feedOptionsWithContent.length && (
<hr className="my-2 opacity-50" />
)}
{otherFeedOptions.map((feed) => {
{feedOptionsWithContent.map((feed) => {
return (
<SidebarMenuItem key={feed.id} className="group flex gap-1">
<SidebarMenuButton
Expand Down Expand Up @@ -273,6 +355,82 @@ export function SidebarFeeds() {
</SidebarMenuItem>
);
})}
{!!feedOptionsWithContent.length && !!emptyFeedOptions.length && (
<hr className="my-2 opacity-50" />
)}
{emptyFeedOptions.map((feed) => {
return (
<SidebarMenuItem key={feed.id} className="group flex gap-1">
<SidebarMenuButton
variant={feed.id === feedFilter ? "outline" : "default"}
onClick={() => {
setFeedFilter(feed.id);
if (!feed.hasEntries) {
deselectViewFilter();
}
}}
>
<Tooltip>
<TooltipTrigger asChild>
<MinusIcon size={16} className="text-sidebar-accent" />
</TooltipTrigger>
<TooltipContent>
This feed has no new content within the last 30 days.
</TooltipContent>
</Tooltip>
<div className="line-clamp-1">{feed.name}</div>
</SidebarMenuButton>
<div className="group/button flex w-fit items-center justify-end">
<SidebarMenuButton
onClick={() => setSelectedFeedForEditing(feed.id)}
>
<Edit2Icon className="opacity-30 transition-opacity group-hover/button:opacity-100" />
</SidebarMenuButton>
</div>
</SidebarMenuItem>
);
})}
{!!emptyFeedOptions.length && !!errorFeedOptions.length && (
<hr className="my-2 opacity-50" />
)}
{errorFeedOptions.map((feed) => {
return (
<SidebarMenuItem key={feed.id} className="group flex gap-1">
<SidebarMenuButton
variant={feed.id === feedFilter ? "outline" : "default"}
onClick={() => {
setFeedFilter(feed.id);
if (!feed.hasEntries) {
deselectViewFilter();
}
}}
>
<Tooltip>
<TooltipTrigger asChild>
<AlertCircleIcon
size={16}
className="text-sidebar-accent"
/>
</TooltipTrigger>
<TooltipContent className="max-w-xs text-center">
Something went wrong fetching content for this feed. If
this continues, try deleting this feed and adding it again
with the correct URL.
</TooltipContent>
</Tooltip>

<div className="line-clamp-1">{feed.name}</div>
</SidebarMenuButton>
<div className="group/button flex w-fit items-center justify-end">
<SidebarMenuButton
onClick={() => setSelectedFeedForEditing(feed.id)}
>
<Edit2Icon className="opacity-30 transition-opacity group-hover/button:opacity-100" />
</SidebarMenuButton>
</div>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
</>
Expand Down
Loading