From 844878432ef26eb9889256e245855f28a28b3c03 Mon Sep 17 00:00:00 2001 From: Thaddeus Jiang Date: Wed, 6 Sep 2023 13:18:09 +0900 Subject: [PATCH] feat: mark avatars for often use (#32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: mark avatars for often use * fix: 🐛 router.replace instead of router.reload * refactor: update cache for client-side data-fetching --- components/lp/Header.tsx | 15 --- .../ui/Avatar/AvatarCardWithMarkIcon.tsx | 91 +++++++++++++++++ .../ui/Avatar/AvatarCardWithSettings.tsx | 41 ++++++++ components/ui/Avatar/AvatarInfoCard.tsx | 40 ++++++++ components/ui/Avatar/AvatarProfileHeader.tsx | 64 +++++++++++- components/ui/AvatarsGrid.tsx | 97 ------------------- components/ui/MarkedAvatarsGrid.tsx | 21 ++++ components/ui/OwnAvatarsGrid.tsx | 32 ++++++ pages/api/avatarMark.ts | 29 ++++++ pages/api/avatarRead.ts | 2 +- pages/api/avatarUnMark.ts | 28 ++++++ pages/avatars/index.tsx | 41 +++++++- pages/home.tsx | 47 +++++++-- pages/settings/avatars/[username]/index.tsx | 2 - pages/settings/avatars/index.tsx | 64 ------------ schema.sql | 11 +++ 16 files changed, 428 insertions(+), 197 deletions(-) create mode 100644 components/ui/Avatar/AvatarCardWithMarkIcon.tsx create mode 100644 components/ui/Avatar/AvatarCardWithSettings.tsx create mode 100644 components/ui/Avatar/AvatarInfoCard.tsx delete mode 100644 components/ui/AvatarsGrid.tsx create mode 100644 components/ui/MarkedAvatarsGrid.tsx create mode 100644 components/ui/OwnAvatarsGrid.tsx create mode 100644 pages/api/avatarMark.ts create mode 100644 pages/api/avatarUnMark.ts delete mode 100644 pages/settings/avatars/index.tsx diff --git a/components/lp/Header.tsx b/components/lp/Header.tsx index f9e3fd4..13efca6 100644 --- a/components/lp/Header.tsx +++ b/components/lp/Header.tsx @@ -81,7 +81,6 @@ function MobileNavigation() { Avatars
Your Profile - Your Avatars Pricing
- - {({ active }) => ( - - Your Avatars - - )} - - {({ active }) => ( { + const res = await fetch("/api/avatarMark", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + return res.json() + }, + onSuccess: () => { + router.replace(router.asPath) + } + }) + + const unMarkAvatarMutation = useMutation({ + mutationFn: async (data: { avatar_username: string }) => { + const res = await fetch("/api/avatarUnMark", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + return res.json() + }, + onSuccess: () => { + router.replace(router.asPath) + } + }) + + return ( +
+ +
+
+
+ +
+ +
+ {avatar.isMarked ? ( + + ) : ( + + )} +
+
+
+
+ ) +} diff --git a/components/ui/Avatar/AvatarCardWithSettings.tsx b/components/ui/Avatar/AvatarCardWithSettings.tsx new file mode 100644 index 0000000..bd8fb27 --- /dev/null +++ b/components/ui/Avatar/AvatarCardWithSettings.tsx @@ -0,0 +1,41 @@ +import Link from "next/link" + +import { IconDatabase, IconMessage } from "@tabler/icons-react" + +import { Avatar } from "~/types" + +import { AvatarInfoCard } from "./AvatarInfoCard" + +export function AvatarCardWithSettings({ avatar }: { avatar: Avatar }) { + return ( +
+ +
+
+
+ +
+ +
+ +
+
+
+
+ ) +} diff --git a/components/ui/Avatar/AvatarInfoCard.tsx b/components/ui/Avatar/AvatarInfoCard.tsx new file mode 100644 index 0000000..55ad11d --- /dev/null +++ b/components/ui/Avatar/AvatarInfoCard.tsx @@ -0,0 +1,40 @@ +import Link from "next/link" + +import { Avatar } from "~/types" + +export function AvatarInfoCard({ avatar }: { avatar: Avatar }) { + return ( +
+
+
+

{avatar.name}

+ {avatar.status !== "public" ? ( + + {avatar.status} + + ) : null} +
+

@{avatar.username}

+

{avatar.bio ?? ""}

+
+ + + {avatar.avatar_url ? ( + <> +
+ {`Avatar +
+ + ) : ( + <> +
+
+ {avatar?.name?.[0]} +
+
+ + )} + +
+ ) +} diff --git a/components/ui/Avatar/AvatarProfileHeader.tsx b/components/ui/Avatar/AvatarProfileHeader.tsx index 1081239..b905049 100644 --- a/components/ui/Avatar/AvatarProfileHeader.tsx +++ b/components/ui/Avatar/AvatarProfileHeader.tsx @@ -4,8 +4,8 @@ import Image from "next/image" import Link from "next/link" import { useRouter } from "next/router" -import { IconLock, IconLockOpen, IconMessage, IconNotes } from "@tabler/icons-react" -import { useMutation, useQuery } from "@tanstack/react-query" +import { IconBookmark, IconLock, IconLockOpen, IconMessage, IconNotes } from "@tabler/icons-react" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Avatar } from "~/types" import { useUser } from "~/utils/useUser" @@ -14,7 +14,13 @@ export const AvatarProfileHeader = ({ username, isSetting = false }: { username: const { user } = useUser() const router = useRouter() - const avatarQuery = useQuery({ + const queryClient = useQueryClient() + + const avatarQuery = useQuery< + Avatar & { + marked: { id: string }[] + } + >({ queryKey: ["avatars", username], queryFn: async () => { return fetch(`/api/avatarRead`, { @@ -56,6 +62,40 @@ export const AvatarProfileHeader = ({ username, isSetting = false }: { username: }) } + const markAvatarMutation = useMutation({ + mutationFn: async (data: { avatar_username: string }) => { + const res = await fetch("/api/avatarMark", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + return res.json() + }, + onSuccess: () => { + toast.success("Avatar marked") + queryClient.refetchQueries(["avatars", username]) + } + }) + + const unMarkAvatarMutation = useMutation({ + mutationFn: async (data: { avatar_username: string }) => { + const res = await fetch("/api/avatarUnMark", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + return res.json() + }, + onSuccess: () => { + toast.success("Avatar unmarked") + queryClient.refetchQueries(["avatars", username]) + } + }) + if (avatarQuery.isLoading) { return } @@ -118,6 +158,24 @@ export const AvatarProfileHeader = ({ username, isSetting = false }: { username: )} + {avatar.marked.map(({ id }: { id: string }) => id).includes(user?.id ?? "") ? ( + + ) : ( + + )} + diff --git a/components/ui/AvatarsGrid.tsx b/components/ui/AvatarsGrid.tsx deleted file mode 100644 index 21ca687..0000000 --- a/components/ui/AvatarsGrid.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import Link from "next/link" - -import { useUser } from "@supabase/auth-helpers-react" -import { IconDatabase, IconMessage, IconPlus } from "@tabler/icons-react" - -import { Avatar } from "~/types" - -export function AvatarsGrid({ avatars, withCreate }: { avatars: Avatar[]; withCreate?: boolean }) { - const user = useUser() - return ( -
    - {avatars.map((avatar) => ( -
  • -
    -
    -
    -

    {avatar.name}

    - {avatar.status !== "public" ? ( - - {avatar.status} - - ) : null} -
    -

    @{avatar.username}

    -

    {avatar.bio ?? ""}

    -
    - - - {avatar.avatar_url ? ( - <> -
    - {`Avatar -
    - - ) : ( - <> -
    -
    - {avatar?.name?.[0]} -
    -
    - - )} - -
    -
    -
    -
    - -
    - {avatar.owner_id === user?.id ? ( -
    - -
    - ) : null} -
    -
    -
  • - ))} - {withCreate ? ( -
  • - - - Create a new avatar - -
  • - ) : null} -
- ) -} diff --git a/components/ui/MarkedAvatarsGrid.tsx b/components/ui/MarkedAvatarsGrid.tsx new file mode 100644 index 0000000..d621277 --- /dev/null +++ b/components/ui/MarkedAvatarsGrid.tsx @@ -0,0 +1,21 @@ +import { Avatar } from "~/types" + +import { AvatarCardWithMarkIcon } from "./Avatar/AvatarCardWithMarkIcon" + +export function MarkedAvatarsGrid({ + avatars +}: { + avatars: (Avatar & { + isMarked: boolean + })[] +}) { + return ( +
    + {avatars.map((avatar) => ( +
  • + +
  • + ))} +
+ ) +} diff --git a/components/ui/OwnAvatarsGrid.tsx b/components/ui/OwnAvatarsGrid.tsx new file mode 100644 index 0000000..195197f --- /dev/null +++ b/components/ui/OwnAvatarsGrid.tsx @@ -0,0 +1,32 @@ +import Link from "next/link" + +import { useUser } from "@supabase/auth-helpers-react" +import { IconPlus } from "@tabler/icons-react" + +import { Avatar } from "~/types" + +import { AvatarCardWithSettings } from "./Avatar/AvatarCardWithSettings" + +export function OwnAvatarsGrid({ avatars, withCreate }: { avatars: Avatar[]; withCreate?: boolean }) { + return ( +
    + {avatars.map((avatar) => ( +
  • + +
  • + ))} + {withCreate ? ( +
  • + + + Create a new avatar + +
  • + ) : null} +
+ ) +} diff --git a/pages/api/avatarMark.ts b/pages/api/avatarMark.ts new file mode 100644 index 0000000..2597042 --- /dev/null +++ b/pages/api/avatarMark.ts @@ -0,0 +1,29 @@ +import { NextApiRequest, NextApiResponse } from "next" + +import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs" + +export default async function avatarMark(req: NextApiRequest, res: NextApiResponse) { + const supabase = createServerSupabaseClient({ req, res }) + + const { + data: { user } + } = await supabase.auth.getUser() + + if (!user) { + return res.status(401).json({ error: "Unauthorized" }) + } + + const { avatar_username } = req.body as { avatar_username: string } + + const { data, error } = await supabase + .from("users_mark_avatars") + .insert({ user_id: user.id, avatar_id: avatar_username }) + .select("*") + .single() + + if (error) { + return res.status(500).json({ error: error.message }) + } + + return res.status(200).json(data) +} diff --git a/pages/api/avatarRead.ts b/pages/api/avatarRead.ts index c2ce3a3..3e2bd78 100644 --- a/pages/api/avatarRead.ts +++ b/pages/api/avatarRead.ts @@ -13,7 +13,7 @@ export default async function avatarRead(req: NextApiRequest, res: NextApiRespon const { data, error } = await supabase .from("avatars") - .select("*, embeddings(count)") + .select("*, embeddings(count), marked:users(id)") .eq("username", avatar_username) .maybeSingle() diff --git a/pages/api/avatarUnMark.ts b/pages/api/avatarUnMark.ts new file mode 100644 index 0000000..c9dc0ba --- /dev/null +++ b/pages/api/avatarUnMark.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from "next" + +import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs" + +export default async function avatarUnMark(req: NextApiRequest, res: NextApiResponse) { + const supabase = createServerSupabaseClient({ req, res }) + + const { + data: { user } + } = await supabase.auth.getUser() + + if (!user) { + return res.status(401).json({ error: "Unauthorized" }) + } + + const { avatar_username } = req.body as { avatar_username: string } + + const { data, error } = await supabase + .from("users_mark_avatars") + .delete() + .match({ user_id: user.id, avatar_id: avatar_username }) + + if (error) { + return res.status(500).json({ error: error.message }) + } + + return res.status(200).json(data) +} diff --git a/pages/avatars/index.tsx b/pages/avatars/index.tsx index 1748798..99518dc 100644 --- a/pages/avatars/index.tsx +++ b/pages/avatars/index.tsx @@ -4,10 +4,16 @@ import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs" import { AvatarsValuesMessage } from "~/components/lp/AvatarsValuesMessage" import { Header } from "~/components/lp/Header" -import { AvatarsGrid } from "~/components/ui/AvatarsGrid" +import { AvatarCardWithMarkIcon } from "~/components/ui/Avatar/AvatarCardWithMarkIcon" import { Avatar } from "~/types" -export default function AvatarsPage({ avatars }: { avatars: Avatar[] }) { +export default function AvatarsPage({ + avatars +}: { + avatars: (Avatar & { + isMarked: boolean + })[] +}) { return ( <>
@@ -19,7 +25,13 @@ export default function AvatarsPage({ avatars }: { avatars: Avatar[] }) {
- +
    + {avatars.map((avatar) => ( +
  • + +
  • + ))} +
@@ -29,8 +41,20 @@ export default function AvatarsPage({ avatars }: { avatars: Avatar[] }) { export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { const supabase = createServerSupabaseClient(ctx) + const { + data: { user } + } = await supabase.auth.getUser() - const { data, error } = await supabase.rpc(`list_avatars_with_embeddings_count`) + if (!user) { + return { + redirect: { + destination: "/signin", + permanent: false + } + } + } + + const { data, error } = await supabase.from("avatars").select("*, users (id)") if (error) { console.error(error) @@ -41,9 +65,16 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { } } + const result = data.map((item) => { + return { + ...item, + isMarked: (item.users || []).map((item: { id: string }) => item.id).includes(user.id) + } + }) + return { props: { - avatars: data + avatars: result } } } diff --git a/pages/home.tsx b/pages/home.tsx index 22834e4..52bfaac 100644 --- a/pages/home.tsx +++ b/pages/home.tsx @@ -3,22 +3,31 @@ import { GetServerSidePropsContext } from "next" import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs" import { Header } from "~/components/lp/Header" -import { AvatarsGrid } from "~/components/ui/AvatarsGrid" +import { MarkedAvatarsGrid } from "~/components/ui/MarkedAvatarsGrid" +import { OwnAvatarsGrid } from "~/components/ui/OwnAvatarsGrid" import { Avatar } from "~/types" -export default function HomePage({ yours, others }: { yours: Avatar[]; others: Avatar[] }) { +export default function HomePage({ + yours, + marked +}: { + yours: Avatar[] + marked: (Avatar & { + isMarked: boolean + })[] +}) { return ( <>

Yours

Talking to them, train them, and they will learn to talk to you.

- +
- {/*

Favorites

*/} - {/* TODO: */} +

Marked

+
@@ -45,24 +54,42 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => } } - const { data: avatars, error } = await supabase.from("avatars").select() + const { data: yours, error } = await supabase.from("avatars").select().eq("owner_id", user.id) if (error) { console.error(error) return { props: { - yours: [] + yours: [], + marked: [] } } } - const yours = avatars.filter((avatar) => avatar.owner_id === user.id) - const others = avatars.filter((avatar) => avatar.owner_id !== user.id) + const { data } = await supabase.from("avatars").select("*, users (id)") + + if (!data) { + return { + props: { + yours: [], + marked: [] + } + } + } + + const marked = data + ?.map((item) => { + return { + ...item, + isMarked: (item.users || []).map((item: { id: string }) => item.id).includes(user.id) + } + }) + .filter((item: { isMarked: boolean }) => item.isMarked) return { props: { yours, - others + marked } } } diff --git a/pages/settings/avatars/[username]/index.tsx b/pages/settings/avatars/[username]/index.tsx index fcc38ae..a6b0da7 100644 --- a/pages/settings/avatars/[username]/index.tsx +++ b/pages/settings/avatars/[username]/index.tsx @@ -2,8 +2,6 @@ import { GetServerSidePropsContext } from "next" import Link from "next/link" import { useRouter } from "next/router" -import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs" - import { Header } from "~/components/lp/Header" import { AvatarProfileHeader } from "~/components/ui/Avatar/AvatarProfileHeader" import { MainLayout } from "~/components/ui/Layouts/MainLayout" diff --git a/pages/settings/avatars/index.tsx b/pages/settings/avatars/index.tsx deleted file mode 100644 index cd2b04a..0000000 --- a/pages/settings/avatars/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { GetServerSidePropsContext } from "next" - -import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs" - -import { Header } from "~/components/lp/Header" -import { AvatarsGrid } from "~/components/ui/AvatarsGrid" -import { Avatar } from "~/types" - -export default function SettingsAvatarsPage({ avatars }: { avatars: Avatar[] }) { - return ( - <> -
-
-
-
-

Your Avatars

-

- Talking to them, train them, and they will learn to talk to you. -

-
- -
-
- - ) -} - -export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { - const supabase = createServerSupabaseClient(ctx) - - const { - data: { user } - } = await supabase.auth.getUser() - - if (!user) { - return { - redirect: { - destination: "/signin", - permanent: false - } - } - } - - const { data, error } = await supabase - .from("avatars") - .select() - .eq("owner_id", user.id) - .order("created_at", { ascending: false }) - - if (error) { - console.error(error) - return { - props: { - avatars: [] - } - } - } - - return { - props: { - avatars: data - } - } -} diff --git a/schema.sql b/schema.sql index c475bf8..8667a97 100644 --- a/schema.sql +++ b/schema.sql @@ -31,6 +31,17 @@ CREATE TABLE avatars ( owner_id uuid references auth.users ); +CREATE TABLE users_mark_avatars ( + user_id uuid not null references users , + avatar_id text not null references avatars, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + + CONSTRAINT "users_mark_avatars_pkey" primary key (user_id, avatar_id) +); + +ALTER TABLE users_mark_avatars ADD CONSTRAINT "users_mark_avatars_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id); +ALTER TABLE users_mark_avatars ADD CONSTRAINT "users_mark_avatars_avatar_id_fkey" FOREIGN KEY (avatar_id) REFERENCES avatars(id); + CREATE OR REPLACE FUNCTION public.list_avatars_with_embeddings_count() RETURNS TABLE ( id text,