-
+
+
+
-
diff --git a/src/app/(feed)/feed/useCheckFilteredFeedItemsForFeed.tsx b/src/app/(feed)/feed/useCheckFilteredFeedItemsForFeed.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/(feed)/feed/useUpdateViewFilter.tsx b/src/app/(feed)/feed/useUpdateViewFilter.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/(feed)/layout.tsx b/src/app/(feed)/layout.tsx
index dba3a3d..af5a3ee 100644
--- a/src/app/(feed)/layout.tsx
+++ b/src/app/(feed)/layout.tsx
@@ -1,19 +1,18 @@
import "~/styles/globals.css";
import { type Metadata, type Viewport } from "next";
-import { KeyboardProvider } from "~/components/KeyboardProvider";
-import { ScrollArea } from "~/components/ui/scroll-area";
-import { AppDialogs } from "./feed/AppDialogs";
-import { Header } from "./feed/Header";
-import { ApplyColorTheme } from "~/components/color-theme/ApplyColorTheme";
+import { redirect } from "next/navigation";
import { Suspense } from "react";
+import { AppLeftSidebar, AppRightSidebar } from "~/components/app-sidebar";
+import { ApplyColorTheme } from "~/components/color-theme/ApplyColorTheme";
+import { KeyboardProvider } from "~/components/KeyboardProvider";
import { ReleaseNotifier } from "~/components/releases/ReleaseNotifier";
+import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
import { InitialClientQueries } from "~/lib/data/InitialClientQueries";
-import FeedLoading from "../loading";
import { isServerAuthed } from "~/server/auth";
-import { redirect } from "next/navigation";
-import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
-import { AppLeftSidebar, AppRightSidebar } from "~/components/app-sidebar";
+import FeedLoading from "../loading";
+import { AppDialogs } from "./feed/AppDialogs";
+import { Header } from "./feed/Header";
const title = "Serial";
const description = "Your personal content newsletter";
diff --git a/src/app/(markdown)/layout.tsx b/src/app/(markdown)/layout.tsx
index 5732b1b..71ec167 100644
--- a/src/app/(markdown)/layout.tsx
+++ b/src/app/(markdown)/layout.tsx
@@ -1,14 +1,7 @@
import "~/styles/globals.css";
-import { Inter } from "next/font/google";
-
import { type Metadata, type Viewport } from "next";
-const inter = Inter({
- subsets: ["latin"],
- variable: "--font-sans",
-});
-
const title = "Serial";
const description = "Your personal content newsletter";
diff --git a/src/app/(markdown)/releases/page.tsx b/src/app/(markdown)/releases/page.tsx
index f15d160..cc176c5 100644
--- a/src/app/(markdown)/releases/page.tsx
+++ b/src/app/(markdown)/releases/page.tsx
@@ -1,11 +1,7 @@
import Link from "next/link";
import { getReleasePages } from "~/lib/markdown/releases";
-export default async function Page({
- params,
-}: {
- params: Promise<{ slug: string }>;
-}) {
+export default async function Page() {
const releases = await getReleasePages();
return (
diff --git a/src/app/auth/AuthHeader.tsx b/src/app/auth/AuthHeader.tsx
index 6210116..2d81952 100644
--- a/src/app/auth/AuthHeader.tsx
+++ b/src/app/auth/AuthHeader.tsx
@@ -5,7 +5,11 @@ export function AuthHeader({ children }: PropsWithChildren) {
return (
-

+
{children}
diff --git a/src/app/auth/reset/AuthResetPageComponent.tsx b/src/app/auth/reset/AuthResetPageComponent.tsx
index 0287198..e3bd4e7 100644
--- a/src/app/auth/reset/AuthResetPageComponent.tsx
+++ b/src/app/auth/reset/AuthResetPageComponent.tsx
@@ -1,24 +1,24 @@
"use client";
-import { InfoIcon, Loader2, UserIcon } from "lucide-react";
+import { useQuery } from "@tanstack/react-query";
+import { InfoIcon, Loader2 } from "lucide-react";
import Link from "next/link";
+import { useSearchParams } from "next/navigation";
import { type PropsWithChildren, useState } from "react";
+import { toast } from "sonner";
+import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardFooter } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
-import { authClient, resetPassword } from "~/lib/auth-client";
+import { authClient } from "~/lib/auth-client";
import {
AUTH_PAGE_URL,
AUTH_RESET_PASSWORD_URL,
AUTH_SIGNED_OUT_URL,
} from "~/server/auth/constants";
-import { AuthHeader } from "../AuthHeader";
-import { useSearchParams } from "next/navigation";
-import { toast } from "sonner";
-import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
-import { useQuery } from "@tanstack/react-query";
import { useTRPC } from "~/trpc/react";
+import { AuthHeader } from "../AuthHeader";
function AlertPane({
title,
diff --git a/src/app/auth/sign-in.tsx b/src/app/auth/sign-in.tsx
index ad077f1..6b467d1 100644
--- a/src/app/auth/sign-in.tsx
+++ b/src/app/auth/sign-in.tsx
@@ -97,10 +97,10 @@ export default function SignIn({
password,
},
{
- onRequest: (ctx) => {
+ onRequest: () => {
setLoading(true);
},
- onResponse: (ctx) => {
+ onResponse: () => {
setLoading(false);
},
onSuccess: async () => {
diff --git a/src/app/auth/sign-up.tsx b/src/app/auth/sign-up.tsx
index 1e11100..22b1ade 100644
--- a/src/app/auth/sign-up.tsx
+++ b/src/app/auth/sign-up.tsx
@@ -108,12 +108,3 @@ export default function SignUp() {
>
);
}
-
-async function convertImageToBase64(file: File): Promise
{
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onloadend = () => resolve(reader.result as string);
- reader.onerror = reject;
- reader.readAsDataURL(file);
- });
-}
diff --git a/src/components/AddContentCategoryButton.tsx b/src/components/AddContentCategoryButton.tsx
new file mode 100644
index 0000000..f474f35
--- /dev/null
+++ b/src/components/AddContentCategoryButton.tsx
@@ -0,0 +1,20 @@
+import { useDialogStore } from "~/app/(feed)/feed/dialogStore";
+import { Button } from "./ui/button";
+import { PlusIcon } from "lucide-react";
+
+export function AddContentCategoriesButton() {
+ const launchDialog = useDialogStore((store) => store.launchDialog);
+
+ return (
+
+ );
+}
diff --git a/src/components/AddContentCategoryDialog.tsx b/src/components/AddContentCategoryDialog.tsx
new file mode 100644
index 0000000..3292b55
--- /dev/null
+++ b/src/components/AddContentCategoryDialog.tsx
@@ -0,0 +1,333 @@
+"use client";
+import { DialogTitle } from "@radix-ui/react-dialog";
+import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
+import { toast } from "sonner";
+import { useDialogStore } from "~/app/(feed)/feed/dialogStore";
+import { useFeedItemsMap, useFeedItemsOrder } from "~/lib/data/atoms";
+import { useContentCategories } from "~/lib/data/content-categories";
+import {
+ useCreateContentCategoryMutation,
+ useDeleteContentCategoryMutation,
+ useUpdateContentCategoryMutation,
+} from "~/lib/data/content-categories/mutations";
+import { useFeedCategories } from "~/lib/data/feed-categories";
+import { useFeeds } from "~/lib/data/feeds";
+import type { FeedCategorization } from "~/server/api/routers/contentCategoriesRouter";
+import type { DatabaseFeed } from "~/server/db/schema";
+import { Button } from "./ui/button";
+import { Checkbox } from "./ui/checkbox";
+import { Dialog, DialogContent, DialogHeader } from "./ui/dialog";
+import { Input } from "./ui/input";
+import { Label } from "./ui/label";
+import { ScrollArea } from "./ui/scroll-area";
+
+function CategoryNameInput({
+ name,
+ setName,
+}: {
+ name: string;
+ setName: (name: string) => void;
+}) {
+ return (
+
+
+ {
+ setName(e.target.value);
+ }}
+ />
+
+ );
+}
+
+function useMostRecentlyAppearingFeeds() {
+ const { feeds } = useFeeds();
+ const order = useFeedItemsOrder();
+ const items = useFeedItemsMap();
+
+ const feedIdsInOrder = order.map((id) => items[id]?.feedId).filter(Boolean);
+ const orderSet = new Set(feedIdsInOrder);
+
+ const foundFeeds: DatabaseFeed[] = [];
+ orderSet.forEach((entry) => {
+ const foundFeed = feeds.find((feed) => feed.id === entry);
+ if (foundFeed) {
+ foundFeeds.push(foundFeed);
+ }
+ });
+
+ return foundFeeds;
+}
+
+function CategoryFeedsInput({
+ updatedFeedIdCategorizations,
+ setUpdatedFeedIdCategorizations,
+ categoryId,
+}: {
+ updatedFeedIdCategorizations: FeedCategorization[];
+ setUpdatedFeedIdCategorizations: Dispatch<
+ SetStateAction
+ >;
+ categoryId: number | null;
+}) {
+ const { feedCategories } = useFeedCategories();
+ const mostRecentlyAppearingFeeds = useMostRecentlyAppearingFeeds();
+
+ if (mostRecentlyAppearingFeeds.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {mostRecentlyAppearingFeeds.map((feed) => {
+ const updatedIsSelected = updatedFeedIdCategorizations.find(
+ (categorization) => categorization.feedId === feed.id,
+ )?.selected;
+ const fallbackIsSelected = !!feedCategories.find(
+ (category) =>
+ category.categoryId === categoryId &&
+ category.feedId === feed.id,
+ );
+ const isSelected = updatedIsSelected ?? fallbackIsSelected;
+
+ return (
+ -
+ {
+ setUpdatedFeedIdCategorizations((categorizations) => {
+ const categorizationIndex = categorizations.findIndex(
+ (categorization) => categorization.feedId === feed.id,
+ );
+
+ const updatedCategorization: FeedCategorization = {
+ feedId: feed.id,
+ selected: Boolean(value),
+ };
+
+ if (categorizationIndex >= 0) {
+ categorizations[categorizationIndex] =
+ updatedCategorization;
+ return [...categorizations];
+ }
+
+ return [...categorizations, updatedCategorization];
+ });
+ }}
+ />
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+export function AddContentCategoryDialog() {
+ const [isAddingContentCategory, setIsAddingContentCategory] = useState(false);
+
+ const { mutateAsync: createContentCategory } =
+ useCreateContentCategoryMutation();
+
+ const [name, setName] = useState("");
+ const [updatedFeedIdCategorizations, setUpdatedFeedIdCategorizations] =
+ useState([]);
+
+ const dialog = useDialogStore((store) => store.dialog);
+ const onOpenChangeDialog = useDialogStore((store) => store.onOpenChange);
+
+ const isDisabled = !name;
+
+ const onOpenChange = (value: boolean) => {
+ onOpenChangeDialog(value);
+
+ if (!value) {
+ setName("");
+ setUpdatedFeedIdCategorizations([]);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export function EditContentCategoryDialog({
+ selectedContentCategoryId,
+ onClose,
+}: {
+ selectedContentCategoryId: null | number;
+ onClose: () => void;
+}) {
+ const [isUpdatingContentCategory, setIsUpdatingContentCategory] =
+ useState(false);
+ const [isDeletingContentCategory, setIsDeletingContentCategory] =
+ useState(false);
+
+ const { mutateAsync: updateContentCategory } =
+ useUpdateContentCategoryMutation();
+ const { mutateAsync: deleteContentCategory } =
+ useDeleteContentCategoryMutation();
+
+ const [name, setName] = useState("");
+ const [updatedFeedIdCategorizations, setUpdatedFeedIdCategorizations] =
+ useState([]);
+
+ const isFormDisabled = !name;
+
+ const { contentCategories } = useContentCategories();
+ useEffect(() => {
+ if (!contentCategories || !selectedContentCategoryId) return;
+
+ const category = contentCategories.find(
+ (v) => v.id === selectedContentCategoryId,
+ );
+ if (!category) return;
+
+ setName(category.name);
+ setUpdatedFeedIdCategorizations([]);
+ }, [contentCategories, selectedContentCategoryId]);
+
+ console.log(updatedFeedIdCategorizations);
+
+ return (
+
+ );
+}
diff --git a/src/components/AddFeedDialog.tsx b/src/components/AddFeedDialog.tsx
index e34d963..67eb081 100644
--- a/src/components/AddFeedDialog.tsx
+++ b/src/components/AddFeedDialog.tsx
@@ -2,45 +2,42 @@
import { DialogTitle } from "@radix-ui/react-dialog";
import { ImportIcon } from "lucide-react";
import Link from "next/link";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { toast } from "sonner";
import { useDialogStore } from "~/app/(feed)/feed/dialogStore";
-import { useContentCategories } from "~/lib/data/content-categories";
-import { useCreateContentCategoryMutation } from "~/lib/data/content-categories/mutations";
-import { useCreateFeedMutation } from "~/lib/data/feeds/mutations";
+import { useFeedCategories } from "~/lib/data/feed-categories";
+import { useFeeds } from "~/lib/data/feeds";
+import {
+ useCreateFeedMutation,
+ useDeleteFeedMutation,
+ useEditFeedMutation,
+} from "~/lib/data/feeds/mutations";
import { validateFeedUrl } from "~/server/rss/validateFeedUrl";
-import { useTRPC } from "~/trpc/react";
+import { ViewCategoriesInput } from "./AddViewDialog";
import { Button } from "./ui/button";
-import { Combobox } from "./ui/combobox";
import { Dialog, DialogContent, DialogHeader } from "./ui/dialog";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
export function AddFeedDialog() {
- const trpc = useTRPC();
const [feedUrl, setFeedUrl] = useState("");
const [isAddingFeed, setIsAddingFeed] = useState(false);
const { mutateAsync: createFeed } = useCreateFeedMutation();
- const {
- contentCategories,
- contentCategoriesQuery: {
- refetch: refetchCategories,
- isLoading: isLoadingCategories,
- },
- } = useContentCategories();
- const [categoryName, setCategoryName] = useState(null);
+ const [selectedCategories, setSelectedCategories] = useState([]);
- const addCategory = useCreateContentCategoryMutation();
+ const dialog = useDialogStore((store) => store.dialog);
+ const onDialogOpenChange = useDialogStore((store) => store.onOpenChange);
- const categoryOptions = contentCategories?.map((category) => ({
- value: category.name,
- label: category.name,
- }));
+ const onOpenChange = (open = false) => {
+ onDialogOpenChange(open);
- const dialog = useDialogStore((store) => store.dialog);
- const onOpenChange = useDialogStore((store) => store.onOpenChange);
+ if (!open) {
+ setFeedUrl("");
+ setSelectedCategories([]);
+ }
+ };
return (