Skip to content

Commit

Permalink
feat: mark avatars for often use (#32)
Browse files Browse the repository at this point in the history
* feat: mark avatars for often use

* fix: 🐛 router.replace instead of router.reload

* refactor: update cache for client-side data-fetching
  • Loading branch information
ThaddeusJiang committed Sep 6, 2023
1 parent bae59e8 commit 8448784
Show file tree
Hide file tree
Showing 16 changed files with 428 additions and 197 deletions.
15 changes: 0 additions & 15 deletions components/lp/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ function MobileNavigation() {
<MobileNavLink href="/avatars">Avatars</MobileNavLink>
<hr className="m-2 border-slate-300/40" />
<MobileNavLink href="/settings/profile">Your Profile</MobileNavLink>
<MobileNavLink href="/settings/avatars">Your Avatars</MobileNavLink>
<MobileNavLink href="/settings/pricing">Pricing</MobileNavLink>
<hr className="m-2 border-slate-300/40" />
<Popover.Button
Expand Down Expand Up @@ -203,20 +202,6 @@ export function Header() {
)}
</Menu.Item>

<Menu.Item>
{({ active }) => (
<Link
href="/settings/avatars"
className={clsx(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-lg tracking-tight text-slate-900"
)}
>
Your Avatars
</Link>
)}
</Menu.Item>

<Menu.Item>
{({ active }) => (
<Link
Expand Down
91 changes: 91 additions & 0 deletions components/ui/Avatar/AvatarCardWithMarkIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Link from "next/link"
import { useRouter } from "next/router"

import { IconBookmark, IconMessage } from "@tabler/icons-react"
import { useMutation } from "@tanstack/react-query"

import { Avatar } from "~/types"

import { AvatarInfoCard } from "./AvatarInfoCard"

export function AvatarCardWithMarkIcon({ avatar }: { avatar: Avatar & { isMarked: boolean } }) {
const router = useRouter()

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: () => {
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 (
<div
key={avatar.id}
className="flex h-full flex-col justify-between divide-y divide-gray-200 rounded-lg bg-white shadow"
>
<AvatarInfoCard avatar={avatar} />
<div>
<div className="-mt-px flex divide-x divide-gray-200">
<div className="flex w-0 flex-1">
<Link
href={`/chat/${avatar.username}`}
className="relative -mr-px inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-gray-900"
>
<IconMessage className="h-5 w-5 text-gray-400" aria-hidden="true" />
Chat
</Link>
</div>

<div className="-ml-px flex w-0 flex-1">
{avatar.isMarked ? (
<button
onClick={() => {
unMarkAvatarMutation.mutate({ avatar_username: avatar.username })
}}
className="relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-gray-900"
>
<IconBookmark className="h-5 w-5 fill-blue-500 text-blue-500" aria-hidden="true" />
marked
</button>
) : (
<button
onClick={() => {
markAvatarMutation.mutate({ avatar_username: avatar.username })
}}
className="relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-gray-900"
>
<IconBookmark className="h-5 w-5 text-gray-400" aria-hidden="true" />
mark
</button>
)}
</div>
</div>
</div>
</div>
)
}
41 changes: 41 additions & 0 deletions components/ui/Avatar/AvatarCardWithSettings.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
key={avatar.id}
className="flex h-full flex-col justify-between divide-y divide-gray-200 rounded-lg bg-white shadow"
>
<AvatarInfoCard avatar={avatar} />
<div>
<div className="-mt-px flex divide-x divide-gray-200">
<div className="flex w-0 flex-1">
<Link
href={`/chat/${avatar.username}`}
className="relative -mr-px inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-gray-900"
>
<IconMessage className="h-5 w-5 text-gray-400" aria-hidden="true" />
Chat
</Link>
</div>

<div className="-ml-px flex w-0 flex-1">
<Link
href={`/settings/avatars/${avatar.username}`}
className="relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-gray-900"
>
<IconDatabase className="h-5 w-5 text-gray-400" aria-hidden="true" />
Settings
</Link>
</div>
</div>
</div>
</div>
)
}
40 changes: 40 additions & 0 deletions components/ui/Avatar/AvatarInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Link from "next/link"

import { Avatar } from "~/types"

export function AvatarInfoCard({ avatar }: { avatar: Avatar }) {
return (
<div className="flex w-full items-center justify-between space-x-6 p-6">
<div className="flex-1 truncate">
<div className="flex items-center justify-between space-x-3">
<h3 className="truncate text-lg font-medium text-gray-900">{avatar.name}</h3>
{avatar.status !== "public" ? (
<span className="inline-block flex-shrink-0 rounded-full bg-slate-300 px-2 py-0.5 text-xs font-medium text-slate-800">
{avatar.status}
</span>
) : null}
</div>
<p className=" text-sm">@{avatar.username}</p>
<p className="mt-1 truncate text-sm text-gray-500">{avatar.bio ?? ""}</p>
</div>

<Link href={`/avatars/${avatar.username}`}>
{avatar.avatar_url ? (
<>
<div className="avatar">
<img className="!h-16 !w-16 rounded-full" src={avatar.avatar_url} alt={`Avatar of ${avatar.name}`} />
</div>
</>
) : (
<>
<div className="placeholder avatar">
<div className="!h-16 !w-16 rounded-full bg-neutral-focus text-neutral-content">
<span className="text-4xl">{avatar?.name?.[0]}</span>
</div>
</div>
</>
)}
</Link>
</div>
)
}
64 changes: 61 additions & 3 deletions components/ui/Avatar/AvatarProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -14,7 +14,13 @@ export const AvatarProfileHeader = ({ username, isSetting = false }: { username:
const { user } = useUser()
const router = useRouter()

const avatarQuery = useQuery<Avatar>({
const queryClient = useQueryClient()

const avatarQuery = useQuery<
Avatar & {
marked: { id: string }[]
}
>({
queryKey: ["avatars", username],
queryFn: async () => {
return fetch(`/api/avatarRead`, {
Expand Down Expand Up @@ -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 <AvatarProfileHeaderSkeleton />
}
Expand Down Expand Up @@ -118,6 +158,24 @@ export const AvatarProfileHeader = ({ username, isSetting = false }: { username:
</label>
)}

{avatar.marked.map(({ id }: { id: string }) => id).includes(user?.id ?? "") ? (
<button
onClick={() => {
unMarkAvatarMutation.mutate({ avatar_username: avatar.username })
}}
>
<IconBookmark className="h-5 w-5 fill-blue-500 text-blue-500" />
</button>
) : (
<button
onClick={() => {
markAvatarMutation.mutate({ avatar_username: avatar.username })
}}
>
<IconBookmark className="h-5 w-5 text-gray-400" />
</button>
)}

<Link href={`/chat/${avatar?.username}`}>
<IconMessage />
</Link>
Expand Down
97 changes: 0 additions & 97 deletions components/ui/AvatarsGrid.tsx

This file was deleted.

Loading

1 comment on commit 8448784

@vercel
Copy link

@vercel vercel bot commented on 8448784 Sep 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

aier – ./

aier-thaddeusjiang.vercel.app
www.aier.app
aier-git-dev-thaddeusjiang.vercel.app
aier.app

Please sign in to comment.