diff --git a/src/app/(site)/app/components/feed/index.tsx b/src/app/(site)/app/components/feed/index.tsx index c9e812f..c33a748 100644 --- a/src/app/(site)/app/components/feed/index.tsx +++ b/src/app/(site)/app/components/feed/index.tsx @@ -1,43 +1,85 @@ -import { getSuapabaseServerComponent } from "@/supabase/models/index.models"; +"use client"; + import { Heading } from "@/components/ui/typography/heading"; -import { PostsGrid } from "@/components/feature/posts-grid"; -import { Suspense } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; +import { useEffect, useState } from "react"; +import { useSupabase } from "@/hooks/use-supabase"; +import { Database } from "@/supabase/types"; +import { PostsGrid } from "@/components/feature/posts-grid/posts-grid.component"; +import { PostsGridSkeleton } from "@/components/feature/posts-grid/components/posts-grid-skeleton"; + +export function Feed() { + const { supabase } = useSupabase(); + + const [posts, setPosts] = useState< + Database["public"]["Tables"]["posts"]["Row"][] + >([]); + + const [error, setError] = useState(false); + + const [isLoading, setIsLoading] = useState(true); + const [isFetching, setIsFetching] = useState(false); + + const [lastPostIndex, setLastPostIndex] = useState(1); + + useEffect(() => { + const controller = new AbortController(); + + try { + setIsLoading(true); + setIsFetching(true); + + supabase + .from("posts") + .select("*") + .order("id", { ascending: false }) + .limit(32) + .range(lastPostIndex, lastPostIndex + 32) + .abortSignal(controller.signal) + .then(({ data: posts, error }) => { + if (error) return; + + if (!posts) return; + + setPosts((prev) => [...prev, ...posts]); + }); + } catch (error: unknown) { + if ((error as Error).name === "AbortError") return; + + setError(true); + } finally { + setIsLoading(false); + setIsFetching(false); + } -export async function Feed() { - const supabase = await getSuapabaseServerComponent(); + return () => { + controller.abort(); + }; - const { data: posts, error } = await supabase - .from("posts") - .select("*") - .order("id", { ascending: false }) - .limit(24); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastPostIndex]); - const doesPostsExist = posts && posts.length > 0 && !error; + function handleScroll() { + setLastPostIndex((prev) => prev + 32); + } return ( - <> - {doesPostsExist ? ( -
- - {Array(16) - .fill(" ") - .map((_, i) => ( - - ))} - - } - > - - -
- ) : ( -
- Something wen wrong, reload the page -
+
+ {isLoading && } + {!isLoading && !isFetching && ( + <> + {!error ? ( +
+ +
+ ) : ( +
+ + Something wen wrong, please reload the page + +
+ )} + )} - +
); } diff --git a/src/app/(site)/app/layout.tsx b/src/app/(site)/app/layout.tsx index 0593fc6..2c82e52 100644 --- a/src/app/(site)/app/layout.tsx +++ b/src/app/(site)/app/layout.tsx @@ -13,7 +13,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
-
+
{children}
diff --git a/src/app/(site)/app/page.tsx b/src/app/(site)/app/page.tsx index 870165c..270338d 100644 --- a/src/app/(site)/app/page.tsx +++ b/src/app/(site)/app/page.tsx @@ -21,22 +21,8 @@ export default function AppPage() { function DesktopLayout() { return ( -
- - {Array(8) - .fill("") - .map((_, i) => ( -
  • - -
  • - ))} - - } - > - -
    +
    +
    ); } diff --git a/src/app/(site)/app/post/[postid]/components/post/components/options/index.tsx b/src/app/(site)/app/post/[postid]/components/post/components/options/index.tsx index c36b1fc..f4c24e1 100644 --- a/src/app/(site)/app/post/[postid]/components/post/components/options/index.tsx +++ b/src/app/(site)/app/post/[postid]/components/post/components/options/index.tsx @@ -45,7 +45,6 @@ export function PostOptions({ return router.push("/app"); // Navigate to '/app' if no history } } - router.refresh(); } catch (e) { console.log(e); } diff --git a/src/app/(site)/app/post/[postid]/components/post/index.tsx b/src/app/(site)/app/post/[postid]/components/post/index.tsx index e594e6e..b09c154 100644 --- a/src/app/(site)/app/post/[postid]/components/post/index.tsx +++ b/src/app/(site)/app/post/[postid]/components/post/index.tsx @@ -22,48 +22,47 @@ export async function Post({ .single(); return ( -
    -
    -
    - -

    - {title} -

    - {postOwnerProfile && ( - -
    - {postOwnerProfile.avatar_url ? ( - - ) : ( -
    - )} -

    - {postOwnerProfile.name} -

    -
    - - )} -
    +
    +
    +

    + {title} +

    + {postOwnerProfile && ( + +
    + {postOwnerProfile.avatar_url ? ( + + ) : ( +
    + )} +

    + {postOwnerProfile.name} +

    +
    + + )} +
    diff --git a/src/app/(site)/app/post/[postid]/components/recent-posts/index.ts b/src/app/(site)/app/post/[postid]/components/recent-posts/index.ts new file mode 100644 index 0000000..04da9a5 --- /dev/null +++ b/src/app/(site)/app/post/[postid]/components/recent-posts/index.ts @@ -0,0 +1 @@ +export * from "./recent-pots.component"; diff --git a/src/app/(site)/app/post/[postid]/components/recent-posts/recent-pots.component.tsx b/src/app/(site)/app/post/[postid]/components/recent-posts/recent-pots.component.tsx new file mode 100644 index 0000000..9902630 --- /dev/null +++ b/src/app/(site)/app/post/[postid]/components/recent-posts/recent-pots.component.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { PostsGridSkeleton } from "@/components/feature/posts-grid/components/posts-grid-skeleton"; +import { PostsGrid } from "@/components/feature/posts-grid/posts-grid.component"; +import { Heading } from "@/components/ui/typography/heading"; +import { useSupabase } from "@/hooks/use-supabase"; +import { Database } from "@/supabase/types"; +import { useEffect, useState } from "react"; + +export function RecentPosts({ + excludedPostId, +}: { + excludedPostId: Database["public"]["Tables"]["posts"]["Row"]["id"]; +}) { + const [posts, setPosts] = useState< + Database["public"]["Tables"]["posts"]["Row"][] + >([]); + + const [isLoading, setIsLoading] = useState(true); + const [isFetching, setIsFetching] = useState(false); + + const [lastPostIndex, setLastPostIndex] = useState(1); + const [error, setError] = useState(false); + + const { supabase } = useSupabase(); + + useEffect(() => { + const controller = new AbortController(); + + try { + setIsLoading(true); + setIsFetching(true); + + supabase + .from("posts") + .select("*") + .order("created_at", { ascending: false }) + .limit(32) + .range(lastPostIndex, lastPostIndex + 32) + .abortSignal(controller.signal) + .then(({ data: posts, error }) => { + if (error) return; + + if (!posts) return; + + setPosts((prev) => [ + ...prev, + ...posts.filter((post) => post.id !== excludedPostId), + ]); + }); + } catch (error: unknown) { + if ((error as Error).name === "AbortError") return; + + setError(true); + } finally { + setIsLoading(false); + setIsFetching(false); + } + + return () => { + controller.abort(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastPostIndex]); + + function handleScroll() { + if (isLoading || isFetching) return; + + setLastPostIndex((prev) => prev + 32); + } + + return ( +
    + {isLoading && } + {!isLoading && !isFetching && ( + <> + {!error ? ( +
    + +
    + ) : ( +
    + + Something wen wrong, please reload the page + +
    + )} + + )} +
    + ); +} diff --git a/src/app/(site)/app/post/[postid]/page.tsx b/src/app/(site)/app/post/[postid]/page.tsx index b35b0d1..898380d 100644 --- a/src/app/(site)/app/post/[postid]/page.tsx +++ b/src/app/(site)/app/post/[postid]/page.tsx @@ -1,9 +1,8 @@ import { BackwardsNav } from "@/components/feature/nav/backwards"; import { Heading } from "@/components/ui/typography/heading"; import { getSuapabaseServerComponent } from "@/supabase/models/index.models"; -import { PostsGrid } from "@/components/feature/posts-grid"; -import { Database } from "@/supabase/types"; import { Post } from "./components/post"; +import { RecentPosts } from "./components/recent-posts"; export default async function PostPage({ params: { postid }, @@ -28,15 +27,15 @@ export default async function PostPage({ ); return ( -
    -
    - - {userPosts?.length > 0 && ( -
    - -
    - )} + {dateWhenUserJoined && ( +
    + + joined in {dateWhenUserJoined} +
    + )} + + + +
    +
    - ); - } - function UserProfileNotFound() { - return <>404; - } - - return ( -
    - {doesUserProfileExist && userProfile ? ( - - ) : ( - - )}
    ); } diff --git a/src/app/(site)/app/search/components/SearchForm/SearchForm.tsx b/src/app/(site)/app/search/components/SearchForm/SearchForm.tsx index c361d8d..a9666e4 100644 --- a/src/app/(site)/app/search/components/SearchForm/SearchForm.tsx +++ b/src/app/(site)/app/search/components/SearchForm/SearchForm.tsx @@ -1,46 +1,46 @@ -'use client' -import { Input } from "@/components/ui/input" -import { useForm } from "react-hook-form" -import { useSearchForm } from "./hooks/useSearchForm" +"use client"; +import { Input } from "@/components/ui/input"; +import { useForm } from "react-hook-form"; +import { useSearchForm } from "./hooks/useSearchForm"; const formInputs = { - search: 'search' -} + search: "search", +}; -export type formType = typeof formInputs +export type formType = typeof formInputs; interface SearchFormProps { - defaultSearchValue: string + defaultSearchValue: string; } -export const SearchForm: React.FC = ({ defaultSearchValue }) => { - const { - register - } = useForm() +export const SearchForm: React.FC = ({ + defaultSearchValue, +}) => { + const { register } = useForm(); - const { changeSearchValue } = useSearchForm(defaultSearchValue) + const { changeSearchValue } = useSearchForm(defaultSearchValue); - const handleOnChangeToSearch = (event: React.ChangeEvent) => { - const newValue = event.currentTarget.value - changeSearchValue(newValue) - } + const handleOnChangeToSearch = ( + event: React.ChangeEvent, + ) => { + const newValue = event.currentTarget.value; + changeSearchValue(newValue); + }; - return ( - - - - ) -} \ No newline at end of file + return ( + + + + ); +}; diff --git a/src/app/(site)/app/search/components/SearchForm/hooks/useSearchForm.ts b/src/app/(site)/app/search/components/SearchForm/hooks/useSearchForm.ts index 7adc47d..f9909be 100644 --- a/src/app/(site)/app/search/components/SearchForm/hooks/useSearchForm.ts +++ b/src/app/(site)/app/search/components/SearchForm/hooks/useSearchForm.ts @@ -1,27 +1,25 @@ -'use client' -import { useDebounce } from "@/hooks/use-debounce" -import { useRouting } from "@/hooks/useRouting" -import { useEffect, useState } from "react" +"use client"; +import { useDebounce } from "@/hooks/use-debounce"; +import { useRouting } from "@/hooks/useRouting"; +import { useEffect, useState } from "react"; export const useSearchForm = (defaultSearchValue: string) => { - const [searchValue, setSearchValue] = useState(defaultSearchValue) - const { debouncedValue } = useDebounce(searchValue) - const { goTo } = useRouting() + const [searchValue, setSearchValue] = useState(defaultSearchValue); + const { debouncedValue } = useDebounce(searchValue, 500); + const { goTo } = useRouting(); - useEffect(() => { - const currentURL = new URL(location.origin + location.pathname) + useEffect(() => { + const currentURL = new URL(location.origin + location.pathname); - if (debouncedValue !== '') { - currentURL.searchParams.set('search_query', debouncedValue) - } + currentURL.searchParams.set("search_query", debouncedValue); - goTo(currentURL.toString()) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedValue]) + goTo(currentURL.toString()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedValue]); - const changeSearchValue = (value: string) => { - setSearchValue(value) - } + const changeSearchValue = (value: string) => { + setSearchValue(value); + }; - return { valueToSearch: debouncedValue, changeSearchValue } -} \ No newline at end of file + return { valueToSearch: debouncedValue, changeSearchValue }; +}; diff --git a/src/app/(site)/app/search/components/searched-posts-grid/index.ts b/src/app/(site)/app/search/components/searched-posts-grid/index.ts new file mode 100644 index 0000000..6c37b1c --- /dev/null +++ b/src/app/(site)/app/search/components/searched-posts-grid/index.ts @@ -0,0 +1 @@ +export * from "./searched-posts-grid.component"; diff --git a/src/app/(site)/app/search/components/searched-posts-grid/searched-posts-grid.component.tsx b/src/app/(site)/app/search/components/searched-posts-grid/searched-posts-grid.component.tsx new file mode 100644 index 0000000..b65d39f --- /dev/null +++ b/src/app/(site)/app/search/components/searched-posts-grid/searched-posts-grid.component.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { PostsGridSkeleton } from "@/components/feature/posts-grid/components/posts-grid-skeleton"; +import { PostsGrid } from "@/components/feature/posts-grid/posts-grid.component"; +import { Heading } from "@/components/ui/typography/heading"; +import { useSupabase } from "@/hooks/use-supabase"; +import { Database } from "@/supabase/types"; +import { useEffect, useState } from "react"; + +export function SearchedPostsGrid({ searchValue }: { searchValue: string }) { + const { supabase } = useSupabase(); + + const [posts, setPosts] = useState< + Database["public"]["Tables"]["posts"]["Row"][] + >([]); + + const [error, setError] = useState(false); + + const [isLoading, setIsLoading] = useState(true); + const [isFetching, setIsFetching] = useState(false); + + const [lastPostIndex, setLastPostIndex] = useState(1); + + const [notFound, setNotFound] = useState(false); + + function getAndSetPosts({ signal }: { signal: AbortSignal }) { + try { + setIsLoading(true); + setIsFetching(true); + + setNotFound(false); + + supabase + .from("posts") + .select("*") + .like("title", `%${searchValue}%`) + .order("created_at", { ascending: false }) + .range(lastPostIndex, lastPostIndex + 32) + .abortSignal(signal) + .limit(32) + .then(({ data: posts, error }) => { + if (error) return; + + if (!posts) return; + + if (posts.length === 0) { + setNotFound(true); + return; + } + setNotFound(false); + + setPosts((prev) => [...prev, ...posts]); + }); + } catch (error: unknown) { + if ((error as Error).name === "AbortError") return; + + setError(true); + } finally { + setIsLoading(false); + setIsFetching(false); + } + } + + useEffect(() => { + const controller = new AbortController(); + + getAndSetPosts({ signal: controller.signal }); + + return () => { + controller.abort(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastPostIndex]); + + useEffect(() => { + const controller = new AbortController(); + + if (isLoading || isFetching) return; + + setLastPostIndex(1); + + setPosts([]); + + getAndSetPosts({ signal: controller.signal }); + + return () => { + controller.abort(); + }; + }, [searchValue]); + + function handleScroll() { + setLastPostIndex((prev) => prev + 32); + } + + return ( +
    + {isLoading && } + {!isLoading && !isFetching && ( + <> + {!error ? ( +
    + +
    + ) : ( +
    + + Something wen wrong, please reload the page + +
    + )} + + )} + {notFound && posts.length === 0 && ( +
    + 404 not found +
    + )} +
    + ); +} diff --git a/src/app/(site)/app/search/page.tsx b/src/app/(site)/app/search/page.tsx index 354cf9d..e068359 100644 --- a/src/app/(site)/app/search/page.tsx +++ b/src/app/(site)/app/search/page.tsx @@ -1,8 +1,5 @@ -import { getSuapabaseServerComponent } from "@/supabase/models/index.models"; import { SearchForm } from "./components/SearchForm/SearchForm"; -import { PostsGrid } from "@/components/feature/posts-grid"; -import { Suspense } from "react"; -import { Skeleton } from "@/components/ui/skeleton"; +import { SearchedPostsGrid } from "./components/searched-posts-grid"; interface SearchParams { search_query?: string; @@ -13,37 +10,12 @@ interface Props { } const SearchPage: React.FC = async ({ searchParams }) => { - const supabase = await getSuapabaseServerComponent(); - const searchValue = searchParams?.search_query ?? ""; - - const { data: posts } = await supabase - .from("posts") - .select("*") - .ilike("title", `%${searchValue}%`) - .order("created_at", { ascending: false }) - .limit(24); - + const searchValue = searchParams.search_query ?? "painting"; return (
    -
    - {posts?.length !== undefined && posts?.length > 0 && ( - - {Array(16) - .fill(" ") - .map((_, i) => ( - - ))} - - } - > - - - )} -
    +
    ); diff --git a/src/components/feature/lazy-image/index.tsx b/src/components/feature/lazy-image/index.tsx index 7f2657f..5bab7b4 100644 --- a/src/components/feature/lazy-image/index.tsx +++ b/src/components/feature/lazy-image/index.tsx @@ -2,7 +2,18 @@ import { Skeleton } from "@/components/ui/skeleton"; import { ImageOff } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { type HTMLAttributes, useEffect, useRef, useState } from "react"; + +type TProps = { + src: string; + alt: string; + className?: string; + height?: number; + width?: number; + containerClassname?: HTMLAttributes["className"]; + skeletonClassName?: string; + skeletonBgColor?: string; +}; export function LazyImage({ src, @@ -12,15 +23,8 @@ export function LazyImage({ height, skeletonClassName = "", skeletonBgColor = "", -}: { - src: string; - alt: string; - className?: string; - height?: number; - width?: number; - skeletonClassName?: string; - skeletonBgColor?: string; -}) { + containerClassname = "", +}: TProps) { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -46,7 +50,7 @@ export function LazyImage({ }, []); return ( -
    +
    {!error && ( (({ children }, ref) => { + return ( +
      + {children} +
    + ); +}); + +PostsGridContainer.displayName = "PostsGridContainer"; + +export { PostsGridContainer }; diff --git a/src/components/feature/posts-grid/components/posts-grid-row/index.ts b/src/components/feature/posts-grid/components/posts-grid-row/index.ts new file mode 100644 index 0000000..2e870e8 --- /dev/null +++ b/src/components/feature/posts-grid/components/posts-grid-row/index.ts @@ -0,0 +1 @@ +export * from "./posts-grid-row.component"; diff --git a/src/components/feature/posts-grid/components/post/post.component.tsx b/src/components/feature/posts-grid/components/posts-grid-row/posts-grid-row.component.tsx similarity index 88% rename from src/components/feature/posts-grid/components/post/post.component.tsx rename to src/components/feature/posts-grid/components/posts-grid-row/posts-grid-row.component.tsx index a5101e3..a920fea 100644 --- a/src/components/feature/posts-grid/components/post/post.component.tsx +++ b/src/components/feature/posts-grid/components/posts-grid-row/posts-grid-row.component.tsx @@ -1,9 +1,9 @@ "use client"; +import Link from "next/link"; import { LazyImage } from "@/components/feature/lazy-image"; import { ClientRouting } from "@/models/routing/client"; import { Database } from "@/supabase/types"; -import Link from "next/link"; export function PostsGridRow({ post, @@ -14,7 +14,7 @@ export function PostsGridRow({ }) { const imageHeight = (post.asset_height * columnWidth) / post.asset_width; return ( -
  • +
  • + {Array(cuantity) + .fill(" ") + .map((_, i) => ( + + ))} + + ); +} diff --git a/src/components/feature/posts-grid/index.ts b/src/components/feature/posts-grid/index.ts new file mode 100644 index 0000000..d670f2d --- /dev/null +++ b/src/components/feature/posts-grid/index.ts @@ -0,0 +1 @@ +export * from "./posts-grid.component"; diff --git a/src/components/feature/posts-grid/index.tsx b/src/components/feature/posts-grid/index.tsx deleted file mode 100644 index 87b5b78..0000000 --- a/src/components/feature/posts-grid/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { PostsGrid } from "./posts-grid.component"; - -export { PostsGrid }; diff --git a/src/components/feature/posts-grid/posts-grid.component.tsx b/src/components/feature/posts-grid/posts-grid.component.tsx index dafd7f7..4ba9fe5 100644 --- a/src/components/feature/posts-grid/posts-grid.component.tsx +++ b/src/components/feature/posts-grid/posts-grid.component.tsx @@ -1,10 +1,15 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { PostsGridRow } from "./components/post"; +import { useCallback, useEffect, useRef, useState } from "react"; import { TPostsGridItem } from "./posts-grid.models"; +import { PostsGridRow } from "./components/posts-grid-row"; +import { PostsGridContainer } from "./components/posts-grid-container"; -export function PostsGrid({ posts }: { posts: TPostsGridItem[] }) { +export function PostsGrid({ + posts, + onFetchNewPosts, +}: { + posts: TPostsGridItem[]; + onFetchNewPosts: () => void; +}) { const [columnWidth, setColumnWidth] = useState(null); const containerRef = useRef(null); @@ -17,7 +22,6 @@ export function PostsGrid({ posts }: { posts: TPostsGridItem[] }) { if (containerWidth <= 0 || !containerWidth) return; const columnCount = window.innerWidth > 1024 ? 3 : 2; - console.log(window.innerWidth); setColumnWidth((containerWidth - 8 * 2) / columnCount); } @@ -27,20 +31,53 @@ export function PostsGrid({ posts }: { posts: TPostsGridItem[] }) { return () => window.removeEventListener("resize", calculateColumnWidth); }, []); + + const [lastElementRef, setLastElementRef] = useState( + null, + ); + + const lastItemRef = useCallback((node: HTMLDivElement) => { + if (!node) return; + + setLastElementRef(node); + }, []); + + useEffect(() => { + if (!lastElementRef || typeof window === "undefined") return; + + const observerOptions: IntersectionObserverInit = { + root: null, + rootMargin: `${window.innerHeight * 2}px`, + threshold: 1.0, + }; + + const observer = new IntersectionObserver(onFetchNewPosts, observerOptions); + + observer.observe(lastElementRef); + + return () => { + observer.unobserve(lastElementRef); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lastElementRef]); + return ( -
      - {posts.length > 0 && - containerRef.current && - posts.map((post) => ( - - ))} -
    + + {posts.length > 0 && containerRef.current && ( + <> + {posts.map((post, i) => ( + + ))} +
    + + )} + ); }