Skip to content

Commit 9f264ca

Browse files
feat: telegam/user page
1 parent 66c73d0 commit 9f264ca

7 files changed

Lines changed: 451 additions & 7 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@base-ui/react": "^1.3.0",
1717
"@better-auth/passkey": "^1.5.5",
1818
"@hookform/resolvers": "^3.9.1",
19-
"@polinetwork/backend": "^0.15.5",
19+
"@polinetwork/backend": "^0.15.6",
2020
"@radix-ui/react-dialog": "^1.1.15",
2121
"@t3-oss/env-nextjs": "^0.13.10",
2222
"@tanstack/react-query": "^5.90.19",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/dashboard/(active)/telegram/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Group, UsersRound } from "lucide-react"
1+
import { Group, Shield, UsersRound } from "lucide-react"
22
import Link from "next/link"
33
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
44

@@ -30,6 +30,18 @@ export default function TelegramIndex() {
3030
</CardHeader>
3131
</Card>
3232
</Link>
33+
34+
<Link href="/dashboard/telegram/users">
35+
<Card className="w-90 hover:bg-accent transition-colors">
36+
<CardHeader>
37+
<CardTitle className="flex gap-2 items-center">
38+
<Shield size={20} />
39+
Users
40+
</CardTitle>
41+
<CardDescription>Manage users roles and group admins</CardDescription>
42+
</CardHeader>
43+
</Card>
44+
</Link>
3345
</div>
3446
</div>
3547
)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use client"
2+
3+
import { useMutation, useQueryClient } from "@tanstack/react-query"
4+
import { Trash2Icon } from "lucide-react"
5+
import { useRouter } from "next/navigation"
6+
import { useState } from "react"
7+
import { toast } from "sonner"
8+
import {
9+
AlertDialog,
10+
AlertDialogAction,
11+
AlertDialogCancel,
12+
AlertDialogContent,
13+
AlertDialogDescription,
14+
AlertDialogFooter,
15+
AlertDialogHeader,
16+
AlertDialogMedia,
17+
AlertDialogTitle,
18+
AlertDialogTrigger,
19+
} from "@/components/ui/alert-dialog"
20+
import { Button } from "@/components/ui/button"
21+
import { useSession } from "@/lib/auth"
22+
import { useTRPC } from "@/lib/trpc/client"
23+
24+
export function DeleteGroupAdmin({ userId, chatId }: { userId: number; chatId: number }) {
25+
const router = useRouter()
26+
const sesh = useSession()
27+
28+
const qc = useQueryClient()
29+
const trpc = useTRPC()
30+
const removerId = sesh.data?.user.telegramId
31+
const { mutateAsync } = useMutation(trpc.tg.permissions.removeGroup.mutationOptions())
32+
33+
const [open, setOpen] = useState(false)
34+
35+
async function deleteGroupAdmin() {
36+
if (!removerId) return toast.error("Invalid session, try to reload the page")
37+
const { error } = await mutateAsync({ removerId, userId, groupId: chatId })
38+
if (error) {
39+
toast.error("There was an error")
40+
console.error(error)
41+
} else {
42+
toast.success("Group Admin deleted!")
43+
}
44+
45+
handleOpenChange(false)
46+
}
47+
48+
function handleOpenChange(v: boolean) {
49+
setOpen(v)
50+
if (v === false) {
51+
qc.invalidateQueries(trpc.tg.permissions.getRoles.queryOptions({ userId }))
52+
router.refresh()
53+
}
54+
}
55+
56+
return (
57+
<AlertDialog open={open} onOpenChange={handleOpenChange}>
58+
<AlertDialogTrigger render={<Button variant="destructive">Delete</Button>}></AlertDialogTrigger>
59+
<AlertDialogContent size="sm">
60+
<AlertDialogHeader>
61+
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
62+
<Trash2Icon />
63+
</AlertDialogMedia>
64+
<AlertDialogTitle>Remove Group Admin</AlertDialogTitle>
65+
<AlertDialogDescription>Are you sure you want to remove the group admin?</AlertDialogDescription>
66+
</AlertDialogHeader>
67+
<AlertDialogFooter>
68+
<AlertDialogCancel>Cancel</AlertDialogCancel>
69+
<AlertDialogAction onClick={deleteGroupAdmin} variant="destructive">
70+
Confirm
71+
</AlertDialogAction>
72+
</AlertDialogFooter>
73+
</AlertDialogContent>
74+
</AlertDialog>
75+
)
76+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"use client"
2+
import { useMutation, useQueryClient } from "@tanstack/react-query"
3+
import { Plus, Search, X } from "lucide-react"
4+
import { useRouter } from "next/navigation"
5+
import { useState } from "react"
6+
import { toast } from "sonner"
7+
import { Button } from "@/components/ui/button"
8+
import {
9+
Dialog,
10+
DialogClose,
11+
DialogContent,
12+
DialogDescription,
13+
DialogFooter,
14+
DialogHeader,
15+
DialogTitle,
16+
DialogTrigger,
17+
} from "@/components/ui/dialog"
18+
import { Input } from "@/components/ui/input"
19+
import { Label } from "@/components/ui/label"
20+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger } from "@/components/ui/select"
21+
import { useSession } from "@/lib/auth"
22+
import { useTRPC } from "@/lib/trpc/client"
23+
import type { ApiOutput } from "@/lib/trpc/types"
24+
25+
type Groups = ApiOutput["tg"]["groups"]["search"]["groups"]
26+
type User = ApiOutput["tg"]["users"]["getByUsername"]["user"]
27+
28+
export function NewGroupAdmin({ user, alreadyIn }: { user: User; alreadyIn: number[] }) {
29+
const sesh = useSession()
30+
const adderId = sesh.data?.user.telegramId
31+
32+
const trpc = useTRPC()
33+
const qc = useQueryClient()
34+
const router = useRouter()
35+
36+
const [open, setOpen] = useState(false)
37+
const [groupQuery, setGroupQuery] = useState("")
38+
const [groups, setGroups] = useState<Groups>([])
39+
const [selectedGroup, setSelectedGroup] = useState<Groups[number] | null>(null)
40+
41+
const queryOpts = trpc.tg.groups.search.queryOptions({ query: groupQuery, limit: 20 })
42+
43+
async function searchGroup(e: React.FormEvent<HTMLFormElement>) {
44+
e.preventDefault()
45+
46+
const res = await qc.fetchQuery(queryOpts)
47+
setGroups(res.groups.filter((g) => !alreadyIn.includes(g.telegramId)))
48+
if (res.count === 0) toast.warning("No groups found with this query")
49+
else toast.info(`Found ${res.count} groups`)
50+
}
51+
52+
const submitMutation = useMutation(trpc.tg.permissions.addGroup.mutationOptions())
53+
54+
async function submit() {
55+
if (!adderId) return toast.warning("Invalid session, try reloading the page")
56+
if (!selectedGroup) return toast.warning("No group selected, cannot proceed")
57+
if (!user) return toast.warning("Invalid user, try restarting the dialog")
58+
59+
try {
60+
await submitMutation.mutateAsync({ adderId, groupId: selectedGroup?.telegramId, userId: user.id })
61+
toast.info(`Group admin added`)
62+
handleOpenChange(false)
63+
router.refresh()
64+
} catch (err) {
65+
console.error(err)
66+
handleOpenChange(false)
67+
toast.error("There was an error, check logs")
68+
}
69+
}
70+
71+
function reset() {
72+
setGroups([])
73+
setGroupQuery("")
74+
setSelectedGroup(null)
75+
}
76+
77+
function handleOpenChange(v: boolean) {
78+
setOpen(v)
79+
if (v === false) {
80+
// closing
81+
qc.invalidateQueries(trpc.tg.permissions.getRoles.queryOptions({ userId: user?.id ?? 0 }))
82+
reset()
83+
}
84+
}
85+
86+
return (
87+
<Dialog open={open} onOpenChange={handleOpenChange}>
88+
<DialogTrigger
89+
render={
90+
<Button variant="outline">
91+
<Plus size={20} /> New
92+
</Button>
93+
}
94+
/>
95+
<DialogContent className="sm:max-w-xl">
96+
<DialogHeader>
97+
<DialogTitle>New Group Admin</DialogTitle>
98+
<DialogDescription>Make the user admin in a group.</DialogDescription>
99+
</DialogHeader>
100+
{user && (
101+
<p>
102+
Target: {user.firstName} {user.username && `@${user.username}`} [{user.id}]
103+
</p>
104+
)}
105+
106+
<form onSubmit={searchGroup} className="pt-2 gap-y-4 flex flex-col justify-start items-start">
107+
<div className="flex gap-2 flex-col items-start justify-start">
108+
<Label htmlFor="email" className="text-base">
109+
Search Group
110+
</Label>
111+
<div className="flex gap-2 items-center justify-start">
112+
<Input
113+
id="group-query"
114+
type="text"
115+
placeholder="Group name"
116+
className="bg-card w-auto"
117+
disabled={groups.length > 0}
118+
required
119+
onChange={(e) => {
120+
setGroupQuery(e.target.value)
121+
}}
122+
value={groupQuery}
123+
/>
124+
<Button type="submit" size="icon">
125+
<Search />
126+
</Button>
127+
{groups.length > 0 && (
128+
<Button variant="outline" onClick={reset}>
129+
<X />
130+
Reset
131+
</Button>
132+
)}
133+
</div>
134+
</div>
135+
</form>
136+
137+
{groups && (
138+
<Select
139+
items={groups.map((g) => ({ value: g, label: g.title }))}
140+
value={selectedGroup}
141+
onValueChange={(v) => setSelectedGroup(v)}
142+
disabled={groups.length === 0}
143+
>
144+
<SelectTrigger className="w-full max-w-48">{selectedGroup?.title ?? "Select a group"}</SelectTrigger>
145+
<SelectContent>
146+
<SelectGroup>
147+
{groups.map((item) => (
148+
<SelectItem key={item.telegramId} value={item}>
149+
{item.title}
150+
</SelectItem>
151+
))}
152+
</SelectGroup>
153+
</SelectContent>
154+
</Select>
155+
)}
156+
<DialogFooter>
157+
<DialogClose render={<Button variant="outline">Cancel</Button>} />
158+
<Button onClick={submit} disabled={!selectedGroup}>
159+
Confirm
160+
</Button>
161+
</DialogFooter>
162+
</DialogContent>
163+
</Dialog>
164+
)
165+
}

0 commit comments

Comments
 (0)