diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index af2268f..a8da174 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,8 +7,6 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME_FRONTEND: ${{ github.repository }}-frontend - IMAGE_NAME_SERVER: ${{ github.repository }}-server permissions: contents: write diff --git a/src/components/header.tsx b/src/components/header.tsx index 13f3921..771d622 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,21 +1,34 @@ import { useAuthMethods } from "@/hooks/useAuthMethods"; import { usePocketBase } from "@/pocketbase"; import { useAuth } from "@/pocketbase/auth"; +import { Post } from "@/pocketbase/models"; +import { getRelativeTime } from "@/utils/relativeTime"; import { + ActionIcon, Anchor, - Avatar, + Badge, Box, Button, + Center, + CloseButton, + Drawer, Group, + Image, Header as MantineHeader, MediaQuery, Menu, + Paper, + ScrollArea, + Stack, Text, Title, + Tooltip, UnstyledButton, em, getBreakpointValue, } from "@mantine/core"; +import { IMAGE_MIME_TYPE } from "@mantine/dropzone"; +import { useDisclosure } from "@mantine/hooks"; import { IconCaretDown, IconCirclePlus, @@ -24,6 +37,9 @@ import { } from "@tabler/icons-react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { Record } from "pocketbase"; +import { useQuery } from "react-query"; +import UserAvatar from "./userAvatar"; interface HeaderProps { signUpEnabled: boolean; @@ -36,110 +52,286 @@ function Header({ signUpEnabled }: HeaderProps) { const { user } = useAuth(); const { usernamePasswordEnabled } = useAuthMethods(); + const { data: userPosts, isLoading } = useQuery( + ["userPosts", user?.id], + ({ queryKey }) => { + const [_, id] = queryKey; + + if (!id) return; + + return pb.collection("posts").getList(1, 20, { + filter: `author.id = "${id}"`, + expand: "files,author", + sort: "-created", + $autoCancel: false, + }); + } + ); + + const [opened, { toggle, close }] = useDisclosure(false); + return ( - - - - ({ - [`@media (max-width: ${em( - getBreakpointValue(theme.breakpoints.sm) - 1 - )})`]: { - fontSize: theme.headings.sizes.h3.fontSize, - }, - })} - > - Share Me - - - - {user ? ( - <> - {router.asPath !== "/posts/create" && ( + <> + + + + - + + {user?.username} - )} - - - + + + + My Posts + + + {userPosts?.items.map((post) => ( + ({ - ":hover": { - background: theme.colors.dark[4], - borderRadius: theme.radius.md, - }, + ":hover": { background: theme.colors.dark[7] }, })} - pr="sm" - py={2} - > - - - - {user.username} - - - - - - - - } + shadow="md" + p="md" component={Link} - href={`/users/${user.id}`} + href={`/posts/${post.id}`} > - Profile - - - } - onClick={() => pb.authStore.clear()} - > - Log Out - - - - - ) : ( - + + + + + + + {getRelativeTime( + new Date(), + new Date(post.created) + )} + + + + {post.title || + `Post by ${ + (post.expand.author as Record).username + }`} + + + {post.public && Public} + {post.nsfw && NSFW} + + + + {Array.isArray(post.expand.files) && ( + + {IMAGE_MIME_TYPE.includes(post.expand.files[0].type) ? ( + { + ) : ( + + )} + + + ))} + + + + - {usernamePasswordEnabled && signUpEnabled && ( + + + + + + + ({ + [`@media (max-width: ${em( + getBreakpointValue(theme.breakpoints.sm) - 1 + )})`]: { + fontSize: theme.headings.sizes.h3.fontSize, + }, + })} + > + Share Me + + + + {user ? ( + <> + {router.asPath !== "/posts/create" && ( + + + + )} + + + + ({ + ":hover": { + background: theme.colors.dark[4], + borderRadius: theme.radius.md, + }, + })} + pr="sm" + py={2} + > + + + + {user.username} + + + + + + + + } + component={Link} + href={`/users/${user.id}`} + > + Profile + + + } + onClick={() => pb.authStore.clear()} + > + Log Out + + + + + + + + + + + ) : ( + - )} - - )} - - + {usernamePasswordEnabled && signUpEnabled && ( + + )} + + )} + + + ); } diff --git a/src/components/layout.tsx b/src/components/layout.tsx index 36f23fb..d6208d0 100644 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -1,34 +1,20 @@ -import { usePocketBase } from "@/pocketbase"; -import { useAuth } from "@/pocketbase/auth"; -import { Post } from "@/pocketbase/models"; -import { getRelativeTime } from "@/utils/relativeTime"; import { ActionIcon, AppShell, - Badge, Box, Center, - Drawer, Group, - Image, - Paper, - ScrollArea, - Stack, + MediaQuery, Text, - Title, - Tooltip, useMantineTheme, } from "@mantine/core"; -import { IMAGE_MIME_TYPE } from "@mantine/dropzone"; -import { useDisclosure } from "@mantine/hooks"; +import { IconCirclePlus, IconPhotoPlus } from "@tabler/icons-react"; import Link from "next/link"; -import { Record } from "pocketbase"; +import { useRouter } from "next/router"; import React, { useEffect, useRef, useState } from "react"; -import { SlDrawer } from "react-icons/sl"; -import Header from "./header"; import { usePasteFiles } from "../hooks/usePasteFiles"; import { MEDIA_MIME_TYPE } from "../utils/mediaTypes"; -import { IconPhotoPlus } from "@tabler/icons-react"; +import Header from "./header"; interface LayoutProps { children?: React.ReactNode; @@ -37,28 +23,9 @@ interface LayoutProps { } function Layout({ children, signUpEnabled, onFiles }: LayoutProps) { - const pb = usePocketBase(); - const { user } = useAuth(); - + const router = useRouter(); const theme = useMantineTheme(); - const [opened, { toggle, close }] = useDisclosure(false); - - const [userPosts, setUserPosts] = useState(); - - useEffect(() => { - user && - (async () => { - const records = await pb.collection("posts").getList(1, 20, { - filter: `author.id = "${user.id}"`, - expand: "files,author", - sort: "-created", - $autoCancel: false, - }); - setUserPosts(records.items); - })(); - }, [user, setUserPosts, pb]); - const [dragging, setDragging] = useState(false); const dragEnterHandler = useRef<(ev: DragEvent) => void>(); @@ -121,104 +88,6 @@ function Layout({ children, signUpEnabled, onFiles }: LayoutProps) { return ( }> - - - {userPosts?.map((post) => ( - ({ - ":hover": { background: theme.colors.dark[7] }, - })} - shadow="md" - p="md" - component={Link} - href={`/posts/${post.id}`} - > - - - - - - - {getRelativeTime(new Date(), new Date(post.created))} - - - - {post.title || - `Post by ${(post.expand.author as Record).username}`} - - - {post.public && Public} - {post.nsfw && NSFW} - - - - {Array.isArray(post.expand.files) && ( - - {IMAGE_MIME_TYPE.includes(post.expand.files[0].type) ? ( - { - ) : ( - - )} - - - ))} - - {dragging && ( )} - {children} - {user && ( - - - + {router.asPath !== "/posts/create" && ( + + + + + )} + {children} ); } diff --git a/src/components/postTitle.tsx b/src/components/postTitle.tsx index 69ee8b9..4b8b40c 100644 --- a/src/components/postTitle.tsx +++ b/src/components/postTitle.tsx @@ -1,8 +1,8 @@ +import { usePocketBase } from "@/pocketbase"; import { Post } from "@/pocketbase/models"; import { getRelativeTime } from "@/utils/relativeTime"; import { Anchor, - Avatar, Badge, Box, BoxProps, @@ -14,6 +14,7 @@ import { } from "@mantine/core"; import Link from "next/link"; import { Record } from "pocketbase"; +import UserAvatar from "./userAvatar"; interface PostTitleProps extends BoxProps { post: Post; @@ -21,6 +22,8 @@ interface PostTitleProps extends BoxProps { } function PostTitle({ post, compact, ...props }: PostTitleProps) { + const pb = usePocketBase(); + return ( @@ -41,7 +44,7 @@ function PostTitle({ post, compact, ...props }: PostTitleProps) { > {(post.expand.author as Record).username} - + diff --git a/src/components/userAvatar.tsx b/src/components/userAvatar.tsx new file mode 100644 index 0000000..973132e --- /dev/null +++ b/src/components/userAvatar.tsx @@ -0,0 +1,27 @@ +import { usePocketBase } from "@/pocketbase"; +import { Avatar, AvatarProps } from "@mantine/core"; +import { Record } from "pocketbase"; + +interface UserAvatarProps extends AvatarProps { + user?: Record | null; +} + +function UserAvatar({ user, ...props }: UserAvatarProps) { + const pb = usePocketBase(); + + return ( + + ); +} + +export default UserAvatar; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6216d33..73bacf7 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -25,7 +25,7 @@ import { GetServerSideProps } from "next"; import Link from "next/link"; import { useRouter } from "next/router"; import { Record } from "pocketbase"; -import { useEffect, useState } from "react"; +import { useQuery } from "react-query"; interface HomeProps extends ShareMeEnv {} @@ -34,17 +34,13 @@ export default function Home({ signUpEnabled }: HomeProps) { const pb = usePocketBase(); const { user } = useAuth(); - const [posts, setPosts] = useState([]); - - useEffect(() => { - pb.collection("posts") - .getList(1, 10, { - expand: "files,author", - sort: "-created", - $autoCancel: false, - }) - .then((records) => setPosts(records.items)); - }, [pb, setPosts]); + const { data: latestPosts } = useQuery(["latestPosts"], () => + pb.collection("posts").getList(1, 10, { + expand: "files,author", + sort: "-created", + $autoCancel: false, + }) + ); const { createPost: _createPost } = useCreatePost({ acceptTypes: MEDIA_MIME_TYPE, @@ -84,7 +80,7 @@ export default function Home({ signUpEnabled }: HomeProps) { - {posts.map((post) => ( + {latestPosts?.items.map((post) => ( {(post.expand.author as Record).username} - + ))} diff --git a/src/pages/users/[id].tsx b/src/pages/users/[id].tsx index 1f29265..f672502 100644 --- a/src/pages/users/[id].tsx +++ b/src/pages/users/[id].tsx @@ -1,6 +1,7 @@ import Head from "@/components/head"; import Layout from "@/components/layout"; import PostCard from "@/components/postCard"; +import UserAvatar from "@/components/userAvatar"; import { useCreatePost } from "@/hooks/useCreatePost"; import { initPocketBaseServer, usePocketBase } from "@/pocketbase"; import { useAuth } from "@/pocketbase/auth"; @@ -8,7 +9,8 @@ import { Post } from "@/pocketbase/models"; import { ShareMeEnv, withEnv } from "@/utils/env"; import { MEDIA_MIME_TYPE } from "@/utils/mediaTypes"; import { - Avatar, + ActionIcon, + Box, Card, Group, Skeleton, @@ -16,12 +18,14 @@ import { Text, Title, } from "@mantine/core"; +import { IMAGE_MIME_TYPE } from "@mantine/dropzone"; import { useIntersection } from "@mantine/hooks"; +import { IconCameraPlus } from "@tabler/icons-react"; import { GetServerSideProps } from "next"; import { useRouter } from "next/router"; import { Record } from "pocketbase"; -import { useEffect } from "react"; -import { useInfiniteQuery, useQuery } from "react-query"; +import { ChangeEvent, useEffect, useRef } from "react"; +import { useInfiniteQuery, useQuery, useQueryClient } from "react-query"; interface PostsProps extends ShareMeEnv { username: string; @@ -36,13 +40,16 @@ export default function Posts({ avatar, }: PostsProps) { const router = useRouter(); + const queryClient = useQueryClient(); const { id } = router.query; const pb = usePocketBase(); const { user: authenticatedUser } = useAuth(); - const { data: userData } = useQuery(["user", id], () => + const avatarInputRef = useRef(null); + + const { data: userData } = useQuery(["users", id], () => pb.collection("users").getOne(Array.isArray(id) ? id[0] : id!) ); @@ -100,6 +107,18 @@ export default function Posts({ fetchNextPage(); }, [isLoading, hasNextPage, entry, isFetchingNextPage, fetchNextPage]); + const onAvatarSelect = (ev: ChangeEvent) => { + if (!ev.target.files || ev.target.files.length < 1) return; + if (userData?.id !== authenticatedUser?.id) return; + const formData = new FormData(); + formData.append("avatar", ev.target.files[0]); + pb.collection("users") + .update(userData!.id, formData, { + $autoCancel: false, + }) + .then((record) => queryClient.invalidateQueries(["users", record.id])); + }; + return ( <> @@ -115,14 +134,31 @@ export default function Posts({ {username} - + onClick={() => + authenticatedUser?.id === userData.id && + avatarInputRef.current?.click() + } + > + + {authenticatedUser?.id === userData.id && ( + <> + + + + + + )} +