diff --git a/package.json b/package.json index 9d621bd..dd6964b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@better-auth/passkey": "^1.5.5", "@hookform/resolvers": "^3.9.1", "@polinetwork/backend": "^0.15.17", + "@polinetwork/backend-test": "file:../backend/package/dist/", "@radix-ui/react-dialog": "^1.1.15", "@t3-oss/env-nextjs": "^0.13.10", "@tanstack/react-table": "^8.21.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3df57b1..44fe139 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@polinetwork/backend': specifier: ^0.15.17 version: 0.15.17 + '@polinetwork/backend-test': + specifier: file:../backend/package/dist/ + version: dist@file:../backend/package/dist '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2164,6 +2167,9 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} + dist@file:../backend/package/dist: + resolution: {directory: ../backend/package/dist, type: directory} + dotenv@17.4.1: resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} @@ -5411,6 +5417,8 @@ snapshots: diff@8.0.4: {} + dist@file:../backend/package/dist: {} + dotenv@17.4.1: {} dunder-proto@1.0.1: diff --git a/src/app/dashboard/(active)/account/page.tsx b/src/app/dashboard/(active)/account/page.tsx index a8f78d8..d48316d 100644 --- a/src/app/dashboard/(active)/account/page.tsx +++ b/src/app/dashboard/(active)/account/page.tsx @@ -1,11 +1,10 @@ -import { Calendar, CircleAlert, KeyIcon, UserIcon } from "lucide-react" +import { Calendar, CircleAlert, KeyIcon } from "lucide-react" import { headers } from "next/headers" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { auth } from "@/lib/auth" -import { getInitials } from "@/lib/utils" import { getServerSession } from "@/server/auth" import { DeletePasskey } from "./delete-passkey" import { NewPasskeyButton } from "./passkey-button" +import { ProfilePic } from "./profile-pic" import { SetName } from "./set-name" import { Telegram } from "./telegram" @@ -25,12 +24,7 @@ export default async function Account() {

Account

- - {user.image && } - - {user.name ? getInitials(user.name) : } - - +
diff --git a/src/app/dashboard/(active)/account/profile-pic.tsx b/src/app/dashboard/(active)/account/profile-pic.tsx new file mode 100644 index 0000000..bc40d5a --- /dev/null +++ b/src/app/dashboard/(active)/account/profile-pic.tsx @@ -0,0 +1,108 @@ +"use client" + +import type { User } from "better-auth" +import { Pencil, Upload, UserIcon, X } from "lucide-react" +import { useRouter } from "next/navigation" +import { forwardRef, useRef } from "react" +import { toast } from "sonner" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { auth, useSession } from "@/lib/auth" +import { getInitials } from "@/lib/utils" +import { updateProfilePic } from "@/server/actions/users" + +type Props = { + user: User +} + +export function ProfilePic({ user }: Props) { + const uploadRef = useRef(null) + const router = useRouter() + const { refetch } = useSession() + + async function handleRemove() { + const ok = confirm("Are you sure you want to reset your profile picture?") + if (!ok) return + + const r = await auth.updateUser({ image: null }) + if (r.error) { + toast.error("There was an error") + console.error(r.error) + return + } + + toast.success("Profile picture removed successfully'") + await refetch() + router.refresh() + } + + return ( +
+ + + +
+ +
+
+ + uploadRef.current?.click()}> + Upload + + + Remove + + +
+ + + + + + {user.name ? getInitials(user.name) : } + +
+

Max 1MB

+
+ ) +} + +const UploadProfilePicture = forwardRef(({ userId }, ref) => { + const router = useRouter() + const { refetch } = useSession() + + const handleFileChange = async (event: React.ChangeEvent) => { + const files = event.target.files + if (files && files.length > 0) { + const selectedFile = files[0] + if (!selectedFile) return + + if (selectedFile.size > 1024 * 1024) { + toast.error("The image must be no larger than 1 MB") + return + } + + if (selectedFile.type !== "image/png" && selectedFile.type !== "image/jpeg") { + toast.error("The image must be jpeg or png") + return + } + + const { success } = await updateProfilePic(userId, selectedFile) + if (success) toast.success("Image changed successfully!") + else toast.error("There was an error, try again") + await refetch() + router.refresh() + } + } + return ( + + ) +}) diff --git a/src/app/dashboard/(active)/telegram/groups/group-row.tsx b/src/app/dashboard/(active)/telegram/groups/group-row.tsx index acc2bac..f5b6e9a 100644 --- a/src/app/dashboard/(active)/telegram/groups/group-row.tsx +++ b/src/app/dashboard/(active)/telegram/groups/group-row.tsx @@ -1,5 +1,5 @@ "use client" -import { Copy, Pen } from "lucide-react" +import { Copy } from "lucide-react" import { useRouter } from "next/navigation" import { toast } from "sonner" import { Badge } from "@/components/ui/badge" diff --git a/src/app/dashboard/(active)/telegram/user-details/remove-role.tsx b/src/app/dashboard/(active)/telegram/user-details/remove-role.tsx index 9235dca..fcf7f23 100644 --- a/src/app/dashboard/(active)/telegram/user-details/remove-role.tsx +++ b/src/app/dashboard/(active)/telegram/user-details/remove-role.tsx @@ -19,7 +19,7 @@ import { import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useSession } from "@/lib/auth" import { delUserRole } from "@/server/actions/users" -import type { ApiOutput, TgUser, TgUserRole } from "@/server/trpc/types" +import type { TgUser, TgUserRole } from "@/server/trpc/types" const ARRAY_USER_ROLES = [ USER_ROLE.ADMIN, diff --git a/src/components/admin-header/right-nav.tsx b/src/components/admin-header/right-nav.tsx index 633341b..808a440 100644 --- a/src/components/admin-header/right-nav.tsx +++ b/src/components/admin-header/right-nav.tsx @@ -1,8 +1,8 @@ "use client" -import { LogOutIcon, Settings2 } from "lucide-react" +import { LogOutIcon, Settings2, UserIcon } from "lucide-react" import Link from "next/link" import { redirect } from "next/navigation" -import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { DropdownMenu, @@ -24,8 +24,9 @@ export function RightNav() { render={ diff --git a/src/server/actions/users.ts b/src/server/actions/users.ts index f8c14ca..a098a3f 100644 --- a/src/server/actions/users.ts +++ b/src/server/actions/users.ts @@ -4,6 +4,13 @@ import { trpc } from "../trpc" import type { ApiOutput, TgUserRole } from "../trpc/types" import { getUserGrant } from "./grants" +export async function updateProfilePic(userId: string, file: File) { + const fd = new FormData() + fd.set("userId", userId) + fd.set("image", file) + return await trpc.auth.updateProfilePic.mutate(fd) +} + export async function getUserInfo(userId: number) { return (await trpc.tg.users.get.query({ userId })).user ?? null } diff --git a/src/server/trpc/index.tsx b/src/server/trpc/index.tsx index d21bdfe..3c6fb0c 100644 --- a/src/server/trpc/index.tsx +++ b/src/server/trpc/index.tsx @@ -1,16 +1,28 @@ import "server-only" -import { type AppRouter, TRPC_PATH } from "@polinetwork/backend" -import { createTRPCClient, httpBatchLink } from "@trpc/client" +import { type AppRouter, TRPC_PATH } from "@polinetwork/backend-test" +import { createTRPCClient, httpBatchLink, httpLink, isNonJsonSerializable, splitLink } from "@trpc/client" import SuperJSON from "superjson" import { env } from "@/env" const url = env.BACKEND_URL + TRPC_PATH export const trpc = createTRPCClient({ links: [ - httpBatchLink({ - url, - transformer: SuperJSON, + splitLink({ + condition: (op) => isNonJsonSerializable(op.input), + true: httpLink({ + url, + transformer: { + // request - convert data before sending to the tRPC server + serialize: (data) => data, + // response - convert the tRPC response before using it in client + deserialize: (data) => SuperJSON.deserialize(data), // or your other transformer + }, + }), + false: httpBatchLink({ + url, + transformer: SuperJSON, // or your other transformer + }), }), ], }) diff --git a/tsconfig.json b/tsconfig.json index 3f6bceb..765e842 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,27 +8,37 @@ "resolveJsonModule": true, "moduleDetection": "force", "isolatedModules": true, - /* Strictness */ "strict": true, "noUncheckedIndexedAccess": true, "checkJs": true, - /* Bundled projects */ "lib": ["dom", "dom.iterable", "ES2022"], "noEmit": true, "module": "ESNext", "moduleResolution": "Bundler", "jsx": "preserve", - "plugins": [{ "name": "next" }], + "plugins": [ + { + "name": "next" + } + ], "incremental": true, - /* Path Aliases */ "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, - "include": [".eslintrc.cjs", "next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.js", ".next/types/**/*.ts"], + "include": [ + ".eslintrc.cjs", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.cjs", + "**/*.js", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], "exclude": ["node_modules"] }